mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-15 15:38:39 +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",
|
"outgoing_rate",
|
||||||
"stock_queue",
|
"stock_queue",
|
||||||
"batch_no",
|
"batch_no",
|
||||||
|
"serial_no",
|
||||||
"stock_value",
|
"stock_value",
|
||||||
"stock_value_difference",
|
"stock_value_difference",
|
||||||
"valuation_rate",
|
"valuation_rate",
|
||||||
@@ -52,16 +53,16 @@ def add_invariant_check_fields(sles, filters):
|
|||||||
balance_qty = 0.0
|
balance_qty = 0.0
|
||||||
balance_stock_value = 0.0
|
balance_stock_value = 0.0
|
||||||
|
|
||||||
incorrect_idx = 0
|
incorrect_idx = None
|
||||||
precision = frappe.get_precision("Stock Ledger Entry", "actual_qty")
|
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):
|
for idx, sle in enumerate(sles):
|
||||||
queue = json.loads(sle.stock_queue) if sle.stock_queue else []
|
if sle.batch_no:
|
||||||
|
sle.use_batchwise_valuation = frappe.db.get_value(
|
||||||
fifo_qty = 0.0
|
"Batch", sle.batch_no, "use_batchwise_valuation", cache=True
|
||||||
fifo_value = 0.0
|
)
|
||||||
for qty, rate in queue:
|
|
||||||
fifo_qty += qty
|
|
||||||
fifo_value += qty * rate
|
|
||||||
|
|
||||||
if sle.actual_qty < 0:
|
if sle.actual_qty < 0:
|
||||||
sle.consumption_rate = sle.stock_value_difference / sle.actual_qty
|
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:
|
if balance_qty is None:
|
||||||
balance_qty = sle.qty_after_transaction
|
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.balance_value_by_qty = (
|
||||||
sle.stock_value / sle.qty_after_transaction if sle.qty_after_transaction else None
|
sle.stock_value / sle.qty_after_transaction if sle.qty_after_transaction else None
|
||||||
)
|
)
|
||||||
sle.expected_qty_after_transaction = balance_qty
|
sle.expected_qty_after_transaction = balance_qty
|
||||||
sle.stock_value_from_diff = balance_stock_value
|
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.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_diff = (
|
||||||
sle.valuation_rate - sle.balance_value_by_qty if sle.balance_value_by_qty else None
|
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
|
sle.diff_value_diff = sle.stock_value_from_diff - sle.stock_value
|
||||||
|
|
||||||
if not incorrect_idx and filters.get("show_incorrect_entries"):
|
if maintains_fifo_queue(sle):
|
||||||
if is_sle_has_correct_data(sle, precision):
|
add_fifo_fields(sle, sles[idx - 1] if idx else None)
|
||||||
continue
|
|
||||||
else:
|
|
||||||
incorrect_idx = idx
|
|
||||||
|
|
||||||
if idx > 0:
|
if incorrect_idx is None and not is_sle_has_correct_data(sle, float_precision, currency_precision):
|
||||||
sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value
|
incorrect_idx = idx
|
||||||
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 filters.get("show_incorrect_entries"):
|
if filters.get("show_incorrect_entries"):
|
||||||
if incorrect_idx > 0:
|
if incorrect_idx is None:
|
||||||
sles = sles[cint(incorrect_idx) - 1 :]
|
return []
|
||||||
|
return sles[max(incorrect_idx - 1, 0) :]
|
||||||
return []
|
|
||||||
|
|
||||||
return sles
|
return sles
|
||||||
|
|
||||||
|
|
||||||
def is_sle_has_correct_data(sle, precision):
|
def maintains_fifo_queue(sle):
|
||||||
if flt(sle.difference_in_qty, precision) != 0.0 or flt(sle.diff_value_diff, precision) != 0:
|
# no queue is maintained for serialized/batchwise-valued stock
|
||||||
print(flt(sle.difference_in_qty, precision), flt(sle.diff_value_diff, precision))
|
return not (
|
||||||
return False
|
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():
|
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 = []
|
data = []
|
||||||
if item_warehouse_map:
|
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:
|
for item_warehouse in item_warehouse_map:
|
||||||
report_data = stock_ledger_invariant_check(item_warehouse)
|
report_data = stock_ledger_invariant_check(item_warehouse)
|
||||||
@@ -215,7 +218,11 @@ def get_data(filters=None):
|
|||||||
|
|
||||||
for row in report_data:
|
for row in report_data:
|
||||||
if has_difference(
|
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(
|
row.update(
|
||||||
{
|
{
|
||||||
@@ -261,23 +268,26 @@ def get_item_warehouse_combinations(filters: dict | None = None) -> dict:
|
|||||||
return query.run(as_dict=1)
|
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":
|
if valuation_method == "Moving Average":
|
||||||
qty_diff = flt(row.difference_in_qty, precision)
|
qty_diff = flt(row.difference_in_qty, float_precision)
|
||||||
value_diff = flt(row.diff_value_diff, precision)
|
value_diff = flt(row.diff_value_diff, currency_precision)
|
||||||
valuation_diff = flt(row.valuation_diff, precision)
|
valuation_diff = flt(row.valuation_diff, currency_precision)
|
||||||
else:
|
else:
|
||||||
qty_diff = flt(row.difference_in_qty, precision)
|
qty_diff = flt(row.difference_in_qty, float_precision)
|
||||||
value_diff = flt(row.diff_value_diff, precision)
|
value_diff = flt(row.diff_value_diff, currency_precision)
|
||||||
|
|
||||||
if row.stock_queue and json.loads(row.stock_queue):
|
if row.stock_queue and json.loads(row.stock_queue):
|
||||||
value_diff = value_diff or (
|
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:
|
if difference_in == "Qty" and qty_diff:
|
||||||
return True
|
return True
|
||||||
|
|||||||
Reference in New Issue
Block a user