[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
5 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.

2 Likes

Just a note that on Version 13 this will not update the qty on the Batch document. Although I did notice that once a movement is recorded the qty gets updated. I’ll have to do some test on how to trigger this update.

Currently though for anyone facing the batch issue problem this is the only solution that we have right now.

Also for anyone reading this, don’t use this version of the code ahmed updated the code to cover the dependencies. If the dependencies doesn’t get updated the report for stock balance becomes incorrect. (Another reason why if you are using this make sure that you check your data and that you know what you are doing)

+1 to what @rtdany10 said. This will most certainly cause inconsistent behaviour as old SLEs won’t have batch number on it.

Best way to transition is to disable old item and create new one. You can “transfer” stock by making reconciliation or repack entry.

1 Like

I do agree and also why there is a huge disclaimer, but the best way is just not feasible currently. I think for someone who already used the system for a while and already has a lot of records it is better to just do a migration from erpnext to erpnext as the best way will definitely affect the users workflow if you are trying to batch hundreds of items.

I haven’t really used this script on any live system and I’m just trying to find a way to make this work as I feel like this can be a very useful feature. I feel like anyone who is looking for this functionality is trying to batch multiple items not just a single one.

1 Like