test: FIFO transfer for multi-batch transaction
(cherry picked from commit b3b7cdfb49)
# Conflicts:
# erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
This commit is contained in:
committed by
Ankush Menat
parent
5be64c2b7f
commit
a84e7c633e
@@ -349,6 +349,359 @@ class TestStockLedgerEntry(ERPNextTestCase):
|
||||
frappe.set_user("Administrator")
|
||||
user.remove_roles("Stock Manager")
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
def test_batchwise_item_valuation_moving_average(self):
|
||||
item, warehouses, batches = setup_item_valuation_test(valuation_method="Moving Average")
|
||||
|
||||
# Incoming Entries for Stock Value check
|
||||
pr_entry_list = [
|
||||
(item, warehouses[0], batches[0], 1, 100),
|
||||
(item, warehouses[0], batches[1], 1, 50),
|
||||
(item, warehouses[0], batches[0], 1, 150),
|
||||
(item, warehouses[0], batches[1], 1, 100),
|
||||
]
|
||||
prs = create_purchase_receipt_entries_for_batchwise_item_valuation_test(pr_entry_list)
|
||||
sle_details = fetch_sle_details_for_doc_list(prs, ['stock_value'])
|
||||
sv_list = [d['stock_value'] for d in sle_details]
|
||||
expected_sv = [100, 150, 300, 400]
|
||||
self.assertEqual(expected_sv, sv_list, "Incorrect 'Stock Value' values")
|
||||
|
||||
# Outgoing Entries for Stock Value Difference check
|
||||
dn_entry_list = [
|
||||
(item, warehouses[0], batches[1], 1, 200),
|
||||
(item, warehouses[0], batches[0], 1, 200),
|
||||
(item, warehouses[0], batches[1], 1, 200),
|
||||
(item, warehouses[0], batches[0], 1, 200)
|
||||
]
|
||||
dns = create_delivery_note_entries_for_batchwise_item_valuation_test(dn_entry_list)
|
||||
sle_details = fetch_sle_details_for_doc_list(dns, ['stock_value_difference'])
|
||||
svd_list = [-1 * d['stock_value_difference'] for d in sle_details]
|
||||
expected_incoming_rates = expected_abs_svd = [75, 125, 75, 125]
|
||||
|
||||
self.assertEqual(expected_abs_svd, svd_list, "Incorrect 'Stock Value Difference' values")
|
||||
for dn, incoming_rate in zip(dns, expected_incoming_rates):
|
||||
self.assertEqual(
|
||||
dn.items[0].incoming_rate, incoming_rate,
|
||||
"Incorrect 'Incoming Rate' values fetched for DN items"
|
||||
)
|
||||
|
||||
|
||||
def assertSLEs(self, doc, expected_sles, sle_filters=None):
|
||||
""" Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
|
||||
|
||||
filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled":0}
|
||||
if sle_filters:
|
||||
filters.update(sle_filters)
|
||||
sles = frappe.get_all("Stock Ledger Entry", fields=["*"], filters=filters,
|
||||
order_by="timestamp(posting_date, posting_time), creation")
|
||||
|
||||
for exp_sle, act_sle in zip(expected_sles, sles):
|
||||
for k, v in exp_sle.items():
|
||||
act_value = act_sle[k]
|
||||
if k == "stock_queue":
|
||||
act_value = json.loads(act_value)
|
||||
if act_value and act_value[0][0] == 0:
|
||||
# ignore empty fifo bins
|
||||
continue
|
||||
|
||||
self.assertEqual(v, act_value, msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}")
|
||||
|
||||
|
||||
def test_batchwise_item_valuation_stock_reco(self):
|
||||
item, warehouses, batches = setup_item_valuation_test()
|
||||
state = {
|
||||
"stock_value" : 0.0,
|
||||
"qty": 0.0
|
||||
}
|
||||
def update_invariants(exp_sles):
|
||||
for sle in exp_sles:
|
||||
state["stock_value"] += sle["stock_value_difference"]
|
||||
state["qty"] += sle["actual_qty"]
|
||||
sle["stock_value"] = state["stock_value"]
|
||||
sle["qty_after_transaction"] = state["qty"]
|
||||
|
||||
osr1 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=10, rate=100, batch_no=batches[1])
|
||||
expected_sles = [
|
||||
{"actual_qty": 10, "stock_value_difference": 1000},
|
||||
]
|
||||
update_invariants(expected_sles)
|
||||
self.assertSLEs(osr1, expected_sles)
|
||||
|
||||
osr2 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=13, rate=200, batch_no=batches[0])
|
||||
expected_sles = [
|
||||
{"actual_qty": 13, "stock_value_difference": 200*13},
|
||||
]
|
||||
update_invariants(expected_sles)
|
||||
self.assertSLEs(osr2, expected_sles)
|
||||
|
||||
sr1 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=5, rate=50, batch_no=batches[1])
|
||||
|
||||
expected_sles = [
|
||||
{"actual_qty": -10, "stock_value_difference": -10 * 100},
|
||||
{"actual_qty": 5, "stock_value_difference": 250}
|
||||
]
|
||||
update_invariants(expected_sles)
|
||||
self.assertSLEs(sr1, expected_sles)
|
||||
|
||||
sr2 = create_stock_reconciliation(warehouse=warehouses[0], item_code=item, qty=20, rate=75, batch_no=batches[0])
|
||||
expected_sles = [
|
||||
{"actual_qty": -13, "stock_value_difference": -13 * 200},
|
||||
{"actual_qty": 20, "stock_value_difference": 20 * 75}
|
||||
]
|
||||
update_invariants(expected_sles)
|
||||
self.assertSLEs(sr2, expected_sles)
|
||||
|
||||
def test_batch_wise_valuation_across_warehouse(self):
|
||||
item_code, warehouses, batches = setup_item_valuation_test()
|
||||
source = warehouses[0]
|
||||
target = warehouses[1]
|
||||
|
||||
unrelated_batch = make_stock_entry(item_code=item_code, target=source, batch_no=batches[1],
|
||||
qty=5, rate=10)
|
||||
self.assertSLEs(unrelated_batch, [
|
||||
{"actual_qty": 5, "stock_value_difference": 10 * 5},
|
||||
])
|
||||
|
||||
reciept = make_stock_entry(item_code=item_code, target=source, batch_no=batches[0], qty=5, rate=10)
|
||||
self.assertSLEs(reciept, [
|
||||
{"actual_qty": 5, "stock_value_difference": 10 * 5},
|
||||
])
|
||||
|
||||
transfer = make_stock_entry(item_code=item_code, source=source, target=target, batch_no=batches[0], qty=5)
|
||||
self.assertSLEs(transfer, [
|
||||
{"actual_qty": -5, "stock_value_difference": -10 * 5, "warehouse": source},
|
||||
{"actual_qty": 5, "stock_value_difference": 10 * 5, "warehouse": target}
|
||||
])
|
||||
|
||||
backdated_receipt = make_stock_entry(item_code=item_code, target=source, batch_no=batches[0],
|
||||
qty=5, rate=20, posting_date=add_days(today(), -1))
|
||||
self.assertSLEs(backdated_receipt, [
|
||||
{"actual_qty": 5, "stock_value_difference": 20 * 5},
|
||||
])
|
||||
|
||||
# check reposted average rate in *future* transfer
|
||||
self.assertSLEs(transfer, [
|
||||
{"actual_qty": -5, "stock_value_difference": -15 * 5, "warehouse": source, "stock_value": 15 * 5 + 10 * 5},
|
||||
{"actual_qty": 5, "stock_value_difference": 15 * 5, "warehouse": target, "stock_value": 15 * 5}
|
||||
])
|
||||
|
||||
transfer_unrelated = make_stock_entry(item_code=item_code, source=source,
|
||||
target=target, batch_no=batches[1], qty=5)
|
||||
self.assertSLEs(transfer_unrelated, [
|
||||
{"actual_qty": -5, "stock_value_difference": -10 * 5, "warehouse": source, "stock_value": 15 * 5},
|
||||
{"actual_qty": 5, "stock_value_difference": 10 * 5, "warehouse": target, "stock_value": 15 * 5 + 10 * 5}
|
||||
])
|
||||
|
||||
def test_intermediate_average_batch_wise_valuation(self):
|
||||
""" A batch has moving average up until posting time,
|
||||
check if same is respected when backdated entry is inserted in middle"""
|
||||
item_code, warehouses, batches = setup_item_valuation_test()
|
||||
warehouse = warehouses[0]
|
||||
|
||||
batch = batches[0]
|
||||
|
||||
yesterday = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batch,
|
||||
qty=1, rate=10, posting_date=add_days(today(), -1))
|
||||
self.assertSLEs(yesterday, [
|
||||
{"actual_qty": 1, "stock_value_difference": 10},
|
||||
])
|
||||
|
||||
tomorrow = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0],
|
||||
qty=1, rate=30, posting_date=add_days(today(), 1))
|
||||
self.assertSLEs(tomorrow, [
|
||||
{"actual_qty": 1, "stock_value_difference": 30},
|
||||
])
|
||||
|
||||
create_today = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0],
|
||||
qty=1, rate=20)
|
||||
self.assertSLEs(create_today, [
|
||||
{"actual_qty": 1, "stock_value_difference": 20},
|
||||
])
|
||||
|
||||
consume_today = make_stock_entry(item_code=item_code, source=warehouse, batch_no=batches[0],
|
||||
qty=1)
|
||||
self.assertSLEs(consume_today, [
|
||||
{"actual_qty": -1, "stock_value_difference": -15},
|
||||
])
|
||||
|
||||
consume_tomorrow = make_stock_entry(item_code=item_code, source=warehouse, batch_no=batches[0],
|
||||
qty=2, posting_date=add_days(today(), 2))
|
||||
self.assertSLEs(consume_tomorrow, [
|
||||
{"stock_value_difference": -(30 + 15), "stock_value": 0, "qty_after_transaction": 0},
|
||||
])
|
||||
|
||||
def test_legacy_item_valuation_stock_entry(self):
|
||||
columns = [
|
||||
'stock_value_difference',
|
||||
'stock_value',
|
||||
'actual_qty',
|
||||
'qty_after_transaction',
|
||||
'stock_queue',
|
||||
]
|
||||
item, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0)
|
||||
|
||||
def check_sle_details_against_expected(sle_details, expected_sle_details, detail, columns):
|
||||
for i, (sle_vals, ex_sle_vals) in enumerate(zip(sle_details, expected_sle_details)):
|
||||
for col, sle_val, ex_sle_val in zip(columns, sle_vals, ex_sle_vals):
|
||||
if col == 'stock_queue':
|
||||
sle_val = get_stock_value_from_q(sle_val)
|
||||
ex_sle_val = get_stock_value_from_q(ex_sle_val)
|
||||
self.assertEqual(
|
||||
sle_val, ex_sle_val,
|
||||
f"Incorrect {col} value on transaction #: {i} in {detail}"
|
||||
)
|
||||
|
||||
# List used to defer assertions to prevent commits cause of error skipped rollback
|
||||
details_list = []
|
||||
|
||||
|
||||
# Test Material Receipt Entries
|
||||
se_entry_list_mr = [
|
||||
(item, None, warehouses[0], batches[0], 1, 50, "2021-01-21"),
|
||||
(item, None, warehouses[0], batches[1], 1, 100, "2021-01-23"),
|
||||
]
|
||||
ses = create_stock_entry_entries_for_batchwise_item_valuation_test(
|
||||
se_entry_list_mr, "Material Receipt"
|
||||
)
|
||||
sle_details = fetch_sle_details_for_doc_list(ses, columns=columns, as_dict=0)
|
||||
expected_sle_details = [
|
||||
(50.0, 50.0, 1.0, 1.0, '[[1.0, 50.0]]'),
|
||||
(100.0, 150.0, 1.0, 2.0, '[[1.0, 50.0], [1.0, 100.0]]'),
|
||||
]
|
||||
details_list.append((
|
||||
sle_details, expected_sle_details,
|
||||
"Material Receipt Entries", columns
|
||||
))
|
||||
|
||||
|
||||
# Test Material Issue Entries
|
||||
se_entry_list_mi = [
|
||||
(item, warehouses[0], None, batches[1], 1, None, "2021-01-29"),
|
||||
]
|
||||
ses = create_stock_entry_entries_for_batchwise_item_valuation_test(
|
||||
se_entry_list_mi, "Material Issue"
|
||||
)
|
||||
sle_details = fetch_sle_details_for_doc_list(ses, columns=columns, as_dict=0)
|
||||
expected_sle_details = [
|
||||
(-50.0, 100.0, -1.0, 1.0, '[[1, 100.0]]')
|
||||
]
|
||||
details_list.append((
|
||||
sle_details, expected_sle_details,
|
||||
"Material Issue Entries", columns
|
||||
))
|
||||
|
||||
|
||||
# Run assertions
|
||||
for details in details_list:
|
||||
check_sle_details_against_expected(*details)
|
||||
|
||||
def test_mixed_valuation_batches_fifo(self):
|
||||
item_code, warehouses, batches = setup_item_valuation_test(use_batchwise_valuation=0)
|
||||
warehouse = warehouses[0]
|
||||
|
||||
state = {
|
||||
"qty": 0.0,
|
||||
"stock_value": 0.0
|
||||
}
|
||||
def update_invariants(exp_sles):
|
||||
for sle in exp_sles:
|
||||
state["stock_value"] += sle["stock_value_difference"]
|
||||
state["qty"] += sle["actual_qty"]
|
||||
sle["stock_value"] = state["stock_value"]
|
||||
sle["qty_after_transaction"] = state["qty"]
|
||||
return exp_sles
|
||||
|
||||
old1 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0],
|
||||
qty=10, rate=10)
|
||||
self.assertSLEs(old1, update_invariants([
|
||||
{"actual_qty": 10, "stock_value_difference": 10*10, "stock_queue": [[10, 10]]},
|
||||
]))
|
||||
old2 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[1],
|
||||
qty=10, rate=20)
|
||||
self.assertSLEs(old2, update_invariants([
|
||||
{"actual_qty": 10, "stock_value_difference": 10*20, "stock_queue": [[10, 10], [10, 20]]},
|
||||
]))
|
||||
old3 = make_stock_entry(item_code=item_code, target=warehouse, batch_no=batches[0],
|
||||
qty=5, rate=15)
|
||||
|
||||
self.assertSLEs(old3, update_invariants([
|
||||
{"actual_qty": 5, "stock_value_difference": 5*15, "stock_queue": [[10, 10], [10, 20], [5, 15]]},
|
||||
]))
|
||||
|
||||
new1 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=40)
|
||||
batches.append(new1.items[0].batch_no)
|
||||
# assert old queue remains
|
||||
self.assertSLEs(new1, update_invariants([
|
||||
{"actual_qty": 10, "stock_value_difference": 10*40, "stock_queue": [[10, 10], [10, 20], [5, 15]]},
|
||||
]))
|
||||
|
||||
new2 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=42)
|
||||
batches.append(new2.items[0].batch_no)
|
||||
self.assertSLEs(new2, update_invariants([
|
||||
{"actual_qty": 10, "stock_value_difference": 10*42, "stock_queue": [[10, 10], [10, 20], [5, 15]]},
|
||||
]))
|
||||
|
||||
# consume old batch as per FIFO
|
||||
consume_old1 = make_stock_entry(item_code=item_code, source=warehouse, qty=15, batch_no=batches[0])
|
||||
self.assertSLEs(consume_old1, update_invariants([
|
||||
{"actual_qty": -15, "stock_value_difference": -10*10 - 5*20, "stock_queue": [[5, 20], [5, 15]]},
|
||||
]))
|
||||
|
||||
# consume new batch as per batch
|
||||
consume_new2 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-1])
|
||||
self.assertSLEs(consume_new2, update_invariants([
|
||||
{"actual_qty": -10, "stock_value_difference": -10*42, "stock_queue": [[5, 20], [5, 15]]},
|
||||
]))
|
||||
|
||||
# finish all old batches
|
||||
consume_old2 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[1])
|
||||
self.assertSLEs(consume_old2, update_invariants([
|
||||
{"actual_qty": -10, "stock_value_difference": -5*20 - 5*15, "stock_queue": []},
|
||||
]))
|
||||
|
||||
# finish all new batches
|
||||
consume_new1 = make_stock_entry(item_code=item_code, source=warehouse, qty=10, batch_no=batches[-2])
|
||||
self.assertSLEs(consume_new1, update_invariants([
|
||||
{"actual_qty": -10, "stock_value_difference": -10*40, "stock_queue": []},
|
||||
]))
|
||||
>>>>>>> b3b7cdfb49 (test: FIFO transfer for multi-batch transaction)
|
||||
|
||||
def test_fifo_dependent_consumption(self):
|
||||
item = make_item("_TestFifoTransferRates")
|
||||
source = "_Test Warehouse - _TC"
|
||||
target = "Stores - _TC"
|
||||
|
||||
rates = [10 * i for i in range(1, 20)]
|
||||
|
||||
receipt = make_stock_entry(item_code=item.name, target=source, qty=10, do_not_save=True, rate=10)
|
||||
for rate in rates[1:]:
|
||||
row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False)
|
||||
row.basic_rate = rate
|
||||
receipt.append("items", row)
|
||||
|
||||
receipt.save()
|
||||
receipt.submit()
|
||||
|
||||
expected_queues = []
|
||||
for idx, rate in enumerate(rates, start=1):
|
||||
expected_queues.append(
|
||||
{"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]}
|
||||
)
|
||||
self.assertSLEs(receipt, expected_queues)
|
||||
|
||||
transfer = make_stock_entry(item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10)
|
||||
for rate in rates[1:]:
|
||||
row = frappe.copy_doc(transfer.items[0], ignore_no_copy=False)
|
||||
row.basic_rate = rate
|
||||
transfer.append("items", row)
|
||||
|
||||
transfer.save()
|
||||
transfer.submit()
|
||||
|
||||
# same exact queue should be transferred
|
||||
self.assertSLEs(transfer, expected_queues, sle_filters={"warehouse": target})
|
||||
|
||||
|
||||
def create_repack_entry(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
Reference in New Issue
Block a user