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..4fecf71b35b 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,34 @@ 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 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, 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 != "" + + def get_unique_names(names): unique_names = [] seen = set()