mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-20 09:49:58 +00:00
fix: submittable product bundle issues
(cherry picked from commit a218b8db8c)
# Conflicts:
# erpnext/selling/doctype/product_bundle/product_bundle.js
# erpnext/selling/doctype/product_bundle/product_bundle_list.js
# erpnext/selling/doctype/product_bundle/test_product_bundle.py
# erpnext/stock/report/item_where_used/item_where_used.py
This commit is contained in:
@@ -9,5 +9,23 @@ frappe.ui.form.on("Product Bundle", {
|
||||
query: "erpnext.selling.doctype.product_bundle.product_bundle.get_new_item_code",
|
||||
};
|
||||
});
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
// 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(
|
||||
__("New Version"),
|
||||
() => {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.selling.doctype.product_bundle.product_bundle.make_new_version",
|
||||
frm: frm,
|
||||
});
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
}
|
||||
>>>>>>> a218b8db8c (fix: submittable product bundle issues)
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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": []
|
||||
|
||||
@@ -7,6 +7,15 @@ frappe.listview_settings["Product Bundle"] = {
|
||||
if (doc.disabled) {
|
||||
return [__("Disabled"), "grey", "disabled,=,1"];
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
return [__("Active"), "green", "disabled,=,0"];
|
||||
=======
|
||||
if (doc.docstatus === 1 && doc.is_active) {
|
||||
return [__("Active"), "green", "is_active,=,1|disabled,=,0|docstatus,=,1"];
|
||||
}
|
||||
if (doc.docstatus === 1 && !doc.is_active) {
|
||||
return [__("Inactive"), "gray", "is_active,=,0|disabled,=,0|docstatus,=,1"];
|
||||
}
|
||||
>>>>>>> a218b8db8c (fix: submittable product bundle issues)
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,3 +16,139 @@ def make_product_bundle(parent, items, qty=None):
|
||||
product_bundle.insert()
|
||||
|
||||
return product_bundle
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
|
||||
class TestProductBundle(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.parent = make_item("_Test PB Parent", {"is_stock_item": 0, "is_sales_item": 1}).name
|
||||
make_item("_Test PB Child A", {"is_stock_item": 1})
|
||||
make_item("_Test PB Child B", {"is_stock_item": 1})
|
||||
|
||||
def test_submit_makes_bundle_active_and_versioned(self):
|
||||
bundle = make_product_bundle(self.parent, ["_Test PB Child A"])
|
||||
self.assertEqual(bundle.docstatus, 1)
|
||||
self.assertEqual(bundle.is_active, 1)
|
||||
self.assertTrue(bundle.name.startswith("PB-"))
|
||||
self.assertEqual(get_active_product_bundle(self.parent), bundle.name)
|
||||
|
||||
def test_new_version_deactivates_previous(self):
|
||||
v1 = make_product_bundle(self.parent, ["_Test PB Child A"])
|
||||
|
||||
v2 = make_new_version(v1.name)
|
||||
v2.items[0].qty = 5
|
||||
v2.insert()
|
||||
v2.submit()
|
||||
|
||||
self.assertNotEqual(v1.name, v2.name)
|
||||
self.assertEqual(get_active_product_bundle(self.parent), v2.name)
|
||||
self.assertEqual(frappe.db.get_value("Product Bundle", v1.name, "is_active"), 0)
|
||||
|
||||
def test_reactivating_old_version_deactivates_current(self):
|
||||
v1 = make_product_bundle(self.parent, ["_Test PB Child A"])
|
||||
|
||||
v2 = make_new_version(v1.name)
|
||||
v2.items[0].qty = 5
|
||||
v2.insert()
|
||||
v2.submit()
|
||||
self.assertEqual(get_active_product_bundle(self.parent), v2.name)
|
||||
|
||||
# switch back to v1 by toggling is_active on the submitted doc (allow_on_submit)
|
||||
v1.reload()
|
||||
v1.is_active = 1
|
||||
v1.save()
|
||||
|
||||
self.assertEqual(get_active_product_bundle(self.parent), v1.name)
|
||||
self.assertEqual(frappe.db.get_value("Product Bundle", v2.name, "is_active"), 0)
|
||||
|
||||
def test_new_bundle_from_scratch_supersedes_existing(self):
|
||||
# An item that already has a bundle must remain selectable so a new version
|
||||
# can be created straight from the New Product Bundle form.
|
||||
from erpnext.selling.doctype.product_bundle.product_bundle import get_new_item_code
|
||||
|
||||
v1 = make_product_bundle(self.parent, ["_Test PB Child A"])
|
||||
|
||||
picker = [row[0] for row in get_new_item_code("Item", self.parent, "name", 0, 20, {})]
|
||||
self.assertIn(self.parent, picker)
|
||||
|
||||
v2 = frappe.get_doc({"doctype": "Product Bundle", "new_item_code": self.parent})
|
||||
v2.append("items", {"item_code": "_Test PB Child B", "qty": 1})
|
||||
v2.insert()
|
||||
v2.submit()
|
||||
|
||||
self.assertNotEqual(v1.name, v2.name)
|
||||
self.assertEqual(get_active_product_bundle(self.parent), v2.name)
|
||||
self.assertEqual(frappe.db.get_value("Product Bundle", v1.name, "is_active"), 0)
|
||||
|
||||
def test_cancel_clears_active(self):
|
||||
bundle = make_product_bundle(self.parent, ["_Test PB Child A"])
|
||||
bundle.cancel()
|
||||
self.assertEqual(frappe.db.get_value("Product Bundle", bundle.name, "is_active"), 0)
|
||||
self.assertIsNone(get_active_product_bundle(self.parent))
|
||||
|
||||
def test_submitted_bundle_is_immutable(self):
|
||||
bundle = make_product_bundle(self.parent, ["_Test PB Child A"])
|
||||
bundle.items[0].qty = 99
|
||||
self.assertRaises(frappe.exceptions.UpdateAfterSubmitError, bundle.save)
|
||||
|
||||
def test_disabled_bundle_is_not_resolved(self):
|
||||
bundle = make_product_bundle(self.parent, ["_Test PB Child A"])
|
||||
|
||||
bundle.disabled = 1
|
||||
bundle.save()
|
||||
self.assertIsNone(get_active_product_bundle(self.parent))
|
||||
|
||||
# disabling parks the version without ceding the active slot, so re-enabling
|
||||
# restores resolution without re-activation
|
||||
self.assertEqual(frappe.db.get_value("Product Bundle", bundle.name, "is_active"), 1)
|
||||
bundle.disabled = 0
|
||||
bundle.save()
|
||||
self.assertEqual(get_active_product_bundle(self.parent), bundle.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, ["_Test PB Child A"])
|
||||
bundle.disabled = 1
|
||||
bundle.save()
|
||||
|
||||
_, component_rows = execute({"item": "_Test PB Child A", "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, 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
|
||||
|
||||
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)
|
||||
>>>>>>> a218b8db8c (fix: submittable product bundle issues)
|
||||
|
||||
@@ -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,7 +279,13 @@ def get_product_bundle_component_rows(item):
|
||||
row_index=row.idx,
|
||||
quantity=row.qty,
|
||||
uom=row.uom,
|
||||
<<<<<<< HEAD
|
||||
is_active=0 if bundle.disabled else 1,
|
||||
=======
|
||||
stock_quantity=row.qty,
|
||||
stock_uom=row.uom,
|
||||
is_active=bundle.is_active,
|
||||
>>>>>>> a218b8db8c (fix: submittable product bundle issues)
|
||||
disabled=bundle.disabled,
|
||||
)
|
||||
)
|
||||
@@ -473,6 +475,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()
|
||||
|
||||
Reference in New Issue
Block a user