fix: submittable product bundle issues

(cherry picked from commit a218b8db8c)
This commit is contained in:
Mihir Kandoi
2026-06-22 15:40:16 +05:30
parent 0051950afb
commit 7a1def07e9
3 changed files with 102 additions and 12 deletions

View File

@@ -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": []

View File

@@ -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)

View File

@@ -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()