test(selling): cover disabled Product Bundle behaviour

Resolution skips disabled bundles, transactions referencing a disabled
version are blocked, rows without an explicit version stop packing, and
the Item Where Used report surfaces the disabled flag on bundle rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit c97c2d1e02)

# Conflicts:
#	erpnext/selling/doctype/product_bundle/test_product_bundle.py
#	erpnext/stock/doctype/packed_item/test_packed_item.py
This commit is contained in:
Mihir Kandoi
2026-06-10 10:57:40 +05:30
committed by Mergify
parent 2292366645
commit f06240314d
2 changed files with 230 additions and 0 deletions

View File

@@ -16,3 +16,119 @@ 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)
_, 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_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)
>>>>>>> c97c2d1e02 (test(selling): cover disabled Product Bundle behaviour)

View File

@@ -76,6 +76,120 @@ class TestPackedItem(ERPNextTestSuite):
self.assertEqual(len(so.packed_items), 0)
<<<<<<< HEAD
=======
def test_item_and_packed_rows_record_bundle_version(self):
"The item row and its packed items record the resolved Product Bundle version."
from erpnext.selling.doctype.product_bundle.product_bundle import get_active_product_bundle
version = get_active_product_bundle(self.bundle)
self.assertTrue(version and version.startswith("PB-"))
so = make_sales_order(item_code=self.bundle, qty=1, warehouse=self.warehouse)
self.assertEqual(so.items[0].product_bundle, version)
self.assertEqual(so.items[0].is_product_bundle, 1)
self.assertEqual(len(so.packed_items), 2)
for pi in so.packed_items:
self.assertEqual(pi.product_bundle, version)
# the version carries onto a Delivery Note mapped from the Sales Order
dn = make_delivery_note(so.name)
self.assertEqual(dn.items[0].product_bundle, version)
for pi in dn.packed_items:
self.assertEqual(pi.product_bundle, version)
def test_clearing_version_keeps_bundle_flag_and_redefaults(self):
"Clearing the version must not lose the bundle flag (keeps the field visible)."
so = make_sales_order(item_code=self.bundle, qty=1, warehouse=self.warehouse, do_not_submit=True)
version = so.items[0].product_bundle
self.assertEqual(so.items[0].is_product_bundle, 1)
# user blanks the version field
so.items[0].product_bundle = None
so.save()
# the flag stays set (so depends_on keeps the field visible) and the value
# re-defaults to the active version
self.assertEqual(so.items[0].is_product_bundle, 1)
self.assertEqual(so.items[0].product_bundle, version)
def test_backfill_patch_stamps_existing_rows(self):
"The backfill patch stamps the version on rows that predate the field."
from erpnext.patches.v16_0.submit_existing_product_bundles import (
stamp_versions_on_transactions as stamp_versions,
)
from erpnext.selling.doctype.product_bundle.product_bundle import get_active_product_bundle
version = get_active_product_bundle(self.bundle)
so = make_sales_order(item_code=self.bundle, qty=1, do_not_submit=True)
# simulate pre-migration rows with no version recorded and no bundle flag
frappe.db.set_value("Sales Order Item", so.items[0].name, "product_bundle", None)
frappe.db.set_value("Sales Order Item", so.items[0].name, "is_product_bundle", 0)
for pi in so.packed_items:
frappe.db.set_value("Packed Item", pi.name, "product_bundle", None)
stamp_versions()
self.assertEqual(frappe.db.get_value("Sales Order Item", so.items[0].name, "product_bundle"), version)
self.assertEqual(frappe.db.get_value("Sales Order Item", so.items[0].name, "is_product_bundle"), 1)
for pi in so.packed_items:
self.assertEqual(frappe.db.get_value("Packed Item", pi.name, "product_bundle"), version)
def test_choosing_an_older_version_packs_its_components(self):
"Default picks the active version; choosing an older version re-packs its components."
from erpnext.selling.doctype.product_bundle.product_bundle import (
get_active_product_bundle,
make_new_version,
)
v1 = get_active_product_bundle(self.bundle)
# new version with a different component becomes the active one
new_component = make_item().name
make_stock_entry(item=new_component, to_warehouse=self.warehouse, qty=50, rate=100)
v2 = make_new_version(v1)
v2.items = []
v2.append("items", {"item_code": new_component, "qty": 1})
v2.insert()
v2.submit()
self.assertEqual(get_active_product_bundle(self.bundle), v2.name)
# default: the active version (v2) and its component
so = make_sales_order(item_code=self.bundle, qty=1, warehouse=self.warehouse, do_not_submit=True)
self.assertEqual(so.items[0].product_bundle, v2.name)
self.assertEqual([pi.item_code for pi in so.packed_items], [new_component])
# choose the older version -> its components are packed instead
so.items[0].product_bundle = v1
so.save()
self.assertEqual(so.items[0].product_bundle, v1)
self.assertEqual(sorted(pi.item_code for pi in so.packed_items), sorted(self.bundle_items))
def test_disabled_bundle_blocks_transaction(self):
"A row that explicitly references a disabled version cannot be saved."
from erpnext.selling.doctype.product_bundle.product_bundle import get_active_product_bundle
version = get_active_product_bundle(self.bundle)
so = make_sales_order(item_code=self.bundle, qty=1, warehouse=self.warehouse, do_not_submit=True)
self.assertEqual(so.items[0].product_bundle, version)
frappe.db.set_value("Product Bundle", version, "disabled", 1)
self.assertRaises(frappe.ValidationError, so.save)
def test_disabled_bundle_is_not_packed(self):
"Without an explicit version, a disabled bundle is not treated as a bundle at all."
from erpnext.selling.doctype.product_bundle.product_bundle import get_active_product_bundle
version = get_active_product_bundle(self.bundle2)
frappe.db.set_value("Product Bundle", version, "disabled", 1)
so = make_sales_order(item_code=self.bundle2, qty=1, warehouse=self.warehouse, do_not_submit=True)
self.assertEqual(so.items[0].is_product_bundle, 0)
self.assertFalse(so.items[0].product_bundle)
self.assertFalse(so.get("packed_items"))
>>>>>>> c97c2d1e02 (test(selling): cover disabled Product Bundle behaviour)
@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1})
def test_recurring_bundle_item(self):
"Test impact on packed items if same bundle item is added and removed."