From 06592a49c85e45ddbb8a2f062dfccb9dffe7768d Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:19:45 +0530 Subject: [PATCH 1/3] test: add coverage for Incorrect Serial No Valuation report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_incorrect_serial_no_valuation.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py new file mode 100644 index 00000000000..96b5ca11451 --- /dev/null +++ b/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py @@ -0,0 +1,66 @@ +# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext.stock.report.incorrect_serial_no_valuation.incorrect_serial_no_valuation import execute +from erpnext.tests.utils import ERPNextTestSuite + + +class TestIncorrectSerialNoValuation(ERPNextTestSuite): + def run_report(self, **extra): + filters = frappe._dict({"company": "_Test Company"}) + filters.update(extra) + return execute(filters)[1] + + def make_serial_item(self): + return make_item( + properties={ + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "ISV-.#####", + } + ).name + + def test_healthy_serial_item_not_flagged(self): + item = self.make_serial_item() + + make_stock_entry( + item_code=item, + to_warehouse="_Test Warehouse - _TC", + qty=3, + rate=100, + posting_date="2026-06-01", + ) + make_stock_entry( + item_code=item, + from_warehouse="_Test Warehouse - _TC", + qty=1, + posting_date="2026-06-02", + ) + + data = self.run_report(item_code=item) + + flagged_items = {row.get("item_code") for row in data if isinstance(row, dict)} + self.assertNotIn(item, flagged_items) + + def test_only_balance_row_when_filtered_to_healthy_item(self): + item = self.make_serial_item() + + make_stock_entry( + item_code=item, + to_warehouse="_Test Warehouse - _TC", + qty=3, + rate=100, + posting_date="2026-06-01", + ) + + data = self.run_report(item_code=item) + + # The report always appends a single "Balance" summary row. A healthy + # serial item contributes no detail rows, so only that summary remains. + self.assertEqual(len(data), 1) + self.assertEqual(data[-1].get("qty"), 0) + self.assertEqual(data[-1].get("valuation_rate"), 0) From 7da4bc46bfa34cf00de716d20cc7cfab1e15e940 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:26:54 +0530 Subject: [PATCH 2/3] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_incorrect_serial_no_valuation.py | 35 +++++++------------ 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py index 96b5ca11451..9015c06cd1d 100644 --- a/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py +++ b/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py @@ -3,11 +3,13 @@ import frappe -from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.incorrect_serial_no_valuation.incorrect_serial_no_valuation import execute from erpnext.tests.utils import ERPNextTestSuite +SERIAL_ITEM = "_Test Serialized Item With Series" +WAREHOUSE = "Stores - _TC" + class TestIncorrectSerialNoValuation(ERPNextTestSuite): def run_report(self, **extra): @@ -15,49 +17,36 @@ class TestIncorrectSerialNoValuation(ERPNextTestSuite): filters.update(extra) return execute(filters)[1] - def make_serial_item(self): - return make_item( - properties={ - "is_stock_item": 1, - "has_serial_no": 1, - "serial_no_series": "ISV-.#####", - } - ).name - def test_healthy_serial_item_not_flagged(self): - item = self.make_serial_item() - make_stock_entry( - item_code=item, - to_warehouse="_Test Warehouse - _TC", + item_code=SERIAL_ITEM, + to_warehouse=WAREHOUSE, qty=3, rate=100, posting_date="2026-06-01", ) make_stock_entry( - item_code=item, - from_warehouse="_Test Warehouse - _TC", + item_code=SERIAL_ITEM, + from_warehouse=WAREHOUSE, qty=1, posting_date="2026-06-02", ) - data = self.run_report(item_code=item) + data = self.run_report(item_code=SERIAL_ITEM) flagged_items = {row.get("item_code") for row in data if isinstance(row, dict)} - self.assertNotIn(item, flagged_items) + self.assertNotIn(SERIAL_ITEM, flagged_items) def test_only_balance_row_when_filtered_to_healthy_item(self): - item = self.make_serial_item() - make_stock_entry( - item_code=item, - to_warehouse="_Test Warehouse - _TC", + item_code=SERIAL_ITEM, + to_warehouse=WAREHOUSE, qty=3, rate=100, posting_date="2026-06-01", ) - data = self.run_report(item_code=item) + data = self.run_report(item_code=SERIAL_ITEM) # The report always appends a single "Balance" summary row. A healthy # serial item contributes no detail rows, so only that summary remains. From 527765001c528d868dad129d22e716db1220a355 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 19:27:56 +0530 Subject: [PATCH 3/3] test: flag a serial with mismatched in/out valuation --- .../test_incorrect_serial_no_valuation.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py index 9015c06cd1d..81f736928fe 100644 --- a/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py +++ b/erpnext/stock/report/incorrect_serial_no_valuation/test_incorrect_serial_no_valuation.py @@ -3,6 +3,7 @@ import frappe +from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.incorrect_serial_no_valuation.incorrect_serial_no_valuation import execute from erpnext.tests.utils import ERPNextTestSuite @@ -53,3 +54,28 @@ class TestIncorrectSerialNoValuation(ERPNextTestSuite): self.assertEqual(len(data), 1) self.assertEqual(data[-1].get("qty"), 0) self.assertEqual(data[-1].get("valuation_rate"), 0) + + def test_mismatched_in_out_valuation_is_flagged(self): + # fresh serial item so only this test's serial movements are considered + item = make_item( + properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "ISV-BAD-.#####"} + ).name + + make_stock_entry(item_code=item, to_warehouse=WAREHOUSE, qty=1, rate=100, posting_date="2026-06-01") + serial_no = frappe.get_all("Serial No", filters={"item_code": item}, pluck="name")[0] + make_stock_entry(item_code=item, from_warehouse=WAREHOUSE, qty=1, posting_date="2026-06-02") + + # corrupt the outgoing valuation so the serial's in (100) and out no longer cancel: + # net qty is 0 but a residual value remains, which the report must flag + frappe.db.set_value( + "Serial and Batch Entry", + {"serial_no": serial_no, "qty": ["<", 0]}, + "incoming_rate", + 60, + update_modified=False, + ) + + data = self.run_report(item_code=item) + + flagged_serials = {row.get("serial_no") for row in data if isinstance(row, dict)} + self.assertIn(serial_no, flagged_serials)