From 6d97a5d543d8f9fe5c07fe0d5ea9ff8b8a3d9a06 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 10:25:33 +0530 Subject: [PATCH 1/5] test: add coverage for Item Prices report Co-Authored-By: Claude Opus 4.8 (1M context) --- .../report/item_prices/test_item_prices.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 erpnext/stock/report/item_prices/test_item_prices.py diff --git a/erpnext/stock/report/item_prices/test_item_prices.py b/erpnext/stock/report/item_prices/test_item_prices.py new file mode 100644 index 00000000000..d77b5f60716 --- /dev/null +++ b/erpnext/stock/report/item_prices/test_item_prices.py @@ -0,0 +1,63 @@ +# 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.report.item_prices.item_prices import execute +from erpnext.tests.utils import ERPNextTestSuite + +# Positional columns returned by the report (it returns string-format columns, +# not dicts, so rows are plain lists indexed by position). +ITEM_CODE = 0 +SALES_PRICE_LIST = 8 +PURCHASE_PRICE_LIST = 9 + + +class TestItemPrices(ERPNextTestSuite): + """Correctness tests for the Item Prices report.""" + + def run_report(self, **extra): + filters = frappe._dict({"items": "Enabled Items only", **extra}) + return execute(filters)[1] + + def row_for(self, data, item_code): + for row in data: + if row[ITEM_CODE] == item_code: + return row + self.fail(f"No report row found for item {item_code}") + + def test_item_selling_price_listed(self): + """A Standard Selling Item Price shows up in the Sales Price List column.""" + item = make_item(properties={"is_stock_item": 1}).name + frappe.get_doc( + { + "doctype": "Item Price", + "item_code": item, + "price_list": "Standard Selling", + "price_list_rate": 250, + } + ).insert() + + row = self.row_for(self.run_report(), item) + self.assertIn("250.0", row[SALES_PRICE_LIST]) + self.assertIn("Standard Selling", row[SALES_PRICE_LIST]) + # A selling price must not leak into the buying column. + self.assertFalse(row[PURCHASE_PRICE_LIST]) + + def test_item_buying_price_listed(self): + """A Standard Buying Item Price shows up in the Purchase Price List column.""" + item = make_item(properties={"is_stock_item": 1}).name + frappe.get_doc( + { + "doctype": "Item Price", + "item_code": item, + "price_list": "Standard Buying", + "price_list_rate": 175, + } + ).insert() + + row = self.row_for(self.run_report(), item) + self.assertIn("175.0", row[PURCHASE_PRICE_LIST]) + self.assertIn("Standard Buying", row[PURCHASE_PRICE_LIST]) + self.assertFalse(row[SALES_PRICE_LIST]) From 79bd6a9b7d9dc16fe5e2d2638f2c760be77ac20b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 13:27:09 +0530 Subject: [PATCH 2/5] test: reuse BootStrapTestData master data to reduce runtime Co-Authored-By: Claude Opus 4.8 (1M context) --- erpnext/stock/report/item_prices/test_item_prices.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/report/item_prices/test_item_prices.py b/erpnext/stock/report/item_prices/test_item_prices.py index d77b5f60716..bbc21396418 100644 --- a/erpnext/stock/report/item_prices/test_item_prices.py +++ b/erpnext/stock/report/item_prices/test_item_prices.py @@ -3,7 +3,6 @@ import frappe -from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.report.item_prices.item_prices import execute from erpnext.tests.utils import ERPNextTestSuite @@ -29,7 +28,7 @@ class TestItemPrices(ERPNextTestSuite): def test_item_selling_price_listed(self): """A Standard Selling Item Price shows up in the Sales Price List column.""" - item = make_item(properties={"is_stock_item": 1}).name + item = "_Test Item" frappe.get_doc( { "doctype": "Item Price", @@ -43,11 +42,12 @@ class TestItemPrices(ERPNextTestSuite): self.assertIn("250.0", row[SALES_PRICE_LIST]) self.assertIn("Standard Selling", row[SALES_PRICE_LIST]) # A selling price must not leak into the buying column. - self.assertFalse(row[PURCHASE_PRICE_LIST]) + self.assertNotIn("250.0", row[PURCHASE_PRICE_LIST] or "") + self.assertNotIn("Standard Selling", row[PURCHASE_PRICE_LIST] or "") def test_item_buying_price_listed(self): """A Standard Buying Item Price shows up in the Purchase Price List column.""" - item = make_item(properties={"is_stock_item": 1}).name + item = "_Test Item 2" frappe.get_doc( { "doctype": "Item Price", @@ -60,4 +60,6 @@ class TestItemPrices(ERPNextTestSuite): row = self.row_for(self.run_report(), item) self.assertIn("175.0", row[PURCHASE_PRICE_LIST]) self.assertIn("Standard Buying", row[PURCHASE_PRICE_LIST]) - self.assertFalse(row[SALES_PRICE_LIST]) + # A buying price must not leak into the selling column. + self.assertNotIn("175.0", row[SALES_PRICE_LIST] or "") + self.assertNotIn("Standard Buying", row[SALES_PRICE_LIST] or "") From 87af67febe34509196f75ad1f0a073a32d835c79 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 26 Jun 2026 15:00:39 +0530 Subject: [PATCH 3/5] test: cover last purchase rate and valuation rate in Item Prices report --- .../report/item_prices/test_item_prices.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/erpnext/stock/report/item_prices/test_item_prices.py b/erpnext/stock/report/item_prices/test_item_prices.py index bbc21396418..6c521c9e025 100644 --- a/erpnext/stock/report/item_prices/test_item_prices.py +++ b/erpnext/stock/report/item_prices/test_item_prices.py @@ -3,12 +3,16 @@ import frappe +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.item_prices.item_prices import execute from erpnext.tests.utils import ERPNextTestSuite # Positional columns returned by the report (it returns string-format columns, # not dicts, so rows are plain lists indexed by position). ITEM_CODE = 0 +LAST_PURCHASE_RATE = 6 +VALUATION_RATE = 7 SALES_PRICE_LIST = 8 PURCHASE_PRICE_LIST = 9 @@ -63,3 +67,24 @@ class TestItemPrices(ERPNextTestSuite): # A buying price must not leak into the selling column. self.assertNotIn("175.0", row[SALES_PRICE_LIST] or "") self.assertNotIn("Standard Buying", row[SALES_PRICE_LIST] or "") + + def test_last_purchase_rate_from_receipt(self): + """The latest purchase rate (from a Purchase Receipt) shows in the Last Purchase Rate column.""" + item = "_Test Item" + make_purchase_receipt( + item_code=item, qty=5, rate=500, company="_Test Company", posting_date="2026-06-01" + ) + + row = self.row_for(self.run_report(), item) + self.assertEqual(row[LAST_PURCHASE_RATE], 500) + + def test_valuation_rate_from_stock(self): + """The Bin valuation rate shows in the Valuation Rate column.""" + # _Test FG Item has no opening-stock baseline, so its valuation reflects only this receipt + item = "_Test FG Item" + make_stock_entry( + item_code=item, to_warehouse="Stores - _TC", qty=10, rate=250, posting_date="2026-06-01" + ) + + row = self.row_for(self.run_report(), item) + self.assertEqual(row[VALUATION_RATE], 250) From 039314c3066775a5a04d7fcfbb01454f45f68661 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 19:22:30 +0530 Subject: [PATCH 4/5] test: make Item Prices tests deterministic (fresh items, label-based columns) --- .../report/item_prices/test_item_prices.py | 64 +++++++++++-------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/erpnext/stock/report/item_prices/test_item_prices.py b/erpnext/stock/report/item_prices/test_item_prices.py index 6c521c9e025..147a3d4bc34 100644 --- a/erpnext/stock/report/item_prices/test_item_prices.py +++ b/erpnext/stock/report/item_prices/test_item_prices.py @@ -3,33 +3,36 @@ import frappe +from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.report.item_prices.item_prices import execute from erpnext.tests.utils import ERPNextTestSuite -# Positional columns returned by the report (it returns string-format columns, -# not dicts, so rows are plain lists indexed by position). -ITEM_CODE = 0 -LAST_PURCHASE_RATE = 6 -VALUATION_RATE = 7 -SALES_PRICE_LIST = 8 -PURCHASE_PRICE_LIST = 9 - class TestItemPrices(ERPNextTestSuite): """Correctness tests for the Item Prices report.""" def run_report(self, **extra): filters = frappe._dict({"items": "Enabled Items only", **extra}) - return execute(filters)[1] + return execute(filters)[:2] - def row_for(self, data, item_code): + # The report returns string-format columns ("Label:fieldtype:width"); resolve positions + # by label so the tests self-correct if the column order changes. + @staticmethod + def labels(columns): + return [c.split(":")[0] if isinstance(c, str) else c.get("label") for c in columns] + + def row_for(self, columns, data, item_code): + item_idx = self.labels(columns).index("Item") for row in data: - if row[ITEM_CODE] == item_code: + if row[item_idx] == item_code: return row self.fail(f"No report row found for item {item_code}") + def cell(self, columns, row, label): + return row[self.labels(columns).index(label)] + def test_item_selling_price_listed(self): """A Standard Selling Item Price shows up in the Sales Price List column.""" item = "_Test Item" @@ -42,12 +45,13 @@ class TestItemPrices(ERPNextTestSuite): } ).insert() - row = self.row_for(self.run_report(), item) - self.assertIn("250.0", row[SALES_PRICE_LIST]) - self.assertIn("Standard Selling", row[SALES_PRICE_LIST]) + columns, data = self.run_report() + row = self.row_for(columns, data, item) + self.assertIn("250.0", self.cell(columns, row, "Sales Price List")) + self.assertIn("Standard Selling", self.cell(columns, row, "Sales Price List")) # A selling price must not leak into the buying column. - self.assertNotIn("250.0", row[PURCHASE_PRICE_LIST] or "") - self.assertNotIn("Standard Selling", row[PURCHASE_PRICE_LIST] or "") + self.assertNotIn("250.0", self.cell(columns, row, "Purchase Price List") or "") + self.assertNotIn("Standard Selling", self.cell(columns, row, "Purchase Price List") or "") def test_item_buying_price_listed(self): """A Standard Buying Item Price shows up in the Purchase Price List column.""" @@ -61,30 +65,34 @@ class TestItemPrices(ERPNextTestSuite): } ).insert() - row = self.row_for(self.run_report(), item) - self.assertIn("175.0", row[PURCHASE_PRICE_LIST]) - self.assertIn("Standard Buying", row[PURCHASE_PRICE_LIST]) + columns, data = self.run_report() + row = self.row_for(columns, data, item) + self.assertIn("175.0", self.cell(columns, row, "Purchase Price List")) + self.assertIn("Standard Buying", self.cell(columns, row, "Purchase Price List")) # A buying price must not leak into the selling column. - self.assertNotIn("175.0", row[SALES_PRICE_LIST] or "") - self.assertNotIn("Standard Buying", row[SALES_PRICE_LIST] or "") + self.assertNotIn("175.0", self.cell(columns, row, "Sales Price List") or "") + self.assertNotIn("Standard Buying", self.cell(columns, row, "Sales Price List") or "") def test_last_purchase_rate_from_receipt(self): """The latest purchase rate (from a Purchase Receipt) shows in the Last Purchase Rate column.""" - item = "_Test Item" + # a fresh item has no other committed purchase records, so it is the only (and latest) row + item = make_item(properties={"is_stock_item": 1, "is_purchase_item": 1}).name make_purchase_receipt( item_code=item, qty=5, rate=500, company="_Test Company", posting_date="2026-06-01" ) - row = self.row_for(self.run_report(), item) - self.assertEqual(row[LAST_PURCHASE_RATE], 500) + columns, data = self.run_report() + row = self.row_for(columns, data, item) + self.assertEqual(self.cell(columns, row, "Last Purchase Rate"), 500) def test_valuation_rate_from_stock(self): """The Bin valuation rate shows in the Valuation Rate column.""" - # _Test FG Item has no opening-stock baseline, so its valuation reflects only this receipt - item = "_Test FG Item" + # a fresh item has no other committed bins, so its average valuation is exactly this receipt's rate + item = make_item(properties={"is_stock_item": 1}).name make_stock_entry( item_code=item, to_warehouse="Stores - _TC", qty=10, rate=250, posting_date="2026-06-01" ) - row = self.row_for(self.run_report(), item) - self.assertEqual(row[VALUATION_RATE], 250) + columns, data = self.run_report() + row = self.row_for(columns, data, item) + self.assertEqual(self.cell(columns, row, "Valuation Rate"), 250) From ea5be1f7a55c63a9522ea760f84bb648c37e8400 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 1 Jul 2026 20:12:42 +0530 Subject: [PATCH 5/5] fix: minor fix Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- erpnext/stock/report/item_prices/test_item_prices.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/report/item_prices/test_item_prices.py b/erpnext/stock/report/item_prices/test_item_prices.py index 147a3d4bc34..49e36c0429a 100644 --- a/erpnext/stock/report/item_prices/test_item_prices.py +++ b/erpnext/stock/report/item_prices/test_item_prices.py @@ -29,6 +29,7 @@ class TestItemPrices(ERPNextTestSuite): if row[item_idx] == item_code: return row self.fail(f"No report row found for item {item_code}") + return None def cell(self, columns, row, label): return row[self.labels(columns).index(label)]