mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-06 19:23:06 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user