From 6bb7fa6d68a45e4e55a860ed4c357c3692a78cd9 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 17 Jun 2026 15:55:22 +0530 Subject: [PATCH] fix: preserve stock ageing on non-serial reconciliation --- .../stock/report/stock_ageing/stock_ageing.py | 3 +- .../report/stock_ageing/test_stock_ageing.py | 61 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 9d1acf9b243..a7b8a8a622a 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -371,7 +371,7 @@ class FIFOSlots: if row.voucher_type != "Stock Reconciliation": return - if not row.batch_no or row.serial_no or row.serial_and_batch_bundle: + if row.has_serial_no and (not row.batch_no or row.serial_no or row.serial_and_batch_bundle): if row.voucher_detail_no in self.stock_reco_voucher_wise_count: # Legacy reconciliation with a single SLE has qty_after_transaction and # stock_value_difference without an outward entry, so reset the queue first. @@ -1080,6 +1080,7 @@ class FIFOSlots: (doctype.voucher_type == "Stock Reconciliation") & (doctype.docstatus < 2) & (doctype.is_cancelled == 0) + & (item.has_serial_no == 1) ) .groupby(doctype.voucher_detail_no) ) diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 96cc5a58866..37b4b081cfe 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -191,6 +191,67 @@ class TestStockAgeing(ERPNextTestSuite): self.assertEqual(queue[0][0], 20.0) self.assertEqual(queue[1][0], 20.0) + def test_non_serial_stock_reco_decrease_preserves_ageing(self): + """ + Non-serial stock reconciliation should adjust FIFO by the balance delta. + Decreasing stock consumes old slots; increasing stock adds only the new qty. + """ + + def make_sle( + posting_date, + voucher_type, + voucher_no, + actual_qty, + qty_after, + voucher_detail_no=None, + stock_value_difference=None, + ): + stock_value_difference = actual_qty if stock_value_difference is None else stock_value_difference + + return frappe._dict( + name="Flask Item", + item_name="Flask Item", + description="Flask Item", + item_group=None, + brand=None, + stock_uom="Nos", + actual_qty=actual_qty, + qty_after_transaction=qty_after, + stock_value_difference=stock_value_difference, + valuation_rate=1, + warehouse="WH 1", + posting_date=posting_date, + voucher_type=voucher_type, + voucher_no=voucher_no, + voucher_detail_no=voucher_detail_no, + has_serial_no=False, + has_batch_no=False, + serial_no=None, + batch_no=None, + serial_and_batch_bundle=None, + ) + + filters = frappe._dict(company="_Test Company", to_date="2026-02-15", ranges=["30", "60", "90"]) + sle = [ + make_sle("2025-11-30", "Stock Entry", "001", 100, 100), + make_sle("2025-12-31", "Stock Reconciliation", "002", 0, 60, "SRI-DECREASE", -40), + make_sle("2026-01-31", "Stock Reconciliation", "003", 0, 90, "SRI-INCREASE", 30), + ] + + fifo_slots = FIFOSlots(filters, sle) + + def prepare_stock_reco_voucher_wise_count(): + fifo_slots.stock_reco_voucher_wise_count = frappe._dict({"SRI-DECREASE": 100, "SRI-INCREASE": 60}) + + fifo_slots.prepare_stock_reco_voucher_wise_count = prepare_stock_reco_voucher_wise_count + + slots = fifo_slots.generate() + queue = slots["Flask Item"]["fifo_queue"] + report_data = format_report_data(filters, slots, filters.to_date) + + self.assertEqual(queue, [[60.0, "2025-11-30", 60.0], [30.0, "2026-01-31", 30.0]]) + self.assertEqual(report_data[0][7:15], [30.0, 30.0, 0.0, 0.0, 60.0, 60.0, 0.0, 0.0]) + def test_sequential_stock_reco_same_warehouse(self): """ Test back to back stock recos (same warehouse).