From 94d63ebb49e3f2d35d754ffac0079ab390b7bcf9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 07:02:41 +0000 Subject: [PATCH] fix(stock): value batched packed-item returns from the original bundle (backport #57327) (#57510) fix(stock): value batched packed-item returns from the original bundle (#57327) * fix(stock): value batched packed-item returns from the original bundle when a return delivery note or sales invoice bundle is built via the use_serial_batch_fields / sle-driven path, its voucher_detail_no keeps the packed item instead of being remapped to the parent dn/si item. the return valuation lookup then misses and the bundle values at zero, so the sle stock_value_difference stays wrong even after a repost. resolve the original dn/si item via the packed item's parent_detail_docname when the direct lookup fails, so the return values from the original outward bundle on both submit and repost. * test(stock): cover batched packed-item return valuation on repost (cherry picked from commit d37e905322978b2d0e5ff0452c2a00db9f8be12f) Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com> --- .../delivery_note/test_delivery_note.py | 70 +++++++++++++++++++ .../serial_and_batch_bundle.py | 15 ++++ 2 files changed, 85 insertions(+) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index e77940b1661..25c86fee7c9 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -706,6 +706,76 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(gle_warehouse_amount, 1400) + def test_return_bundle_voucher_detail_no_as_packed_item(self): + """Return bundle whose voucher_detail_no is the Packed Item (SLE-driven path) must still value on repost.""" + from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return + + warehouse = "_Test Warehouse - _TC" + packed_item = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BATCH-DN-RET-VDN-.#####", + } + ).name + bundle_item = make_item(properties={"is_stock_item": 0, "is_sales_item": 1}).name + make_product_bundle(bundle_item, [packed_item], qty=20) + + make_stock_entry(item_code=packed_item, target=warehouse, qty=60, basic_rate=35) + + dn = create_delivery_note(item_code=bundle_item, warehouse=warehouse, qty=3) + + return_dn = make_sales_return(dn.name) + return_dn.items[0].qty = -2 + return_dn.submit() + return_dn.reload() + + packed_row = return_dn.packed_items[0] + bundle = frappe.get_doc("Serial and Batch Bundle", packed_row.serial_and_batch_bundle) + + # Reproduce the reported state: bundle points at the Packed Item (not the DN Item), valuation at 0. + bundle.db_set("voucher_detail_no", packed_row.name) + bundle.db_set({"avg_rate": 0, "total_amount": 0}) + for entry in bundle.entries: + entry.db_set({"incoming_rate": 0, "stock_value_difference": 0}) + packed_row.db_set("incoming_rate", 0) + frappe.db.set_value( + "Stock Ledger Entry", + { + "voucher_type": "Delivery Note", + "voucher_no": return_dn.name, + "item_code": packed_item, + "is_cancelled": 0, + }, + {"incoming_rate": 0, "stock_value_difference": 0}, + ) + + frappe.get_doc( + doctype="Repost Item Valuation", + based_on="Transaction", + voucher_type="Delivery Note", + voucher_no=return_dn.name, + posting_date=return_dn.posting_date, + posting_time=return_dn.posting_time, + ).submit() + + bundle.reload() + self.assertEqual(flt(bundle.avg_rate), 35) + + incoming_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Delivery Note", + "voucher_no": return_dn.name, + "item_code": packed_item, + "is_cancelled": 0, + }, + ["incoming_rate", "stock_value_difference"], + ) + self.assertEqual(flt(incoming_rate), 35) + self.assertEqual(flt(stock_value_difference), 1400) + def test_bin_details_of_packed_item(self): from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.stock.doctype.item.test_item import make_item diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index fd958d55c61..ca18baac969 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -505,6 +505,11 @@ class SerialandBatchBundle(Document): self.child_table, self.voucher_detail_no, field ) + if not return_against_voucher_detail_no and self.voucher_type in ("Delivery Note", "Sales Invoice"): + # Bundles built via the use_serial_batch_fields / SLE-driven path keep the Packed Item + # as voucher_detail_no (not remapped to the DN/SI Item), so the lookup above misses. + return_against_voucher_detail_no = self.get_return_against_packed_item(field) + filters = [ ["Serial and Batch Bundle", "voucher_no", "=", return_against], ["Serial and Batch Entry", "docstatus", "=", 1], @@ -548,6 +553,16 @@ class SerialandBatchBundle(Document): return valuation_details + def get_return_against_packed_item(self, field): + """Resolve the original DN/SI Item when a return bundle's voucher_detail_no is the Packed Item.""" + parent_detail_docname = frappe.db.get_value( + "Packed Item", self.voucher_detail_no, "parent_detail_docname" + ) + if not parent_detail_docname: + return + + return frappe.db.get_value(self.child_table, parent_detail_docname, field) + def get_legacy_valuation_rate_for_return_entry( self, return_against, return_against_voucher_detail_no, return_warehouse=None ):