From 88ab9be79c5e094769645b142e71c6e5a2241409 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 13 Jan 2025 16:34:08 +0530 Subject: [PATCH] fix: auto fetch batch and serial no for draft stock transactions --- erpnext/controllers/accounts_controller.py | 9 +- erpnext/public/js/controllers/transaction.js | 2 + erpnext/stock/doctype/batch/batch.py | 9 +- .../delivery_note/test_delivery_note.py | 71 ++++++++++++++ erpnext/stock/get_item_details.py | 93 +++++++++++++++++++ 5 files changed, 180 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 0b23f1bcfa6..8628fe647bf 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -779,7 +779,14 @@ class AccountsController(TransactionBase): ret = get_item_details(ctx, self, for_validate=for_validate, overwrite_warehouse=False) for fieldname, value in ret.items(): if item.meta.get_field(fieldname) and value is not None: - if item.get(fieldname) is None or fieldname in force_item_fields: + if ( + item.get(fieldname) is None + or fieldname in force_item_fields + or ( + fieldname in ["serial_no", "batch_no"] + and item.get("use_serial_batch_fields") + ) + ): item.set(fieldname, value) elif fieldname in ["cost_center", "conversion_factor"] and not item.get( diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 20fce33e82c..8e3767d7450 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -596,6 +596,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe child_doctype: item.doctype, child_docname: item.name, is_old_subcontracting_flow: me.frm.doc.is_old_subcontracting_flow, + use_serial_batch_fields: item.use_serial_batch_fields, + serial_and_batch_bundle: item.serial_and_batch_bundle, } }, diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 5d71ca06cb4..3882e5b2424 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -2,7 +2,7 @@ # License: GNU General Public License v3. See license.txt -from collections import defaultdict +from collections import OrderedDict, defaultdict import frappe from frappe import _ @@ -449,11 +449,14 @@ def get_available_batches(kwargs): get_auto_batch_nos, ) - batchwise_qty = defaultdict(float) + batchwise_qty = OrderedDict() batches = get_auto_batch_nos(kwargs) for batch in batches: - batchwise_qty[batch.get("batch_no")] += batch.get("qty") + if batch.get("batch_no") not in batchwise_qty: + batchwise_qty[batch.get("batch_no")] = batch.get("qty") + else: + batchwise_qty[batch.get("batch_no")] += batch.get("qty") return batchwise_qty diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 6d44af4f783..ca1af9b81ad 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -2375,6 +2375,77 @@ class TestDeliveryNote(IntegrationTestCase): for d in bundle_data: self.assertEqual(d.incoming_rate, serial_no_valuation[d.serial_no]) + def test_auto_set_serial_batch_for_draft_dn(self): + frappe.db.set_single_value("Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 1) + frappe.db.set_single_value("Stock Settings", "pick_serial_and_batch_based_on", "FIFO") + + batch_item = make_item( + "_Test Auto Set Serial Batch Draft DN", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "is_stock_item": 1, + "batch_number_series": "TAS-BASD-.#####", + }, + ) + + serial_item = make_item( + "_Test Auto Set Serial Batch Draft DN Serial Item", + properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "TAS-SASD-.#####"}, + ) + + batch_serial_item = make_item( + "_Test Auto Set Serial Batch Draft DN Batch Serial Item", + properties={ + "has_batch_no": 1, + "has_serial_no": 1, + "is_stock_item": 1, + "create_new_batch": 1, + "batch_number_series": "TAS-BSD-.#####", + "serial_no_series": "TAS-SSD-.#####", + }, + ) + + for item in [batch_item, serial_item, batch_serial_item]: + make_stock_entry(item_code=item.name, target="_Test Warehouse - _TC", qty=5, basic_rate=100) + + dn = create_delivery_note( + item_code=batch_item, + qty=5, + rate=500, + use_serial_batch_fields=1, + do_not_submit=True, + ) + + for item in [serial_item, batch_serial_item]: + dn.append( + "items", + { + "item_code": item.name, + "qty": 5, + "rate": 500, + "base_rate": 500, + "item_name": item.name, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "warehouse": dn.items[0].warehouse, + "use_serial_batch_fields": 1, + }, + ) + + dn.save() + for row in dn.items: + if row.item_code == batch_item.name: + self.assertTrue(row.batch_no) + + if row.item_code == serial_item.name: + self.assertTrue(row.serial_no) + + if row.item_code == batch_serial_item.name: + self.assertTrue(row.batch_no) + self.assertTrue(row.serial_no) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 3ba80e0e785..f263ec69c92 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -134,6 +134,13 @@ def get_item_details( out.update(data) + if ( + frappe.db.get_single_value("Stock Settings", "auto_create_serial_and_batch_bundle_for_outward") + and not ctx.get("serial_and_batch_bundle") + and (ctx.get("use_serial_batch_fields") or ctx.get("doctype") == "POS Invoice") + ): + update_stock(ctx, out, doc) + if ctx.transaction_date and item.lead_time_days: out.schedule_date = out.lead_time_date = add_days(ctx.transaction_date, item.lead_time_days) @@ -174,6 +181,92 @@ def set_valuation_rate(out: ItemDetails | dict, ctx: ItemDetailsCtx): out.update(get_valuation_rate(ctx.item_code, ctx.company, out.get("warehouse"))) +def update_stock(ctx, out, doc=None): + from erpnext.stock.doctype.batch.batch import get_available_batches + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward + + if ( + ( + ctx.get("doctype") in ["Delivery Note", "POS Invoice"] + or (ctx.get("doctype") == "Sales Invoice" and ctx.get("update_stock")) + ) + and out.warehouse + and out.stock_qty > 0 + ): + kwargs = frappe._dict( + { + "item_code": ctx.item_code, + "warehouse": ctx.warehouse, + "based_on": frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"), + } + ) + + if ctx.get("ignore_serial_nos"): + kwargs["ignore_serial_nos"] = ctx.get("ignore_serial_nos") + + qty = out.stock_qty + batches = [] + if out.has_batch_no and not ctx.get("batch_no"): + batches = get_available_batches(kwargs) + if doc: + filter_batches(batches, doc) + + for batch_no, batch_qty in batches.items(): + if batch_qty >= qty: + out.update({"batch_no": batch_no, "actual_batch_qty": qty}) + break + else: + qty -= batch_qty + + out.update({"batch_no": batch_no, "actual_batch_qty": batch_qty}) + + if out.has_serial_no and out.has_batch_no and has_incorrect_serial_nos(ctx, out): + kwargs["batches"] = [ctx.get("batch_no")] if ctx.get("batch_no") else [out.get("batch_no")] + serial_nos = get_serial_nos_for_outward(kwargs) + serial_nos = get_filtered_serial_nos(serial_nos, doc) + + out["serial_no"] = "\n".join(serial_nos[: cint(out.stock_qty)]) + + elif out.has_serial_no and not ctx.get("serial_no"): + serial_nos = get_serial_nos_for_outward(kwargs) + serial_nos = get_filtered_serial_nos(serial_nos, doc) + + out["serial_no"] = "\n".join(serial_nos[: cint(out.stock_qty)]) + + +def has_incorrect_serial_nos(ctx, out): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + if not ctx.get("serial_no"): + return True + + serial_nos = get_serial_nos(ctx.get("serial_no")) + if len(serial_nos) != out.get("stock_qty"): + return True + + return False + + +def filter_batches(batches, doc): + for row in doc.get("items"): + if row.get("batch_no") in batches: + batches[row.get("batch_no")] -= row.get("qty") + if batches[row.get("batch_no")] <= 0: + del batches[row.get("batch_no")] + + +def get_filtered_serial_nos(serial_nos, doc): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + for row in doc.get("items"): + if row.get("serial_no"): + for serial_no in get_serial_nos(row.get("serial_no")): + if serial_no in serial_nos: + serial_nos.remove(serial_no) + + return serial_nos + + def update_bin_details(ctx: ItemDetailsCtx, out: ItemDetails, doc): if ctx.doctype == "Material Request" and ctx.material_request_type == "Material Transfer": out.update(get_bin_details(ctx.item_code, ctx.from_warehouse))