fix: maintain FIFO queue even if outgoing_rate is not found (#30560)
diff --git a/erpnext/stock/tests/test_valuation.py b/erpnext/stock/tests/test_valuation.py
index 506a666..e60c1ca 100644
--- a/erpnext/stock/tests/test_valuation.py
+++ b/erpnext/stock/tests/test_valuation.py
@@ -60,9 +60,9 @@
self.queue.remove_stock(1, 5)
self.assertEqual(self.queue, [[-1, 5]])
- # XXX
- self.queue.remove_stock(1, 10)
+ self.queue.remove_stock(1)
self.assertTotalQty(-2)
+ self.assertEqual(self.queue, [[-2, 5]])
self.queue.add_stock(2, 10)
self.assertTotalQty(0)
@@ -93,7 +93,7 @@
self.queue.remove_stock(3, 20)
self.assertEqual(self.queue, [[1, 10], [5, 20]])
- def test_collapsing_of_queue(self):
+ def test_queue_with_unknown_rate(self):
self.queue.add_stock(1, 1)
self.queue.add_stock(1, 2)
self.queue.add_stock(1, 3)
@@ -102,8 +102,7 @@
self.assertTotalValue(10)
self.queue.remove_stock(3, 1)
- # XXX
- self.assertEqual(self.queue, [[1, 7]])
+ self.assertEqual(self.queue, [[1, 4]])
def test_rounding_off(self):
self.queue.add_stock(1.0, 1.0)
@@ -172,6 +171,32 @@
self.assertTotalQty(total_qty)
self.assertTotalValue(total_value)
+ @given(stock_queue_generator, st.floats(min_value=0.1, max_value=1e6))
+ def test_fifo_qty_value_nonneg_hypothesis_with_outgoing_rate(self, stock_queue, outgoing_rate):
+ self.queue = FIFOValuation([])
+ total_qty = 0.0
+ total_value = 0.0
+
+ for qty, rate in stock_queue:
+ # don't allow negative stock
+ if qty == 0 or total_qty + qty < 0 or abs(qty) < 0.1:
+ continue
+ if qty > 0:
+ self.queue.add_stock(qty, rate)
+ total_qty += qty
+ total_value += qty * rate
+ else:
+ qty = abs(qty)
+ consumed = self.queue.remove_stock(qty, outgoing_rate)
+ self.assertAlmostEqual(
+ qty, sum(q for q, _ in consumed), msg=f"incorrect consumption {consumed}"
+ )
+ total_qty -= qty
+ total_value -= sum(q * r for q, r in consumed)
+ self.assertTotalQty(total_qty)
+ self.assertTotalValue(total_value)
+ self.assertGreaterEqual(total_value, 0)
+
class TestLIFOValuation(unittest.TestCase):
def setUp(self):
diff --git a/erpnext/stock/valuation.py b/erpnext/stock/valuation.py
index 648b218..35f4f12 100644
--- a/erpnext/stock/valuation.py
+++ b/erpnext/stock/valuation.py
@@ -60,9 +60,7 @@
# specifying the attributes to save resources
# ref: https://docs.python.org/3/reference/datamodel.html#slots
- __slots__ = [
- "queue",
- ]
+ __slots__ = ["queue"]
def __init__(self, state: Optional[List[StockBin]]):
self.queue: List[StockBin] = state if state is not None else []
@@ -123,15 +121,9 @@
index = idx
break
- # If no entry found with outgoing rate, collapse queue
+ # If no entry found with outgoing rate, consume as per FIFO
if index is None: # nosemgrep
- new_stock_value = sum(d[QTY] * d[RATE] for d in self.queue) - qty * outgoing_rate
- new_stock_qty = sum(d[QTY] for d in self.queue) - qty
- self.queue = [
- [new_stock_qty, new_stock_value / new_stock_qty if new_stock_qty > 0 else outgoing_rate]
- ]
- consumed_bins.append([qty, outgoing_rate])
- break
+ index = 0
else:
index = 0
@@ -172,9 +164,7 @@
# specifying the attributes to save resources
# ref: https://docs.python.org/3/reference/datamodel.html#slots
- __slots__ = [
- "stack",
- ]
+ __slots__ = ["stack"]
def __init__(self, state: Optional[List[StockBin]]):
self.stack: List[StockBin] = state if state is not None else []