From 21d13859a0c3bdff789f46eda0e1f079e60808c9 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 8 Jan 2026 13:39:03 +0530 Subject: [PATCH 1/3] fix: negative stock issue for higher precision (cherry picked from commit 87be020c783f99f21dbbdcb653f538b97431ebdd) # Conflicts: # erpnext/stock/doctype/delivery_note/test_delivery_note.py (cherry picked from commit 1bbeecff12ee22efccf0c1e8583d52aecd78bbb7) # Conflicts: # erpnext/stock/doctype/delivery_note/test_delivery_note.py --- .../delivery_note/test_delivery_note.py | 633 ++++++++++++++++++ erpnext/stock/stock_ledger.py | 6 +- 2 files changed, 638 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 507fb78663e..4e0cebc3f3d 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1507,6 +1507,639 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(stock_value_difference, 100.0 * 5) +<<<<<<< HEAD +======= + def test_delivery_note_return_valuation_without_use_serial_batch_field(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + batch_item = make_item( + "_Test Delivery Note Return Valuation Batch Item", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "is_stock_item": 1, + "batch_number_series": "BRTN-DNN-BI-.#####", + }, + ).name + + serial_item = make_item( + "_Test Delivery Note Return Valuation Serial Item", + properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-DNN-TP-.#####"}, + ).name + + batches = {} + serial_nos = [] + for qty, rate in {3: 300, 2: 100}.items(): + se = make_stock_entry( + item_code=batch_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate + ) + batches[get_batch_from_bundle(se.items[0].serial_and_batch_bundle)] = qty + + for qty, rate in {2: 100, 1: 50}.items(): + make_stock_entry(item_code=serial_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate) + serial_nos.extend(get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)) + + dn = create_delivery_note( + item_code=batch_item, + qty=5, + rate=1000, + use_serial_batch_fields=0, + batches=batches, + do_not_submit=True, + ) + + bundle_id = make_serial_batch_bundle( + frappe._dict( + { + "item_code": serial_item, + "warehouse": dn.items[0].warehouse, + "qty": 3, + "voucher_type": "Delivery Note", + "serial_nos": serial_nos, + "posting_date": dn.posting_date, + "posting_time": dn.posting_time, + "type_of_transaction": "Outward", + "do_not_submit": True, + } + ) + ).name + + dn.append( + "items", + { + "item_code": serial_item, + "qty": 3, + "rate": 700, + "base_rate": 700, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "warehouse": dn.items[0].warehouse, + "use_serial_batch_fields": 0, + "serial_and_batch_bundle": bundle_id, + }, + ) + + dn.save() + dn.submit() + dn.reload() + + batch_no_valuation = defaultdict(float) + serial_no_valuation = defaultdict(float) + + for row in dn.items: + if row.serial_and_batch_bundle: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "serial_no", "batch_no"], + ) + + for d in bundle_data: + if d.batch_no: + batch_no_valuation[d.batch_no] = d.incoming_rate + elif d.serial_no: + serial_no_valuation[d.serial_no] = d.incoming_rate + + return_entry = make_sales_return(dn.name) + + return_entry.save() + return_entry.submit() + return_entry.reload() + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "batch_no"], + ) + + for d in bundle_data: + self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no]) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "serial_no"], + ) + + for d in bundle_data: + self.assertEqual(d.incoming_rate, serial_no_valuation[d.serial_no]) + + def test_delivery_note_return_valuation_with_use_serial_batch_field(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + batch_item = make_item( + "_Test Delivery Note Return Valuation WITH Batch Item", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "is_stock_item": 1, + "batch_number_series": "BRTN-DNN-BIW-.#####", + }, + ).name + + serial_item = make_item( + "_Test Delivery Note Return Valuation WITH Serial Item", + properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-DNN-TPW-.#####"}, + ).name + + batches = [] + serial_nos = [] + for qty, rate in {3: 300, 2: 100}.items(): + se = make_stock_entry( + item_code=batch_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate + ) + batches.append(get_batch_from_bundle(se.items[0].serial_and_batch_bundle)) + + for qty, rate in {2: 100, 1: 50}.items(): + se = make_stock_entry( + item_code=serial_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate + ) + serial_nos.extend(get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)) + + dn = create_delivery_note( + item_code=batch_item, + qty=3, + rate=1000, + use_serial_batch_fields=1, + batch_no=batches[0], + do_not_submit=True, + ) + + dn.append( + "items", + { + "item_code": batch_item, + "qty": 2, + "rate": 1000, + "base_rate": 1000, + "item_name": batch_item, + "uom": dn.items[0].uom, + "stock_uom": dn.items[0].uom, + "conversion_factor": 1, + "warehouse": dn.items[0].warehouse, + "use_serial_batch_fields": 1, + "batch_no": batches[1], + }, + ) + + dn.append( + "items", + { + "item_code": serial_item, + "qty": 2, + "rate": 700, + "base_rate": 700, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "warehouse": dn.items[0].warehouse, + "use_serial_batch_fields": 1, + "serial_no": "\n".join(serial_nos[0:2]), + }, + ) + + dn.append( + "items", + { + "item_code": serial_item, + "qty": 1, + "rate": 700, + "base_rate": 700, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "warehouse": dn.items[0].warehouse, + "use_serial_batch_fields": 1, + "serial_no": serial_nos[-1], + }, + ) + + dn.save() + dn.submit() + dn.reload() + + batch_no_valuation = defaultdict(float) + serial_no_valuation = defaultdict(float) + + for row in dn.items: + if row.serial_and_batch_bundle: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "serial_no", "batch_no"], + ) + + for d in bundle_data: + if d.batch_no: + batch_no_valuation[d.batch_no] = d.incoming_rate + elif d.serial_no: + serial_no_valuation[d.serial_no] = d.incoming_rate + + return_entry = make_sales_return(dn.name) + + return_entry.save() + return_entry.submit() + return_entry.reload() + + for row in return_entry.items: + if row.item_code == batch_item: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "batch_no"], + ) + + for d in bundle_data: + self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no]) + else: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "serial_no"], + ) + + 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) + + def test_delivery_note_return_for_batch_item_with_different_warehouse(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + batch_item = make_item( + "_Test Delivery Note Return Valuation WITH Batch Item", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "is_stock_item": 1, + "batch_number_series": "BRTN-DNN-BIW-.#####", + }, + ).name + + batches = [] + for qty, rate in {5: 300}.items(): + se = make_stock_entry( + item_code=batch_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate + ) + batches.append(get_batch_from_bundle(se.items[0].serial_and_batch_bundle)) + + warehouse = create_warehouse("Sales Return Test Warehouse 1", company="_Test Company") + + dn = create_delivery_note( + item_code=batch_item, + qty=5, + rate=1000, + use_serial_batch_fields=1, + batch_no=batches[0], + do_not_submit=True, + ) + + self.assertEqual(dn.items[0].warehouse, "_Test Warehouse - _TC") + + dn.save() + dn.submit() + dn.reload() + + batch_no_valuation = defaultdict(float) + + for row in dn.items: + if row.serial_and_batch_bundle: + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "serial_no", "batch_no"], + ) + + for d in bundle_data: + if d.batch_no: + batch_no_valuation[d.batch_no] = d.incoming_rate + + return_entry = make_sales_return(dn.name) + return_entry.items[0].warehouse = warehouse + + return_entry.save() + return_entry.submit() + return_entry.reload() + + for row in return_entry.items: + self.assertEqual(row.warehouse, warehouse) + bundle_data = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": row.serial_and_batch_bundle}, + fields=["incoming_rate", "batch_no"], + ) + + for d in bundle_data: + self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no]) + + def test_delivery_note_per_billed_after_return(self): + from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note + + so = make_sales_order(qty=2) + dn = make_delivery_note(so.name) + dn.submit() + self.assertEqual(dn.per_billed, 0) + self.assertEqual(dn.status, "To Bill") + + si = make_sales_invoice(dn.name) + si.location = "Test Location" + si.submit() + + dn_return = create_delivery_note(is_return=1, return_against=dn.name, qty=-2, do_not_submit=True) + dn_return.items[0].dn_detail = dn.items[0].name + dn_return.submit() + + returned = frappe.get_doc("Delivery Note", dn_return.name) + returned.update_prevdoc_status() + dn.load_from_db() + self.assertEqual(dn.per_billed, 100) + self.assertEqual(dn.per_returned, 100) + self.assertEqual(returned.status, "Return") + + def test_sales_return_for_product_bundle(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + from erpnext.stock.doctype.item.test_item import make_item + + rm_items = [] + for item_code, properties in { + "_Packed Service Item": {"is_stock_item": 0}, + "_Packed FG Item New 1": { + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "SN-PACKED-1-.#####", + }, + "_Packed FG Item New 2": { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BATCH-PACKED-2-.#####", + }, + "_Packed FG Item New 3": { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BATCH-PACKED-3-.#####", + "has_serial_no": 1, + "serial_no_series": "SN-PACKED-3-.#####", + }, + }.items(): + if not frappe.db.exists("Item", item_code): + make_item(item_code, properties) + + if item_code != "_Packed Service Item": + rm_items.append(item_code) + + for rate in [100, 200]: + make_stock_entry(item=item_code, target="_Test Warehouse - _TC", qty=5, rate=rate) + + make_product_bundle("_Packed Service Item", rm_items) + dn = create_delivery_note( + item_code="_Packed Service Item", + warehouse="_Test Warehouse - _TC", + qty=5, + ) + + dn.reload() + + serial_batch_map = {} + for row in dn.packed_items: + self.assertTrue(row.serial_and_batch_bundle) + if row.item_code not in serial_batch_map: + serial_batch_map[row.item_code] = frappe._dict( + { + "serial_nos": [], + "batches": defaultdict(int), + "serial_no_valuation": defaultdict(float), + "batch_no_valuation": defaultdict(float), + } + ) + + doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle) + for entry in doc.entries: + if entry.serial_no: + serial_batch_map[row.item_code].serial_nos.append(entry.serial_no) + serial_batch_map[row.item_code].serial_no_valuation[entry.serial_no] = entry.incoming_rate + if entry.batch_no: + serial_batch_map[row.item_code].batches[entry.batch_no] += entry.qty + serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no] = entry.incoming_rate + + dn1 = make_sales_return(dn.name) + dn1.items[0].qty = -2 + dn1.submit() + dn1.reload() + + for row in dn1.packed_items: + doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle) + for entry in doc.entries: + if entry.serial_no: + self.assertTrue(entry.serial_no in serial_batch_map[row.item_code].serial_nos) + self.assertEqual( + entry.incoming_rate, + serial_batch_map[row.item_code].serial_no_valuation[entry.serial_no], + ) + serial_batch_map[row.item_code].serial_nos.remove(entry.serial_no) + serial_batch_map[row.item_code].serial_no_valuation.pop(entry.serial_no) + + elif entry.batch_no: + serial_batch_map[row.item_code].batches[entry.batch_no] += entry.qty + self.assertTrue(entry.batch_no in serial_batch_map[row.item_code].batches) + self.assertEqual(entry.qty, 2.0) + self.assertEqual( + entry.incoming_rate, + serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no], + ) + + dn2 = make_sales_return(dn.name) + dn2.items[0].qty = -3 + dn2.submit() + dn2.reload() + + for row in dn2.packed_items: + doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle) + for entry in doc.entries: + if entry.serial_no: + self.assertTrue(entry.serial_no in serial_batch_map[row.item_code].serial_nos) + self.assertEqual( + entry.incoming_rate, + serial_batch_map[row.item_code].serial_no_valuation[entry.serial_no], + ) + serial_batch_map[row.item_code].serial_nos.remove(entry.serial_no) + serial_batch_map[row.item_code].serial_no_valuation.pop(entry.serial_no) + + elif entry.batch_no: + serial_batch_map[row.item_code].batches[entry.batch_no] += entry.qty + self.assertEqual(serial_batch_map[row.item_code].batches[entry.batch_no], 0.0) + + self.assertTrue(entry.batch_no in serial_batch_map[row.item_code].batches) + + self.assertEqual(entry.qty, 3.0) + self.assertEqual( + entry.incoming_rate, + serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no], + ) + +<<<<<<< HEAD + @change_settings("Stock Settings", {"allow_negative_stock": 0, "enable_stock_reservation": 1}) + def test_partial_delivery_note_against_reserved_stock(self): + from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( + get_stock_reservation_entries_for_voucher, + ) + + # create batch item + batch_item = make_item( + "_Test Batch Item For DN Reserve Check", + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBDNR.#####", + }, + ) + serial_item = make_item( + "_Test Serial Item For DN Reserve Check", + { + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "TSNDNR.#####", + }, + ) + + company = "_Test Company" + + warehouse = create_warehouse("Test Partial DN Reserved Stock", company=company) + customer = "_Test Customer" + + items = [batch_item.name, serial_item.name] + + for idx, item in enumerate(items): + # make inward entry for batch item + se = make_stock_entry(item_code=item, purpose="Material Receipt", qty=10, to_warehouse=warehouse) + sabb = se.items[0].serial_and_batch_bundle + + batch_no = get_batch_from_bundle(sabb) if not idx else None + serial_nos = get_serial_nos_from_bundle(sabb) if idx else None + + # make sales order and reserve the quantites against the so + so = make_sales_order(item_code=item, qty=10, rate=100, customer=customer, warehouse=warehouse) + so.submit() + so.create_stock_reservation_entries() + so.reload() + + # create a delivery note with partial quantity from resreved quantity + dn = create_dn_against_so(so=so.name, delivered_qty=5, do_not_submit=True) + dn.items[0].use_serial_batch_fields = 1 + if batch_no: + dn.items[0].batch_no = batch_no + else: + dn.items[0].serial_no = "\n".join(serial_nos[:5]) + + dn.save() + dn.submit() + + against_sales_order = dn.items[0].against_sales_order + so_detail = dn.items[0].so_detail + + sre_details = get_stock_reservation_entries_for_voucher( + so.doctype, against_sales_order, so_detail, ["reserved_qty", "delivered_qty", "status"] + ) + + # check partially delivered reserved stock + self.assertEqual(sre_details[0].status, "Partially Delivered") + self.assertEqual(sre_details[0].reserved_qty, so.items[0].qty) + self.assertEqual(sre_details[0].delivered_qty, dn.items[0].qty) +======= + def test_negative_stock_with_higher_precision(self): + original_flt_precision = frappe.db.get_default("float_precision") + frappe.db.set_single_value("System Settings", "float_precision", 7) + + item_code = make_item( + "Test Negative Stock High Precision Item", properties={"is_stock_item": 1, "valuation_rate": 1} + ).name + dn = create_delivery_note( + item_code=item_code, + qty=0.0000010, + do_not_submit=True, + ) + + self.assertRaises(frappe.ValidationError, dn.submit) + + frappe.db.set_single_value("System Settings", "float_precision", original_flt_precision) +>>>>>>> 87be020c78 (fix: negative stock issue for higher precision) + +>>>>>>> 1bbeecff12 (fix: negative stock issue for higher precision) def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 68c7c314091..5be94cc799e 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -715,7 +715,11 @@ class update_entries_after: diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) diff = flt(diff, self.flt_precision) # respect system precision - if diff < 0 and abs(diff) > 0.0001: + diff_threshold = 0.0001 + if self.flt_precision > 4: + diff_threshold = 10 ** (-1 * self.flt_precision) + + if diff < 0 and abs(diff) > diff_threshold: # negative stock! exc = sle.copy().update({"diff": diff}) self.exceptions.setdefault(sle.warehouse, []).append(exc) From 22e5aba02bc09eee27573f52b12439341dc9eff4 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 8 Jan 2026 14:45:39 +0530 Subject: [PATCH 2/3] chore: fix conflicts Refactor test cases for delivery notes to handle negative stock and higher precision. (cherry picked from commit 5193dbba9b630daf53fdefe92d4a5dac783dda91) --- erpnext/stock/doctype/delivery_note/test_delivery_note.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 4e0cebc3f3d..22bc81194ec 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -2051,7 +2051,6 @@ class TestDeliveryNote(FrappeTestCase): serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no], ) -<<<<<<< HEAD @change_settings("Stock Settings", {"allow_negative_stock": 0, "enable_stock_reservation": 1}) def test_partial_delivery_note_against_reserved_stock(self): from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( @@ -2120,7 +2119,7 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(sre_details[0].status, "Partially Delivered") self.assertEqual(sre_details[0].reserved_qty, so.items[0].qty) self.assertEqual(sre_details[0].delivered_qty, dn.items[0].qty) -======= + def test_negative_stock_with_higher_precision(self): original_flt_precision = frappe.db.get_default("float_precision") frappe.db.set_single_value("System Settings", "float_precision", 7) @@ -2137,7 +2136,6 @@ class TestDeliveryNote(FrappeTestCase): self.assertRaises(frappe.ValidationError, dn.submit) frappe.db.set_single_value("System Settings", "float_precision", original_flt_precision) ->>>>>>> 87be020c78 (fix: negative stock issue for higher precision) >>>>>>> 1bbeecff12 (fix: negative stock issue for higher precision) From cddf1e1ee53ec4011a9d9a51a4d45dde39188709 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 8 Jan 2026 15:18:53 +0530 Subject: [PATCH 3/3] chore: fix conflicts Refactor delivery note tests to improve clarity and organization. Remove redundant test cases and ensure proper valuation checks for batch and serial items. --- .../delivery_note/test_delivery_note.py | 614 ------------------ 1 file changed, 614 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 22bc81194ec..8f8d76e6737 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1507,619 +1507,6 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(stock_value_difference, 100.0 * 5) -<<<<<<< HEAD -======= - def test_delivery_note_return_valuation_without_use_serial_batch_field(self): - from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return - - batch_item = make_item( - "_Test Delivery Note Return Valuation Batch Item", - properties={ - "has_batch_no": 1, - "create_new_batch": 1, - "is_stock_item": 1, - "batch_number_series": "BRTN-DNN-BI-.#####", - }, - ).name - - serial_item = make_item( - "_Test Delivery Note Return Valuation Serial Item", - properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-DNN-TP-.#####"}, - ).name - - batches = {} - serial_nos = [] - for qty, rate in {3: 300, 2: 100}.items(): - se = make_stock_entry( - item_code=batch_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate - ) - batches[get_batch_from_bundle(se.items[0].serial_and_batch_bundle)] = qty - - for qty, rate in {2: 100, 1: 50}.items(): - make_stock_entry(item_code=serial_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate) - serial_nos.extend(get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)) - - dn = create_delivery_note( - item_code=batch_item, - qty=5, - rate=1000, - use_serial_batch_fields=0, - batches=batches, - do_not_submit=True, - ) - - bundle_id = make_serial_batch_bundle( - frappe._dict( - { - "item_code": serial_item, - "warehouse": dn.items[0].warehouse, - "qty": 3, - "voucher_type": "Delivery Note", - "serial_nos": serial_nos, - "posting_date": dn.posting_date, - "posting_time": dn.posting_time, - "type_of_transaction": "Outward", - "do_not_submit": True, - } - ) - ).name - - dn.append( - "items", - { - "item_code": serial_item, - "qty": 3, - "rate": 700, - "base_rate": 700, - "item_name": serial_item, - "uom": "Nos", - "stock_uom": "Nos", - "conversion_factor": 1, - "warehouse": dn.items[0].warehouse, - "use_serial_batch_fields": 0, - "serial_and_batch_bundle": bundle_id, - }, - ) - - dn.save() - dn.submit() - dn.reload() - - batch_no_valuation = defaultdict(float) - serial_no_valuation = defaultdict(float) - - for row in dn.items: - if row.serial_and_batch_bundle: - bundle_data = frappe.get_all( - "Serial and Batch Entry", - filters={"parent": row.serial_and_batch_bundle}, - fields=["incoming_rate", "serial_no", "batch_no"], - ) - - for d in bundle_data: - if d.batch_no: - batch_no_valuation[d.batch_no] = d.incoming_rate - elif d.serial_no: - serial_no_valuation[d.serial_no] = d.incoming_rate - - return_entry = make_sales_return(dn.name) - - return_entry.save() - return_entry.submit() - return_entry.reload() - - for row in return_entry.items: - if row.item_code == batch_item: - bundle_data = frappe.get_all( - "Serial and Batch Entry", - filters={"parent": row.serial_and_batch_bundle}, - fields=["incoming_rate", "batch_no"], - ) - - for d in bundle_data: - self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no]) - else: - bundle_data = frappe.get_all( - "Serial and Batch Entry", - filters={"parent": row.serial_and_batch_bundle}, - fields=["incoming_rate", "serial_no"], - ) - - for d in bundle_data: - self.assertEqual(d.incoming_rate, serial_no_valuation[d.serial_no]) - - def test_delivery_note_return_valuation_with_use_serial_batch_field(self): - from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return - - batch_item = make_item( - "_Test Delivery Note Return Valuation WITH Batch Item", - properties={ - "has_batch_no": 1, - "create_new_batch": 1, - "is_stock_item": 1, - "batch_number_series": "BRTN-DNN-BIW-.#####", - }, - ).name - - serial_item = make_item( - "_Test Delivery Note Return Valuation WITH Serial Item", - properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-DNN-TPW-.#####"}, - ).name - - batches = [] - serial_nos = [] - for qty, rate in {3: 300, 2: 100}.items(): - se = make_stock_entry( - item_code=batch_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate - ) - batches.append(get_batch_from_bundle(se.items[0].serial_and_batch_bundle)) - - for qty, rate in {2: 100, 1: 50}.items(): - se = make_stock_entry( - item_code=serial_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate - ) - serial_nos.extend(get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)) - - dn = create_delivery_note( - item_code=batch_item, - qty=3, - rate=1000, - use_serial_batch_fields=1, - batch_no=batches[0], - do_not_submit=True, - ) - - dn.append( - "items", - { - "item_code": batch_item, - "qty": 2, - "rate": 1000, - "base_rate": 1000, - "item_name": batch_item, - "uom": dn.items[0].uom, - "stock_uom": dn.items[0].uom, - "conversion_factor": 1, - "warehouse": dn.items[0].warehouse, - "use_serial_batch_fields": 1, - "batch_no": batches[1], - }, - ) - - dn.append( - "items", - { - "item_code": serial_item, - "qty": 2, - "rate": 700, - "base_rate": 700, - "item_name": serial_item, - "uom": "Nos", - "stock_uom": "Nos", - "conversion_factor": 1, - "warehouse": dn.items[0].warehouse, - "use_serial_batch_fields": 1, - "serial_no": "\n".join(serial_nos[0:2]), - }, - ) - - dn.append( - "items", - { - "item_code": serial_item, - "qty": 1, - "rate": 700, - "base_rate": 700, - "item_name": serial_item, - "uom": "Nos", - "stock_uom": "Nos", - "conversion_factor": 1, - "warehouse": dn.items[0].warehouse, - "use_serial_batch_fields": 1, - "serial_no": serial_nos[-1], - }, - ) - - dn.save() - dn.submit() - dn.reload() - - batch_no_valuation = defaultdict(float) - serial_no_valuation = defaultdict(float) - - for row in dn.items: - if row.serial_and_batch_bundle: - bundle_data = frappe.get_all( - "Serial and Batch Entry", - filters={"parent": row.serial_and_batch_bundle}, - fields=["incoming_rate", "serial_no", "batch_no"], - ) - - for d in bundle_data: - if d.batch_no: - batch_no_valuation[d.batch_no] = d.incoming_rate - elif d.serial_no: - serial_no_valuation[d.serial_no] = d.incoming_rate - - return_entry = make_sales_return(dn.name) - - return_entry.save() - return_entry.submit() - return_entry.reload() - - for row in return_entry.items: - if row.item_code == batch_item: - bundle_data = frappe.get_all( - "Serial and Batch Entry", - filters={"parent": row.serial_and_batch_bundle}, - fields=["incoming_rate", "batch_no"], - ) - - for d in bundle_data: - self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no]) - else: - bundle_data = frappe.get_all( - "Serial and Batch Entry", - filters={"parent": row.serial_and_batch_bundle}, - fields=["incoming_rate", "serial_no"], - ) - - 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) - - def test_delivery_note_return_for_batch_item_with_different_warehouse(self): - from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return - from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse - - batch_item = make_item( - "_Test Delivery Note Return Valuation WITH Batch Item", - properties={ - "has_batch_no": 1, - "create_new_batch": 1, - "is_stock_item": 1, - "batch_number_series": "BRTN-DNN-BIW-.#####", - }, - ).name - - batches = [] - for qty, rate in {5: 300}.items(): - se = make_stock_entry( - item_code=batch_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate - ) - batches.append(get_batch_from_bundle(se.items[0].serial_and_batch_bundle)) - - warehouse = create_warehouse("Sales Return Test Warehouse 1", company="_Test Company") - - dn = create_delivery_note( - item_code=batch_item, - qty=5, - rate=1000, - use_serial_batch_fields=1, - batch_no=batches[0], - do_not_submit=True, - ) - - self.assertEqual(dn.items[0].warehouse, "_Test Warehouse - _TC") - - dn.save() - dn.submit() - dn.reload() - - batch_no_valuation = defaultdict(float) - - for row in dn.items: - if row.serial_and_batch_bundle: - bundle_data = frappe.get_all( - "Serial and Batch Entry", - filters={"parent": row.serial_and_batch_bundle}, - fields=["incoming_rate", "serial_no", "batch_no"], - ) - - for d in bundle_data: - if d.batch_no: - batch_no_valuation[d.batch_no] = d.incoming_rate - - return_entry = make_sales_return(dn.name) - return_entry.items[0].warehouse = warehouse - - return_entry.save() - return_entry.submit() - return_entry.reload() - - for row in return_entry.items: - self.assertEqual(row.warehouse, warehouse) - bundle_data = frappe.get_all( - "Serial and Batch Entry", - filters={"parent": row.serial_and_batch_bundle}, - fields=["incoming_rate", "batch_no"], - ) - - for d in bundle_data: - self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no]) - - def test_delivery_note_per_billed_after_return(self): - from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note - - so = make_sales_order(qty=2) - dn = make_delivery_note(so.name) - dn.submit() - self.assertEqual(dn.per_billed, 0) - self.assertEqual(dn.status, "To Bill") - - si = make_sales_invoice(dn.name) - si.location = "Test Location" - si.submit() - - dn_return = create_delivery_note(is_return=1, return_against=dn.name, qty=-2, do_not_submit=True) - dn_return.items[0].dn_detail = dn.items[0].name - dn_return.submit() - - returned = frappe.get_doc("Delivery Note", dn_return.name) - returned.update_prevdoc_status() - dn.load_from_db() - self.assertEqual(dn.per_billed, 100) - self.assertEqual(dn.per_returned, 100) - self.assertEqual(returned.status, "Return") - - def test_sales_return_for_product_bundle(self): - from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle - from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return - from erpnext.stock.doctype.item.test_item import make_item - - rm_items = [] - for item_code, properties in { - "_Packed Service Item": {"is_stock_item": 0}, - "_Packed FG Item New 1": { - "is_stock_item": 1, - "has_serial_no": 1, - "serial_no_series": "SN-PACKED-1-.#####", - }, - "_Packed FG Item New 2": { - "is_stock_item": 1, - "has_batch_no": 1, - "create_new_batch": 1, - "batch_number_series": "BATCH-PACKED-2-.#####", - }, - "_Packed FG Item New 3": { - "is_stock_item": 1, - "has_batch_no": 1, - "create_new_batch": 1, - "batch_number_series": "BATCH-PACKED-3-.#####", - "has_serial_no": 1, - "serial_no_series": "SN-PACKED-3-.#####", - }, - }.items(): - if not frappe.db.exists("Item", item_code): - make_item(item_code, properties) - - if item_code != "_Packed Service Item": - rm_items.append(item_code) - - for rate in [100, 200]: - make_stock_entry(item=item_code, target="_Test Warehouse - _TC", qty=5, rate=rate) - - make_product_bundle("_Packed Service Item", rm_items) - dn = create_delivery_note( - item_code="_Packed Service Item", - warehouse="_Test Warehouse - _TC", - qty=5, - ) - - dn.reload() - - serial_batch_map = {} - for row in dn.packed_items: - self.assertTrue(row.serial_and_batch_bundle) - if row.item_code not in serial_batch_map: - serial_batch_map[row.item_code] = frappe._dict( - { - "serial_nos": [], - "batches": defaultdict(int), - "serial_no_valuation": defaultdict(float), - "batch_no_valuation": defaultdict(float), - } - ) - - doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle) - for entry in doc.entries: - if entry.serial_no: - serial_batch_map[row.item_code].serial_nos.append(entry.serial_no) - serial_batch_map[row.item_code].serial_no_valuation[entry.serial_no] = entry.incoming_rate - if entry.batch_no: - serial_batch_map[row.item_code].batches[entry.batch_no] += entry.qty - serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no] = entry.incoming_rate - - dn1 = make_sales_return(dn.name) - dn1.items[0].qty = -2 - dn1.submit() - dn1.reload() - - for row in dn1.packed_items: - doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle) - for entry in doc.entries: - if entry.serial_no: - self.assertTrue(entry.serial_no in serial_batch_map[row.item_code].serial_nos) - self.assertEqual( - entry.incoming_rate, - serial_batch_map[row.item_code].serial_no_valuation[entry.serial_no], - ) - serial_batch_map[row.item_code].serial_nos.remove(entry.serial_no) - serial_batch_map[row.item_code].serial_no_valuation.pop(entry.serial_no) - - elif entry.batch_no: - serial_batch_map[row.item_code].batches[entry.batch_no] += entry.qty - self.assertTrue(entry.batch_no in serial_batch_map[row.item_code].batches) - self.assertEqual(entry.qty, 2.0) - self.assertEqual( - entry.incoming_rate, - serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no], - ) - - dn2 = make_sales_return(dn.name) - dn2.items[0].qty = -3 - dn2.submit() - dn2.reload() - - for row in dn2.packed_items: - doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle) - for entry in doc.entries: - if entry.serial_no: - self.assertTrue(entry.serial_no in serial_batch_map[row.item_code].serial_nos) - self.assertEqual( - entry.incoming_rate, - serial_batch_map[row.item_code].serial_no_valuation[entry.serial_no], - ) - serial_batch_map[row.item_code].serial_nos.remove(entry.serial_no) - serial_batch_map[row.item_code].serial_no_valuation.pop(entry.serial_no) - - elif entry.batch_no: - serial_batch_map[row.item_code].batches[entry.batch_no] += entry.qty - self.assertEqual(serial_batch_map[row.item_code].batches[entry.batch_no], 0.0) - - self.assertTrue(entry.batch_no in serial_batch_map[row.item_code].batches) - - self.assertEqual(entry.qty, 3.0) - self.assertEqual( - entry.incoming_rate, - serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no], - ) - - @change_settings("Stock Settings", {"allow_negative_stock": 0, "enable_stock_reservation": 1}) - def test_partial_delivery_note_against_reserved_stock(self): - from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( - get_stock_reservation_entries_for_voucher, - ) - - # create batch item - batch_item = make_item( - "_Test Batch Item For DN Reserve Check", - { - "is_stock_item": 1, - "has_batch_no": 1, - "create_new_batch": 1, - "batch_number_series": "TBDNR.#####", - }, - ) - serial_item = make_item( - "_Test Serial Item For DN Reserve Check", - { - "is_stock_item": 1, - "has_serial_no": 1, - "serial_no_series": "TSNDNR.#####", - }, - ) - - company = "_Test Company" - - warehouse = create_warehouse("Test Partial DN Reserved Stock", company=company) - customer = "_Test Customer" - - items = [batch_item.name, serial_item.name] - - for idx, item in enumerate(items): - # make inward entry for batch item - se = make_stock_entry(item_code=item, purpose="Material Receipt", qty=10, to_warehouse=warehouse) - sabb = se.items[0].serial_and_batch_bundle - - batch_no = get_batch_from_bundle(sabb) if not idx else None - serial_nos = get_serial_nos_from_bundle(sabb) if idx else None - - # make sales order and reserve the quantites against the so - so = make_sales_order(item_code=item, qty=10, rate=100, customer=customer, warehouse=warehouse) - so.submit() - so.create_stock_reservation_entries() - so.reload() - - # create a delivery note with partial quantity from resreved quantity - dn = create_dn_against_so(so=so.name, delivered_qty=5, do_not_submit=True) - dn.items[0].use_serial_batch_fields = 1 - if batch_no: - dn.items[0].batch_no = batch_no - else: - dn.items[0].serial_no = "\n".join(serial_nos[:5]) - - dn.save() - dn.submit() - - against_sales_order = dn.items[0].against_sales_order - so_detail = dn.items[0].so_detail - - sre_details = get_stock_reservation_entries_for_voucher( - so.doctype, against_sales_order, so_detail, ["reserved_qty", "delivered_qty", "status"] - ) - - # check partially delivered reserved stock - self.assertEqual(sre_details[0].status, "Partially Delivered") - self.assertEqual(sre_details[0].reserved_qty, so.items[0].qty) - self.assertEqual(sre_details[0].delivered_qty, dn.items[0].qty) - def test_negative_stock_with_higher_precision(self): original_flt_precision = frappe.db.get_default("float_precision") frappe.db.set_single_value("System Settings", "float_precision", 7) @@ -2137,7 +1524,6 @@ class TestDeliveryNote(FrappeTestCase): frappe.db.set_single_value("System Settings", "float_precision", original_flt_precision) ->>>>>>> 1bbeecff12 (fix: negative stock issue for higher precision) def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note")