diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 19df2d0489c..7074710dfee 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -777,7 +777,14 @@ class AccountsController(TransactionBase): ret = get_item_details(args, 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 fb43cb46860..d63461c23e0 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -579,6 +579,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 9acdce8bebc..667710fee1b 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -2339,6 +2339,77 @@ class TestDeliveryNote(FrappeTestCase): 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 17a8fe2cb6a..d4415540105 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -115,8 +115,20 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru out.update(data) +<<<<<<< HEAD if args.transaction_date and item.lead_time_days: out.schedule_date = out.lead_time_date = add_days(args.transaction_date, item.lead_time_days) +======= + 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) +>>>>>>> 88ab9be79c (fix: auto fetch batch and serial no for draft stock transactions) if args.get("is_subcontracted"): out.bom = args.get("bom") or get_default_bom(args.item_code) @@ -155,9 +167,101 @@ def set_valuation_rate(out, args): out.update(get_valuation_rate(args.item_code, args.company, out.get("warehouse"))) +<<<<<<< HEAD def update_bin_details(args, out, doc): if args.get("doctype") == "Material Request" and args.get("material_request_type") == "Material Transfer": out.update(get_bin_details(args.item_code, args.get("from_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)) +>>>>>>> 88ab9be79c (fix: auto fetch batch and serial no for draft stock transactions) elif out.get("warehouse"): company = args.company if (doc and doc.get("doctype") == "Purchase Order") else None