diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 8f01dcb136c..ef43b684fcd 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -370,7 +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._revalue_stock_reconciliation_slots(row, fifo_queue, batch_nos) self._update_balances(row, key) self._trim_serial_fifo_queue(row, key, fifo_queue) @@ -394,14 +394,29 @@ 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: + def _revalue_stock_reconciliation_slots(self, row: dict, fifo_queue: list, batch_nos: list) -> None: + if row.voucher_type != "Stock Reconciliation" or row.has_serial_no: + return + + if row.has_batch_no: + if flt(row.actual_qty) > 0: + self._revalue_reconciled_batch_slots(fifo_queue, batch_nos) 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 _revalue_reconciled_batch_slots(self, fifo_queue: list, batch_nos: list) -> None: + for batch_no, _use_batchwise_valuation, qty, stock_value_difference in batch_nos: + if not flt(qty): + continue + + rate = flt(stock_value_difference) / flt(qty) + for slot in fifo_queue: + if is_batch_slot(slot) and slot[BATCH_SLOT_BATCH_INDEX] == batch_no: + slot[BATCH_SLOT_VALUE_INDEX] = flt(slot[BATCH_SLOT_QTY_INDEX] * 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 6e32bde647d..4875ea1bace 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -464,6 +464,56 @@ class TestStockAgeing(ERPNextTestSuite): self.assertEqual(queue, [[10.0, "2021-12-01", 20.0], [2.0, "2021-12-02", 4.0]]) + def test_batch_stock_reco_revaluation_rescales_slot_values(self): + "Ledger (same wh, batch B): [+10 @ 100, reco out >> 0, reco in >> 12 @ 2]" + from erpnext.stock.doctype.item.test_item import make_item + + item_code = make_item( + "Test Stock Ageing Batch Reco Revaluation", + {"is_stock_item": 1, "has_batch_no": 1, "valuation_method": "FIFO"}, + ).name + + batch_no = "SA-RECO-REVALUE-BATCH" + if not frappe.db.exists("Batch", batch_no): + frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert( + ignore_permissions=True + ) + frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1) + + def make_sle(posting_date, voucher_type, voucher_no, actual_qty, qty_after, stock_value_difference): + return frappe._dict( + name=item_code, + actual_qty=actual_qty, + qty_after_transaction=qty_after, + stock_value_difference=stock_value_difference, + valuation_rate=abs(stock_value_difference / actual_qty), + warehouse="WH 1", + posting_date=posting_date, + voucher_type=voucher_type, + voucher_no=voucher_no, + has_serial_no=False, + has_batch_no=True, + serial_no=None, + batch_no=batch_no, + ) + + sle = [ + make_sle("2021-12-01", "Stock Entry", "001", 10, 10, 1000), + make_sle("2021-12-02", "Stock Reconciliation", "002", -10, 0, -1000), + make_sle("2021-12-02", "Stock Reconciliation", "002", 12, 12, 24), + ] + + slots = FIFOSlots(self.filters, sle).generate() + queue = slots[item_code]["fifo_queue"] + + self.assertEqual( + queue, + [ + [batch_no, 1, 10.0, "2021-12-01", 20.0], + [batch_no, 1, 2.0, "2021-12-02", 4.0], + ], + ) + def test_sequential_stock_reco_same_warehouse(self): """ Test back to back stock recos (same warehouse).