mirror of
https://github.com/frappe/erpnext.git
synced 2026-10-29 22:57:42 +00:00
fix: reject self-referencing and nested product bundles
This commit is contained in:
@@ -32,6 +32,7 @@ class ProductBundle(Document):
|
||||
def validate(self):
|
||||
self.validate_main_item()
|
||||
self.validate_child_items()
|
||||
self.validate_not_nested()
|
||||
self.validate_child_items_uom()
|
||||
self.validate_child_items_qty_non_zero()
|
||||
from erpnext.utilities.transaction_base import validate_uom_is_integer
|
||||
@@ -83,6 +84,12 @@ class ProductBundle(Document):
|
||||
|
||||
def validate_child_items(self):
|
||||
for item in self.items:
|
||||
if item.item_code == self.new_item_code:
|
||||
frappe.throw(
|
||||
_("Row #{0}: A Product Bundle cannot contain its own parent Item {1}.").format(
|
||||
item.idx, frappe.bold(item.item_code)
|
||||
)
|
||||
)
|
||||
if frappe.db.exists("Product Bundle", {"name": item.item_code, "disabled": 0}):
|
||||
frappe.throw(
|
||||
_(
|
||||
@@ -90,6 +97,27 @@ class ProductBundle(Document):
|
||||
).format(item.idx, frappe.bold(item.item_code))
|
||||
)
|
||||
|
||||
def validate_not_nested(self):
|
||||
# the parent item must not already be a component of another enabled bundle,
|
||||
# otherwise that bundle would silently nest this one
|
||||
pb = frappe.qb.DocType("Product Bundle")
|
||||
pbi = frappe.qb.DocType("Product Bundle Item")
|
||||
outer = (
|
||||
frappe.qb.from_(pbi)
|
||||
.join(pb)
|
||||
.on(pbi.parent == pb.name)
|
||||
.select(pb.name)
|
||||
.where((pbi.item_code == self.new_item_code) & (pb.name != self.name) & (pb.disabled == 0))
|
||||
.limit(1)
|
||||
).run(pluck=True)
|
||||
if outer:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Item {0} is already a component of Product Bundle {1}; nested bundles are not supported."
|
||||
).format(frappe.bold(self.new_item_code), frappe.bold(outer[0])),
|
||||
title=_("Nested Product Bundle"),
|
||||
)
|
||||
|
||||
def validate_child_items_uom(self):
|
||||
# a component's UOM must resolve the same way packing's get_conversion_factor does:
|
||||
# the item or its template's UOM Conversion Detail, else a global UOM Conversion Factor
|
||||
|
||||
@@ -47,3 +47,20 @@ class TestProductBundle(FrappeTestCase):
|
||||
doc.append("items", {"item_code": child, "qty": 1, "uom": "Kg"})
|
||||
doc.insert() # must not raise
|
||||
self.assertTrue(doc.name)
|
||||
|
||||
def test_bundle_cannot_reference_itself(self):
|
||||
doc = frappe.get_doc({"doctype": "Product Bundle", "new_item_code": self.parent})
|
||||
doc.append("items", {"item_code": self.parent, "qty": 1})
|
||||
self.assertRaises(frappe.ValidationError, doc.insert)
|
||||
|
||||
def test_parent_used_as_component_cannot_become_a_bundle(self):
|
||||
# OUTER bundles INNER while INNER is still a plain item; turning INNER into its
|
||||
# own bundle afterwards would nest it under OUTER, so it must be rejected
|
||||
suffix = frappe.generate_hash(length=8)
|
||||
inner = make_item(f"_Test PB Nest Inner {suffix}", {"is_stock_item": 0, "is_sales_item": 1}).name
|
||||
outer = make_item(f"_Test PB Nest Outer {suffix}", {"is_stock_item": 0, "is_sales_item": 1}).name
|
||||
make_product_bundle(outer, [inner])
|
||||
|
||||
doc = frappe.get_doc({"doctype": "Product Bundle", "new_item_code": inner})
|
||||
doc.append("items", {"item_code": self.child, "qty": 1})
|
||||
self.assertRaises(frappe.ValidationError, doc.insert)
|
||||
|
||||
Reference in New Issue
Block a user