mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-13 22:51:49 +00:00
Merge pull request #55793 from mihir-kandoi/fix-bundle-dialog-lookup
fix(buying): resolve Get Items from Product Bundle by document name
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,
|
||||
|
||||
@@ -175,31 +175,6 @@ def reset_packing_list(doc):
|
||||
return reset_table
|
||||
|
||||
|
||||
def get_product_bundle_items(item_code):
|
||||
product_bundle = frappe.qb.DocType("Product Bundle")
|
||||
product_bundle_item = frappe.qb.DocType("Product Bundle Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(product_bundle_item)
|
||||
.join(product_bundle)
|
||||
.on(product_bundle_item.parent == product_bundle.name)
|
||||
.select(
|
||||
product_bundle_item.item_code,
|
||||
product_bundle_item.qty,
|
||||
product_bundle_item.uom,
|
||||
product_bundle_item.description,
|
||||
)
|
||||
.where(
|
||||
(product_bundle.new_item_code == item_code)
|
||||
& (product_bundle.is_active == 1)
|
||||
& (product_bundle.docstatus == 1)
|
||||
& (product_bundle.disabled == 0)
|
||||
)
|
||||
.orderby(product_bundle_item.idx)
|
||||
)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_product_bundle_items_by_name(bundle_name):
|
||||
"Component rows of a specific Product Bundle version."
|
||||
product_bundle_item = frappe.qb.DocType("Product Bundle Item")
|
||||
@@ -458,9 +433,31 @@ 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.
|
||||
"""
|
||||
from erpnext.selling.doctype.product_bundle.product_bundle import get_active_product_bundle
|
||||
|
||||
row, items = ItemDetailsCtx(json.loads(row)), []
|
||||
|
||||
bundled_items = get_product_bundle_items(row["item_code"])
|
||||
if bundle_name := row.get("product_bundle"):
|
||||
frappe.has_permission("Product Bundle", "read", bundle_name, throw=True)
|
||||
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)
|
||||
)
|
||||
)
|
||||
elif bundle_name := get_active_product_bundle(row.get("item_code")):
|
||||
frappe.has_permission("Product Bundle", "read", bundle_name, throw=True)
|
||||
|
||||
bundled_items = get_product_bundle_items_by_name(bundle_name) if bundle_name else []
|
||||
for item in bundled_items:
|
||||
row.update(
|
||||
{
|
||||
|
||||
@@ -188,6 +188,57 @@ 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.addCleanup(frappe.db.set_value, "Product Bundle", version, "disabled", 0)
|
||||
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