diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 39698354a278b6a877dc5ead695e7f512eb41747..d1c528847dd4a4ee4108c88349678015af042b2f 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -17,6 +17,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( ) from erpnext.accounts.party import get_due_date, get_party_account from erpnext.controllers.queries import item_query as _item_query +from erpnext.stock.doctype.packed_item.packed_item import get_product_bundle, get_product_bundle_name from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -343,7 +344,9 @@ class POSInvoice(SalesInvoice): if is_negative_stock_allowed(item_code=d.item_code): return - available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse) + available_stock, is_stock_item = get_stock_availability( + d.item_code, d.warehouse, d.product_bundle_name + ) item_code, warehouse, qty = ( frappe.bold(d.item_code), @@ -747,7 +750,7 @@ class POSInvoice(SalesInvoice): @frappe.whitelist() -def get_stock_availability(item_code, warehouse): +def get_stock_availability(item_code, warehouse, product_bundle_name=""): if frappe.db.get_value("Item", item_code, "is_stock_item"): is_stock_item = True bin_qty = get_bin_qty(item_code, warehouse) @@ -756,16 +759,18 @@ def get_stock_availability(item_code, warehouse): return bin_qty - pos_sales_qty, is_stock_item else: is_stock_item = True - if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}): - return get_bundle_availability(item_code, warehouse), is_stock_item + if get_product_bundle_name(item_code, product_bundle_name=product_bundle_name): + return get_bundle_availability( + item_code, warehouse, product_bundle_name=product_bundle_name + ), is_stock_item else: is_stock_item = False # Is a service item or non_stock item return 0, is_stock_item -def get_bundle_availability(bundle_item_code, warehouse): - product_bundle = frappe.get_doc("Product Bundle", bundle_item_code) +def get_bundle_availability(bundle_item_code, warehouse, product_bundle_name=""): + product_bundle = get_product_bundle(bundle_item_code, product_bundle_name=product_bundle_name) bundle_bin_qty = 1000000 for item in product_bundle.items: @@ -779,7 +784,7 @@ def get_bundle_availability(bundle_item_code, warehouse): ): bundle_bin_qty = max_available_bundles - pos_sales_qty = get_pos_reserved_qty(bundle_item_code, warehouse) + pos_sales_qty = get_pos_reserved_qty(product_bundle.new_item_code, warehouse) return bundle_bin_qty - pos_sales_qty diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index f9ec1453347cb271daa5af3b410b042ca17a8ece..8b77b22dc33c6334a259151a2967ea790139342e 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -104,6 +104,7 @@ "sales_order", "so_detail", "sales_invoice_item", + "product_bundle_name", "column_break_74", "delivery_note", "dn_detail", @@ -996,6 +997,14 @@ "options": "POS Invoice", "print_hide": 1, "search_index": 1 + }, + { + "fieldname": "product_bundle_name", + "fieldtype": "Link", + "hidden": 1, + "label": "Product Bundle", + "options": "Product Bundle", + "read_only": 1 } ], "grid_page_length": 50, diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py index b50c85b975b7cda29e6a5958148cec1e2e7bcd6f..a56ed7200c107c01852eefb50ddc6fbdf15494b8 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py @@ -77,6 +77,7 @@ class SalesInvoiceItem(Document): pos_invoice_item: DF.Data | None price_list_rate: DF.Currency pricing_rules: DF.SmallText | None + product_bundle_name: DF.Link | None project: DF.Link | None purchase_order: DF.Link | None purchase_order_item: DF.Data | None diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b1f6994e99e00625177a85be18f2bce685771d5d..b204cd88993da1e99a5dee76a01af6ac3521c845 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -61,7 +61,7 @@ from erpnext.controllers.sales_and_purchase_return import validate_return from erpnext.exceptions import InvalidCurrency from erpnext.setup.utils import get_exchange_rate from erpnext.stock.doctype.item.item import get_uom_conv_factor -from erpnext.stock.doctype.packed_item.packed_item import make_packing_list +from erpnext.stock.doctype.packed_item.packed_item import get_product_bundle_name, make_packing_list from erpnext.stock.get_item_details import ( ItemDetailsCtx, _get_item_tax_template, @@ -3937,6 +3937,13 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil if new_child_flag: child_item.fg_item = d["fg_item"] + if parent.doctype == "Sales Order": + if product_bundle_name := get_product_bundle_name( + child_item.item_code, + product_bundle_name=child_item.product_bundle_name, + ): + child_item.product_bundle_name = product_bundle_name + child_item.qty = flt(d.get("qty")) rate_precision = child_item.precision("rate") or 2 conv_fac_precision = child_item.precision("conversion_factor") or 2 @@ -4059,6 +4066,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil ) else: # Sales Order parent.validate_for_duplicate_items() + parent.validate_items_product_bundle() parent.validate_warehouse() parent.update_reserved_qty() parent.update_project() @@ -4088,6 +4096,16 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil if parent.per_picked == 0: parent.create_stock_reservation_entries() + # Check that old packed items were not already ordered + # old_packed_items = (parent.get_doc_before_save() or {}).get("packed_items") or [] + # new_packed_items = parent.get("packed_items") + # print(old_packed_items, new_packed_items) + # removed = {pi.name for pi in old_packed_items} - {pi.name for pi in new_packed_items} + # if frappe.db.exists("Purchase Order Item", {"sales_order_packed_item": ("in", removed), "docstatus": 1}): + # frappe.throw("Cannot edit Product Bundle already ordered in Purchase Order") + + # frappe.throw("lorem") + def check_if_child_table_updated(child_table_before_update, child_table_after_update, fields_to_check): fields_to_check = list(fields_to_check) + get_accounting_dimensions() + ["cost_center", "project"] diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index b3bce7de2814ed454427c731e0ebdc0d088781ec..d557501cd5df91f883ff8816e8b547eaba347b94 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -11,6 +11,7 @@ from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.controllers.stock_controller import StockController from erpnext.stock.doctype.item.item import set_item_default +from erpnext.stock.doctype.packed_item.packed_item import get_product_bundle from erpnext.stock.get_item_details import ( GROSS_PROFIT_CALCULATION_RULES, get_bin_details, @@ -38,6 +39,7 @@ class SellingController(StockController): def validate(self): super().validate() self.validate_items() + self.validate_items_product_bundle() if not (self.get("is_debit_note") or self.get("is_return")): self.validate_max_discount() self.validate_selling_price() @@ -321,7 +323,7 @@ class SellingController(StockController): def get_item_list(self): il = [] for d in self.get("items"): - if self.has_product_bundle(d.item_code): + if d.product_bundle_name or self.has_product_bundle(d.item_code): for p in self.get("packed_items"): if p.parent_detail_docname == d.name and p.parent_item == d.item_code: # the packing details table's qty is already multiplied with parent's qty @@ -839,6 +841,18 @@ class SellingController(StockController): validate_item_type(self, "is_sales_item", _("sales")) + def validate_items_product_bundle(self): + for item in self.items: + bundle = None + if self.has_product_bundle(item.item_code): + bundle = get_product_bundle(item.item_code, product_bundle_name=item.product_bundle_name) + if not bundle or bundle.new_item_code != item.item_code: + # The row item and the bundle's item are mismatched, override the product_bundle_name + bundle = get_product_bundle(item.item_code) # Fetch any bundle of the correct item + + # If there is a bundle, write its name, if not clear the field. + item.product_bundle_name = bundle.name if bundle else None + def validate_recurring_items(self): if self.recurrence_period: for item in self.items: diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.json b/erpnext/selling/doctype/product_bundle/product_bundle.json index 14c6250f5ccd3cc2bb3c38a0bc1d185760b4b837..b053a4f11900737d81d4a6786be9fb56ec655587 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.json +++ b/erpnext/selling/doctype/product_bundle/product_bundle.json @@ -1,6 +1,7 @@ { "actions": [], "allow_import": 1, + "allow_rename": 1, "creation": "2013-06-20 11:53:21", "description": "Aggregate a group of Items into another Item. This is useful if you are maintaining the stock of the packed items and not the bundled item", "doctype": "DocType", @@ -36,7 +37,7 @@ }, { "fieldname": "description", - "fieldtype": "Data", + "fieldtype": "Text Editor", "in_list_view": 1, "label": "Description" }, @@ -84,7 +85,7 @@ "icon": "fa fa-sitemap", "idx": 1, "links": [], - "modified": "2025-01-21 11:50:07.516599", + "modified": "2025-03-06 11:46:14.675103", "modified_by": "Administrator", "module": "Selling", "name": "Product Bundle", @@ -122,5 +123,6 @@ ], "sort_field": "creation", "sort_order": "ASC", - "states": [] + "states": [], + "title_field": "new_item_code" } diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index eba763fa96638b84b074f206ad0e6403cf21f87d..af7f6852b7ef0ee6ecdd4b69c98272bad817385e 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -8,6 +8,8 @@ from frappe.model.document import Document from frappe.query_builder import Criterion from frappe.utils import get_link_to_form +from erpnext.stock.doctype.packed_item.packed_item import get_product_bundle_name + class ProductBundle(Document): # begin: auto-generated types @@ -20,7 +22,7 @@ class ProductBundle(Document): from erpnext.selling.doctype.product_bundle_item.product_bundle_item import ProductBundleItem - description: DF.Data | None + description: DF.TextEditor | None disabled: DF.Check editable_bundle_item_rates: DF.Check items: DF.Table[ProductBundleItem] @@ -29,6 +31,8 @@ class ProductBundle(Document): def autoname(self): self.name = self.new_item_code + if frappe.db.exists(self.doctype, self.name): + self.name = f"{self.new_item_code}-{frappe.generate_hash(length=8)}" def validate(self): self.validate_main_item() @@ -82,7 +86,7 @@ class ProductBundle(Document): def validate_child_items(self): for item in self.items: - if frappe.db.exists("Product Bundle", {"name": item.item_code, "disabled": 0}): + if get_product_bundle_name(item.item_code): frappe.throw( _( "Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save" @@ -93,7 +97,7 @@ class ProductBundle(Document): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_new_item_code(doctype, txt, searchfield, start, page_len, filters): - product_bundles = frappe.db.get_list("Product Bundle", {"disabled": 0}, pluck="name") + # product_bundles = frappe.db.get_list("Product Bundle", {"disabled": 0}, pluck="name") if not searchfield or searchfield == "name": searchfield = frappe.get_meta("Item").get("search_fields") @@ -115,7 +119,7 @@ def get_new_item_code(doctype, txt, searchfield, start, page_len, filters): Criterion.any([item[fieldname.strip()].like(f"%{txt}%") for fieldname in searchfield]) ) - if product_bundles: - query = query.where(item.name.notin(product_bundles)) + # if product_bundles: + # query = query.where(item.name.notin(product_bundles)) return query.run() diff --git a/erpnext/selling/doctype/product_bundle_item/product_bundle_item.json b/erpnext/selling/doctype/product_bundle_item/product_bundle_item.json index 3727b4e18cc75163b8f3bf94028942e70161554a..10b493fc2d312ac64fe4d7ad822e96ba48fb3131 100644 --- a/erpnext/selling/doctype/product_bundle_item/product_bundle_item.json +++ b/erpnext/selling/doctype/product_bundle_item/product_bundle_item.json @@ -69,7 +69,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2024-05-08 10:08:01.818998", + "modified": "2025-02-14 13:41:35.762125", "modified_by": "Administrator", "module": "Selling", "name": "Product Bundle Item", diff --git a/erpnext/selling/doctype/product_bundle_item/product_bundle_item.py b/erpnext/selling/doctype/product_bundle_item/product_bundle_item.py index 7b3d97469626b52c4f946ee496b9a29187ab545f..830b07ec529b30406baaa267f8a3038ca0c07ca7 100644 --- a/erpnext/selling/doctype/product_bundle_item/product_bundle_item.py +++ b/erpnext/selling/doctype/product_bundle_item/product_bundle_item.py @@ -7,4 +7,22 @@ from frappe.model.document import Document class ProductBundleItem(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + description: DF.TextEditor | None + item_code: DF.Link + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + qty: DF.Float + rate: DF.Float + uom: DF.Link | None + # end: auto-generated types + pass diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index f05d3104636638ff453d02e02277fdfe3dae435e..d19f1823d59e5057d68a1c81efd5c92f2c867319 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -84,6 +84,7 @@ "against_blanket_order", "blanket_order", "blanket_order_rate", + "product_bundle_name", "column_break_30", "prevdoc_doctype", "prevdoc_docname", @@ -720,8 +721,8 @@ "fieldname": "last_purchase_rate", "fieldtype": "Currency", "label": "Last Purchase Rate", - "read_only": 1, - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "fieldname": "supplier", @@ -793,8 +794,8 @@ "fieldname": "unit_cost_price", "fieldtype": "Currency", "label": "Unit Cost Price", - "read_only": 1, - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "description": "Set a custom mark-up percentage to recalculate your item selling rate", @@ -802,12 +803,20 @@ "fieldtype": "Percent", "label": "Mark-up percentage", "print_hide": 1 + }, + { + "fieldname": "product_bundle_name", + "fieldtype": "Link", + "hidden": 1, + "label": "Product Bundle", + "options": "Product Bundle", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2024-12-12 13:49:17.765883", + "modified": "2025-02-14 14:53:07.282841", "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item", diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.py b/erpnext/selling/doctype/quotation_item/quotation_item.py index bde7d81e604dda8abda14605171b10bac00326c1..19a56e9cc75709b82b5471996371e5b26c4c6f9c 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.py +++ b/erpnext/selling/doctype/quotation_item/quotation_item.py @@ -67,6 +67,7 @@ class QuotationItem(Document): prevdoc_doctype: DF.Link | None price_list_rate: DF.Currency pricing_rules: DF.SmallText | None + product_bundle_name: DF.Link | None projected_qty: DF.Float qty: DF.Float rate: DF.Currency diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index fc9f114f89f145a72c20dcabc98753d5a3ee35da..4082bc5fef600f197d714a7a427ecafe3a3ab5fb 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -1395,8 +1395,9 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex // calculate ordered qty based on packed items in case of product bundle let packed_items = so.packed_items.filter((pi) => pi.parent_detail_docname == item.name); if (packed_items && packed_items.length) { - ordered_qty = packed_items.reduce((sum, pi) => sum + flt(pi.ordered_qty), 0); - ordered_qty = ordered_qty / packed_items.length; + const pi_qty = packed_items.reduce((sum, pi) => sum + flt(pi.qty), 0); + const pi_ordered_qty = packed_items.reduce((sum, pi) => sum + flt(pi.ordered_qty), 0); + ordered_qty = item.stock_qty * (pi_ordered_qty / pi_qty); } } return ordered_qty; diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 80809021a65628928ad25b167ef77f03f71abca5..bf4c5396b676f7301f73c26ccb2eb2358d40a4ec 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -379,7 +379,7 @@ class SalesOrder(SellingController): frappe.get_cached_value("Item", d.item_code, "is_stock_item") == 1 or ( self.has_product_bundle(d.item_code) - and self.product_bundle_has_stock_item(d.item_code) + and self.product_bundle_has_stock_item(d.product_bundle_name or d.item_code) ) ) and not d.warehouse @@ -553,7 +553,7 @@ class SalesOrder(SellingController): for d in self.get("items"): if (not so_item_rows or d.name in so_item_rows) and not d.delivered_by_supplier: - if self.has_product_bundle(d.item_code): + if d.product_bundle_name or self.has_product_bundle(d.item_code): for p in self.get("packed_items"): if p.parent_detail_docname == d.name and p.parent_item == d.item_code: _valid_for_reserve(p.item_code, p.warehouse) @@ -623,9 +623,11 @@ class SalesOrder(SellingController): per_picked = 0.0 for so_item in self.items: - if cint( - frappe.get_cached_value("Item", so_item.item_code, "is_stock_item") - ) or self.has_product_bundle(so_item.item_code): + if ( + cint(frappe.get_cached_value("Item", so_item.item_code, "is_stock_item")) + or so_item.product_bundle_name + or self.has_product_bundle(so_item.item_code) + ): total_picked_qty += flt(so_item.picked_qty) total_qty += flt(so_item.stock_qty) @@ -939,8 +941,9 @@ def make_material_request(source_name, target_doc=None): "delivery_date": "required_by", "bom_no": "bom_no", }, - "condition": lambda item: item.item_code - and not frappe.db.exists("Product Bundle", {"name": item.item_code, "disabled": 0}) # @dokos + "condition": lambda item: not is_product_bundle( + item.item_code, product_bundle_name=item.get("product_bundle_name") + ) and get_remaining_qty(item) > 0, "postprocess": update_item, }, @@ -1647,7 +1650,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): "postprocess": update_item, "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.item_code in items_to_map - and not is_product_bundle(doc.item_code), + and not is_product_bundle(doc.item_code, product_bundle_name=doc.get("product_bundle_name")), }, "Packed Item": { "doctype": "Purchase Order Item", @@ -1695,8 +1698,10 @@ def set_delivery_date(items, sales_order): item.schedule_date = delivery_by_item[item.product_bundle] -def is_product_bundle(item_code): - return frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}) +def is_product_bundle(item_code="", *, product_bundle_name=""): + from erpnext.stock.doctype.packed_item.packed_item import get_product_bundle_name + + return get_product_bundle_name(item_code, product_bundle_name=product_bundle_name) @frappe.whitelist() @@ -1803,7 +1808,7 @@ def make_inter_company_purchase_order(source_name, target_doc=None): @frappe.whitelist() def create_pick_list(source_name, target_doc=None): - from erpnext.stock.doctype.packed_item.packed_item import is_product_bundle + from erpnext.stock.doctype.packed_item.packed_item import get_product_bundle_name def validate_sales_order(): so = frappe.get_doc("Sales Order", source_name) @@ -1836,7 +1841,7 @@ def create_pick_list(source_name, target_doc=None): item.item_code # @dokos and abs(item.delivered_qty) < abs(item.qty) and item.delivered_by_supplier != 1 - and not is_product_bundle(item.item_code) + and not get_product_bundle_name(item.item_code, product_bundle_name=item.product_bundle_name) ) # Don't allow a Pick List to be created against a Sales Order that has reserved stock. @@ -1906,12 +1911,11 @@ def get_work_order_items(sales_order, for_raw_material_request=0): items = [] item_codes = [i.item_code for i in so.items] - product_bundle_parents = [ - pb.new_item_code - for pb in frappe.get_all( - "Product Bundle", {"new_item_code": ["in", item_codes], "disabled": 0}, ["new_item_code"] - ) - ] + product_bundle_parents = frappe.get_all( + "Product Bundle", + {"new_item_code": ["in", item_codes], "disabled": 0}, + pluck="new_item_code", + ) for table in [so.items, so.packed_items]: for i in table: diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 2e37be72c326fc9ee2b3a915b1e7fa507cf751af..cacd051fb20246417eda3a195d3b696ec5e0ec54 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -89,6 +89,7 @@ "target_warehouse", "quotation_item", "prevdoc_docname", + "product_bundle_name", "col_break4", "against_blanket_order", "blanket_order", @@ -1028,15 +1029,15 @@ "fieldname": "unit_cost_price", "fieldtype": "Currency", "label": "Unit Cost Price", - "read_only": 1, - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "fieldname": "last_purchase_rate", "fieldtype": "Currency", "label": "Last Purchase Rate", - "read_only": 1, - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "collapsible": 1, @@ -1066,6 +1067,14 @@ "label": "Project", "options": "Project", "search_index": 1 + }, + { + "fieldname": "product_bundle_name", + "fieldtype": "Link", + "hidden": 1, + "label": "Product Bundle", + "options": "Product Bundle", + "read_only": 1 } ], "idx": 1, diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.py b/erpnext/selling/doctype/sales_order_item/sales_order_item.py index 3a2eff6eaa7ca2d3216fb9b2a80a2626ab1e0982..90216faef3f48bfa3bd34c7c038a4a231f0d1421 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.py +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.py @@ -79,6 +79,7 @@ class SalesOrderItem(Document): price_list_rate: DF.Currency pricing_rules: DF.SmallText | None produced_qty: DF.Float + product_bundle_name: DF.Link | None production_plan_qty: DF.Float project: DF.Link | None projected_qty: DF.Float diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index b7cc86039cdc507f0fc10a8b3038dca614835eb2..887ade0793b695e18fbdf4c40728f99a697256f5 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -53,7 +53,12 @@ def search_by_term(search_term, warehouse, price_list): } ) - item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse) + # Get last product bundle + product_bundle_name = frappe.db.get_value("Product Bundle", {"new_item_code": item_code, "disabled": 0}) + + item_stock_qty, is_stock_item = get_stock_availability( + item_code, warehouse, product_bundle_name=product_bundle_name + ) item_stock_qty = item_stock_qty // item.get("conversion_factor", 1) item.update({"actual_qty": item_stock_qty}) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index d87f04a674b1b3cb5f79fc863de74d8fe37a55a0..43ff3480f3fbfba6cfcc422a0d97ee8532c49bc8 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -398,7 +398,8 @@ erpnext.PointOfSale.Controller = class { this.cart.prev_action = null; this.cart.toggle_item_highlight(); }, - get_available_stock: (item_code, warehouse) => this.get_available_stock(item_code, warehouse), + get_available_stock: (item_code, warehouse, product_bundle_name) => + this.get_available_stock(item_code, warehouse, product_bundle_name), }, }); } @@ -743,7 +744,9 @@ erpnext.PointOfSale.Controller = class { } async check_stock_availability(item_row, qty_needed, warehouse) { - const resp = (await this.get_available_stock(item_row.item_code, warehouse)).message; + const resp = ( + await this.get_available_stock(item_row.item_code, warehouse, item_row.product_bundle_name) + ).message; const available_qty = resp[0]; const is_stock_item = resp[1]; @@ -793,13 +796,14 @@ erpnext.PointOfSale.Controller = class { } } - get_available_stock(item_code, warehouse) { + get_available_stock(item_code, warehouse, product_bundle_name) { const me = this; return frappe.call({ method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.get_stock_availability", args: { item_code: item_code, warehouse: warehouse, + product_bundle_name: product_bundle_name, }, callback(res) { if (!me.item_stock_map[item_code]) me.item_stock_map[item_code] = {}; diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index cd3a7813ee0c976698869e4215eb57f88078769e..7bc4e8bb597ad86959381854d147beb7b494b6b8 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -261,10 +261,16 @@ erpnext.PointOfSale.ItemDetails = class { const available_qty = stock_map?.[0]; const is_stock_item = Boolean(stock_map?.[1]); if (available_qty === undefined) { - me.events.get_available_stock(me.item_row.item_code, this.value).then(() => { - // item stock map is updated now reset warehouse - me.warehouse_control.set_value(this.value); - }); + me.events + .get_available_stock( + me.item_row.item_code, + this.value, + me.item_row.product_bundle_name + ) + .then(() => { + // item stock map is updated now reset warehouse + me.warehouse_control.set_value(this.value); + }); } else if (available_qty === 0 && is_stock_item) { me.warehouse_control.set_value(""); const bold_item_code = me.item_row.item_code.bold(); diff --git a/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py b/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py index 680e379651b50cb8ffe96ac815beefb06fe801b4..b4182dcb3ec908f668db48af2ecc93d884dff1a7 100644 --- a/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py +++ b/erpnext/selling/report/pending_so_items_for_purchase_request/pending_so_items_for_purchase_request.py @@ -81,9 +81,7 @@ def get_data(): bundled_item_map = get_packed_items(sales_orders) - item_with_product_bundle = get_items_with_product_bundle( - [row.item_code for row in sales_order_entry] - ) + item_with_product_bundle = get_items_with_product_bundle([row.item_code for row in sales_order_entry]) materials_request_dict = {} @@ -143,12 +141,12 @@ def get_data(): def get_items_with_product_bundle(item_list): - bundled_items = frappe.get_all( - "Product Bundle", filters=[("new_item_code", "IN", item_list)], fields=["new_item_code"] + return frappe.get_all( + "Product Bundle", + filters=[("new_item_code", "IN", item_list)], + pluck="new_item_code", ) - return [d.new_item_code for d in bundled_items] - def get_packed_items(sales_order_list): packed_items = frappe.get_all( diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 0f46bc3fd345f8ec3900d0cd3bb7b76810fa72bd..bdd834fff6a0642aa59d415df13f3f05ed5bf623 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -676,8 +676,8 @@ class DeliveryNote(SellingController): items_list = [item.item_code for item in self.items] return frappe.db.get_all( "Product Bundle", - filters={"new_item_code": ["in", items_list], "disabled": 0}, - pluck="name", + filters={"new_item_code": ["in", set(items_list)], "disabled": 0}, + pluck="new_item_code", ) diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 0f15c8e3c09b49dbaf2d3d599aa23c57427e57d7..58e41c322ed73b28fce19d6bcd9ce3997c51472e 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -97,6 +97,7 @@ "packed_qty", "column_break_fguf", "received_qty", + "product_bundle_name", "accounting_details_section", "expense_account", "column_break_71", @@ -940,6 +941,14 @@ { "fieldname": "column_break_fguf", "fieldtype": "Column Break" + }, + { + "fieldname": "product_bundle_name", + "fieldtype": "Link", + "hidden": 1, + "label": "Product Bundle", + "options": "Product Bundle", + "read_only": 1 } ], "grid_page_length": 50, diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py index 8c01ba4173c9e1ed7a9c23368429755a68e00f47..ea2df753dec2f67a54c989184ff3fc2b1e74e4b0 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py @@ -66,6 +66,7 @@ class DeliveryNoteItem(Document): pick_list_item: DF.Data | None price_list_rate: DF.Currency pricing_rules: DF.SmallText | None + product_bundle_name: DF.Link | None project: DF.Link | None purchase_order: DF.Link | None purchase_order_item: DF.Data | None diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index dd0d0b5407d99b525c9d81b6f03d3b78ab7d5da5..6983d21ed883e88c302acd3c3dd42213f3787043 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -654,17 +654,18 @@ class Item(Document): def validate_duplicate_product_bundles_before_merge(self, old_name, new_name): "Block merge if both old and new items have product bundles." - old_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": old_name, "disabled": 0}) - new_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": new_name, "disabled": 0}) - - if old_bundle and new_bundle: - bundle_link = get_link_to_form("Product Bundle", old_bundle) - old_name, new_name = frappe.bold(old_name), frappe.bold(new_name) - - msg = _("Please delete Product Bundle {0}, before merging {1} into {2}").format( - bundle_link, old_name, new_name - ) - frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError) + pass + # old_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": old_name, "disabled": 0}) + # new_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": new_name, "disabled": 0}) + + # if old_bundle and new_bundle: + # bundle_link = get_link_to_form("Product Bundle", old_bundle) + # old_name, new_name = frappe.bold(old_name), frappe.bold(new_name) + + # msg = _("Please delete Product Bundle {0}, before merging {1} into {2}").format( + # bundle_link, old_name, new_name + # ) + # frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError) def set_last_purchase_rate(self, new_name): last_purchase_rate = get_last_purchase_details(new_name).get("base_net_rate", 0) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 68e1a10fbf7d7647aa48320bb39a617cb577d933..98110ccc095412652e5c35d5a5e8252fae50bff1 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -594,8 +594,8 @@ class TestItem(IntegrationTestCase): bundle1 = make_product_bundle("Test Item Bundle Item 1", bundle_items, qty=2) make_product_bundle("Test Item Bundle Item 2", bundle_items, qty=2) - with self.assertRaises(DataValidationError): - frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True) + # with self.assertRaises(DataValidationError): + # frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True) bundle1.delete() frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 6891c22f9cc745c09dfc591d6ebc81e4ac0890d7..2188947424812e75082b00bf5c37625a8638ac0a 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -5,6 +5,7 @@ import json +from typing import TYPE_CHECKING import frappe from frappe.model.document import Document @@ -12,6 +13,9 @@ from frappe.utils import flt from erpnext.stock.get_item_details import ItemDetailsCtx, get_item_details, get_price_list_rate +if TYPE_CHECKING: + from erpnext.selling.doctype.product_bundle.product_bundle import ProductBundle + class PackedItem(Document): # begin: auto-generated types @@ -76,11 +80,13 @@ def make_packing_list(doc): reset = reset_packing_list(doc) for item_row in doc.get("items"): - if is_product_bundle(item_row.item_code): - editable_bundle_item_rates = frappe.db.get_value( - "Product Bundle", {"new_item_code": item_row.item_code}, "editable_bundle_item_rates" - ) - for bundle_item in get_product_bundle_items(item_row.item_code): + product_bundle_name = item_row.get("product_bundle_name") + product_bundle_name = get_product_bundle_name( + item_row.item_code, product_bundle_name=product_bundle_name + ) + if product_bundle_name: + bundle: "ProductBundle" = frappe.get_cached_doc("Product Bundle", product_bundle_name) # type: ignore + for bundle_item in bundle.items: pi_row = add_packed_item_row( doc=doc, packing_item=bundle_item, @@ -94,7 +100,7 @@ def make_packing_list(doc): update_packed_item_price_data(pi_row, item_data, doc) update_packed_item_from_cancelled_doc(item_row, bundle_item, pi_row, doc) - if editable_bundle_item_rates: # create/update bundle item wise price dict + if bundle.editable_bundle_item_rates: # create/update bundle item wise price dict update_product_bundle_rate(parent_items_price, pi_row, item_row) if parent_items_price: @@ -102,8 +108,29 @@ def make_packing_list(doc): doc.calculate_taxes_and_totals() -def is_product_bundle(item_code: str) -> bool: - return bool(frappe.db.exists("Product Bundle", {"new_item_code": item_code, "disabled": 0})) +def get_product_bundle_name(item_code="", *, product_bundle_name="") -> str: + if item_code and product_bundle_name: + if name := frappe.db.exists( + "Product Bundle", {"name": product_bundle_name, "new_item_code": item_code, "disabled": 0} + ): + return name # type: ignore + elif item_code: + if name := frappe.db.exists("Product Bundle", {"new_item_code": item_code, "disabled": 0}): + return name # type: ignore + return "" + + +def get_product_bundle(item_code="", *, product_bundle_name="") -> "ProductBundle | None": + try: + if item_code and product_bundle_name: + return frappe.get_last_doc( + "Product Bundle", {"name": product_bundle_name, "new_item_code": item_code, "disabled": 0} + ) # type: ignore + elif item_code: + return frappe.get_last_doc("Product Bundle", {"new_item_code": item_code, "disabled": 0}) # type: ignore + except frappe.exceptions.DoesNotExistError: + frappe.clear_last_message() + return None def get_indexed_packed_items_table(doc): @@ -113,10 +140,12 @@ def get_indexed_packed_items_table(doc): Use: to quickly retrieve/check if row existed in table instead of looping n times """ - indexed_table = {} + from collections import defaultdict + + indexed_table = defaultdict(list) for packed_item in doc.get("packed_items"): key = (packed_item.parent_item, packed_item.item_code, packed_item.parent_detail_docname) - indexed_table[key] = packed_item + indexed_table[key].append(packed_item) return indexed_table @@ -131,8 +160,13 @@ def reset_packing_list(doc): # 1. items were deleted # 2. if bundle item replaced by another item (same no. of items but different items) # we maintain list to track recurring item rows as well - items_before_save = [(item.name, item.item_code) for item in doc_before_save.get("items")] - items_after_save = [(item.name, item.item_code) for item in doc.get("items")] + items_before_save = [ + (item.name, item.item_code, item.product_bundle_name or "") + for item in doc_before_save.get("items") + ] + items_after_save = [ + (item.name, item.item_code, item.product_bundle_name or "") for item in doc.get("items") + ] reset_table = items_before_save != items_after_save else: # reset: if via Update Items OR @@ -145,7 +179,10 @@ def reset_packing_list(doc): return reset_table -def get_product_bundle_items(item_code): +def get_product_bundle_items(item_code="", *, product_bundle_name=""): + if not product_bundle_name: + product_bundle_name = get_product_bundle_name(item_code) + product_bundle = frappe.qb.DocType("Product Bundle") product_bundle_item = frappe.qb.DocType("Product Bundle Item") @@ -161,12 +198,14 @@ def get_product_bundle_items(item_code): product_bundle.editable_bundle_item_rates, ) .where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0)) + .where(product_bundle.name == product_bundle_name) .orderby(product_bundle_item.idx) ) + return query.run(as_dict=True) -def add_packed_item_row(doc, packing_item, main_item_row, packed_items_table, reset): +def add_packed_item_row(doc, packing_item, main_item_row, packed_items_table: dict[tuple, list], reset): """Add and return packed item row. doc: Transaction document packing_item (dict): Packed Item details @@ -179,7 +218,7 @@ def add_packed_item_row(doc, packing_item, main_item_row, packed_items_table, re # check if row already exists in packed items table key = (main_item_row.item_code, packing_item.item_code, main_item_row.name) if packed_items_table.get(key): - pi_row, exists = packed_items_table.get(key), True + pi_row, exists = packed_items_table[key].pop(), True if not exists: pi_row = doc.append("packed_items", {}) @@ -330,7 +369,9 @@ def on_doctype_update(): def get_items_from_product_bundle(row): row, items = ItemDetailsCtx(json.loads(row)), [] - bundled_items = get_product_bundle_items(row["item_code"]) + bundled_items = get_product_bundle_items( + row["item_code"], product_bundle_name=row.get("product_bundle_name") + ) for item in bundled_items: row.update({"item_code": item.item_code, "qty": flt(row["quantity"]) * flt(item.qty)}) items.append(get_item_details(row)) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 46b6364bc16be927bea8110f60ac652af01a6872..9e7468e0aaedf5322ae2007f80bd2768e644a26e 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -18,6 +18,7 @@ from frappe.utils.nestedset import get_descendants_of from erpnext.selling.doctype.sales_order.sales_order import ( make_delivery_note as create_delivery_note_from_sales_order, ) +from erpnext.stock.doctype.packed_item.packed_item import get_product_bundle, get_product_bundle_name from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( get_auto_batch_nos, get_picked_serial_nos, @@ -557,7 +558,7 @@ class PickList(Document): frappe.throw(f"Row #{item.idx}: Item Code is Mandatory") if not cint( frappe.get_cached_value("Item", item.item_code, "is_stock_item") - ) and not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code, "disabled": 0}): + ) and not get_product_bundle_name(item.item_code): continue item_code = item.item_code reference = item.sales_order_item or item.material_request_item @@ -616,8 +617,8 @@ class PickList(Document): product_bundles = self._get_product_bundles() product_bundle_qty_map = self._get_product_bundle_qty_map(product_bundles.values()) - for so_row, item_code in product_bundles.items(): - picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[item_code]) + for so_row, bundle_tup in product_bundles.items(): + picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[bundle_tup]) item_table = "Sales Order Item" already_picked = frappe.db.get_value(item_table, so_row, "picked_qty", for_update=True) frappe.db.set_value( @@ -733,8 +734,8 @@ class PickList(Document): return query.run(as_dict=True) - def _get_product_bundles(self) -> dict[str, str]: - # Dict[so_item_row: item_code] + def _get_product_bundles(self) -> dict[str, tuple[str, str | None]]: + # Dict[so_item_row: (item_code, product_bundle_name)] product_bundles = {} for item in self.locations: if not item.product_bundle_item: @@ -742,19 +743,23 @@ class PickList(Document): product_bundles[item.product_bundle_item] = frappe.db.get_value( "Sales Order Item", item.product_bundle_item, - "item_code", + ["item_code", "product_bundle_name"], ) return product_bundles - def _get_product_bundle_qty_map(self, bundles: list[str]) -> dict[str, dict[str, float]]: + def _get_product_bundle_qty_map( + self, bundles: list[tuple[str, str | None]] + ) -> dict[tuple[str, str | None], dict[str, float]]: # bundle_item_code: Dict[component, qty] product_bundle_qty_map = {} - for bundle_item_code in bundles: - bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code, "disabled": 0}) - product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items} + for tup in bundles: + item_code, product_bundle_name = tup + bundle = get_product_bundle(item_code, product_bundle_name=product_bundle_name) + # TODO: Rows with same item code are not summed correctly + product_bundle_qty_map[tup] = {item.item_code: item.qty for item in bundle.items} return product_bundle_qty_map - def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> int: + def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items: dict[str, float]) -> int: """Compute how many full bundles can be created from picked items.""" precision = frappe.get_precision("Stock Ledger Entry", "qty_after_transaction") @@ -1277,11 +1282,11 @@ def add_product_bundles_to_delivery_note(pick_list: "PickList", delivery_note, i product_bundles = pick_list._get_product_bundles() product_bundle_qty_map = pick_list._get_product_bundle_qty_map(product_bundles.values()) - for so_row, item_code in product_bundles.items(): + for so_row, bundle_tup in product_bundles.items(): sales_order_item = frappe.get_doc("Sales Order Item", so_row) dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper) dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle( - so_row, product_bundle_qty_map[item_code] + so_row, product_bundle_qty_map[bundle_tup] ) update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 4b4b57605288dcf04909a3aeff82fdb0d82a82c7..70e7d88c7d5ffe75bbe976b0af56e2924e3a78ab 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -169,11 +169,13 @@ def remove_standard_fields(out: ItemDetails): def set_valuation_rate(out: ItemDetails | dict, ctx: ItemDetailsCtx): - if frappe.db.exists("Product Bundle", {"name": ctx.item_code, "disabled": 0}, cache=True): + from erpnext.stock.doctype.packed_item.packed_item import get_product_bundle, get_product_bundle_name + + if get_product_bundle_name(ctx.item_code, product_bundle_name=ctx.product_bundle_name): valuation_rate = 0.0 - bundled_items = frappe.get_doc("Product Bundle", ctx.item_code) + product_bundle = get_product_bundle(ctx.item_code, product_bundle_name=ctx.product_bundle_name) - for bundle_item in bundled_items.items: + for bundle_item in product_bundle.items: valuation_rate += flt( get_valuation_rate(bundle_item.item_code, ctx.company, out.get("warehouse")).get( "valuation_rate" diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.js b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.js index f8779c64e2def885d442f8315d1607768d934612..d2ea6f817c7c85c906893d284351cbb24d4d7a9f 100644 --- a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.js +++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.js @@ -55,7 +55,15 @@ frappe.query_reports["Product Bundle Balance"] = { ], initial_depth: 0, formatter: function (value, row, column, data, default_formatter) { - value = default_formatter(value, row, column, data); + if (data?.product_bundle_name && column?.fieldname === "item_code") { + value = frappe.format(data.product_bundle_name, { + fieldtype: "Link", + options: "Product Bundle", + }); + } else { + value = default_formatter(value, row, column, data); + } + if (!data.parent_item) { value = $(`${value}`); var $value = $(value).css("font-weight", "bold"); diff --git a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py index 10f8650b525c5024790b2144f4dc0d9932023a9c..9251cac14f30c190e625ec9a5c4e2f034b079d52 100644 --- a/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py +++ b/erpnext/stock/report/product_bundle_balance/product_bundle_balance.py @@ -16,14 +16,13 @@ def execute(filters=None): filters = frappe._dict() columns = get_columns() - item_details, pb_details, parent_items, child_items = get_items(filters) + bundle_details, bundle_contents, bundle_names, child_items = get_items(filters) stock_balance = get_stock_balance(filters, child_items) data = [] - for parent_item in parent_items: - parent_item_detail = item_details[parent_item] - - required_items = pb_details[parent_item] + for parent_bundle in bundle_names: + parent_item_detail = bundle_details[parent_bundle] + required_items = bundle_contents[parent_bundle] warehouse_company_map = {} for child_item in required_items: child_item_balance = stock_balance.get(child_item.item_code, frappe._dict()) @@ -34,7 +33,7 @@ def execute(filters=None): for warehouse, company in warehouse_company_map.items(): parent_row = { "indent": 0, - "item_code": parent_item, + "product_bundle_name": parent_bundle, "item_name": parent_item_detail.item_name, "item_group": parent_item_detail.item_group, "brand": parent_item_detail.brand, @@ -51,7 +50,7 @@ def execute(filters=None): ) child_row = { "indent": 1, - "parent_item": parent_item, + "parent_item": parent_bundle, "item_code": child_item_detail.item_code, "item_name": child_item_detail.item_name, "item_group": child_item_detail.item_group, @@ -122,8 +121,8 @@ def get_columns(): def get_items(filters): - pb_details = frappe._dict() - item_details = frappe._dict() + bundle_contents = frappe._dict() + bundle_details = frappe._dict() item = frappe.qb.DocType("Item") pb = frappe.qb.DocType("Product Bundle") @@ -133,6 +132,7 @@ def get_items(filters): .inner_join(pb) .on(pb.new_item_code == item.name) .select( + pb.name.as_("product_bundle_name"), item.name.as_("item_code"), item.item_name, pb.description, @@ -141,6 +141,7 @@ def get_items(filters): item.stock_uom, ) .where(IfNull(item.disabled, 0) == 0) + .where(IfNull(pb.disabled, 0) == 0) ) if item_code := filters.get("item_code"): @@ -154,13 +155,13 @@ def get_items(filters): parent_item_details = query.run(as_dict=True) - parent_items = [] + bundle_names = [] for d in parent_item_details: - parent_items.append(d.item_code) - item_details[d.item_code] = d + bundle_names.append(d.product_bundle_name) + bundle_details[d.product_bundle_name] = d child_item_details = [] - if parent_items: + if bundle_names: item = frappe.qb.DocType("Item") pb = frappe.qb.DocType("Product Bundle") pbi = frappe.qb.DocType("Product Bundle Item") @@ -172,7 +173,7 @@ def get_items(filters): .inner_join(item) .on(item.name == pbi.item_code) .select( - pb.new_item_code.as_("parent_item"), + pb.name.as_("product_bundle_name"), pbi.item_code, item.item_name, pbi.description, @@ -182,18 +183,16 @@ def get_items(filters): pbi.uom, pbi.qty, ) - .where(pb.new_item_code.isin(parent_items)) + .where(pb.name.isin(bundle_names)) ).run(as_dict=1) child_items = set() for d in child_item_details: - if d.item_code != d.parent_item: - pb_details.setdefault(d.parent_item, []).append(d) - child_items.add(d.item_code) - item_details[d.item_code] = d + bundle_contents.setdefault(d.product_bundle_name, []).append(d) + child_items.add(d.item_code) child_items = list(child_items) - return item_details, pb_details, parent_items, child_items + return bundle_details, bundle_contents, bundle_names, child_items def get_stock_balance(filters, items):