From affe09ee0bcc9ddab5f98638dadbfacaf08a1686 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 4 Sep 2025 00:48:45 +0530 Subject: [PATCH] fix: non batch-wise valuation for batch item (cherry picked from commit 11b82ba00822a883193f4a8d119da572ef9b19f5) --- erpnext/stock/deprecated_serial_batch.py | 5 +++ .../serial_and_batch_bundle.py | 33 ++++++++++++++++++- erpnext/stock/serial_batch_bundle.py | 14 ++++---- erpnext/stock/stock_ledger.py | 9 +++++ erpnext/stock/utils.py | 1 + 5 files changed, 53 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 825b1fe8d7e..19e77b3e068 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -1,4 +1,5 @@ import datetime +import json from collections import defaultdict import frappe @@ -197,6 +198,9 @@ class DeprecatedBatchNoValuation: @deprecated def set_balance_value_for_non_batchwise_valuation_batches(self): self.last_sle = self.get_last_sle_for_non_batch() + if self.last_sle and self.last_sle.stock_value: + self.stock_queue = json.loads(self.last_sle.stock_queue or "[]") or [] + self.set_balance_value_from_sl_entries() self.set_balance_value_from_bundle() @@ -271,6 +275,7 @@ class DeprecatedBatchNoValuation: .select( sle.stock_value, sle.qty_after_transaction, + sle.stock_queue, ) .where( (sle.item_code == self.sle.item_code) 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 a453457f919..e7d7446cea6 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 @@ -3,6 +3,7 @@ import collections import csv +import json from collections import Counter, defaultdict import frappe @@ -29,6 +30,7 @@ from erpnext.stock.serial_batch_bundle import ( get_batches_from_bundle, ) from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle +from erpnext.stock.valuation import FIFOValuation class SerialNoExistsInFutureTransactionError(frappe.ValidationError): @@ -463,6 +465,8 @@ class SerialandBatchBundle(Document): ) def set_incoming_rate_for_outward_transaction(self, row=None, save=False, allow_negative_stock=False): + from erpnext.stock.utils import get_valuation_method + sle = self.get_sle_for_outward_transaction() if self.has_serial_no: @@ -479,13 +483,40 @@ class SerialandBatchBundle(Document): warehouse=self.warehouse, ) + stock_queue = [] + if hasattr(sn_obj, "stock_queue") and sn_obj.stock_queue: + stock_queue = parse_json(sn_obj.stock_queue) + + val_method = get_valuation_method(self.item_code) + for d in self.entries: available_qty = 0 if self.has_serial_no: d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0)) else: - d.incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(d.batch_no))) + actual_qty = d.qty + if ( + stock_queue + and val_method == "FIFO" + and d.batch_no in sn_obj.non_batchwise_valuation_batches + ): + if actual_qty < 0: + stock_queue = FIFOValuation(stock_queue) + _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value() + + stock_queue.remove_stock(qty=abs(actual_qty)) + _qty, stock_value = stock_queue.get_total_stock_and_value() + + stock_value_difference = stock_value - prev_stock_value + d.incoming_rate = abs(flt(stock_value_difference) / abs(flt(actual_qty))) + stock_queue = stock_queue.state + else: + d.incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(d.batch_no))) + stock_queue.append([d.qty, d.incoming_rate]) + d.stock_queue = json.dumps(stock_queue) + else: + d.incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(d.batch_no))) available_qty = flt(sn_obj.available_qty.get(d.batch_no), d.precision("qty")) if self.docstatus == 1: diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 2a1fe92ae28..0cf0a940cf3 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -674,6 +674,7 @@ class BatchNoValuation(DeprecatedBatchNoValuation): for key, value in kwargs.items(): setattr(self, key, value) + self.stock_queue = [] self.batch_nos = self.get_batch_nos() self.prepare_batches() self.calculate_avg_rate() @@ -770,15 +771,12 @@ class BatchNoValuation(DeprecatedBatchNoValuation): self.non_batchwise_valuation_batches = self.batches return - if get_valuation_method(self.sle.item_code) == "FIFO": - self.batchwise_valuation_batches = self.batches - else: - batches = frappe.get_all( - "Batch", filters={"name": ("in", self.batches), "use_batchwise_valuation": 1}, fields=["name"] - ) + batches = frappe.get_all( + "Batch", filters={"name": ("in", self.batches), "use_batchwise_valuation": 1}, fields=["name"] + ) - for batch in batches: - self.batchwise_valuation_batches.append(batch.name) + for batch in batches: + self.batchwise_valuation_batches.append(batch.name) self.non_batchwise_valuation_batches = list(set(self.batches) - set(self.batchwise_valuation_batches)) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 950206b665b..a4d8695784d 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1042,6 +1042,15 @@ class update_entries_after: doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock) doc.calculate_qty_and_amount(save=True) + if stock_queue := frappe.get_all( + "Serial and Batch Entry", + filters={"parent": sle.serial_and_batch_bundle, "stock_queue": ("is", "set")}, + pluck="stock_queue", + order_by="idx desc", + limit=1, + ): + self.wh_data.stock_queue = json.loads(stock_queue[0]) if stock_queue else [] + self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount) self.wh_data.qty_after_transaction += flt(doc.total_qty, self.flt_precision) if flt(self.wh_data.qty_after_transaction, self.flt_precision): diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 781fc81445c..58ecb24db48 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -373,6 +373,7 @@ def get_avg_purchase_rate(serial_nos): ) +@frappe.request_cache def get_valuation_method(item_code): """get valuation method from item or default""" val_method = frappe.db.get_value("Item", item_code, "valuation_method", cache=True)