From 825d24f406ad581a8a8234ab4cd1e4db87d07726 Mon Sep 17 00:00:00 2001 From: Pandiyan P Date: Fri, 11 Sep 2026 18:20:49 +0530 Subject: [PATCH] fix(stock): calculate batch bundle valuation per unit (#58994) --- .../test_serial_and_batch_bundle.py | 61 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 12 ++-- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index 5d2c919c314..8f397adb736 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -1176,6 +1176,67 @@ class TestSerialandBatchBundle(ERPNextTestSuite): self.assertEqual(bundle_doc.docstatus, 0) self.assertRaises(frappe.ValidationError, bundle_doc.submit) + @ERPNextTestSuite.change_settings("Stock Settings", {"do_not_use_batchwise_valuation": 0}) + def test_amended_material_receipt_rate_after_batch_selection(self): + warehouse = "_Test Warehouse - _TC" + for valuation_method in ("FIFO", "Moving Average"): + with self.subTest(valuation_method=valuation_method): + item = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "stock_uom": "Nos", + "valuation_method": valuation_method, + } + ) + batches = [ + frappe.get_doc( + {"doctype": "Batch", "item": item.name, "batch_id": f"{item.name}-{index}"} + ) + .insert() + .name + for index in range(2) + ] + for index, (batch, qty) in enumerate(((batches[0], 10), (batches[1], 10), (batches[0], 5))): + receipt = make_stock_entry( + item_code=item.name, + company="_Test Company", + to_warehouse=warehouse, + qty=qty, + rate=10, + batch_no=batch, + posting_date=add_days(today(), index - 2), + posting_time="10:00:00", + ) + + receipt.cancel() + amended = frappe.copy_doc(receipt, ignore_no_copy=False) + amended.amended_from = receipt.name + amended.docstatus = 0 + row = amended.items[0] + row.batch_no = None + row.serial_and_batch_bundle = None + row.use_serial_batch_fields = 0 + + # The selector returns an unpriced bundle and copies its rate to the receipt row. + bundle = add_serial_batch_ledgers( + [{"batch_no": batches[1], "qty": 5}], + row.as_dict(), + amended.as_dict(), + warehouse, + ) + row.serial_and_batch_bundle = bundle.name + row.basic_rate = bundle.avg_rate + amended.insert() + self.assertEqual(row.basic_rate, 10) + self.assertEqual(row.basic_amount, 50) + + amended.submit() + ledger = frappe.get_doc("Stock Ledger Entry", {"voucher_no": amended.name, "is_cancelled": 0}) + self.assertEqual(ledger.incoming_rate, 10) + self.assertEqual(ledger.stock_value_difference, 50) + self.assertEqual(ledger.stock_value, 250) + def test_reference_voucher_on_cancel(self): """ When a source document is cancelled, the reference voucher field diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index f5e95256782..e6b5dc11940 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -2195,16 +2195,20 @@ def get_valuation_rate( # Get moving average rate of a specific batch number if warehouse and serial_and_batch_bundle: + bundle = frappe.get_value( + "Serial and Batch Bundle", + serial_and_batch_bundle, + ["total_qty", "posting_datetime"], + as_dict=True, + ) batch_obj = BatchNoValuation( sle=frappe._dict( { "item_code": item_code, "warehouse": warehouse, - "actual_qty": -1, + "actual_qty": -abs(flt(bundle.total_qty)), "serial_and_batch_bundle": serial_and_batch_bundle, - "posting_datetime": frappe.get_value( - "Serial and Batch Bundle", serial_and_batch_bundle, "posting_datetime" - ), + "posting_datetime": bundle.posting_datetime, } ) )