Merge pull request #49453 from rohitwaghchaure/fixed-fifo-valuation-for-non-batchwise-valuation

fix: non batch-wise valuation for batch item
This commit is contained in:
rohitwaghchaure
2025-09-04 14:30:17 +05:30
committed by GitHub
6 changed files with 110 additions and 57 deletions

View File

@@ -1,4 +1,5 @@
import datetime
import json
from collections import defaultdict
import frappe
@@ -226,6 +227,9 @@ class DeprecatedBatchNoValuation:
)
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_queue:
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()
@@ -305,6 +309,7 @@ class DeprecatedBatchNoValuation:
.select(
sle.stock_value,
sle.qty_after_transaction,
sle.stock_queue,
)
.where(
(sle.item_code == self.sle.item_code)

View File

@@ -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):
@@ -467,6 +469,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:
@@ -483,13 +487,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:

View File

@@ -203,7 +203,10 @@ class TestSerialandBatchBundle(IntegrationTestCase):
batch_item_code,
{
"has_batch_no": 1,
"batch_number_series": "TEST-OLD-BAT-VAL-.#####",
"create_new_batch": 1,
"is_stock_item": 1,
"valuation_method": "FIFO",
},
)
@@ -256,57 +259,63 @@ class TestSerialandBatchBundle(IntegrationTestCase):
doc.submit()
doc.reload()
bundle_doc = make_serial_batch_bundle(
{
"item_code": batch_item_code,
"warehouse": "_Test Warehouse - _TC",
"voucher_type": "Stock Entry",
"posting_date": today(),
"posting_time": nowtime(),
"qty": -10,
"batches": frappe._dict({batch_id: 10}),
"type_of_transaction": "Outward",
"do_not_submit": True,
}
)
bundle_doc.reload()
for row in bundle_doc.entries:
self.assertEqual(flt(row.stock_value_difference, 2), -1666.67)
bundle_doc.flags.ignore_permissions = True
bundle_doc.flags.ignore_mandatory = True
bundle_doc.flags.ignore_links = True
bundle_doc.flags.ignore_validate = True
bundle_doc.submit()
bundle_doc = make_serial_batch_bundle(
{
"item_code": batch_item_code,
"warehouse": "_Test Warehouse - _TC",
"voucher_type": "Stock Entry",
"posting_date": today(),
"posting_time": nowtime(),
"qty": -20,
"batches": frappe._dict({batch_id: 20}),
"type_of_transaction": "Outward",
"do_not_submit": True,
}
)
bundle_doc.reload()
for row in bundle_doc.entries:
self.assertEqual(flt(row.stock_value_difference, 2), -3333.33)
bundle_doc.flags.ignore_permissions = True
bundle_doc.flags.ignore_mandatory = True
bundle_doc.flags.ignore_links = True
bundle_doc.flags.ignore_validate = True
bundle_doc.submit()
frappe.flags.ignore_serial_batch_bundle_validation = False
frappe.flags.use_serial_and_batch_fields = False
se = make_stock_entry(
item_code=batch_item_code,
source="_Test Warehouse - _TC",
qty=10,
use_serial_batch_fields=True,
batch_no=batch_id,
)
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"],
as_dict=True,
)
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,
use_serial_batch_fields=True,
)
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"],
as_dict=True,
)
self.assertEqual(flt(sle.stock_value_difference), 1000.00)
self.assertEqual(json.loads(sle.stock_queue), [[20, 200]])
se = make_stock_entry(
item_code=batch_item_code,
source="_Test Warehouse - _TC",
qty=30,
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), 5000.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):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt

View File

@@ -708,6 +708,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()
@@ -804,15 +805,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))

View File

@@ -1046,6 +1046,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):

View File

@@ -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.get_cached_value("Item", item_code, "valuation_method")