mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-12 14:11:46 +00:00
fix(stock): value batch/serial return from ledger when original receipt has no bundle (backport #56631) (#56646)
fix(stock): value batch/serial return from ledger when original receipt has no bundle (#56631)
* fix(stock): value batch/serial return from ledger when original receipt has no bundle
* test(stock): add test to validate the valuation of serial/batch for return when original receipt has no bundle
(cherry picked from commit 6184c057db)
# Conflicts:
# erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
This commit is contained in:
@@ -525,10 +525,12 @@ class SerialandBatchBundle(Document):
|
||||
]
|
||||
|
||||
# Added to handle rejected warehouse case
|
||||
return_warehouse = None
|
||||
if self.voucher_type in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
warehouses = get_warehouses_for_return(self.voucher_type, return_against_voucher_detail_no)
|
||||
if self.warehouse in warehouses:
|
||||
filters.append(["Serial and Batch Entry", "warehouse", "=", self.warehouse])
|
||||
return_warehouse = self.warehouse
|
||||
filters.append(["Serial and Batch Entry", "warehouse", "=", return_warehouse])
|
||||
|
||||
bundle_data = frappe.get_all(
|
||||
"Serial and Batch Bundle",
|
||||
@@ -541,6 +543,11 @@ class SerialandBatchBundle(Document):
|
||||
order_by="`tabSerial and Batch Bundle`.`creation`, `tabSerial and Batch Entry`.`idx`",
|
||||
)
|
||||
|
||||
if not bundle_data:
|
||||
bundle_data = self.get_legacy_valuation_rate_for_return_entry(
|
||||
return_against, return_against_voucher_detail_no, return_warehouse
|
||||
)
|
||||
|
||||
if not bundle_data:
|
||||
return {}
|
||||
|
||||
@@ -552,6 +559,49 @@ class SerialandBatchBundle(Document):
|
||||
|
||||
return valuation_details
|
||||
|
||||
def get_legacy_valuation_rate_for_return_entry(
|
||||
self, return_against, return_against_voucher_detail_no, return_warehouse=None
|
||||
):
|
||||
"""Return the original line's incoming rate per serial no / batch from the SLE, for legacy receipts with no bundle."""
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
if not (self.has_serial_no or self.has_batch_no):
|
||||
return []
|
||||
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
query = (
|
||||
frappe.qb.from_(sle)
|
||||
.select(sle.serial_no, sle.batch_no, sle.incoming_rate)
|
||||
.where(
|
||||
(sle.voucher_no == return_against)
|
||||
& (sle.voucher_detail_no == return_against_voucher_detail_no)
|
||||
& (sle.item_code == self.item_code)
|
||||
& (sle.is_cancelled == 0)
|
||||
& (sle.serial_and_batch_bundle.isnull())
|
||||
)
|
||||
)
|
||||
|
||||
if return_warehouse:
|
||||
query = query.where(sle.warehouse == return_warehouse)
|
||||
|
||||
data = []
|
||||
for d in query.run(as_dict=True):
|
||||
if d.serial_no:
|
||||
for serial_no in get_serial_nos(d.serial_no):
|
||||
data.append(
|
||||
frappe._dict(
|
||||
{"serial_no": serial_no, "batch_no": d.batch_no, "incoming_rate": d.incoming_rate}
|
||||
)
|
||||
)
|
||||
elif d.batch_no:
|
||||
data.append(
|
||||
frappe._dict(
|
||||
{"serial_no": None, "batch_no": d.batch_no, "incoming_rate": d.incoming_rate}
|
||||
)
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
def calculate_total_qty(self, save=True):
|
||||
self.total_qty = 0.0
|
||||
for d in self.entries:
|
||||
|
||||
@@ -1330,6 +1330,91 @@ class TestSerialandBatchBundle(ERPNextTestSuite):
|
||||
# Stock queue should have the returned stock: [[5, 100]]
|
||||
self.assertEqual(json.loads(return_sle.stock_queue), [[5, 100]])
|
||||
|
||||
def _assert_legacy_return_valuation(self, item_code, props, batch_no=None):
|
||||
"""Return against a legacy serial/batch receipt (no Serial and Batch Bundle) must value outgoing stock from the original ledger rate."""
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
make_item(item_code, props)
|
||||
if batch_no and not frappe.db.exists("Batch", batch_no):
|
||||
frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert()
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
item_code=item_code, qty=10, rate=100, batch_no=batch_no, use_serial_batch_fields=True
|
||||
)
|
||||
|
||||
# Simulate a receipt migrated from an older version: serial nos / batch tracked via the
|
||||
# deprecated fields on the Stock Ledger Entry, with no Serial and Batch Bundle.
|
||||
serial_nos = []
|
||||
for row in pr.items:
|
||||
if row.serial_and_batch_bundle:
|
||||
serial_nos = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": row.serial_and_batch_bundle},
|
||||
pluck="serial_no",
|
||||
)
|
||||
frappe.db.delete("Serial and Batch Bundle", {"name": row.serial_and_batch_bundle})
|
||||
frappe.db.set_value("Purchase Receipt Item", row.name, "serial_and_batch_bundle", None)
|
||||
|
||||
serial_nos = [sn for sn in serial_nos if sn]
|
||||
legacy = {"serial_and_batch_bundle": None}
|
||||
if batch_no:
|
||||
legacy["batch_no"] = batch_no
|
||||
if serial_nos:
|
||||
legacy["serial_no"] = "\n".join(serial_nos)
|
||||
for sle in frappe.get_all("Stock Ledger Entry", filters={"voucher_no": pr.name}, pluck="name"):
|
||||
frappe.db.set_value("Stock Ledger Entry", sle, legacy)
|
||||
|
||||
rt = make_return_doc("Purchase Receipt", pr.name)
|
||||
rt.items[0].qty = -4
|
||||
rt.items[0].received_qty = -4
|
||||
rt.items[0].use_serial_batch_fields = 1
|
||||
if batch_no:
|
||||
rt.items[0].batch_no = batch_no
|
||||
if serial_nos:
|
||||
rt.items[0].serial_no = "\n".join(serial_nos[:4])
|
||||
rt.submit()
|
||||
|
||||
difference_in_stock_value = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_no": rt.name, "is_cancelled": 0, "voucher_type": "Purchase Receipt"},
|
||||
"stock_value_difference",
|
||||
)
|
||||
# 4 units returned at the original ledger rate of 100 -> -400 (must not be zero)
|
||||
self.assertEqual(flt(difference_in_stock_value, 2), -400.0)
|
||||
|
||||
def test_return_valuation_for_legacy_batch_without_bundle(self):
|
||||
self._assert_legacy_return_valuation(
|
||||
"Test Legacy Batch Return Valuation",
|
||||
{
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "LBRV-.#####",
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
batch_no="LBRV-BATCH-0001",
|
||||
)
|
||||
|
||||
def test_return_valuation_for_legacy_serial_without_bundle(self):
|
||||
self._assert_legacy_return_valuation(
|
||||
"Test Legacy Serial Return Valuation",
|
||||
{"has_serial_no": 1, "serial_no_series": "LSRV-.#####", "is_stock_item": 1},
|
||||
)
|
||||
|
||||
def test_return_valuation_for_legacy_serial_and_batch_without_bundle(self):
|
||||
self._assert_legacy_return_valuation(
|
||||
"Test Legacy Serial Batch Return Valuation",
|
||||
{
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "LSBRV-.#####",
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "LSBRVB-.#####",
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
batch_no="LSBRV-BATCH-0001",
|
||||
)
|
||||
|
||||
|
||||
def get_batch_from_bundle(bundle):
|
||||
from erpnext.stock.serial_batch_bundle import get_batch_nos
|
||||
|
||||
Reference in New Issue
Block a user