Merge pull request #29522 from ankush/drop_bom_level
refactor!: dynamically compute bom_level
This commit is contained in:
commit
f026ec6bad
@ -37,7 +37,6 @@
|
|||||||
"inspection_required",
|
"inspection_required",
|
||||||
"quality_inspection_template",
|
"quality_inspection_template",
|
||||||
"column_break_31",
|
"column_break_31",
|
||||||
"bom_level",
|
|
||||||
"section_break_33",
|
"section_break_33",
|
||||||
"items",
|
"items",
|
||||||
"scrap_section",
|
"scrap_section",
|
||||||
@ -522,13 +521,6 @@
|
|||||||
"fieldname": "column_break_31",
|
"fieldname": "column_break_31",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "bom_level",
|
|
||||||
"fieldtype": "Int",
|
|
||||||
"label": "BOM Level",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_33",
|
"fieldname": "section_break_33",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
@ -540,7 +532,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-11-18 13:04:16.271975",
|
"modified": "2022-01-30 21:27:54.727298",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM",
|
"name": "BOM",
|
||||||
@ -577,5 +569,6 @@
|
|||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
@ -155,7 +155,6 @@ class BOM(WebsiteGenerator):
|
|||||||
self.calculate_cost()
|
self.calculate_cost()
|
||||||
self.update_stock_qty()
|
self.update_stock_qty()
|
||||||
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
|
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
|
||||||
self.set_bom_level()
|
|
||||||
self.validate_scrap_items()
|
self.validate_scrap_items()
|
||||||
|
|
||||||
def get_context(self, context):
|
def get_context(self, context):
|
||||||
@ -716,20 +715,6 @@ class BOM(WebsiteGenerator):
|
|||||||
"""Get a complete tree representation preserving order of child items."""
|
"""Get a complete tree representation preserving order of child items."""
|
||||||
return BOMTree(self.name)
|
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 validate_scrap_items(self):
|
def validate_scrap_items(self):
|
||||||
for item in self.scrap_items:
|
for item in self.scrap_items:
|
||||||
msg = ""
|
msg = ""
|
||||||
|
@ -559,9 +559,11 @@ class ProductionPlan(Document):
|
|||||||
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
|
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)
|
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):
|
self.sub_assembly_items.sort(key= lambda d: d.bom_level, reverse=True)
|
||||||
bom_data = sorted(bom_data, key = lambda i: i.bom_level)
|
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:
|
for data in bom_data:
|
||||||
data.qty = data.stock_qty
|
data.qty = data.stock_qty
|
||||||
data.production_plan_item = row.name
|
data.production_plan_item = row.name
|
||||||
@ -1004,9 +1006,6 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
|
|||||||
for d in data:
|
for d in data:
|
||||||
if d.expandable:
|
if d.expandable:
|
||||||
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
|
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)
|
stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
|
||||||
bom_data.append(frappe._dict({
|
bom_data.append(frappe._dict({
|
||||||
'parent_item_code': parent_item_code,
|
'parent_item_code': parent_item_code,
|
||||||
@ -1017,7 +1016,7 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
|
|||||||
'uom': d.stock_uom,
|
'uom': d.stock_uom,
|
||||||
'bom_no': d.value,
|
'bom_no': d.value,
|
||||||
'is_sub_contracted_item': d.is_sub_contracted_item,
|
'is_sub_contracted_item': d.is_sub_contracted_item,
|
||||||
'bom_level': bom_level,
|
'bom_level': indent,
|
||||||
'indent': indent,
|
'indent': indent,
|
||||||
'stock_qty': stock_qty
|
'stock_qty': stock_qty
|
||||||
}))
|
}))
|
||||||
|
@ -347,6 +347,45 @@ class TestProductionPlan(ERPNextTestCase):
|
|||||||
|
|
||||||
frappe.db.rollback()
|
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):
|
def create_production_plan(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|
||||||
|
@ -102,7 +102,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": 1,
|
"columns": 1,
|
||||||
"fetch_from": "bom_no.bom_level",
|
|
||||||
"fieldname": "bom_level",
|
"fieldname": "bom_level",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@ -189,7 +188,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-28 20:10:56.296410",
|
"modified": "2022-01-30 21:31:10.527559",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Production Plan Sub Assembly Item",
|
"name": "Production Plan Sub Assembly Item",
|
||||||
@ -198,5 +197,6 @@
|
|||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
@ -26,8 +26,7 @@ def get_exploded_items(bom, data, indent=0, qty=1):
|
|||||||
'item_code': item.item_code,
|
'item_code': item.item_code,
|
||||||
'item_name': item.item_name,
|
'item_name': item.item_name,
|
||||||
'indent': indent,
|
'indent': indent,
|
||||||
'bom_level': (frappe.get_cached_value("BOM", item.bom_no, "bom_level")
|
'bom_level': indent,
|
||||||
if item.bom_no else ""),
|
|
||||||
'bom': item.bom_no,
|
'bom': item.bom_no,
|
||||||
'qty': item.qty * qty,
|
'qty': item.qty * qty,
|
||||||
'uom': item.uom,
|
'uom': item.uom,
|
||||||
@ -73,7 +72,7 @@ def get_columns():
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "BOM Level",
|
"label": "BOM Level",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Int",
|
||||||
"fieldname": "bom_level",
|
"fieldname": "bom_level",
|
||||||
"width": 100
|
"width": 100
|
||||||
},
|
},
|
||||||
|
@ -48,7 +48,7 @@ def get_production_plan_item_details(filters, data, order_details):
|
|||||||
"qty": row.planned_qty,
|
"qty": row.planned_qty,
|
||||||
"document_type": "Work Order",
|
"document_type": "Work Order",
|
||||||
"document_name": work_order or "",
|
"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),
|
"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))
|
"pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0))
|
||||||
})
|
})
|
||||||
|
@ -273,7 +273,6 @@ erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
|
|||||||
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
|
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
|
||||||
erpnext.patches.v13_0.update_response_by_variance
|
erpnext.patches.v13_0.update_response_by_variance
|
||||||
erpnext.patches.v13_0.update_job_card_details
|
erpnext.patches.v13_0.update_job_card_details
|
||||||
erpnext.patches.v13_0.update_level_in_bom #1234sswef
|
|
||||||
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
|
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_subscription_status_in_memberships
|
||||||
erpnext.patches.v13_0.update_amt_in_work_order_required_items
|
erpnext.patches.v13_0.update_amt_in_work_order_required_items
|
||||||
|
@ -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)
|
|
Loading…
x
Reference in New Issue
Block a user