Skip to content

Commit

Permalink
fix: DB update child items, remove redundancy, fix perf
Browse files Browse the repository at this point in the history
- Move `get_boms_in_bottom_up_order` in bom update tool’s file
- Remove repeated rm cost update from `update_cost`. `calculate_cost` handles RM cost update
- db_update children in `calculate_cost` optionally
- Don’t call `update_exploded_items` and regenerate exploded items in `update_cost`. They will stay the same (except cost)
  • Loading branch information
marination committed May 20, 2022
1 parent 87c2b3b commit cbc52a2
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 113 deletions.
121 changes: 12 additions & 109 deletions erpnext/manufacturing/doctype/bom/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 _
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 = []
Expand Down
97 changes: 93 additions & 4 deletions erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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, '')!='')"""
)

0 comments on commit cbc52a2

Please sign in to comment.