fix: purchase return of batchwise valuation batch valued at original receipt rate instead of batch avg rate (version-15-hotfix) (#57837)

* fix: use current batch avg rate for outward returns of batchwise valuation batches

* fix: honor zero batch average and avoid duplicate batch classification query
This commit is contained in:
rohitwaghchaure
2026-08-06 15:20:34 +05:30
committed by GitHub
parent 1bfe2e25ef
commit e2ded11e09
3 changed files with 113 additions and 0 deletions

View File

@@ -4996,6 +4996,66 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(frappe.parse_json(stock_queue), [[20, 0.0]])
def test_purchase_return_valuation_for_batchwise_valuation_batch(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
item_code = make_item(
"Test Purchase Return Batchwise Valn Item",
{
"is_stock_item": 1,
"has_batch_no": 1,
"batch_number_series": "BN-TPRBWV-.#####",
},
).name
batch_no = "BN-TPRBWV-00001"
batch = frappe.new_doc("Batch").update({"batch_id": batch_no, "item": item_code}).insert()
self.assertEqual(batch.use_batchwise_valuation, 1)
warehouse = "_Test Warehouse - _TC"
pr = make_purchase_receipt(
item_code=item_code,
qty=100,
rate=1000,
warehouse=warehouse,
batch_no=batch_no,
use_serial_batch_fields=1,
)
make_purchase_receipt(
item_code=item_code,
qty=100,
rate=400,
warehouse=warehouse,
batch_no=batch_no,
use_serial_batch_fields=1,
)
create_delivery_note(
item_code=item_code,
qty=100,
warehouse=warehouse,
batch_no=batch_no,
use_serial_batch_fields=1,
)
return_pr = make_return_doc("Purchase Receipt", pr.name)
return_pr.submit()
sle = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_no": return_pr.name, "is_cancelled": 0},
["stock_value_difference", "qty_after_transaction", "stock_value", "serial_and_batch_bundle"],
as_dict=True,
)
self.assertEqual(flt(sle.qty_after_transaction), 0.0)
self.assertEqual(flt(sle.stock_value_difference, 2), -70000.0)
self.assertEqual(flt(sle.stock_value, 2), 0.0)
rate = frappe.db.get_value(
"Serial and Batch Entry", {"parent": sle.serial_and_batch_bundle}, "incoming_rate"
)
self.assertEqual(flt(rate, 2), 700.0)
def test_negative_stock_error_for_purchase_return(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry

View File

@@ -402,6 +402,13 @@ class SerialandBatchBundle(Document):
valuation_method = get_valuation_method(self.item_code)
# An outward return must go out at the batch's current average rate for a
# batchwise valuation batch. The original receipt rate is only correct while
# the batch still holds stock at that rate; once other receipts have changed
# the average, removing at the original rate strands a residue in the batch
# value (negative when returning the costlier receipt).
batchwise_avg_rates = self.get_batchwise_return_avg_rates()
stock_queue = []
non_batchwise_batches = []
if not self.has_serial_no and valuation_method == "FIFO":
@@ -435,6 +442,12 @@ class SerialandBatchBundle(Document):
batches = sorted(list(valuation_details["batches"].keys()))
valuation_rate = valuation_details["batches"].get(batches[cint(row.idx) - 1])
# a batch with an available balance goes out at its current average rate (a
# valid 0.0 included); the original receipt rate applies only when there is
# no balance to average
if not row.serial_no and row.batch_no in batchwise_avg_rates:
valuation_rate = batchwise_avg_rates[row.batch_no]
row.incoming_rate = flt(valuation_rate)
row.stock_value_difference = flt(row.qty) * flt(row.incoming_rate)
@@ -463,6 +476,41 @@ class SerialandBatchBundle(Document):
elif self.type_of_transaction == "Inward":
self.set_incoming_rate_for_inward_transaction(row, save, prev_sle=prev_sle)
def get_batchwise_return_avg_rates(self):
from erpnext.stock.utils import get_valuation_method
if self.type_of_transaction != "Outward" or self.has_serial_no:
return {}
batch_nos = [d.batch_no for d in self.entries if d.batch_no]
if not batch_nos:
return {}
if get_valuation_method(self.item_code) == "Moving Average" and frappe.db.get_single_value(
"Stock Settings", "do_not_use_batchwise_valuation"
):
return {}
batchwise_batches = frappe.get_all(
"Batch",
filters={"name": ("in", batch_nos), "use_batchwise_valuation": 1},
pluck="name",
)
if not batchwise_batches:
return {}
# scoped to batchwise batches only, so BatchNoValuation's non-batchwise
# machinery never runs for them
sle = self.get_sle_for_outward_transaction()
sle.batch_nos = {batch_no: sle.batch_nos[batch_no] for batch_no in batchwise_batches}
sle.batchwise_valuation_batches = batchwise_batches
sn_obj = BatchNoValuation(sle=sle, item_code=self.item_code, warehouse=self.warehouse)
return {
batch_no: abs(flt(sn_obj.batch_avg_rate.get(batch_no)))
for batch_no in batchwise_batches
if flt(sn_obj.available_qty.get(batch_no))
}
def validate_returned_serial_batch_no(self, return_against, row, original_inv_details):
if frappe.flags.through_repost_item_valuation:
return

View File

@@ -910,6 +910,11 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
self.batchwise_valuation_batches = []
self.non_batchwise_valuation_batches = []
if batchwise_batches := self.sle.get("batchwise_valuation_batches"):
self.batchwise_valuation_batches = list(batchwise_batches)
self.non_batchwise_valuation_batches = list(set(self.batches) - set(batchwise_batches))
return
if get_valuation_method(self.sle.item_code) == "Moving Average" and frappe.db.get_single_value(
"Stock Settings", "do_not_use_batchwise_valuation"
):