Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Add exception handling in background job within BOM Update Tool #30146

Merged
merged 17 commits into from
Mar 31, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion erpnext/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@
],
"daily_long": [
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms",
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms",
"erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation",
"erpnext.hr.utils.generate_leave_encashment",
"erpnext.hr.utils.allocate_earned_leaves",
Expand Down
9 changes: 0 additions & 9 deletions erpnext/manufacturing/doctype/bom/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,15 +697,6 @@ def calculate_sm_cost(self):
self.scrap_material_cost = total_sm_cost
self.base_scrap_material_cost = base_total_sm_cost

def update_new_bom(self, old_bom, new_bom, rate):
for d in self.get("items"):
if d.bom_no != old_bom:
continue

d.bom_no = new_bom
d.rate = rate
d.amount = (d.stock_qty or d.qty) * rate

def update_exploded_items(self, save=True):
"""Update Flat BOM, following will be correct data"""
self.get_exploded_items()
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt

frappe.ui.form.on('BOM Update Log', {
// refresh: function(frm) {

// }
});
109 changes: 109 additions & 0 deletions erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
{
"actions": [],
"autoname": "BOM-UPDT-LOG-.#####",
"creation": "2022-03-16 14:23:35.210155",
"description": "BOM Update Tool Log with job status maintained",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"current_bom",
"new_bom",
"column_break_3",
"update_type",
"status",
"error_log",
"amended_from"
],
"fields": [
{
"fieldname": "current_bom",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Current BOM",
"options": "BOM"
},
{
"fieldname": "new_bom",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "New BOM",
"options": "BOM"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "update_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Update Type",
"options": "Replace BOM\nUpdate Cost"
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Queued\nIn Progress\nCompleted\nFailed"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "BOM Update Log",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "error_log",
"fieldtype": "Link",
"label": "Error Log",
"options": "Error Log"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-03-31 12:51:44.885102",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Update Log",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Manufacturing Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
164 changes: 164 additions & 0 deletions erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from typing import Dict, List, Literal, Optional

import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cstr, flt

from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost


class BOMMissingError(frappe.ValidationError):
pass


class BOMUpdateLog(Document):
def validate(self):
if self.update_type == "Replace BOM":
self.validate_boms_are_specified()
self.validate_same_bom()
self.validate_bom_items()

self.status = "Queued"

def validate_boms_are_specified(self):
if self.update_type == "Replace BOM" and not (self.current_bom and self.new_bom):
frappe.throw(
msg=_("Please mention the Current and New BOM for replacement."),
title=_("Mandatory"),
exc=BOMMissingError,
)

def validate_same_bom(self):
if cstr(self.current_bom) == cstr(self.new_bom):
frappe.throw(_("Current BOM and New BOM can not be same"))

def validate_bom_items(self):
current_bom_item = frappe.db.get_value("BOM", self.current_bom, "item")
new_bom_item = frappe.db.get_value("BOM", self.new_bom, "item")

if current_bom_item != new_bom_item:
frappe.throw(_("The selected BOMs are not for the same item"))

def on_submit(self):
if frappe.flags.in_test:
return

if self.update_type == "Replace BOM":
boms = {"current_bom": self.current_bom, "new_bom": self.new_bom}
frappe.enqueue(
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job",
doc=self,
boms=boms,
timeout=40000,
)
else:
frappe.enqueue(
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job",
doc=self,
update_type="Update Cost",
timeout=40000,
)


def replace_bom(boms: Dict) -> None:
"""Replace current BOM with new BOM in parent BOMs."""
current_bom = boms.get("current_bom")
new_bom = boms.get("new_bom")

unit_cost = get_new_bom_unit_cost(new_bom)
update_new_bom_in_bom_items(unit_cost, current_bom, new_bom)

frappe.cache().delete_key("bom_children")
parent_boms = get_parent_boms(new_bom)

for bom in parent_boms:
bom_obj = frappe.get_doc("BOM", bom)
# this is only used for versioning and we do not want
# to make separate db calls by using load_doc_before_save
# which proves to be expensive while doing bulk replace
bom_obj._doc_before_save = bom_obj
bom_obj.update_exploded_items()
bom_obj.calculate_cost()
bom_obj.update_parent_cost()
bom_obj.db_update()
if bom_obj.meta.get("track_changes") and not bom_obj.flags.ignore_version:
bom_obj.save_version()


def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None:
bom_item = frappe.qb.DocType("BOM Item")
(
frappe.qb.update(bom_item)
.set(bom_item.bom_no, new_bom)
.set(bom_item.rate, unit_cost)
.set(bom_item.amount, (bom_item.stock_qty * unit_cost))
.where(
(bom_item.bom_no == current_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM")
)
).run()
marination marked this conversation as resolved.
Show resolved Hide resolved


def get_parent_boms(new_bom: str, bom_list: Optional[List] = None) -> List:
bom_list = bom_list or []
bom_item = frappe.qb.DocType("BOM Item")

parents = (
frappe.qb.from_(bom_item)
.select(bom_item.parent)
.where((bom_item.bom_no == new_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM"))
.run(as_dict=True)
)

for d in parents:
if new_bom == d.parent:
frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent))

bom_list.append(d.parent)
get_parent_boms(d.parent, bom_list)

return list(set(bom_list))


def get_new_bom_unit_cost(new_bom: str) -> float:
bom = frappe.qb.DocType("BOM")
new_bom_unitcost = (
frappe.qb.from_(bom).select(bom.total_cost / bom.quantity).where(bom.name == new_bom).run()
)

return flt(new_bom_unitcost[0][0])


def run_bom_job(
doc: "BOMUpdateLog",
boms: Optional[Dict[str, str]] = None,
update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM",
) -> None:
try:
doc.db_set("status", "In Progress")
if not frappe.flags.in_test:
frappe.db.commit()

frappe.db.auto_commit_on_many_writes = 1

boms = frappe._dict(boms or {})

if update_type == "Replace BOM":
replace_bom(boms)
else:
update_cost()

doc.db_set("status", "Completed")

except Exception:
frappe.db.rollback()
error_log = frappe.log_error(message=frappe.get_traceback(), title=_("BOM Update Tool Error"))

doc.db_set("status", "Failed")
doc.db_set("error_log", error_log.name)

finally:
frappe.db.auto_commit_on_many_writes = 0
frappe.db.commit() # nosemgrep
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
frappe.listview_settings['BOM Update Log'] = {
add_fields: ["status"],
get_indicator: function(doc) {
let status_map = {
"Queued": "orange",
"In Progress": "blue",
"Completed": "green",
"Failed": "red"
};

return [__(doc.status), status_map[doc.status], "status,=," + doc.status];
}
};
Loading