Rucha Mahabal
3 years ago
committed by
GitHub
17 changed files with 567 additions and 195 deletions
@ -0,0 +1,73 @@ |
|||
### Concept of FIFO Slots |
|||
|
|||
Since we need to know age-wise remaining stock, we maintain all the inward entries as slots. So each time stock comes in, a slot is added for the same. |
|||
|
|||
Eg. For Item A: |
|||
---------------------- |
|||
Date | Qty | Queue |
|||
---------------------- |
|||
1st | +50 | [[50, 1-12-2021]] |
|||
2nd | +20 | [[50, 1-12-2021], [20, 2-12-2021]] |
|||
---------------------- |
|||
|
|||
Now the queue can tell us the total stock and also how old the stock is. |
|||
Here, the balance qty is 70. |
|||
50 qty is (today-the 1st) days old |
|||
20 qty is (today-the 2nd) days old |
|||
|
|||
### Calculation of FIFO Slots |
|||
|
|||
#### Case 1: Outward from sufficient balance qty |
|||
---------------------- |
|||
Date | Qty | Queue |
|||
---------------------- |
|||
1st | +50 | [[50, 1-12-2021]] |
|||
2nd | -20 | [[30, 1-12-2021]] |
|||
2nd | +20 | [[30, 1-12-2021], [20, 2-12-2021]] |
|||
|
|||
Here after the first entry, while issuing 20 qty: |
|||
- **since 20 is lesser than the balance**, **qty_to_pop (20)** is simply consumed from first slot (FIFO consumption) |
|||
- Any inward entry after as usual will get its own slot added to the queue |
|||
|
|||
#### Case 2: Outward from sufficient cumulative (slots) balance qty |
|||
---------------------- |
|||
Date | Qty | Queue |
|||
---------------------- |
|||
1st | +50 | [[50, 1-12-2021]] |
|||
2nd | +20 | [[50, 1-12-2021], [20, 2-12-2021]] |
|||
2nd | -60 | [[10, 2-12-2021]] |
|||
|
|||
- Consumption happens slot wise. First slot 1 is consumed |
|||
- Since **qty_to_pop (60) is greater than slot 1 qty (50)**, the entire slot is consumed and popped |
|||
- Now the queue is [[20, 2-12-2021]], and **qty_to_pop=10** (remaining qty to pop) |
|||
- It then goes ahead to the next slot and consumes 10 from it |
|||
- Now the queue is [[10, 2-12-2021]] |
|||
|
|||
#### Case 3: Outward from insufficient balance qty |
|||
> This case is possible only if **Allow Negative Stock** was enabled at some point/is enabled. |
|||
|
|||
---------------------- |
|||
Date | Qty | Queue |
|||
---------------------- |
|||
1st | +50 | [[50, 1-12-2021]] |
|||
2nd | -60 | [[-10, 1-12-2021]] |
|||
|
|||
- Since **qty_to_pop (60)** is more than the balance in slot 1, the entire slot is consumed and popped |
|||
- Now the queue is **empty**, and **qty_to_pop=10** (remaining qty to pop) |
|||
- Since we still have more to consume, we append the balance since 60 is issued from 50 i.e. -10. |
|||
- We register this negative value, since the stock issue has caused the balance to become negative |
|||
|
|||
Now when stock is inwarded: |
|||
- Instead of adding a slot we check if there are any negative balances. |
|||
- If yes, we keep adding positive stock to it until we make the balance positive. |
|||
- Once the balance is positive, the next inward entry will add a new slot in the queue |
|||
|
|||
Eg: |
|||
---------------------- |
|||
Date | Qty | Queue |
|||
---------------------- |
|||
1st | +50 | [[50, 1-12-2021]] |
|||
2nd | -60 | [[-10, 1-12-2021]] |
|||
3rd | +5 | [[-5, 3-12-2021]] |
|||
4th | +10 | [[5, 4-12-2021]] |
|||
4th | +20 | [[5, 4-12-2021], [20, 4-12-2021]] |
@ -0,0 +1,126 @@ |
|||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors |
|||
# See license.txt |
|||
|
|||
import frappe |
|||
|
|||
from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots |
|||
from erpnext.tests.utils import ERPNextTestCase |
|||
|
|||
|
|||
class TestStockAgeing(ERPNextTestCase): |
|||
def setUp(self) -> None: |
|||
self.filters = frappe._dict( |
|||
company="_Test Company", |
|||
to_date="2021-12-10" |
|||
) |
|||
|
|||
def test_normal_inward_outward_queue(self): |
|||
"Reference: Case 1 in stock_ageing_fifo_logic.md" |
|||
sle = [ |
|||
frappe._dict( |
|||
name="Flask Item", |
|||
actual_qty=30, qty_after_transaction=30, |
|||
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=20, qty_after_transaction=50, |
|||
posting_date="2021-12-02", voucher_type="Stock Entry", |
|||
voucher_no="002", |
|||
has_serial_no=False, serial_no=None |
|||
), |
|||
frappe._dict( |
|||
name="Flask Item", |
|||
actual_qty=(-10), qty_after_transaction=40, |
|||
posting_date="2021-12-03", voucher_type="Stock Entry", |
|||
voucher_no="003", |
|||
has_serial_no=False, serial_no=None |
|||
) |
|||
] |
|||
|
|||
slots = FIFOSlots(self.filters, sle).generate() |
|||
|
|||
self.assertTrue(slots["Flask Item"]["fifo_queue"]) |
|||
result = slots["Flask Item"] |
|||
queue = result["fifo_queue"] |
|||
|
|||
self.assertEqual(result["qty_after_transaction"], result["total_qty"]) |
|||
self.assertEqual(queue[0][0], 20.0) |
|||
|
|||
def test_insufficient_balance(self): |
|||
"Reference: Case 3 in stock_ageing_fifo_logic.md" |
|||
sle = [ |
|||
frappe._dict( |
|||
name="Flask Item", |
|||
actual_qty=(-30), qty_after_transaction=(-30), |
|||
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=20, qty_after_transaction=(-10), |
|||
posting_date="2021-12-02", voucher_type="Stock Entry", |
|||
voucher_no="002", |
|||
has_serial_no=False, serial_no=None |
|||
), |
|||
frappe._dict( |
|||
name="Flask Item", |
|||
actual_qty=20, qty_after_transaction=10, |
|||
posting_date="2021-12-03", voucher_type="Stock Entry", |
|||
voucher_no="003", |
|||
has_serial_no=False, serial_no=None |
|||
), |
|||
frappe._dict( |
|||
name="Flask Item", |
|||
actual_qty=10, qty_after_transaction=20, |
|||
posting_date="2021-12-03", voucher_type="Stock Entry", |
|||
voucher_no="004", |
|||
has_serial_no=False, serial_no=None |
|||
) |
|||
] |
|||
|
|||
slots = FIFOSlots(self.filters, sle).generate() |
|||
|
|||
result = slots["Flask Item"] |
|||
queue = result["fifo_queue"] |
|||
|
|||
self.assertEqual(result["qty_after_transaction"], result["total_qty"]) |
|||
self.assertEqual(queue[0][0], 10.0) |
|||
self.assertEqual(queue[1][0], 10.0) |
|||
|
|||
def test_stock_reconciliation(self): |
|||
sle = [ |
|||
frappe._dict( |
|||
name="Flask Item", |
|||
actual_qty=30, qty_after_transaction=30, |
|||
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=50, |
|||
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=(-10), qty_after_transaction=40, |
|||
posting_date="2021-12-03", voucher_type="Stock Entry", |
|||
voucher_no="003", |
|||
has_serial_no=False, serial_no=None |
|||
) |
|||
] |
|||
|
|||
slots = FIFOSlots(self.filters, sle).generate() |
|||
|
|||
result = slots["Flask Item"] |
|||
queue = result["fifo_queue"] |
|||
|
|||
self.assertEqual(result["qty_after_transaction"], result["total_qty"]) |
|||
self.assertEqual(queue[0][0], 20.0) |
|||
self.assertEqual(queue[1][0], 20.0) |
Loading…
Reference in new issue