From 92a3c020363963bdcc9e5c1815f65d624bf37bc1 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 30 Jan 2022 21:32:18 +0530 Subject: [PATCH 1/5] refactor!: dynamically compute bom_level (cherry picked from commit 157b4b33fe633ed96c7e36fdcc81c9f2245241ec) # Conflicts: # erpnext/manufacturing/doctype/bom/bom.py # erpnext/patches.txt --- erpnext/manufacturing/doctype/bom/bom.json | 11 ++----- erpnext/manufacturing/doctype/bom/bom.py | 10 ++++++ .../production_plan/production_plan.py | 7 ++--- .../production_plan_sub_assembly_item.json | 4 +-- .../report/bom_explorer/bom_explorer.py | 5 ++- .../production_plan_summary.py | 2 +- erpnext/patches.txt | 3 ++ erpnext/patches/v13_0/update_level_in_bom.py | 31 ------------------- 8 files changed, 22 insertions(+), 51 deletions(-) delete mode 100644 erpnext/patches/v13_0/update_level_in_bom.py diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 218ac64d8da5..0b4419694002 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -37,7 +37,6 @@ "inspection_required", "quality_inspection_template", "column_break_31", - "bom_level", "section_break_33", "items", "scrap_section", @@ -522,13 +521,6 @@ "fieldname": "column_break_31", "fieldtype": "Column Break" }, - { - "default": "0", - "fieldname": "bom_level", - "fieldtype": "Int", - "label": "BOM Level", - "read_only": 1 - }, { "fieldname": "section_break_33", "fieldtype": "Section Break", @@ -540,7 +532,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2021-11-18 13:04:16.271975", + "modified": "2022-01-30 21:27:54.727298", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", @@ -577,5 +569,6 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 0b5207ef8598..43b7bb082fa6 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -156,8 +156,12 @@ def validate(self): self.update_stock_qty() self.validate_scrap_items() self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False) +<<<<<<< HEAD self.set_bom_level() +======= + self.validate_scrap_items() +>>>>>>> 157b4b33fe (refactor!: dynamically compute bom_level) def get_context(self, context): context.parents = [{'name': 'boms', 'title': _('All BOMs') }] @@ -704,6 +708,12 @@ def validate_operations(self): if not d.batch_size or d.batch_size <= 0: d.batch_size = 1 +<<<<<<< HEAD +======= + def get_tree_representation(self) -> BOMTree: + """Get a complete tree representation preserving order of child items.""" + return BOMTree(self.name) +>>>>>>> 157b4b33fe (refactor!: dynamically compute bom_level) def validate_scrap_items(self): for item in self.scrap_items: diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 106777b82efe..0769371a0912 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -561,7 +561,7 @@ def get_sub_assembly_items(self, manufacturing_type=None): self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type) def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None): - bom_data = sorted(bom_data, key = lambda i: i.bom_level) + bom_data = sorted(bom_data, key = lambda i: i.bom_level, reverse=True) for data in bom_data: data.qty = data.stock_qty @@ -1005,9 +1005,6 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): for d in data: if d.expandable: parent_item_code = frappe.get_cached_value("BOM", bom_no, "item") - bom_level = (frappe.get_cached_value("BOM", d.value, "bom_level") - if d.value else 0) - stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) bom_data.append(frappe._dict({ 'parent_item_code': parent_item_code, @@ -1018,7 +1015,7 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): 'uom': d.stock_uom, 'bom_no': d.value, 'is_sub_contracted_item': d.is_sub_contracted_item, - 'bom_level': bom_level, + 'bom_level': indent, 'indent': indent, 'stock_qty': stock_qty })) diff --git a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json index 657ee35a852d..45ea26c3a8a4 100644 --- a/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json +++ b/erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json @@ -102,7 +102,6 @@ }, { "columns": 1, - "fetch_from": "bom_no.bom_level", "fieldname": "bom_level", "fieldtype": "Int", "in_list_view": 1, @@ -189,7 +188,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-28 20:10:56.296410", + "modified": "2022-01-30 21:31:10.527559", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan Sub Assembly Item", @@ -198,5 +197,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py index 25de2e03797b..19a80ab4076e 100644 --- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py +++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py @@ -26,8 +26,7 @@ def get_exploded_items(bom, data, indent=0, qty=1): 'item_code': item.item_code, 'item_name': item.item_name, 'indent': indent, - 'bom_level': (frappe.get_cached_value("BOM", item.bom_no, "bom_level") - if item.bom_no else ""), + 'bom_level': indent, 'bom': item.bom_no, 'qty': item.qty * qty, 'uom': item.uom, @@ -73,7 +72,7 @@ def get_columns(): }, { "label": "BOM Level", - "fieldtype": "Data", + "fieldtype": "Int", "fieldname": "bom_level", "width": 100 }, diff --git a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py index 55b1a3f2f9ae..aaa231466fda 100644 --- a/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py +++ b/erpnext/manufacturing/report/production_plan_summary/production_plan_summary.py @@ -48,7 +48,7 @@ def get_production_plan_item_details(filters, data, order_details): "qty": row.planned_qty, "document_type": "Work Order", "document_name": work_order or "", - "bom_level": frappe.get_cached_value("BOM", row.bom_no, "bom_level"), + "bom_level": 0, "produced_qty": order_details.get((work_order, row.item_code), {}).get("produced_qty", 0), "pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0)) }) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b5ee1a4ce8c6..cfe33faf5af3 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -292,8 +292,11 @@ erpnext.patches.v13_0.set_training_event_attendance erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.update_job_card_details +<<<<<<< HEAD erpnext.patches.v13_0.update_level_in_bom #1234sswef erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021 +======= +>>>>>>> 157b4b33fe (refactor!: dynamically compute bom_level) erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry erpnext.patches.v13_0.update_subscription_status_in_memberships erpnext.patches.v13_0.update_amt_in_work_order_required_items diff --git a/erpnext/patches/v13_0/update_level_in_bom.py b/erpnext/patches/v13_0/update_level_in_bom.py deleted file mode 100644 index 499412ee270d..000000000000 --- a/erpnext/patches/v13_0/update_level_in_bom.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) 2020, Frappe and Contributors -# License: GNU General Public License v3. See license.txt - - -import frappe - - -def execute(): - for document in ["bom", "bom_item", "bom_explosion_item"]: - frappe.reload_doc('manufacturing', 'doctype', document) - - frappe.db.sql(" update `tabBOM` set bom_level = 0 where docstatus = 1") - - bom_list = frappe.db.sql_list("""select name from `tabBOM` bom - where docstatus=1 and is_active=1 and not exists(select bom_no from `tabBOM Item` - where parent=bom.name and ifnull(bom_no, '')!='')""") - - count = 0 - while(count < len(bom_list)): - for parent_bom in get_parent_boms(bom_list[count]): - bom_doc = frappe.get_cached_doc("BOM", parent_bom) - bom_doc.set_bom_level(update=True) - bom_list.append(parent_bom) - count += 1 - -def get_parent_boms(bom_no): - return frappe.db.sql_list(""" - select distinct bom_item.parent from `tabBOM Item` bom_item - where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM' - and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1) - """, bom_no) From c8cfef077c049e8aad31026feb7b053fd98a7683 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 30 Jan 2022 22:57:15 +0530 Subject: [PATCH 2/5] fix(ux): sort multi-production item plans correctly (cherry picked from commit d38fd8635cdf3ad5aa8d9c441e4d7cbcf9aa4190) --- .../production_plan/production_plan.py | 6 ++- .../production_plan/test_production_plan.py | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 0769371a0912..f06624fe92c7 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -560,9 +560,11 @@ def get_sub_assembly_items(self, manufacturing_type=None): 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) - def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None): - bom_data = sorted(bom_data, key = lambda i: i.bom_level, reverse=True) + 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 + def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None): for data in bom_data: data.qty = data.stock_qty data.production_plan_item = row.name diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 2febc1e23c0a..21a126b2a79e 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -347,6 +347,45 @@ def test_get_sales_order_with_variant(self): frappe.db.rollback() + def test_subassmebly_sorting(self): + """ Test subassembly sorting in case of multiple items with nested BOMs""" + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + + prefix = "_TestLevel_" + boms = { + "Assembly": { + "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},}, + "SubAssembly2": {"ChildPart3": {}}, + "SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}}, + "ChildPart5": {}, + "ChildPart6": {}, + "SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}}, + }, + "MegaDeepAssy": { + "SecretSubassy": {"SecretPart": {"VerySecret" : { "SuperSecret": {"Classified": {}}}},}, + # ^ assert that this is + # first item in subassy table + } + } + create_nested_bom(boms, prefix=prefix) + + items = [prefix + item_code for item_code in boms.keys()] + plan = create_production_plan(item_code=items[0], do_not_save=True) + plan.append("po_items", { + 'use_multi_level_bom': 1, + 'item_code': items[1], + 'bom_no': frappe.db.get_value('Item', items[1], 'default_bom'), + 'planned_qty': 1, + 'planned_start_date': now_datetime() + }) + plan.get_sub_assembly_items() + + bom_level_order = [d.bom_level for d in plan.sub_assembly_items] + self.assertEqual(bom_level_order, sorted(bom_level_order, reverse=True)) + # lowest most level of subassembly should be first + self.assertIn("SuperSecret", plan.sub_assembly_items[0].production_item) + + def create_production_plan(**args): args = frappe._dict(args) From 323c251617d6922b573ca11fcc8c1d7f15053786 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 31 Jan 2022 16:29:34 +0530 Subject: [PATCH 3/5] fix: conflicts --- erpnext/manufacturing/doctype/bom/bom.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 43b7bb082fa6..73c6e34dc825 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -156,12 +156,6 @@ def validate(self): self.update_stock_qty() self.validate_scrap_items() self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False) -<<<<<<< HEAD - self.set_bom_level() - -======= - self.validate_scrap_items() ->>>>>>> 157b4b33fe (refactor!: dynamically compute bom_level) def get_context(self, context): context.parents = [{'name': 'boms', 'title': _('All BOMs') }] @@ -708,13 +702,6 @@ def validate_operations(self): if not d.batch_size or d.batch_size <= 0: d.batch_size = 1 -<<<<<<< HEAD -======= - def get_tree_representation(self) -> BOMTree: - """Get a complete tree representation preserving order of child items.""" - return BOMTree(self.name) ->>>>>>> 157b4b33fe (refactor!: dynamically compute bom_level) - def validate_scrap_items(self): for item in self.scrap_items: msg = "" From 4a036da32cded9cabfa4d890f8cd34fbe61aab95 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 31 Jan 2022 16:30:29 +0530 Subject: [PATCH 4/5] fix: removed set_bom_level --- erpnext/manufacturing/doctype/bom/bom.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 73c6e34dc825..b97dcab632f8 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -732,20 +732,6 @@ def get_tree_representation(self) -> BOMTree: """Get a complete tree representation preserving order of child items.""" return BOMTree(self.name) - def set_bom_level(self, update=False): - levels = [] - - self.bom_level = 0 - for row in self.items: - if row.bom_no: - levels.append(frappe.get_cached_value("BOM", row.bom_no, "bom_level") or 0) - - if levels: - self.bom_level = max(levels) + 1 - - if update: - self.db_set("bom_level", self.bom_level) - def get_bom_item_rate(args, bom_doc): if bom_doc.rm_cost_as_per == 'Valuation Rate': rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1) From 9cfe2a8b25d7d0e655aaea7a98ee08870c9a9548 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 31 Jan 2022 16:32:38 +0530 Subject: [PATCH 5/5] fix: conflict --- erpnext/patches.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index cfe33faf5af3..bcc0c019bf1f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -292,11 +292,7 @@ erpnext.patches.v13_0.set_training_event_attendance erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice erpnext.patches.v13_0.update_job_card_details -<<<<<<< HEAD -erpnext.patches.v13_0.update_level_in_bom #1234sswef erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021 -======= ->>>>>>> 157b4b33fe (refactor!: dynamically compute bom_level) erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry erpnext.patches.v13_0.update_subscription_status_in_memberships erpnext.patches.v13_0.update_amt_in_work_order_required_items