fix: submittable product bundle issues

This commit is contained in:
Mihir Kandoi
2026-06-22 15:40:16 +05:30
parent 9436ab7f19
commit a218b8db8c
5 changed files with 68 additions and 57 deletions

View File

@@ -13,50 +13,16 @@ frappe.ui.form.on("Product Bundle", {
// A submitted bundle is immutable. To change it, create a new version
// (a fresh draft copied from this one) and submit that instead.
if (frm.doc.docstatus === 1) {
frm.add_custom_button(__("Create New Version"), () => {
frappe.model.open_mapped_doc({
method: "erpnext.selling.doctype.product_bundle.product_bundle.make_new_version",
frm: frm,
});
});
frm.add_custom_button(
__("New Version"),
() => {
frappe.model.open_mapped_doc({
method: "erpnext.selling.doctype.product_bundle.product_bundle.make_new_version",
frm: frm,
});
},
__("Actions")
);
}
show_supersede_hint(frm);
},
new_item_code: function (frm) {
show_supersede_hint(frm);
},
});
function show_supersede_hint(frm) {
// Warn (non-blocking) when the chosen Parent Item already has an active bundle:
// submitting this draft will create a new version and deactivate that one.
frm.set_intro("");
if (frm.doc.docstatus !== 0 || !frm.doc.new_item_code) {
return;
}
frappe.db
.get_value(
"Product Bundle",
{
new_item_code: frm.doc.new_item_code,
is_active: 1,
docstatus: 1,
},
"name"
)
.then((r) => {
const active = r.message && r.message.name;
if (active && active !== frm.doc.name) {
frm.set_intro(
__(
"Item {0} already has an active Product Bundle ({1}). Submitting this will create a new version and deactivate {1}.",
[frm.doc.new_item_code, active]
),
"orange"
);
}
});
}

View File

@@ -28,6 +28,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",
@@ -145,6 +146,7 @@
"write": 1
}
],
"search_fields": "new_item_code,description",
"sort_field": "creation",
"sort_order": "ASC",
"states": []

View File

@@ -12,6 +12,8 @@ frappe.listview_settings["Product Bundle"] = {
if (doc.docstatus === 1 && doc.is_active) {
return [__("Active"), "green", "is_active,=,1|disabled,=,0|docstatus,=,1"];
}
// inactive submitted versions keep the default "Submitted" indicator
if (doc.docstatus === 1 && !doc.is_active) {
return [__("Inactive"), "gray", "is_active,=,0|disabled,=,0|docstatus,=,1"];
}
},
};

View File

@@ -129,12 +129,32 @@ class TestProductBundle(ERPNextTestSuite):
self.assertTrue(rows)
self.assertEqual(rows[0].disabled, 1)
self.assertEqual(rows[0].is_active, 1)
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, ["_Test PB Child A"])
columns, rows = execute({"item": "_Test PB Child A", "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_child_cannot_be_active_bundle(self):
make_product_bundle(self.parent, ["_Test PB Child A"])
outer = make_item("_Test PB Outer", {"is_stock_item": 0, "is_sales_item": 1}).name

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=bundle.is_active,
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()