From 7a1def07e9d83f638a6643fc9547351146d3c9e3 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 22 Jun 2026 15:40:16 +0530 Subject: [PATCH 1/2] fix: submittable product bundle issues (cherry picked from commit a218b8db8cd732803afd46c8b8707a3d1714b600) --- .../product_bundle/product_bundle.json | 2 + .../product_bundle/test_product_bundle.py | 67 +++++++++++++++++++ .../report/item_where_used/item_where_used.py | 45 +++++++++---- 3 files changed, 102 insertions(+), 12 deletions(-) diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.json b/erpnext/selling/doctype/product_bundle/product_bundle.json index de335c55fca..803dfd3a61e 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.json +++ b/erpnext/selling/doctype/product_bundle/product_bundle.json @@ -26,6 +26,7 @@ "fieldtype": "Link", "in_global_search": 1, "in_list_view": 1, + "in_standard_filter": 1, "label": "Parent Item", "no_copy": 1, "oldfieldname": "new_item_code", @@ -116,6 +117,7 @@ "write": 1 } ], + "search_fields": "new_item_code,description", "sort_field": "creation", "sort_order": "ASC", "states": [] diff --git a/erpnext/selling/doctype/product_bundle/test_product_bundle.py b/erpnext/selling/doctype/product_bundle/test_product_bundle.py index 4a5df94da54..42c58aea44e 100644 --- a/erpnext/selling/doctype/product_bundle/test_product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/test_product_bundle.py @@ -3,6 +3,9 @@ import frappe +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.tests.utils import ERPNextTestSuite + def make_product_bundle(parent, items, qty=None): if frappe.db.exists("Product Bundle", parent): @@ -16,3 +19,67 @@ def make_product_bundle(parent, items, qty=None): product_bundle.insert() return product_bundle + + +class TestProductBundle(ERPNextTestSuite): + def setUp(self): + suffix = frappe.generate_hash(length=8) + self.parent = make_item(f"_Test PB Parent {suffix}", {"is_stock_item": 0, "is_sales_item": 1}).name + self.child = make_item(f"_Test PB Child {suffix}", {"is_stock_item": 1}).name + + def test_item_where_used_report_shows_disabled_flag(self): + from erpnext.stock.report.item_where_used.item_where_used import execute + + bundle = make_product_bundle(self.parent, [self.child]) + bundle.disabled = 1 + bundle.save() + + _, component_rows = execute({"item": self.child, "section": "Where Used"}) + rows = [r for r in component_rows if r.document_name == bundle.name] + self.assertTrue(rows) + self.assertEqual(rows[0].disabled, 1) + self.assertEqual(rows[0].is_active, 0) + self.assertEqual(rows[0].stock_quantity, rows[0].quantity) + self.assertEqual(rows[0].stock_uom, rows[0].uom) + + _, parent_rows = execute({"item": self.parent, "section": "References"}) + rows = [r for r in parent_rows if r.document_name == bundle.name] + self.assertTrue(rows) + self.assertEqual(rows[0].disabled, 1) + + def test_item_where_used_report_hides_internal_and_empty_columns(self): + from erpnext.stock.report.item_where_used.item_where_used import execute + + bundle = make_product_bundle(self.parent, [self.child]) + + columns, rows = execute({"item": self.child, "section": "Where Used"}) + fieldnames = [column["fieldname"] for column in columns] + + self.assertIn("stock_quantity", fieldnames) + self.assertIn("stock_uom", fieldnames) + self.assertNotIn("matched_field", fieldnames) + self.assertNotIn("company", fieldnames) + + rows = [r for r in rows if r.document_name == bundle.name] + self.assertTrue(rows) + self.assertEqual(rows[0].stock_quantity, rows[0].quantity) + self.assertEqual(rows[0].stock_uom, rows[0].uom) + + def test_item_where_used_report_hides_false_check_columns(self): + from erpnext.stock.report.item_where_used.item_where_used import get_columns + + columns = get_columns([frappe._dict(stock_quantity=0, is_default=0)]) + fieldnames = [column["fieldname"] for column in columns] + + self.assertIn("stock_quantity", fieldnames) + self.assertNotIn("is_default", fieldnames) + + def test_child_cannot_be_active_bundle(self): + make_product_bundle(self.parent, [self.child]) + outer = make_item( + f"_Test PB Outer {frappe.generate_hash(length=8)}", {"is_stock_item": 0, "is_sales_item": 1} + ).name + + doc = frappe.get_doc({"doctype": "Product Bundle", "new_item_code": outer}) + doc.append("items", {"item_code": self.parent, "qty": 1}) + self.assertRaises(frappe.ValidationError, doc.insert) diff --git a/erpnext/stock/report/item_where_used/item_where_used.py b/erpnext/stock/report/item_where_used/item_where_used.py index 2c193b4a6bd..6f95bd51463 100644 --- a/erpnext/stock/report/item_where_used/item_where_used.py +++ b/erpnext/stock/report/item_where_used/item_where_used.py @@ -10,16 +10,16 @@ REFERENCES_SECTION = "References" def execute(filters=None): filters = frappe._dict(filters or {}) - columns = get_columns() + data = [] - if not filters.get("item"): - return columns, [] + if filters.get("item"): + data = get_data(filters) - return columns, get_data(filters) + return get_columns(data), data -def get_columns(): - return [ +def get_columns(data=None): + columns = [ { "fieldname": "section", "label": _("Section"), @@ -52,12 +52,6 @@ def get_columns(): "options": "Item", "width": 180, }, - { - "fieldname": "matched_field", - "label": _("Matched Field"), - "fieldtype": "Data", - "width": 180, - }, { "fieldname": "row_index", "label": _("Row"), @@ -123,6 +117,8 @@ def get_columns(): }, ] + return hide_empty_optional_columns(columns, data or []) + def get_data(filters): data = [] @@ -283,6 +279,8 @@ def get_product_bundle_component_rows(item): row_index=row.idx, quantity=row.qty, uom=row.uom, + stock_quantity=row.qty, + stock_uom=row.uom, is_active=0 if bundle.disabled else 1, disabled=bundle.disabled, ) @@ -473,6 +471,29 @@ def build_row(**kwargs): return frappe._dict(kwargs) +def hide_empty_optional_columns(columns, data): + optional_fields = {"stock_quantity", "stock_uom", "company", "is_default", "details"} + + if not data: + return columns + + fields_with_values = set() + for row in data: + for fieldname in optional_fields: + if field_has_value(row, fieldname): + fields_with_values.add(fieldname) + + return [column for column in columns if column["fieldname"] not in optional_fields - fields_with_values] + + +def field_has_value(row, fieldname): + if fieldname not in row: + return False + + value = row.get(fieldname) + return value is not None and value != "" + + def get_unique_names(names): unique_names = [] seen = set() From c0668809786211fdf62b9d12eb7d7137b6394af1 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 22 Jun 2026 15:53:52 +0530 Subject: [PATCH 2/2] fix: address product bundle review comments (cherry picked from commit d48a1e0d1698bb1fcba38584ba4623f9acddba9e) --- .../stock/report/item_where_used/item_where_used.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/report/item_where_used/item_where_used.py b/erpnext/stock/report/item_where_used/item_where_used.py index 6f95bd51463..4fecf71b35b 100644 --- a/erpnext/stock/report/item_where_used/item_where_used.py +++ b/erpnext/stock/report/item_where_used/item_where_used.py @@ -479,18 +479,23 @@ def hide_empty_optional_columns(columns, data): fields_with_values = set() for row in data: - for fieldname in optional_fields: - if field_has_value(row, fieldname): - fields_with_values.add(fieldname) + for column in columns: + fieldname = column["fieldname"] + if fieldname in optional_fields and field_has_value(row, column): + fields_with_values.add(column["fieldname"]) return [column for column in columns if column["fieldname"] not in optional_fields - fields_with_values] -def field_has_value(row, fieldname): +def field_has_value(row, column): + fieldname = column["fieldname"] if fieldname not in row: return False value = row.get(fieldname) + if column.get("fieldtype") == "Check": + return bool(value) + return value is not None and value != ""