perf(stock): skip the negative batch scan when nothing is negative (#59061)

This commit is contained in:
Mihir Kandoi
2026-09-17 11:07:27 +05:30
committed by GitHub
parent ded6df3614
commit 0e31182dca
2 changed files with 67 additions and 0 deletions

View File

@@ -288,6 +288,7 @@ class FIFOSlots:
self.transferred_item_details = {}
self.serial_no_details = {}
self.batch_no_details = {}
self.batches_with_negative_slots = set()
self.batchwise_valuation_by_batch = {}
self.valuation_method_by_item = {}
self.filters = filters
@@ -659,6 +660,11 @@ class FIFOSlots:
if not qty:
return qty, stock_value_difference
if (batch_no, row.warehouse) not in self.batches_with_negative_slots:
return qty, stock_value_difference
negative_slot_may_remain = False
for slot in list(fifo_queue):
if not self._is_matching_negative_batch_slot(slot, batch_no, use_batchwise_valuation):
continue
@@ -679,10 +685,16 @@ class FIFOSlots:
if not flt(slot[BATCH_SLOT_QTY_INDEX]) and not flt(slot[BATCH_SLOT_VALUE_INDEX]):
fifo_queue.remove(slot)
elif flt(slot[BATCH_SLOT_QTY_INDEX]) < 0:
negative_slot_may_remain = True
if not qty:
negative_slot_may_remain = True
break
if not negative_slot_may_remain:
self.batches_with_negative_slots.discard((batch_no, row.warehouse))
return qty, stock_value_difference
def _is_matching_negative_batch_slot(
@@ -795,9 +807,13 @@ class FIFOSlots:
qty: float,
stock_value_difference: float,
) -> None:
"""The only place a batch slot goes negative, so it is also where the warehouse
is recorded as owing stock on that batch. The record is discarded again by a walk
that reaches the end of the queue and leaves nothing negative behind."""
fifo_queue.append(
[batch_no, use_batchwise_valuation, -(qty), row.posting_date, -(stock_value_difference)]
)
self.batches_with_negative_slots.add((batch_no, row.warehouse))
self.transferred_item_details[transfer_key].append([qty, row.posting_date, stock_value_difference])
def _consume_fifo_slots(

View File

@@ -1884,6 +1884,57 @@ class TestStockAgeing(ERPNextTestSuite):
[[5.0, "2021-09-01", 50.0]],
)
def test_second_negative_batch_slot_survives_a_partial_refill(self):
"""Ledger (same wh, batch B): two issues against no stock, then two receipts.
The first receipt clears only the first negative slot, so the second receipt
must still find and clear the one left behind."""
from erpnext.stock.doctype.item.test_item import make_item
item_code = make_item(
"Test Stock Ageing Twice Negative Batch",
{"is_stock_item": 1, "has_batch_no": 1, "valuation_method": "FIFO"},
).name
batch_no = "SA-TWICE-NEGATIVE-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)
qty_after = 0
def make_sle(posting_date, voucher_no, actual_qty):
nonlocal qty_after
qty_after += actual_qty
return frappe._dict(
name=item_code,
actual_qty=actual_qty,
qty_after_transaction=qty_after,
stock_value_difference=actual_qty * 10,
valuation_rate=10,
warehouse="WH 1",
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,
)
sle = [
make_sle("2021-12-01", "001", -10),
make_sle("2021-12-02", "002", -5),
make_sle("2021-12-03", "003", 10),
make_sle("2021-12-04", "004", 5),
]
result = FIFOSlots(self.filters, sle).generate()[item_code]
self.assertEqual(result["fifo_queue"], [])
self.assertEqual(result["total_qty"], 0)
def test_batchwise_valuation_negative_stock_same_voucher(self):
from erpnext.stock.doctype.item.test_item import make_item