diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 007f4bbd8f1f..5a6187b346fa 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -3,9 +3,9 @@ import functools import re -from collections import defaultdict, deque +from collections import deque from operator import itemgetter -from typing import List, Optional +from typing import List import frappe from frappe import _ @@ -373,35 +373,9 @@ def update_cost(self, update_parent=True, from_child_bom=False, update_hour_rate existing_bom_cost = self.total_cost - for d in self.get("items"): - if not d.item_code: - continue - - rate = self.get_rm_rate( - { - "company": self.company, - "item_code": d.item_code, - "bom_no": d.bom_no, - "qty": d.qty, - "uom": d.uom, - "stock_uom": d.stock_uom, - "conversion_factor": d.conversion_factor, - "sourced_by_supplier": d.sourced_by_supplier, - } - ) - - if rate: - d.rate = rate - d.amount = flt(d.rate) * flt(d.qty) - d.base_rate = flt(d.rate) * flt(self.conversion_rate) - d.base_amount = flt(d.amount) * flt(self.conversion_rate) - - if save: - d.db_update() - if self.docstatus == 1: self.flags.ignore_validate_update_after_submit = True - self.calculate_cost(update_hour_rate) + self.calculate_cost(save_updates=save, update_hour_rate=update_hour_rate) if save: self.db_update() @@ -603,11 +577,11 @@ def _get_children(bom_no): bom_list.reverse() return bom_list - def calculate_cost(self, update_hour_rate=False): + def calculate_cost(self, save_update=False, update_hour_rate=False): """Calculate bom totals""" self.calculate_op_cost(update_hour_rate) - self.calculate_rm_cost() - self.calculate_sm_cost() + self.calculate_rm_cost(save=save_update) + self.calculate_sm_cost(save=save_update) self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost self.base_total_cost = ( self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost @@ -649,7 +623,7 @@ def update_rate_and_time(self, row, update_hour_rate=False): if update_hour_rate: row.db_update() - def calculate_rm_cost(self): + def calculate_rm_cost(self, save=False): """Fetch RM rate as per today's valuation rate and calculate totals""" total_rm_cost = 0 base_total_rm_cost = 0 @@ -664,11 +638,13 @@ def calculate_rm_cost(self): total_rm_cost += d.amount base_total_rm_cost += d.base_amount + if save: + d.db_update() self.raw_material_cost = total_rm_cost self.base_raw_material_cost = base_total_rm_cost - def calculate_sm_cost(self): + def calculate_sm_cost(self, save=False): """Fetch RM rate as per today's valuation rate and calculate totals""" total_sm_cost = 0 base_total_sm_cost = 0 @@ -683,6 +659,8 @@ def calculate_sm_cost(self): ) total_sm_cost += d.amount base_total_sm_cost += d.base_amount + if save: + d.db_update() self.scrap_material_cost = total_sm_cost self.base_scrap_material_cost = base_total_sm_cost @@ -1120,81 +1098,6 @@ def get_children(doctype, parent=None, is_root=False, **filters): return bom_items -def get_boms_in_bottom_up_order(bom_no: Optional[str] = None) -> List: - def _generate_child_parent_map(): - bom = frappe.qb.DocType("BOM") - bom_item = frappe.qb.DocType("BOM Item") - - bom_parents = ( - frappe.qb.from_(bom_item) - .join(bom) - .on(bom_item.parent == bom.name) - .select(bom_item.bom_no, bom_item.parent) - .where( - (bom_item.bom_no.isnotnull()) - & (bom_item.bom_no != "") - & (bom.docstatus == 1) - & (bom.is_active == 1) - & (bom_item.parenttype == "BOM") - ) - ).run(as_dict=True) - - child_parent_map = defaultdict(list) - for bom in bom_parents: - child_parent_map[bom.bom_no].append(bom.parent) - - return child_parent_map - - def _get_flat_parent_map(leaf, child_parent_map): - parents_list = [] - - def _get_parents(node, parents_list): - "Returns updated ancestors list." - first_parents = child_parent_map.get(node) # immediate parents of node - if not first_parents: # top most node - return parents_list - - parents_list.extend(first_parents) - parents_list = list(dict.fromkeys(parents_list).keys()) # remove duplicates - - for nth_node in first_parents: - # recursively find parents - parents_list = _get_parents(nth_node, parents_list) - - return parents_list - - parents_list = _get_parents(leaf, parents_list) - return parents_list - - def _get_leaf_boms(): - return 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, '')!='')""" - ) - - bom_list = [] - if bom_no: - bom_list.append(bom_no) - else: - bom_list = _get_leaf_boms() - - child_parent_map = _generate_child_parent_map() - - for leaf_bom in bom_list: - # generate list recursively bottom to top - parent_list = _get_flat_parent_map(leaf_bom, child_parent_map) - - if not parent_list: - continue - - bom_list.extend(parent_list) - bom_list = list(dict.fromkeys(bom_list).keys()) # remove duplicates - - return bom_list - - def add_additional_cost(stock_entry, work_order): # Add non stock items cost in the additional cost stock_entry.additional_costs = [] diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index 4061c5af7c2a..41922fb82459 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -2,7 +2,8 @@ # For license information, please see license.txt import json -from typing import TYPE_CHECKING, Dict, Optional, Union +from collections import defaultdict +from typing import TYPE_CHECKING, Dict, List, Optional, Union from typing_extensions import Literal @@ -12,8 +13,6 @@ import frappe from frappe.model.document import Document -from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order - class BOMUpdateTool(Document): pass @@ -49,7 +48,10 @@ def update_cost() -> None: """Updates Cost for all BOMs from bottom to top.""" bom_list = get_boms_in_bottom_up_order() for bom in bom_list: - frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True) + bom_doc = frappe.get_doc("BOM", bom) + bom_doc.calculate_cost(save_updates=True, update_hour_rate=True) + # bom_doc.update_exploded_items(save=True) #TODO: edit exploded items rate + bom_doc.db_update() def create_bom_update_log( @@ -69,3 +71,90 @@ def create_bom_update_log( "update_type": update_type, } ).submit() + + +def get_boms_in_bottom_up_order(bom_no: Optional[str] = None) -> List: + """ + Eg: Main BOM + |- Sub BOM 1 + |- Leaf BOM 1 + |- Sub BOM 2 + |- Leaf BOM 2 + Result: [Leaf BOM 1, Leaf BOM 2, Sub BOM 1, Sub BOM 2, Main BOM] + """ + leaf_boms = [] + if bom_no: + leaf_boms.append(bom_no) + else: + leaf_boms = _get_leaf_boms() + + child_parent_map = _generate_child_parent_map() + bom_list = leaf_boms.copy() + + for leaf_bom in leaf_boms: + parent_list = _get_flat_parent_map(leaf_bom, child_parent_map) + + if not parent_list: + continue + + bom_list.extend(parent_list) + bom_list = list(dict.fromkeys(bom_list).keys()) # remove duplicates + + return bom_list + + +def _generate_child_parent_map(): + bom = frappe.qb.DocType("BOM") + bom_item = frappe.qb.DocType("BOM Item") + + bom_parents = ( + frappe.qb.from_(bom_item) + .join(bom) + .on(bom_item.parent == bom.name) + .select(bom_item.bom_no, bom_item.parent) + .where( + (bom_item.bom_no.isnotnull()) + & (bom_item.bom_no != "") + & (bom.docstatus == 1) + & (bom.is_active == 1) + & (bom_item.parenttype == "BOM") + ) + ).run(as_dict=True) + + child_parent_map = defaultdict(list) + for bom in bom_parents: + child_parent_map[bom.bom_no].append(bom.parent) + + return child_parent_map + + +def _get_flat_parent_map(leaf, child_parent_map): + "Get ancestors at all levels of a leaf BOM." + parents_list = [] + + def _get_parents(node, parents_list): + "Returns recursively updated ancestors list." + first_parents = child_parent_map.get(node) # immediate parents of node + if not first_parents: # top most node + return parents_list + + parents_list.extend(first_parents) + parents_list = list(dict.fromkeys(parents_list).keys()) # remove duplicates + + for nth_node in first_parents: + # recursively find parents + parents_list = _get_parents(nth_node, parents_list) + + return parents_list + + parents_list = _get_parents(leaf, parents_list) + return parents_list + + +def _get_leaf_boms(): + return 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, '')!='')""" + )