ERPNext.com Frappe Cloud Support Partners Foundation Frappe School

[Tutorial] Enable has_batch for items with transactions

[Tutorial]
Dear All,
i solved this problem of changing has_batch for an item(s) after making transactions easily as below : (Use carefully and only if you know what you are doing)

First create a custom app with any name with a single DocType
then create a button field named convert_non_batch_to_has_batch
Second use the below code for client-side

frappe.ui.form.on('Infinity', {
	refresh: function(frm) {

	},
	convert_non_batch_to_has_batch: function(frm){
		handle_convert_non_batch_to_has_batch(frm);
	}
});


var handle_convert_non_batch_to_has_batch = function(frm){
	frappe.warn('Are you sure you want to proceed?',
				'This is a low level procedure and may harm your data if misused!!',
				() => {
					// action to perform if Continue is selected
					frappe.prompt([
						{
							label: 'Item Group',
							fieldname: 'item_group',
							fieldtype: 'Link',
							options: 'Item Group'
						},
						{
							label: 'Batch Number Series',
							fieldname: 'batch_number_series',
							fieldtype: 'Data'
						}
					], (values) => {
						var group = values.item_group;
						var series = values.batch_number_series;
						frappe.call({
                            method: "ifp.ifp_utils.get_items_without_has_batch_in_item_group",
                            args: {
                                group: group,
								series: series
                            },
                            callback: function(res){
                                if (res && res.message){
									frappe.confirm('Are you sure you want to proceed with ' + res.message +' items in '+ group + ' Group?',
    									() => {
        										// action to perform if Yes is selected
												frappe.call({
													method: "ifp.ifp_utils.handle_convert_non_batch_to_has_batch",
													args: {
														group: group,
														series: series
													},
													callback: function(res){
														if (res && res.message){
															frappe.msgprint('Successfully processed '+ res.message +' Items.');
														}
													}
												});
    									}, () => {
        										// action to perform if No is selected
    									}
									)
								}
							}
						});
					})
				},
				'Continue',
					//true // Sets dialog as minimizable
				)
}

Third use the below code for server-side

from __future__ import unicode_literals
from frappe import publish_progress
import frappe
import json


@frappe.whitelist()
def get_items_without_has_batch_in_item_group(group, series):
    if not group or not series:
        return 0
    counter = 0
    for item in frappe.db.get_list('Item', filters={'item_group': group, 'has_batch_no': 0}, fields=['item_code', 'item_name'], order_by='item_code', as_list=False):
        counter+=1
    return counter

def set_item_batch_properities(item_code, series):
    frappe.db.set_value('Item', item_code, {
        'has_batch_no': 1,
        'create_new_batch': 1,
        'batch_number_series': series
    })

def create_new_batch(item_code):
    batch_no = frappe.get_doc(dict(
	    doctype='Batch',
		item=item_code)).insert().name
    
    return batch_no

def update_stock_ledger(item_code):
    batch_no =''
    size = len(frappe.db.get_list('Stock Ledger Entry', filters={'item_code': item_code}, fields=['name', 'actual_qty', 'qty_after_transaction', 'voucher_type', 'voucher_detail_no', 'voucher_no'], order_by='name', as_list=False))
    if size > 0:
        batch_no = create_new_batch(item_code)
        for entry in frappe.db.get_list('Stock Ledger Entry', filters={'item_code': item_code}, fields=['name', 'actual_qty', 'qty_after_transaction', 'voucher_type', 'voucher_detail_no', 'voucher_no'], order_by='name', as_list=False):
            frappe.db.set_value('Stock Ledger Entry', entry.name, {
                'batch_no': batch_no
            })
            if entry.voucher_type == 'Stock Reconciliation':
                srd = frappe.get_doc('Stock Reconciliation', entry.voucher_no)
                if srd.purpose == 'Opening Stock' and entry.actual_qty == 0:
                    frappe.db.set_value('Stock Ledger Entry', entry.name, {
                        'actual_qty': entry.qty_after_transaction
                })      

         


@frappe.whitelist()
def handle_convert_non_batch_to_has_batch(group, series):
    if not group or not series:
        return 0
    counter = 0
    size = len(frappe.db.get_list('Item', filters={'item_group': group, 'has_batch_no': 0}, fields=['item_code', 'item_name'], order_by='item_code', as_list=False))
    for item in frappe.db.get_list('Item', filters={'item_group': group, 'has_batch_no': 0}, fields=['item_code', 'item_name'], order_by='item_code', as_list=False):
        if counter > -1:
            set_item_batch_properities(item.item_code, series)
            update_stock_ledger(item.item_code)
            counter+=1
            
            progress = counter / size * 100
            frappe.publish_progress(float(counter*100)/size, title = "Processing Items...")
    
    
    return counter
4 Likes

So, if I do it, after changing, and need to insert the batch numbers, Can I do it, to past transactions?

No, need.
as you can see in the code ,

def create_new_batch(item_code):
    batch_no = frappe.get_doc(dict(
	    doctype='Batch',
		item=item_code)).insert().name
    
    return batch_no

def update_stock_ledger(item_code):
    batch_no =''
    size = len(frappe.db.get_list('Stock Ledger Entry', filters={'item_code': item_code}, fields=['name', 'actual_qty', 'qty_after_transaction', 'voucher_type', 'voucher_detail_no', 'voucher_no'], order_by='name', as_list=False))
    if size > 0:
        batch_no = create_new_batch(item_code)
        for entry in frappe.db.get_list('Stock Ledger Entry', filters={'item_code': item_code}, fields=['name', 'actual_qty', 'qty_after_transaction', 'voucher_type', 'voucher_detail_no', 'voucher_no'], order_by='name', as_list=False):
            frappe.db.set_value('Stock Ledger Entry', entry.name, {
                'batch_no': batch_no
            })
            if entry.voucher_type == 'Stock Reconciliation':
                srd = frappe.get_doc('Stock Reconciliation', entry.voucher_no)
                if srd.purpose == 'Opening Stock' and entry.actual_qty == 0:
                    frappe.db.set_value('Stock Ledger Entry', entry.name, {
                        'actual_qty': entry.qty_after_transaction
                })      

the solution will made a batch number and update all the previous stock ledger entries with that batch no.

but as a further development, you may also in the same way update all delivery note and purchase details and stock entry and reconciliation … etc. items with this batch.

Please don’t do this.
Just disable the item and let it be there.

Create new item with batch and other needed stuff. And use that from now.

could you please clarify the reason?

There are lot of dependencies and links.
Also, looking at the documentation of frappe.db.set_value, you can find:

This method won’t call ORM triggers like validate and on_update. Use this method to update hidden fields or if you know what you are doing.

The data gets corrupted and the system could break. Batch is linked to a lot of documents. And most of the people implementing your code won’t have any idea of how to fix the errors that follows while doing this. Essentially making their system useless.

Sure, you have some right … and this is why i noted in my post (Use carefully and only if you know what you are doing)

But, according to my tests and as i tested on production site with thousands of items and great no. of transactions … it gave a very fine results comparing with other solutions.

And again, i vote for your comment as a must to be careful and must not be used without knowing what are the results.

1 Like