From c9394c030f2f0927016c47fae79fe3de6e8a6456 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 21 Jul 2026 13:44:36 +0530 Subject: [PATCH] fix: rescale stock ageing FIFO slot values on stock reconciliation A reconciliation's stock_value_difference includes the revaluation of stock already in the FIFO queue, but the whole amount was attached to the qty-delta slot while older slots kept pre-revaluation values. A downward revaluation therefore produced negative bucket values in the Stock Ageing report, and repeated recos let the queue total drift away from Stock Balance. Re-derive every slot value as qty * valuation_rate after processing a reco SLE, since a reconciliation values the entire balance at its rate. Covers both single-SLE recos and the zero-out/re-add pair that flows through the transfer bucket. --- .../stock/report/stock_ageing/stock_ageing.py | 9 ++ .../report/stock_ageing/test_stock_ageing.py | 85 +++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 343ec5539fe..8f01dcb136c 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -370,6 +370,7 @@ class FIFOSlots: row, fifo_queue, transferred_item_key, serial_nos, batch_nos, from_end ) + self._revalue_stock_reconciliation_slots(row, fifo_queue) self._update_balances(row, key) self._trim_serial_fifo_queue(row, key, fifo_queue) @@ -393,6 +394,14 @@ class FIFOSlots: # Stock reconciliation stores the final balance; FIFO needs the movement delta. row.actual_qty = flt(row.qty_after_transaction) - flt(prev_balance_qty) + def _revalue_stock_reconciliation_slots(self, row: dict, fifo_queue: list) -> None: + if row.voucher_type != "Stock Reconciliation" or row.has_serial_no or row.has_batch_no: + return + + for slot in fifo_queue: + if is_qty_slot(slot): + slot[FIFO_VALUE_INDEX] = flt(slot[FIFO_QTY_INDEX] * flt(row.valuation_rate)) + def _get_serial_and_batch_nos( self, row: dict, bundle_wise_serial_nos: dict, bundle_wise_batch_nos: dict ) -> tuple[list, list]: diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 180a424b209..6e32bde647d 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -379,6 +379,91 @@ class TestStockAgeing(ERPNextTestSuite): 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_stock_reco_revaluation_rescales_queue_values(self): + "Ledger (same wh): [+15 @ 100, reco reset >> 20 @ 50]" + sle = [ + frappe._dict( + name="Flask Item", + actual_qty=15, + qty_after_transaction=15, + stock_value_difference=1500, + valuation_rate=100, + warehouse="WH 1", + posting_date="2021-12-01", + voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, + serial_no=None, + ), + frappe._dict( + name="Flask Item", + actual_qty=0, + qty_after_transaction=20, + stock_value_difference=(-500), + valuation_rate=50, + warehouse="WH 1", + posting_date="2021-12-02", + voucher_type="Stock Reconciliation", + voucher_no="002", + has_serial_no=False, + serial_no=None, + ), + ] + + slots = FIFOSlots(self.filters, sle).generate() + queue = slots["Flask Item"]["fifo_queue"] + + self.assertEqual(queue, [[15.0, "2021-12-01", 750.0], [5.0, "2021-12-02", 250.0]]) + + def test_stock_reco_with_split_out_and_in_sles_revalues_queue(self): + "Ledger (same wh): [+10 @ 100, reco out >> 0, reco in >> 12 @ 2]" + sle = [ + frappe._dict( + name="Flask Item", + actual_qty=10, + qty_after_transaction=10, + stock_value_difference=1000, + valuation_rate=100, + warehouse="WH 1", + posting_date="2021-12-01", + voucher_type="Stock Entry", + voucher_no="001", + has_serial_no=False, + serial_no=None, + ), + frappe._dict( + name="Flask Item", + actual_qty=(-10), + qty_after_transaction=0, + stock_value_difference=(-1000), + valuation_rate=100, + warehouse="WH 1", + posting_date="2021-12-02", + voucher_type="Stock Reconciliation", + voucher_no="002", + has_serial_no=False, + serial_no=None, + ), + frappe._dict( + name="Flask Item", + actual_qty=12, + qty_after_transaction=12, + stock_value_difference=24, + valuation_rate=2, + warehouse="WH 1", + posting_date="2021-12-02", + voucher_type="Stock Reconciliation", + voucher_no="002", + has_serial_no=False, + serial_no=None, + ), + ] + + slots = FIFOSlots(self.filters, sle).generate() + queue = slots["Flask Item"]["fifo_queue"] + + self.assertEqual(queue, [[10.0, "2021-12-01", 20.0], [2.0, "2021-12-02", 4.0]]) + def test_sequential_stock_reco_same_warehouse(self): """ Test back to back stock recos (same warehouse).