From 37a03f10ab9d054a1e8df22c28516cef303f5124 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 30 Sep 2025 15:17:10 +0530 Subject: [PATCH] fix: valuation rate for old batch (cherry picked from commit d864d166f9651e8b87c57a360ad58b36701bf6d5) --- .../serial_and_batch_bundle.py | 49 +++++++++++++++---- .../test_serial_and_batch_bundle.py | 39 ++++++++++++++- erpnext/stock/stock_ledger.py | 4 +- 3 files changed, 81 insertions(+), 11 deletions(-) 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 aff309e5f80..9c69d856d45 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 @@ -311,7 +311,7 @@ class SerialandBatchBundle(Document): def throw_error_message(self, message, exception=frappe.ValidationError): frappe.throw(_(message), exception, title=_("Error")) - def set_incoming_rate(self, parent=None, row=None, save=False, allow_negative_stock=False): + def set_incoming_rate(self, parent=None, row=None, save=False, allow_negative_stock=False, prev_sle=None): if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [ "Installation Note", "Job Card", @@ -321,15 +321,15 @@ class SerialandBatchBundle(Document): return if return_against := self.get_return_against(parent=parent): - self.set_valuation_rate_for_return_entry(return_against, row, save) + self.set_valuation_rate_for_return_entry(return_against, row, save, prev_sle=prev_sle) elif self.type_of_transaction == "Outward": self.set_incoming_rate_for_outward_transaction( row, save, allow_negative_stock=allow_negative_stock ) else: - self.set_incoming_rate_for_inward_transaction(row, save) + self.set_incoming_rate_for_inward_transaction(row, save, prev_sle=prev_sle) - def set_valuation_rate_for_return_entry(self, return_against, row, save=False): + def set_valuation_rate_for_return_entry(self, return_against, row, save=False, prev_sle=None): if valuation_details := self.get_valuation_rate_for_return_entry(return_against): for row in self.entries: if valuation_details: @@ -361,7 +361,7 @@ class SerialandBatchBundle(Document): ) elif self.type_of_transaction == "Inward": - self.set_incoming_rate_for_inward_transaction(row, save) + self.set_incoming_rate_for_inward_transaction(row, save, prev_sle=prev_sle) def validate_returned_serial_batch_no(self, return_against, row, original_inv_details): if frappe.flags.through_repost_item_valuation: @@ -529,7 +529,11 @@ class SerialandBatchBundle(Document): if save: d.db_set( - {"incoming_rate": d.incoming_rate, "stock_value_difference": d.stock_value_difference} + { + "incoming_rate": d.incoming_rate, + "stock_value_difference": d.stock_value_difference, + "stock_queue": d.get("stock_queue"), + } ) def validate_negative_batch(self, batch_no, available_qty): @@ -606,7 +610,11 @@ class SerialandBatchBundle(Document): return return_against - def set_incoming_rate_for_inward_transaction(self, row=None, save=False): + def set_incoming_rate_for_inward_transaction(self, row=None, save=False, prev_sle=None): + from erpnext.stock.utils import get_valuation_method + + valuation_method = get_valuation_method(self.item_code) + valuation_field = "valuation_rate" if self.voucher_type in ["Sales Invoice", "Delivery Note", "Quotation"]: valuation_field = "incoming_rate" @@ -630,19 +638,42 @@ class SerialandBatchBundle(Document): if not rate and self.voucher_detail_no and self.voucher_no: rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field) + stock_queue = [] + batches = [] + if prev_sle and prev_sle.stock_queue: + batches = frappe.get_all( + "Batch", + filters={ + "name": ("in", [d.batch_no for d in self.entries if d.batch_no]), + "use_batchwise_valuation": 0, + }, + pluck="name", + ) + + if batches and valuation_method == "FIFO": + stock_queue = parse_json(prev_sle.stock_queue) + for d in self.entries: if self.is_rejected: rate = 0.0 - elif (d.incoming_rate == rate) and d.qty and d.stock_value_difference: + elif (d.incoming_rate == rate) and not stock_queue and d.qty and d.stock_value_difference: continue d.incoming_rate = flt(rate) if d.qty: d.stock_value_difference = flt(d.qty) * d.incoming_rate + if stock_queue and valuation_method == "FIFO" and d.batch_no in batches: + stock_queue.append([d.qty, d.incoming_rate]) + d.stock_queue = json.dumps(stock_queue) + if save: d.db_set( - {"incoming_rate": d.incoming_rate, "stock_value_difference": d.stock_value_difference} + { + "incoming_rate": d.incoming_rate, + "stock_value_difference": d.stock_value_difference, + "stock_queue": d.get("stock_queue"), + } ) def set_serial_and_batch_values(self, parent, row, qty_field=None): 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 b81fbc8bc55..eec91b2c282 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 @@ -285,6 +285,25 @@ class TestSerialandBatchBundle(FrappeTestCase): self.assertEqual(flt(sle.stock_value_difference), 1000.00 * -1) self.assertEqual(json.loads(sle.stock_queue), [[20, 200]]) + se = make_stock_entry( + item_code=batch_item_code, + target="_Test Warehouse - _TC", + qty=10, + rate=100, + batch_no=batch_id, + use_serial_batch_fields=True, + ) + + sle = frappe.db.get_value( + "Serial and Batch Entry", + {"parent": se.items[0].serial_and_batch_bundle, "docstatus": 1}, + ["stock_value_difference", "stock_queue"], + as_dict=True, + ) + + self.assertEqual(flt(sle.stock_value_difference), 1000.00) + self.assertEqual(json.loads(sle.stock_queue), [[20, 200], [10, 100]]) + se = make_stock_entry( item_code=batch_item_code, target="_Test Warehouse - _TC", @@ -301,7 +320,7 @@ class TestSerialandBatchBundle(FrappeTestCase): ) self.assertEqual(flt(sle.stock_value_difference), 1000.00) - self.assertEqual(json.loads(sle.stock_queue), [[20, 200]]) + self.assertEqual(json.loads(sle.stock_queue), [[20, 200], [10, 100]]) se = make_stock_entry( item_code=batch_item_code, @@ -319,6 +338,24 @@ class TestSerialandBatchBundle(FrappeTestCase): self.assertEqual(flt(sle.stock_value_difference), 5000.00 * -1) self.assertFalse(json.loads(sle.stock_queue or "[]")) + self.assertEqual(flt(sle.stock_value), 1000.0) + + se = make_stock_entry( + item_code=batch_item_code, + source="_Test Warehouse - _TC", + qty=10, + use_serial_batch_fields=False, + ) + + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": se.name}, + ["stock_value_difference", "stock_queue", "stock_value"], + as_dict=True, + ) + + self.assertEqual(flt(sle.stock_value_difference), 1000.00 * -1) + self.assertFalse(json.loads(sle.stock_queue or "[]")) self.assertEqual(flt(sle.stock_value), 0.0) def test_old_serial_no_valuation(self): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index e4674f7d0f5..40e576987f8 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1019,7 +1019,9 @@ class update_entries_after: ) else: doc = frappe.get_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle) - doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock) + doc.set_incoming_rate( + save=True, allow_negative_stock=self.allow_negative_stock, prev_sle=self.wh_data + ) doc.calculate_qty_and_amount(save=True) if stock_queue := frappe.get_all(