diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index d640f3fda7..37d2b9ff97 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -918,7 +918,7 @@ def validate_bom_no(item, bom_no): frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item)) @frappe.whitelist() -def get_children(doctype, parent=None, is_root=False, **filters): +def get_children(parent=None, is_root=False, **filters): if not parent or parent=="BOM": frappe.msgprint(_('Please select a BOM')) return diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 0babf875e7..c78ff51a7c 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -232,7 +232,7 @@ frappe.ui.form.on('Production Plan', { }); }, combine_items: function (frm) { - frm.clear_table('prod_plan_references'); + frm.clear_table("prod_plan_references"); frappe.call({ method: "get_items", @@ -247,6 +247,13 @@ frappe.ui.form.on('Production Plan', { }); }, + combine_sub_items: (frm) => { + if (frm.doc.sub_assembly_items.length > 0) { + frm.clear_table("sub_assembly_items"); + frm.trigger("get_sub_assembly_items"); + } + }, + get_sub_assembly_items: function(frm) { frm.dirty(); diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 56cf2b4f08..3bfb764ba5 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -36,6 +36,7 @@ "prod_plan_references", "section_break_24", "get_sub_assembly_items", + "combine_sub_items", "sub_assembly_items", "material_request_planning", "include_non_stock_items", @@ -340,7 +341,6 @@ { "fieldname": "prod_plan_references", "fieldtype": "Table", - "hidden": 1, "label": "Production Plan Item Reference", "options": "Production Plan Item Reference" }, @@ -370,16 +370,23 @@ "fieldname": "to_delivery_date", "fieldtype": "Date", "label": "To Delivery Date" + }, + { + "default": "0", + "fieldname": "combine_sub_items", + "fieldtype": "Check", + "label": "Consolidate Sub Assembly Items" } ], "icon": "fa fa-calendar", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-09-06 18:35:59.642232", + "modified": "2022-02-23 17:16:10.629378", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 80003dab78..48cd753d75 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -21,7 +21,8 @@ from frappe.utils import ( ) from frappe.utils.csvutils import build_csv_response -from erpnext.manufacturing.doctype.bom.bom import get_children, validate_bom_no +from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_children +from erpnext.manufacturing.doctype.bom.bom import validate_bom_no from erpnext.manufacturing.doctype.work_order.work_order import get_item_details from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults @@ -570,17 +571,28 @@ class ProductionPlan(Document): @frappe.whitelist() def get_sub_assembly_items(self, manufacturing_type=None): + "Fetch sub assembly items and optionally combine them." self.sub_assembly_items = [] + sub_assembly_items_store = [] # temporary store to process all subassembly items + for row in self.po_items: bom_data = [] get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty) self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type) + sub_assembly_items_store.extend(bom_data) - self.sub_assembly_items.sort(key= lambda d: d.bom_level, reverse=True) - for idx, row in enumerate(self.sub_assembly_items, start=1): - row.idx = idx + if self.combine_sub_items: + # Combine subassembly items + sub_assembly_items_store = self.combine_subassembly_items(sub_assembly_items_store) + + sub_assembly_items_store.sort(key= lambda d: d.bom_level, reverse=True) # sort by bom level + + for idx, row in enumerate(sub_assembly_items_store): + row.idx = idx + 1 + self.append("sub_assembly_items", row) def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None): + "Modify bom_data, set additional details." for data in bom_data: data.qty = data.stock_qty data.production_plan_item = row.name @@ -589,7 +601,32 @@ class ProductionPlan(Document): data.type_of_manufacturing = manufacturing_type or ("Subcontract" if data.is_sub_contracted_item else "In House") - self.append("sub_assembly_items", data) + def combine_subassembly_items(self, sub_assembly_items_store): + "Aggregate if same: Item, Warehouse, Inhouse/Outhouse Manu.g, BOM No." + key_wise_data = {} + for row in sub_assembly_items_store: + key = ( + row.get("production_item"), row.get("fg_warehouse"), + row.get("bom_no"), row.get("type_of_manufacturing") + ) + if key not in key_wise_data: + # intialise (item, wh, bom no, man.g type) wise dict + key_wise_data[key] = row + continue + + existing_row = key_wise_data[key] + if existing_row: + # if row with same (item, wh, bom no, man.g type) key, merge + existing_row.qty += flt(row.qty) + existing_row.stock_qty += flt(row.stock_qty) + existing_row.bom_level = max(existing_row.bom_level, row.bom_level) + continue + else: + # add row with key + key_wise_data[key] = row + + sub_assembly_items_store = [key_wise_data[key] for key in key_wise_data] # unpack into single level list + return sub_assembly_items_store def all_items_completed(self): all_items_produced = all(flt(d.planned_qty) - flt(d.produced_qty) < 0.000001 @@ -1031,7 +1068,7 @@ def get_item_data(item_code): } def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): - data = get_children('BOM', parent = bom_no) + data = get_bom_children(parent=bom_no) for d in data: if d.expandable: parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")