diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index 463d195d38f..c0c676855e2 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -629,7 +629,7 @@ class FIFOSlots: def _add_serial_fifo_slots(self, row: dict, fifo_queue: list, serial_nos: list) -> None: valuation = row.stock_value_difference / row.actual_qty for serial_no in serial_nos: - posting_date = self.serial_no_details.setdefault(serial_no, row.posting_date) + posting_date = self.serial_no_details.setdefault((serial_no, row.warehouse), row.posting_date) fifo_queue.append([serial_no, posting_date, valuation]) def _add_batch_fifo_slots(self, row: dict, fifo_queue: list, batch_nos: list) -> None: @@ -641,7 +641,7 @@ class FIFOSlots: if not qty: continue - posting_date = self.batch_no_details.setdefault(batch_no, row.posting_date) + posting_date = self.batch_no_details.setdefault((batch_no, row.warehouse), row.posting_date) fifo_queue.append([batch_no, use_batchwise_valuation, qty, posting_date, stock_value_difference]) def _neutralize_negative_batch_stock( @@ -852,13 +852,25 @@ class FIFOSlots: transfer_qty_to_pop -= transfer_qty stock_value -= transfer_value self._add_incoming_transfer_slots( - fifo_queue, batch_nos, transfer_qty, transfer_date, transfer_value, serial_nos + fifo_queue, + row.warehouse, + batch_nos, + transfer_qty, + transfer_date, + transfer_value, + serial_nos, ) transfer_data.pop(0) elif not transfer_data: # transfer bucket is empty, extra incoming qty self._add_incoming_transfer_slots( - fifo_queue, batch_nos, transfer_qty_to_pop, row.posting_date, stock_value, serial_nos + fifo_queue, + row.warehouse, + batch_nos, + transfer_qty_to_pop, + row.posting_date, + stock_value, + serial_nos, ) transfer_qty_to_pop = 0 stock_value = 0 @@ -868,6 +880,7 @@ class FIFOSlots: transfer_data[0][FIFO_VALUE_INDEX] -= stock_value self._add_incoming_transfer_slots( fifo_queue, + row.warehouse, batch_nos, transfer_qty_to_pop, transfer_data[0][FIFO_DATE_INDEX], @@ -880,17 +893,21 @@ class FIFOSlots: def _add_incoming_transfer_slots( self, fifo_queue: list, + warehouse: str, batch_nos: list, qty: float, posting_date: str, value: float, serial_nos: list | None = None, ) -> None: - for slot in self._get_incoming_transfer_slots(batch_nos, qty, posting_date, value, serial_nos): + for slot in self._get_incoming_transfer_slots( + warehouse, batch_nos, qty, posting_date, value, serial_nos + ): self._add_transfer_slot_to_fifo_queue(fifo_queue, slot) def _get_incoming_transfer_slots( self, + warehouse: str, batch_nos: list, qty: float, posting_date: str, @@ -898,7 +915,7 @@ class FIFOSlots: serial_nos: list | None = None, ) -> list: if serial_nos: - return self._get_serial_incoming_transfer_slots(serial_nos, qty, posting_date, value) + return self._get_serial_incoming_transfer_slots(serial_nos, warehouse, qty, posting_date, value) if not batch_nos: return [[qty, posting_date, value]] @@ -932,7 +949,7 @@ class FIFOSlots: return incoming_slots def _get_serial_incoming_transfer_slots( - self, serial_nos: list, qty: float, posting_date: str, value: float + self, serial_nos: list, warehouse: str, qty: float, posting_date: str, value: float ) -> list: incoming_slots = [] remaining_value = flt(value) @@ -941,7 +958,7 @@ class FIFOSlots: for index in range(serial_count): serial_no = serial_nos.pop(0) serial_value = remaining_value if index == serial_count - 1 else flt(value / serial_count) - serial_posting_date = self.serial_no_details.setdefault(serial_no, posting_date) + serial_posting_date = self.serial_no_details.setdefault((serial_no, warehouse), posting_date) incoming_slots.append([serial_no, serial_posting_date, serial_value]) remaining_value = flt(remaining_value - serial_value) diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 39c046fb689..367b6dffe7d 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -676,6 +676,113 @@ class TestStockAgeing(ERPNextTestSuite): ], ) + def test_batch_age_in_warehouse_ignores_receipt_in_another_warehouse(self): + """Ledger (batch B): +10 into WH 1, transferred to WH 2 four days later. + WH 2 has held the batch since the transfer, so it ages from the transfer + whether or not a warehouse filter narrowed the scan to WH 2.""" + from erpnext.stock.doctype.item.test_item import make_item + + item_code = make_item( + "Test Stock Ageing Batch Warehouse Scope", + {"is_stock_item": 1, "has_batch_no": 1, "valuation_method": "FIFO"}, + ).name + + batch_no = "SA-WAREHOUSE-SCOPE-BATCH" + frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert( + ignore_permissions=True, ignore_if_duplicate=True + ) + frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1) + + def make_sle(posting_date, voucher_no, warehouse, 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=10, + warehouse=warehouse, + posting_date=posting_date, + voucher_type="Stock Entry", + voucher_no=voucher_no, + has_serial_no=False, + has_batch_no=True, + serial_no=None, + batch_no=batch_no, + ) + + def make_transfer_in(): + return make_sle("2021-12-05", "002", "WH 2", 10, 10, 100) + + def make_whole_ledger(): + return [ + make_sle("2021-12-01", "001", "WH 1", 10, 10, 100), + make_sle("2021-12-05", "002", "WH 1", -10, 0, -100), + make_transfer_in(), + ] + + self.filters.show_warehouse_wise_stock = True + try: + unfiltered = FIFOSlots(self.filters, make_whole_ledger()).generate() + warehouse_filtered = FIFOSlots(self.filters, [make_transfer_in()]).generate() + finally: + self.filters.show_warehouse_wise_stock = False + + aged_from_transfer = [[batch_no, 1, 10.0, "2021-12-05", 100.0]] + self.assertEqual(unfiltered[(item_code, "WH 2")]["fifo_queue"], aged_from_transfer) + self.assertEqual(warehouse_filtered[(item_code, "WH 2")]["fifo_queue"], aged_from_transfer) + self.assertEqual(unfiltered[(item_code, "WH 1")]["fifo_queue"], []) + + def test_serial_age_in_warehouse_ignores_receipt_in_another_warehouse(self): + """Ledger (serial SN): received into WH 1, transferred to WH 2 four days later. + WH 2 has held the serial since the transfer, whether or not the warehouse + filter narrowed the scanned entries to WH 2.""" + from erpnext.stock.doctype.item.test_item import make_item + + item_code = make_item( + "Test Stock Ageing Serial Warehouse Scope", + {"is_stock_item": 1, "has_serial_no": 1, "valuation_method": "FIFO"}, + ).name + serial_no = "SA-WAREHOUSE-SCOPE-SN1" + + def make_sle(posting_date, voucher_no, warehouse, actual_qty, qty_after): + return frappe._dict( + name=item_code, + actual_qty=actual_qty, + qty_after_transaction=qty_after, + stock_value_difference=actual_qty * 100, + valuation_rate=100, + warehouse=warehouse, + posting_date=posting_date, + voucher_type="Stock Entry", + voucher_no=voucher_no, + has_serial_no=True, + has_batch_no=False, + serial_no=serial_no, + batch_no=None, + ) + + def make_transfer_in(): + return make_sle("2021-12-05", "002", "WH 2", 1, 1) + + def make_whole_ledger(): + return [ + make_sle("2021-12-01", "001", "WH 1", 1, 1), + make_sle("2021-12-05", "002", "WH 1", -1, 0), + make_transfer_in(), + ] + + self.filters.show_warehouse_wise_stock = True + try: + unfiltered = FIFOSlots(self.filters, make_whole_ledger()).generate() + warehouse_filtered = FIFOSlots(self.filters, [make_transfer_in()]).generate() + finally: + self.filters.show_warehouse_wise_stock = False + + aged_from_transfer = [[serial_no, "2021-12-05", 100.0]] + self.assertEqual(unfiltered[(item_code, "WH 2")]["fifo_queue"], aged_from_transfer) + self.assertEqual(warehouse_filtered[(item_code, "WH 2")]["fifo_queue"], aged_from_transfer) + self.assertEqual(unfiltered[(item_code, "WH 1")]["fifo_queue"], []) + def test_batch_pooling_preserves_total_on_repeating_rate(self): """Ledger (same wh, batch B): [+3 @ 100/3, +6 @ 0, +2 @ 0] The pooled rate does not terminate, so assert the redistributed