Merge pull request #38316 from frappe/mergify/bp/version-15-hotfix/pr-38269

fix: skip fixed assets in product bundle (backport #38269)
This commit is contained in:
Deepesh Garg 2023-11-24 16:30:29 +05:30 committed by GitHub
commit 64d9c5d61c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 147 additions and 325 deletions

View File

@ -556,7 +556,7 @@ 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", item_code):
if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}):
return get_bundle_availability(item_code, warehouse), is_stock_item
else:
is_stock_item = False

View File

@ -350,11 +350,12 @@ class SellingController(StockController):
return il
def has_product_bundle(self, item_code):
return frappe.db.sql(
"""select name from `tabProduct Bundle`
where new_item_code=%s and docstatus != 2""",
item_code,
)
product_bundle = frappe.qb.DocType("Product Bundle")
return (
frappe.qb.from_(product_bundle)
.select(product_bundle.name)
.where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0))
).run()
def get_already_delivered_qty(self, current_docname, so, so_detail):
delivered_via_dn = frappe.db.sql(

View File

@ -1,315 +1,119 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 0,
"beta": 0,
"creation": "2013-06-20 11:53:21",
"custom": 0,
"description": "Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have \"Is Stock Item\" as \"No\" and \"Is Sales Item\" as \"Yes\".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials",
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 0,
"actions": [],
"allow_import": 1,
"creation": "2013-06-20 11:53:21",
"description": "Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have \"Is Stock Item\" as \"No\" and \"Is Sales Item\" as \"Yes\".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"basic_section",
"new_item_code",
"description",
"column_break_eonk",
"disabled",
"item_section",
"items",
"section_break_4",
"about"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "basic_section",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "basic_section",
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "",
"fieldname": "new_item_code",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Parent Item",
"length": 0,
"no_copy": 1,
"oldfieldname": "new_item_code",
"oldfieldtype": "Data",
"options": "Item",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "new_item_code",
"fieldtype": "Link",
"in_global_search": 1,
"in_list_view": 1,
"label": "Parent Item",
"no_copy": 1,
"oldfieldname": "new_item_code",
"oldfieldtype": "Data",
"options": "Item",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "description",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Description"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "List items that form the package.",
"fieldname": "item_section",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Items",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"description": "List items that form the package.",
"fieldname": "item_section",
"fieldtype": "Section Break",
"label": "Items"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "items",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Items",
"length": 0,
"no_copy": 0,
"oldfieldname": "sales_bom_items",
"oldfieldtype": "Table",
"options": "Product Bundle Item",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
"oldfieldname": "sales_bom_items",
"oldfieldtype": "Table",
"options": "Product Bundle Item",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_4",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "section_break_4",
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "about",
"fieldtype": "HTML",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "",
"length": 0,
"no_copy": 0,
"options": "<h3>About Product Bundle</h3>\n\n<p>Aggregate group of <b>Items</b> into another <b>Item</b>. This is useful if you are bundling a certain <b>Items</b> into a package and you maintain stock of the packed <b>Items</b> and not the aggregate <b>Item</b>.</p>\n<p>The package <b>Item</b> will have <code>Is Stock Item</code> as <b>No</b> and <code>Is Sales Item</code> as <b>Yes</b>.</p>\n<h4>Example:</h4>\n<p>If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.</p>",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"fieldname": "about",
"fieldtype": "HTML",
"options": "<h3>About Product Bundle</h3>\n\n<p>Aggregate group of <b>Items</b> into another <b>Item</b>. This is useful if you are bundling a certain <b>Items</b> into a package and you maintain stock of the packed <b>Items</b> and not the aggregate <b>Item</b>.</p>\n<p>The package <b>Item</b> will have <code>Is Stock Item</code> as <b>No</b> and <code>Is Sales Item</code> as <b>Yes</b>.</p>\n<h4>Example:</h4>\n<p>If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.</p>"
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"fieldname": "column_break_eonk",
"fieldtype": "Column Break"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-sitemap",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Selling",
"name": "Product Bundle",
"owner": "Administrator",
],
"icon": "fa fa-sitemap",
"idx": 1,
"links": [],
"modified": "2023-11-22 15:20:46.805114",
"modified_by": "Administrator",
"module": "Selling",
"name": "Product Bundle",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock Manager",
"share": 1,
"write": 1
},
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
},
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock User"
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"share": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_order": "ASC",
"track_changes": 0,
"track_seen": 0
],
"sort_field": "modified",
"sort_order": "ASC",
"states": []
}

View File

@ -59,10 +59,12 @@ class ProductBundle(Document):
"""Validates, main Item is not a stock item"""
if frappe.db.get_value("Item", self.new_item_code, "is_stock_item"):
frappe.throw(_("Parent Item {0} must not be a Stock Item").format(self.new_item_code))
if frappe.db.get_value("Item", self.new_item_code, "is_fixed_asset"):
frappe.throw(_("Parent Item {0} must not be a Fixed Asset").format(self.new_item_code))
def validate_child_items(self):
for item in self.items:
if frappe.db.exists("Product Bundle", item.item_code):
if frappe.db.exists("Product Bundle", {"name": item.item_code, "disabled": 0}):
frappe.throw(
_(
"Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save"
@ -73,12 +75,17 @@ class ProductBundle(Document):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_new_item_code(doctype, txt, searchfield, start, page_len, filters):
from erpnext.controllers.queries import get_match_cond
return frappe.db.sql(
"""select name, item_name, description from tabItem
where is_stock_item=0 and name not in (select name from `tabProduct Bundle`)
and %s like %s %s limit %s offset %s"""
% (searchfield, "%s", get_match_cond(doctype), "%s", "%s"),
("%%%s%%" % txt, page_len, start),
)
product_bundles = frappe.db.get_list("Product Bundle", {"disabled": 0}, pluck="name")
item = frappe.qb.DocType("Item")
return (
frappe.qb.from_(item)
.select("*")
.where(
(item.is_stock_item == 0)
& (item.is_fixed_asset == 0)
& (item.name.notin(product_bundles))
& (item[searchfield].like(f"%{txt}%"))
)
.limit(page_len)
.offset(start)
).run()

View File

@ -688,7 +688,9 @@ def make_material_request(source_name, target_doc=None):
"Sales Order Item": {
"doctype": "Material Request Item",
"field_map": {"name": "sales_order_item", "parent": "sales_order"},
"condition": lambda item: not frappe.db.exists("Product Bundle", item.item_code)
"condition": lambda item: not frappe.db.exists(
"Product Bundle", {"name": item.item_code, "disabled": 0}
)
and get_remaining_qty(item) > 0,
"postprocess": update_item,
},
@ -1309,7 +1311,7 @@ def set_delivery_date(items, sales_order):
def is_product_bundle(item_code):
return frappe.db.exists("Product Bundle", item_code)
return frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0})
@frappe.whitelist()
@ -1521,7 +1523,7 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
product_bundle_parents = [
pb.new_item_code
for pb in frappe.get_all(
"Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"]
"Product Bundle", {"new_item_code": ["in", item_codes], "disabled": 0}, ["new_item_code"]
)
]

View File

@ -615,7 +615,7 @@ 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]},
filters={"new_item_code": ["in", items_list], "disabled": 0},
pluck="name",
)
@ -938,7 +938,7 @@ def make_packing_slip(source_name, target_doc=None):
},
"postprocess": update_item,
"condition": lambda item: (
not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code})
not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code, "disabled": 0})
and flt(item.packed_qty) < flt(item.qty)
),
},

View File

@ -512,8 +512,12 @@ 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})
new_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": new_name})
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)

View File

@ -55,7 +55,7 @@ def make_packing_list(doc):
def is_product_bundle(item_code: str) -> bool:
return bool(frappe.db.exists("Product Bundle", {"new_item_code": item_code}))
return bool(frappe.db.exists("Product Bundle", {"new_item_code": item_code, "disabled": 0}))
def get_indexed_packed_items_table(doc):
@ -111,7 +111,7 @@ def get_product_bundle_items(item_code):
product_bundle_item.uom,
product_bundle_item.description,
)
.where(product_bundle.new_item_code == item_code)
.where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0))
.orderby(product_bundle_item.idx)
)
return query.run(as_dict=True)

View File

@ -368,7 +368,9 @@ class PickList(Document):
frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx))
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}):
) and not frappe.db.exists(
"Product Bundle", {"new_item_code": item.item_code, "disabled": 0}
):
continue
item_code = item.item_code
reference = item.sales_order_item or item.material_request_item
@ -507,7 +509,9 @@ class PickList(Document):
# 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})
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}
return product_bundle_qty_map

View File

@ -149,7 +149,7 @@ def remove_standard_fields(details):
def set_valuation_rate(out, args):
if frappe.db.exists("Product Bundle", args.item_code, cache=True):
if frappe.db.exists("Product Bundle", {"name": args.item_code, "disabled": 0}, cache=True):
valuation_rate = 0.0
bundled_items = frappe.get_doc("Product Bundle", args.item_code)