mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-11 13:41:47 +00:00
fix(buying): resolve Get Items from Product Bundle by document name
Since Product Bundles became versioned, their names are PB-prefixed and no longer double as the parent item code. The buying dialog kept passing the picked bundle name as `item_code`, so the component lookup (which filters `new_item_code`) matched nothing and the dialog silently added no items. The dialog now sends the selection as `product_bundle` and the endpoint fetches that version's components by document name (rejecting unsubmitted versions); passing `item_code` still resolves the parent item's active version, preserving the legacy contract of the whitelisted endpoint. The picker is also restricted to submitted bundles. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -628,7 +628,7 @@ erpnext.buying.get_items_from_product_bundle = function (frm) {
|
||||
method: "erpnext.stock.doctype.packed_item.packed_item.get_items_from_product_bundle",
|
||||
args: {
|
||||
row: {
|
||||
item_code: args.product_bundle,
|
||||
product_bundle: args.product_bundle,
|
||||
quantity: args.quantity,
|
||||
parenttype: frm.doc.doctype,
|
||||
parent: frm.doc.name,
|
||||
|
||||
@@ -458,9 +458,29 @@ def on_doctype_update():
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_items_from_product_bundle(row: str):
|
||||
"""Item details for each component of a Product Bundle.
|
||||
|
||||
``row.product_bundle`` selects a specific version by document name (the buying
|
||||
dialog passes this); ``row.item_code`` is the legacy contract, resolving the
|
||||
parent item's active version.
|
||||
"""
|
||||
row, items = ItemDetailsCtx(json.loads(row)), []
|
||||
|
||||
bundled_items = get_product_bundle_items(row["item_code"])
|
||||
if bundle_name := row.get("product_bundle"):
|
||||
bundle = frappe.db.get_value(
|
||||
"Product Bundle", bundle_name, ["docstatus", "disabled"], as_dict=True
|
||||
)
|
||||
if not bundle or bundle.docstatus != 1:
|
||||
frappe.throw(_("Product Bundle {0} is not submitted").format(frappe.bold(bundle_name)))
|
||||
if bundle.disabled:
|
||||
frappe.throw(
|
||||
_("Product Bundle {0} is disabled and cannot be used in transactions.").format(
|
||||
frappe.bold(bundle_name)
|
||||
)
|
||||
)
|
||||
bundled_items = get_product_bundle_items_by_name(bundle_name)
|
||||
else:
|
||||
bundled_items = get_product_bundle_items(row["item_code"])
|
||||
for item in bundled_items:
|
||||
row.update(
|
||||
{
|
||||
|
||||
@@ -188,6 +188,56 @@ class TestPackedItem(ERPNextTestSuite):
|
||||
self.assertFalse(so.items[0].product_bundle)
|
||||
self.assertFalse(so.get("packed_items"))
|
||||
|
||||
def test_get_items_from_product_bundle_endpoint(self):
|
||||
"The buying dialog passes the chosen version by document name (legacy: parent item code)."
|
||||
import json
|
||||
|
||||
from erpnext.selling.doctype.product_bundle.product_bundle import get_active_product_bundle
|
||||
from erpnext.stock.doctype.packed_item.packed_item import get_items_from_product_bundle
|
||||
|
||||
ctx = {
|
||||
"quantity": 2,
|
||||
"doctype": "Purchase Order",
|
||||
"parenttype": "Purchase Order",
|
||||
"company": "_Test Company",
|
||||
"currency": "INR",
|
||||
"conversion_rate": 1,
|
||||
"transaction_date": nowdate(),
|
||||
}
|
||||
|
||||
# by document name, as the buying dialog sends it (bundle names are PB-prefixed
|
||||
# since versioning, so they no longer double as the parent item code)
|
||||
version = get_active_product_bundle(self.bundle)
|
||||
items = get_items_from_product_bundle(json.dumps({"product_bundle": version, **ctx}))
|
||||
self.assertEqual(sorted(i.item_code for i in items), sorted(self.bundle_items))
|
||||
self.assertEqual([i.qty for i in items], [4, 4])
|
||||
|
||||
# legacy contract: the parent item code resolves to its active version
|
||||
items = get_items_from_product_bundle(json.dumps({"item_code": self.bundle, **ctx}))
|
||||
self.assertEqual(sorted(i.item_code for i in items), sorted(self.bundle_items))
|
||||
|
||||
# an unsubmitted version is rejected
|
||||
draft = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Product Bundle",
|
||||
"new_item_code": make_item(properties={"is_stock_item": 0}).name,
|
||||
"items": [{"item_code": self.bundle_items[0], "qty": 1}],
|
||||
}
|
||||
).insert()
|
||||
self.assertRaises(
|
||||
frappe.ValidationError,
|
||||
get_items_from_product_bundle,
|
||||
json.dumps({"product_bundle": draft.name, **ctx}),
|
||||
)
|
||||
|
||||
# a disabled version is rejected
|
||||
frappe.db.set_value("Product Bundle", version, "disabled", 1)
|
||||
self.assertRaises(
|
||||
frappe.ValidationError,
|
||||
get_items_from_product_bundle,
|
||||
json.dumps({"product_bundle": version, **ctx}),
|
||||
)
|
||||
|
||||
@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."
|
||||
|
||||
Reference in New Issue
Block a user