mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-17 04:08:49 +00:00
fix: FIFO queue checks and incorrect entries filter in stock ledger reports
- 'Show Incorrect Entries' always returned an empty result (regression
from #43619); now returns entries from one row before the first
incorrect one
- FIFO queue columns were computed for serialized/batched SLEs that
don't maintain a stock queue, showing false differences; left empty
for such rows
- compare value/valuation differences at currency precision, qty at
float precision
(cherry picked from commit 94ab09e4a3)
# Conflicts:
# erpnext/stock/report/stock_ledger_invariant_check/test_stock_ledger_invariant_check.py
This commit is contained in:
@@ -20,6 +20,7 @@ SLE_FIELDS = (
|
||||
"outgoing_rate",
|
||||
"stock_queue",
|
||||
"batch_no",
|
||||
"serial_no",
|
||||
"stock_value",
|
||||
"stock_value_difference",
|
||||
"valuation_rate",
|
||||
@@ -52,16 +53,16 @@ def add_invariant_check_fields(sles, filters):
|
||||
balance_qty = 0.0
|
||||
balance_stock_value = 0.0
|
||||
|
||||
incorrect_idx = 0
|
||||
precision = frappe.get_precision("Stock Ledger Entry", "actual_qty")
|
||||
incorrect_idx = None
|
||||
float_precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) or 3
|
||||
currency_precision = (
|
||||
cint(frappe.db.get_single_value("System Settings", "currency_precision")) or float_precision
|
||||
)
|
||||
for idx, sle in enumerate(sles):
|
||||
queue = json.loads(sle.stock_queue) if sle.stock_queue else []
|
||||
|
||||
fifo_qty = 0.0
|
||||
fifo_value = 0.0
|
||||
for qty, rate in queue:
|
||||
fifo_qty += qty
|
||||
fifo_value += qty * rate
|
||||
if sle.batch_no:
|
||||
sle.use_batchwise_valuation = frappe.db.get_value(
|
||||
"Batch", sle.batch_no, "use_batchwise_valuation", cache=True
|
||||
)
|
||||
|
||||
if sle.actual_qty < 0:
|
||||
sle.consumption_rate = sle.stock_value_difference / sle.actual_qty
|
||||
@@ -77,57 +78,66 @@ def add_invariant_check_fields(sles, filters):
|
||||
if balance_qty is None:
|
||||
balance_qty = sle.qty_after_transaction
|
||||
|
||||
sle.fifo_queue_qty = fifo_qty
|
||||
sle.fifo_stock_value = fifo_value
|
||||
sle.fifo_valuation_rate = fifo_value / fifo_qty if fifo_qty else None
|
||||
sle.balance_value_by_qty = (
|
||||
sle.stock_value / sle.qty_after_transaction if sle.qty_after_transaction else None
|
||||
)
|
||||
sle.expected_qty_after_transaction = balance_qty
|
||||
sle.stock_value_from_diff = balance_stock_value
|
||||
|
||||
# set difference fields
|
||||
sle.difference_in_qty = sle.qty_after_transaction - sle.expected_qty_after_transaction
|
||||
sle.fifo_qty_diff = sle.qty_after_transaction - fifo_qty
|
||||
sle.fifo_value_diff = sle.stock_value - fifo_value
|
||||
sle.fifo_valuation_diff = (
|
||||
sle.valuation_rate - sle.fifo_valuation_rate if sle.fifo_valuation_rate else None
|
||||
)
|
||||
sle.valuation_diff = (
|
||||
sle.valuation_rate - sle.balance_value_by_qty if sle.balance_value_by_qty else None
|
||||
)
|
||||
sle.diff_value_diff = sle.stock_value_from_diff - sle.stock_value
|
||||
|
||||
if not incorrect_idx and filters.get("show_incorrect_entries"):
|
||||
if is_sle_has_correct_data(sle, precision):
|
||||
continue
|
||||
else:
|
||||
incorrect_idx = idx
|
||||
if maintains_fifo_queue(sle):
|
||||
add_fifo_fields(sle, sles[idx - 1] if idx else None)
|
||||
|
||||
if idx > 0:
|
||||
sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value
|
||||
sle.fifo_difference_diff = sle.fifo_stock_diff - sle.stock_value_difference
|
||||
|
||||
if sle.batch_no:
|
||||
sle.use_batchwise_valuation = frappe.db.get_value(
|
||||
"Batch", sle.batch_no, "use_batchwise_valuation", cache=True
|
||||
)
|
||||
if incorrect_idx is None and not is_sle_has_correct_data(sle, float_precision, currency_precision):
|
||||
incorrect_idx = idx
|
||||
|
||||
if filters.get("show_incorrect_entries"):
|
||||
if incorrect_idx > 0:
|
||||
sles = sles[cint(incorrect_idx) - 1 :]
|
||||
|
||||
return []
|
||||
if incorrect_idx is None:
|
||||
return []
|
||||
return sles[max(incorrect_idx - 1, 0) :]
|
||||
|
||||
return sles
|
||||
|
||||
|
||||
def is_sle_has_correct_data(sle, precision):
|
||||
if flt(sle.difference_in_qty, precision) != 0.0 or flt(sle.diff_value_diff, precision) != 0:
|
||||
print(flt(sle.difference_in_qty, precision), flt(sle.diff_value_diff, precision))
|
||||
return False
|
||||
def maintains_fifo_queue(sle):
|
||||
# no queue is maintained for serialized/batchwise-valued stock
|
||||
return not (
|
||||
sle.serial_and_batch_bundle or sle.serial_no or (sle.batch_no and sle.use_batchwise_valuation)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def add_fifo_fields(sle, prev_sle):
|
||||
queue = json.loads(sle.stock_queue) if sle.stock_queue else []
|
||||
|
||||
fifo_qty = 0.0
|
||||
fifo_value = 0.0
|
||||
for qty, rate in queue:
|
||||
fifo_qty += qty
|
||||
fifo_value += qty * rate
|
||||
|
||||
sle.fifo_queue_qty = fifo_qty
|
||||
sle.fifo_stock_value = fifo_value
|
||||
sle.fifo_valuation_rate = fifo_value / fifo_qty if fifo_qty else None
|
||||
sle.fifo_qty_diff = sle.qty_after_transaction - fifo_qty
|
||||
sle.fifo_value_diff = sle.stock_value - fifo_value
|
||||
sle.fifo_valuation_diff = (
|
||||
sle.valuation_rate - sle.fifo_valuation_rate if sle.fifo_valuation_rate else None
|
||||
)
|
||||
if prev_sle and prev_sle.fifo_stock_value is not None:
|
||||
sle.fifo_stock_diff = sle.fifo_stock_value - prev_sle.fifo_stock_value
|
||||
sle.fifo_difference_diff = sle.fifo_stock_diff - sle.stock_value_difference
|
||||
|
||||
|
||||
def is_sle_has_correct_data(sle, float_precision, currency_precision):
|
||||
return (
|
||||
flt(sle.difference_in_qty, float_precision) == 0.0
|
||||
and flt(sle.diff_value_diff, currency_precision) == 0.0
|
||||
)
|
||||
|
||||
|
||||
def get_columns():
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
WAREHOUSE = "Stores - _TC"
|
||||
COMPANY = "_Test Company"
|
||||
ITEM = "_Test Item"
|
||||
|
||||
|
||||
class TestStockLedgerInvariantCheck(ERPNextTestSuite):
|
||||
def run_report(self, **extra):
|
||||
filters = frappe._dict({"company": COMPANY, "warehouse": WAREHOUSE})
|
||||
filters.update(extra)
|
||||
return execute(filters)[1]
|
||||
|
||||
def make_movements(self) -> str:
|
||||
frappe.db.set_value("Item", ITEM, "valuation_method", "FIFO")
|
||||
make_stock_entry(item_code=ITEM, to_warehouse=WAREHOUSE, qty=10, rate=100, posting_date="2026-06-01")
|
||||
make_stock_entry(item_code=ITEM, to_warehouse=WAREHOUSE, qty=5, rate=120, posting_date="2026-06-02")
|
||||
make_stock_entry(item_code=ITEM, from_warehouse=WAREHOUSE, qty=4, rate=0, posting_date="2026-06-03")
|
||||
return ITEM
|
||||
|
||||
def test_diagnostic_rows_have_no_discrepancy(self):
|
||||
item = self.make_movements()
|
||||
|
||||
data = self.run_report(item_code=item)
|
||||
|
||||
self.assertEqual(len(data), 3)
|
||||
for row in data:
|
||||
self.assertLess(abs(row.difference_in_qty), 0.01)
|
||||
self.assertLess(abs(row.fifo_qty_diff), 0.01)
|
||||
self.assertLess(abs(row.diff_value_diff), 0.01)
|
||||
|
||||
def test_running_balance_matches(self):
|
||||
item = self.make_movements()
|
||||
|
||||
data = self.run_report(item_code=item)
|
||||
|
||||
self.assertEqual(data[-1].qty_after_transaction, 11)
|
||||
|
||||
def test_show_incorrect_entries(self):
|
||||
item = self.make_movements()
|
||||
|
||||
self.assertEqual(self.run_report(item_code=item, show_incorrect_entries=1), [])
|
||||
|
||||
sle = frappe.get_last_doc(
|
||||
"Stock Ledger Entry", {"item_code": item, "warehouse": WAREHOUSE, "is_cancelled": 0}
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Stock Ledger Entry", sle.name, "qty_after_transaction", sle.qty_after_transaction + 5
|
||||
)
|
||||
|
||||
data = self.run_report(item_code=item, show_incorrect_entries=1)
|
||||
self.assertEqual(len(data), 2) # incorrect entry + one before it for context
|
||||
self.assertEqual(data[-1].name, sle.name)
|
||||
|
||||
def test_batch_item_skips_fifo_queue_checks(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item = make_item(
|
||||
properties={"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "SLIC-BAT-.####"}
|
||||
).name
|
||||
make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=10, rate=100)
|
||||
|
||||
data = self.run_report(item_code=item)
|
||||
self.assertTrue(data)
|
||||
for row in data:
|
||||
self.assertIsNone(row.fifo_qty_diff)
|
||||
self.assertIsNone(row.fifo_value_diff)
|
||||
|
||||
self.assertEqual(self.run_report(item_code=item, show_incorrect_entries=1), [])
|
||||
@@ -205,7 +205,10 @@ def get_data(filters=None):
|
||||
|
||||
data = []
|
||||
if item_warehouse_map:
|
||||
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
|
||||
float_precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) or 3
|
||||
currency_precision = (
|
||||
cint(frappe.db.get_single_value("System Settings", "currency_precision")) or float_precision
|
||||
)
|
||||
|
||||
for item_warehouse in item_warehouse_map:
|
||||
report_data = stock_ledger_invariant_check(item_warehouse)
|
||||
@@ -215,7 +218,11 @@ def get_data(filters=None):
|
||||
|
||||
for row in report_data:
|
||||
if has_difference(
|
||||
row, precision, filters.difference_in, item_warehouse.valuation_method or valuation_method
|
||||
row,
|
||||
float_precision,
|
||||
currency_precision,
|
||||
filters.difference_in,
|
||||
item_warehouse.valuation_method or valuation_method,
|
||||
):
|
||||
row.update(
|
||||
{
|
||||
@@ -261,23 +268,26 @@ def get_item_warehouse_combinations(filters: dict | None = None) -> dict:
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
def has_difference(row, precision, difference_in, valuation_method):
|
||||
def has_difference(row, float_precision, currency_precision, difference_in, valuation_method):
|
||||
if valuation_method == "Moving Average":
|
||||
qty_diff = flt(row.difference_in_qty, precision)
|
||||
value_diff = flt(row.diff_value_diff, precision)
|
||||
valuation_diff = flt(row.valuation_diff, precision)
|
||||
qty_diff = flt(row.difference_in_qty, float_precision)
|
||||
value_diff = flt(row.diff_value_diff, currency_precision)
|
||||
valuation_diff = flt(row.valuation_diff, currency_precision)
|
||||
else:
|
||||
qty_diff = flt(row.difference_in_qty, precision)
|
||||
value_diff = flt(row.diff_value_diff, precision)
|
||||
qty_diff = flt(row.difference_in_qty, float_precision)
|
||||
value_diff = flt(row.diff_value_diff, currency_precision)
|
||||
|
||||
if row.stock_queue and json.loads(row.stock_queue):
|
||||
value_diff = value_diff or (
|
||||
flt(row.fifo_value_diff, precision) or flt(row.fifo_difference_diff, precision)
|
||||
flt(row.fifo_value_diff, currency_precision)
|
||||
or flt(row.fifo_difference_diff, currency_precision)
|
||||
)
|
||||
|
||||
qty_diff = qty_diff or flt(row.fifo_qty_diff, precision)
|
||||
qty_diff = qty_diff or flt(row.fifo_qty_diff, float_precision)
|
||||
|
||||
valuation_diff = flt(row.valuation_diff, precision) or flt(row.fifo_valuation_diff, precision)
|
||||
valuation_diff = flt(row.valuation_diff, currency_precision) or flt(
|
||||
row.fifo_valuation_diff, currency_precision
|
||||
)
|
||||
|
||||
if difference_in == "Qty" and qty_diff:
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user