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 faf926e7a14..3de5c7417a7 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 @@ -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: diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index 47caf5fca8f..e0b8cb750ac 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -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