From 7317a0696b7370631cd22b8faafe3594adcb9909 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 9 Mar 2022 19:03:07 +0530 Subject: [PATCH 01/15] refactor: Add exception handling in background job within BOM Update Tool (cherry picked from commit f57725f8fa016b9826e8fdf2f14dbf1a3d9991f7) --- .../bom_update_tool/bom_update_tool.py | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) 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 9f120d175ed..11092702ca3 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -117,21 +117,32 @@ def update_latest_price_in_all_boms(): def replace_bom(args): - frappe.db.auto_commit_on_many_writes = 1 - args = frappe._dict(args) - - doc = frappe.get_doc("BOM Update Tool") - doc.current_bom = args.current_bom - doc.new_bom = args.new_bom - doc.replace_bom() - - frappe.db.auto_commit_on_many_writes = 0 + try: + frappe.db.auto_commit_on_many_writes = 1 + args = frappe._dict(args) + doc = frappe.get_doc("BOM Update Tool") + doc.current_bom = args.current_bom + doc.new_bom = args.new_bom + doc.replace_bom() + except Exception: + frappe.log_error( + msg=frappe.get_traceback(), + title=_("BOM Update Tool Error") + ) + finally: + frappe.db.auto_commit_on_many_writes = 0 def update_cost(): - frappe.db.auto_commit_on_many_writes = 1 - 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) - - frappe.db.auto_commit_on_many_writes = 0 + try: + frappe.db.auto_commit_on_many_writes = 1 + 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) + except Exception: + frappe.log_error( + msg=frappe.get_traceback(), + title=_("BOM Update Tool Error") + ) + finally: + frappe.db.auto_commit_on_many_writes = 0 From 7aa37ec5114d8b5aefb2d1d87bb6a4be2a5afe2a Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 16 Mar 2022 19:45:03 +0530 Subject: [PATCH 02/15] feat: BOM Update Log - Created BOM Update Log that will handle queued job status and failures - Moved validation and BG job to thus new doctype - BOM Update Tool only works as an endpoint (cherry picked from commit 4283a13e5a6a6b9f1e8e1cbcc639646a4e957b36) # Conflicts: # erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py --- erpnext/hooks.py | 2 +- .../doctype/bom_update_log/__init__.py | 0 .../doctype/bom_update_log/bom_update_log.js | 8 ++ .../bom_update_log/bom_update_log.json | 101 +++++++++++++++ .../doctype/bom_update_log/bom_update_log.py | 117 ++++++++++++++++++ .../bom_update_log/test_bom_update_log.py | 9 ++ .../bom_update_tool/bom_update_tool.py | 34 ++++- 7 files changed, 266 insertions(+), 5 deletions(-) create mode 100644 erpnext/manufacturing/doctype/bom_update_log/__init__.py create mode 100644 erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js create mode 100644 erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json create mode 100644 erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py create mode 100644 erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 906eb10c64f..a21a0313544 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -511,7 +511,7 @@ scheduler_events = { ], "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", diff --git a/erpnext/manufacturing/doctype/bom_update_log/__init__.py b/erpnext/manufacturing/doctype/bom_update_log/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js new file mode 100644 index 00000000000..6da808e26d1 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.js @@ -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) { + + // } +}); diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json new file mode 100644 index 00000000000..222168be8cf --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json @@ -0,0 +1,101 @@ +{ + "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", + "amended_from" + ], + "fields": [ + { + "fieldname": "current_bom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Current BOM", + "options": "BOM", + "reqd": 1 + }, + { + "fieldname": "new_bom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "New BOM", + "options": "BOM", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "update_type", + "fieldtype": "Select", + "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 + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2022-03-16 18:25:49.833836", + "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 +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py new file mode 100644 index 00000000000..10db0de9a11 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -0,0 +1,117 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import cstr + +from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order + +from rq.timeouts import JobTimeoutException + + +class BOMMissingError(frappe.ValidationError): pass + +class BOMUpdateLog(Document): + def validate(self): + 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_tool.bom_update_tool.replace_bom", + boms=boms, doc=self, timeout=40000 + ) + else: + frappe.enqueue( + method="erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_cost_queue", + doc=self, timeout=40000 + ) + +def replace_bom(boms, doc): + try: + doc.db_set("status", "In Progress") + if not frappe.flags.in_test: + frappe.db.commit() + + frappe.db.auto_commit_on_many_writes = 1 + + args = frappe._dict(boms) + doc = frappe.get_doc("BOM Update Tool") + doc.current_bom = args.current_bom + doc.new_bom = args.new_bom + doc.replace_bom() + + doc.db_set("status", "Completed") + + except (Exception, JobTimeoutException): + frappe.db.rollback() + frappe.log_error( + msg=frappe.get_traceback(), + title=_("BOM Update Tool Error") + ) + doc.db_set("status", "Failed") + + finally: + frappe.db.auto_commit_on_many_writes = 0 + frappe.db.commit() + +def update_cost_queue(doc): + try: + doc.db_set("status", "In Progress") + if not frappe.flags.in_test: + frappe.db.commit() + + frappe.db.auto_commit_on_many_writes = 1 + + 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) + + doc.db_set("status", "Completed") + + except (Exception, JobTimeoutException): + frappe.db.rollback() + frappe.log_error( + msg=frappe.get_traceback(), + title=_("BOM Update Tool Error") + ) + doc.db_set("status", "Failed") + + finally: + frappe.db.auto_commit_on_many_writes = 0 + frappe.db.commit() + +def update_cost(): + 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) \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py new file mode 100644 index 00000000000..f74bdc356a7 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestBOMUpdateLog(FrappeTestCase): + pass 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 11092702ca3..7e072a9d1b2 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -11,13 +11,11 @@ from frappe.model.document import Document from frappe.utils import cstr, flt from six import string_types -from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order +from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import update_cost class BOMUpdateTool(Document): def replace_bom(self): - self.validate_bom() - unit_cost = get_new_bom_unit_cost(self.new_bom) self.update_new_bom(unit_cost) @@ -43,6 +41,7 @@ class BOMUpdateTool(Document): except Exception: frappe.log_error(frappe.get_traceback()) +<<<<<<< HEAD def validate_bom(self): if cstr(self.current_bom) == cstr(self.new_bom): frappe.throw(_("Current BOM and New BOM can not be same")) @@ -52,6 +51,8 @@ class BOMUpdateTool(Document): ): frappe.throw(_("The selected BOMs are not for the same item")) +======= +>>>>>>> 4283a13e5a (feat: BOM Update Log) def update_new_bom(self, unit_cost): frappe.db.sql( """update `tabBOM Item` set bom_no=%s, @@ -93,16 +94,21 @@ def enqueue_replace_bom(args): if isinstance(args, string_types): args = json.loads(args) +<<<<<<< HEAD frappe.enqueue( "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.replace_bom", args=args, timeout=40000, ) +======= + create_bom_update_log(boms=args) +>>>>>>> 4283a13e5a (feat: BOM Update Log) frappe.msgprint(_("Queued for replacing the BOM. It may take a few minutes.")) @frappe.whitelist() def enqueue_update_cost(): +<<<<<<< HEAD frappe.enqueue( "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_cost", timeout=40000 ) @@ -110,11 +116,18 @@ def enqueue_update_cost(): _("Queued for updating latest price in all Bill of Materials. It may take a few minutes.") ) +======= + create_bom_update_log(update_type="Update Cost") + frappe.msgprint(_("Queued for updating latest price in all Bill of Materials. It may take a few minutes.")) +>>>>>>> 4283a13e5a (feat: BOM Update Log) -def update_latest_price_in_all_boms(): + +def auto_update_latest_price_in_all_boms(): + "Called via hooks.py." if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"): update_cost() +<<<<<<< HEAD def replace_bom(args): try: @@ -146,3 +159,16 @@ def update_cost(): ) finally: frappe.db.auto_commit_on_many_writes = 0 +======= +def create_bom_update_log(boms=None, update_type="Replace BOM"): + "Creates a BOM Update Log that handles the background job." + current_bom = boms.get("current_bom") if boms else None + new_bom = boms.get("new_bom") if boms else None + log_doc = frappe.get_doc({ + "doctype": "BOM Update Log", + "current_bom": current_bom, + "new_bom": new_bom, + "update_type": update_type + }) + log_doc.submit() +>>>>>>> 4283a13e5a (feat: BOM Update Log) From 59af5562413e1616c2c0a24541c2aabf1e80538f Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 17 Mar 2022 12:32:37 +0530 Subject: [PATCH 03/15] chore: Polish error handling and code sepration - Added Typing - Moved all job business logic to bom update log - Added `run_bom_job` that handles errors and runs either of two methods - UX: Replace button disabled until both inputs are filled - Show log creation message on UI for correctness - APIs return log document as result - Converted raw sql to QB (cherry picked from commit cff91558d4f380cc7566d009ea85ccba36976f69) # Conflicts: # erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py --- .../bom_update_log/bom_update_log.json | 8 +- .../doctype/bom_update_log/bom_update_log.py | 146 ++++++++++++------ .../bom_update_tool/bom_update_tool.js | 43 +++++- .../bom_update_tool/bom_update_tool.py | 55 ++++++- 4 files changed, 186 insertions(+), 66 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json index 222168be8cf..d89427edc0b 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json @@ -20,16 +20,14 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Current BOM", - "options": "BOM", - "reqd": 1 + "options": "BOM" }, { "fieldname": "new_bom", "fieldtype": "Link", "in_list_view": 1, "label": "New BOM", - "options": "BOM", - "reqd": 1 + "options": "BOM" }, { "fieldname": "column_break_3", @@ -61,7 +59,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-03-16 18:25:49.833836", + "modified": "2022-03-17 12:21:16.156437", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Update Log", diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index 10db0de9a11..b08d6f906c2 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -1,23 +1,27 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +from typing import Dict, List, Optional +import click import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cstr - -from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order - +from frappe.utils import cstr, flt from rq.timeouts import JobTimeoutException +from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost -class BOMMissingError(frappe.ValidationError): pass + +class BOMMissingError(frappe.ValidationError): + pass class BOMUpdateLog(Document): def validate(self): - self.validate_boms_are_specified() - self.validate_same_bom() - self.validate_bom_items() + 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): @@ -48,16 +52,88 @@ class BOMUpdateLog(Document): "new_bom": self.new_bom } frappe.enqueue( - method="erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.replace_bom", - boms=boms, doc=self, timeout=40000 + 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_tool.bom_update_tool.update_cost_queue", - doc=self, timeout=40000 + 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, doc): +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(unit_cost, current_bom, new_bom) + + frappe.cache().delete_key('bom_children') + parent_boms = get_parent_boms(new_bom) + + with click.progressbar(parent_boms) as parent_boms: + pass + for bom in parent_boms: + bom_obj = frappe.get_cached_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_new_bom(unit_cost, current_bom, new_bom) + 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(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() + +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] = None, update_type: Optional[str] = "Replace BOM") -> None: try: doc.db_set("status", "In Progress") if not frappe.flags.in_test: @@ -65,18 +141,19 @@ def replace_bom(boms, doc): frappe.db.auto_commit_on_many_writes = 1 - args = frappe._dict(boms) - doc = frappe.get_doc("BOM Update Tool") - doc.current_bom = args.current_bom - doc.new_bom = args.new_bom - doc.replace_bom() + boms = frappe._dict(boms or {}) + + if update_type == "Replace BOM": + replace_bom(boms) + else: + update_cost() doc.db_set("status", "Completed") except (Exception, JobTimeoutException): frappe.db.rollback() frappe.log_error( - msg=frappe.get_traceback(), + message=frappe.get_traceback(), title=_("BOM Update Tool Error") ) doc.db_set("status", "Failed") @@ -84,34 +161,3 @@ def replace_bom(boms, doc): finally: frappe.db.auto_commit_on_many_writes = 0 frappe.db.commit() - -def update_cost_queue(doc): - try: - doc.db_set("status", "In Progress") - if not frappe.flags.in_test: - frappe.db.commit() - - frappe.db.auto_commit_on_many_writes = 1 - - 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) - - doc.db_set("status", "Completed") - - except (Exception, JobTimeoutException): - frappe.db.rollback() - frappe.log_error( - msg=frappe.get_traceback(), - title=_("BOM Update Tool Error") - ) - doc.db_set("status", "Failed") - - finally: - frappe.db.auto_commit_on_many_writes = 0 - frappe.db.commit() - -def update_cost(): - 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) \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js index bf5fe2e18de..ec6a76d61c4 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js @@ -20,30 +20,63 @@ frappe.ui.form.on('BOM Update Tool', { refresh: function(frm) { frm.disable_save(); + frm.events.disable_button(frm, "replace"); }, - replace: function(frm) { + disable_button: (frm, field, disable=true) => { + frm.get_field(field).input.disabled = disable; + }, + + current_bom: (frm) => { + if (frm.doc.current_bom && frm.doc.new_bom){ + frm.events.disable_button(frm, "replace", false); + } + }, + + new_bom: (frm) => { + if (frm.doc.current_bom && frm.doc.new_bom){ + frm.events.disable_button(frm, "replace", false); + } + }, + + replace: (frm) => { if (frm.doc.current_bom && frm.doc.new_bom) { frappe.call({ method: "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.enqueue_replace_bom", freeze: true, args: { - args: { + boms: { "current_bom": frm.doc.current_bom, "new_bom": frm.doc.new_bom } + }, + callback: result => { + if (result && result.message && !result.exc) { + frm.events.confirm_job_start(frm, result.message); + } } }); } }, - update_latest_price_in_all_boms: function() { + update_latest_price_in_all_boms: (frm) => { frappe.call({ method: "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.enqueue_update_cost", freeze: true, - callback: function() { - frappe.msgprint(__("Latest price updated in all BOMs")); + callback: result => { + if (result && result.message && !result.exc) { + frm.events.confirm_job_start(frm, result.message); + } } }); + }, + + confirm_job_start: (frm, log_data) => { + let log_link = frappe.utils.get_form_link("BOM Update Log", log_data.name, true) + frappe.msgprint({ + "message": __(`BOM Updation is queued and may take a few minutes. Check ${log_link} for progress.`), + "title": __("BOM Update Initiated"), + "indicator": "blue" + }); } }); 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 7e072a9d1b2..448b73a531d 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -1,20 +1,23 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - import json +from typing import Dict, List, Optional, TYPE_CHECKING, Union + +if TYPE_CHECKING: + from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog -import click import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import cstr, flt from six import string_types -from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import update_cost +from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order class BOMUpdateTool(Document): +<<<<<<< HEAD def replace_bom(self): unit_cost = get_new_bom_unit_cost(self.new_bom) self.update_new_bom(unit_cost) @@ -87,9 +90,13 @@ def get_new_bom_unit_cost(bom): ) return flt(new_bom_unitcost[0][0]) if new_bom_unitcost else 0 +======= + pass +>>>>>>> cff91558d4 (chore: Polish error handling and code sepration) @frappe.whitelist() +<<<<<<< HEAD def enqueue_replace_bom(args): if isinstance(args, string_types): args = json.loads(args) @@ -104,9 +111,19 @@ def enqueue_replace_bom(args): create_bom_update_log(boms=args) >>>>>>> 4283a13e5a (feat: BOM Update Log) frappe.msgprint(_("Queued for replacing the BOM. It may take a few minutes.")) +======= +def enqueue_replace_bom(boms: Optional[Union[Dict, str]] = None, args: Optional[Union[Dict, str]] = None) -> "BOMUpdateLog": + """Returns a BOM Update Log (that queues a job) for BOM Replacement.""" + boms = boms or args + if isinstance(boms, str): + boms = json.loads(boms) +>>>>>>> cff91558d4 (chore: Polish error handling and code sepration) + update_log = create_bom_update_log(boms=boms) + return update_log @frappe.whitelist() +<<<<<<< HEAD def enqueue_update_cost(): <<<<<<< HEAD frappe.enqueue( @@ -120,14 +137,21 @@ def enqueue_update_cost(): create_bom_update_log(update_type="Update Cost") frappe.msgprint(_("Queued for updating latest price in all Bill of Materials. It may take a few minutes.")) >>>>>>> 4283a13e5a (feat: BOM Update Log) +======= +def enqueue_update_cost() -> "BOMUpdateLog": + """Returns a BOM Update Log (that queues a job) for BOM Cost Updation.""" + update_log = create_bom_update_log(update_type="Update Cost") + return update_log +>>>>>>> cff91558d4 (chore: Polish error handling and code sepration) -def auto_update_latest_price_in_all_boms(): - "Called via hooks.py." +def auto_update_latest_price_in_all_boms() -> None: + """Called via hooks.py.""" if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"): update_cost() <<<<<<< HEAD +<<<<<<< HEAD def replace_bom(args): try: @@ -172,3 +196,22 @@ def create_bom_update_log(boms=None, update_type="Replace BOM"): }) log_doc.submit() >>>>>>> 4283a13e5a (feat: BOM Update Log) +======= +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) + +def create_bom_update_log(boms: Optional[Dict] = None, update_type: str = "Replace BOM") -> "BOMUpdateLog": + """Creates a BOM Update Log that handles the background job.""" + boms = boms or {} + current_bom = boms.get("current_bom") + new_bom = boms.get("new_bom") + return frappe.get_doc({ + "doctype": "BOM Update Log", + "current_bom": current_bom, + "new_bom": new_bom, + "update_type": update_type, + }).submit() +>>>>>>> cff91558d4 (chore: Polish error handling and code sepration) From 444af4588f8c194d800044017fd9e7bb8dfe71b2 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 17 Mar 2022 12:58:09 +0530 Subject: [PATCH 04/15] feat: List View indicators for Log and Error Log link in log (cherry picked from commit 8aff75f8e8f6cf885f0e59ead89b8596d6f56c0a) --- .../doctype/bom_update_log/bom_update_log.json | 9 ++++++++- .../doctype/bom_update_log/bom_update_log.py | 4 +++- .../doctype/bom_update_log/bom_update_log_list.js | 13 +++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json index d89427edc0b..38c685a64f1 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json @@ -12,6 +12,7 @@ "column_break_3", "update_type", "status", + "error_log", "amended_from" ], "fields": [ @@ -53,13 +54,19 @@ "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-17 12:21:16.156437", + "modified": "2022-03-17 12:51:28.067900", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Update Log", diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index b08d6f906c2..a69b15c5274 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -152,11 +152,13 @@ def run_bom_job(doc: "BOMUpdateLog", boms: Optional[Dict] = None, update_type: O except (Exception, JobTimeoutException): frappe.db.rollback() - frappe.log_error( + 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 diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js new file mode 100644 index 00000000000..8b3dc520cfa --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js @@ -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]; + } +}; \ No newline at end of file From 8b5e759965f19e0af5b6017fe5b7e9c400fe61a3 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 17 Mar 2022 15:03:20 +0530 Subject: [PATCH 05/15] fix: Sider and Linter (cherry picked from commit 3e3af95712b5241a243a5b6169be2fc888bb4c39) --- .../doctype/bom_update_log/bom_update_log.py | 56 +++++++++---------- .../bom_update_log/bom_update_log_list.js | 2 +- .../bom_update_tool/bom_update_tool.js | 6 +- .../bom_update_tool/bom_update_tool.py | 2 +- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index a69b15c5274..7f60d8fc7df 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -1,8 +1,8 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt from typing import Dict, List, Optional -import click +import click import frappe from frappe import _ from frappe.model.document import Document @@ -89,39 +89,39 @@ def replace_bom(boms: Dict) -> None: bom_obj.save_version() def update_new_bom(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() + 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() 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") + 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) + 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)) + 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) + bom_list.append(d.parent) + get_parent_boms(d.parent, bom_list) - return list(set(bom_list)) + return list(set(bom_list)) def get_new_bom_unit_cost(new_bom: str) -> float: bom = frappe.qb.DocType("BOM") diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js index 8b3dc520cfa..e39b5637c78 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log_list.js @@ -6,7 +6,7 @@ frappe.listview_settings['BOM Update Log'] = { "In Progress": "blue", "Completed": "green", "Failed": "red" - } + }; return [__(doc.status), status_map[doc.status], "status,=," + doc.status]; } diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js index ec6a76d61c4..0c9816712c2 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js @@ -28,13 +28,13 @@ frappe.ui.form.on('BOM Update Tool', { }, current_bom: (frm) => { - if (frm.doc.current_bom && frm.doc.new_bom){ + if (frm.doc.current_bom && frm.doc.new_bom) { frm.events.disable_button(frm, "replace", false); } }, new_bom: (frm) => { - if (frm.doc.current_bom && frm.doc.new_bom){ + if (frm.doc.current_bom && frm.doc.new_bom) { frm.events.disable_button(frm, "replace", false); } }, @@ -72,7 +72,7 @@ frappe.ui.form.on('BOM Update Tool', { }, confirm_job_start: (frm, log_data) => { - let log_link = frappe.utils.get_form_link("BOM Update Log", log_data.name, true) + let log_link = frappe.utils.get_form_link("BOM Update Log", log_data.name, true); frappe.msgprint({ "message": __(`BOM Updation is queued and may take a few minutes. Check ${log_link} for progress.`), "title": __("BOM Update Initiated"), 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 448b73a531d..55674890b15 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,7 @@ # For license information, please see license.txt import json -from typing import Dict, List, Optional, TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Dict, Optional, Union if TYPE_CHECKING: from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog From 5dca5563ff4db075071a9dd1d34ea7c6ae80cdd8 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 17 Mar 2022 17:43:12 +0530 Subject: [PATCH 06/15] fix: Test, Sider and Added button to access log from Tool (cherry picked from commit f3715ab38260f21f5be8c6f9bdfcf8a02c051556) # Conflicts: # erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py --- .../doctype/bom_update_tool/bom_update_tool.js | 4 ++++ .../doctype/bom_update_tool/bom_update_tool.py | 4 +++- .../bom_update_tool/test_bom_update_tool.py | 16 +++++++++------- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js index 0c9816712c2..a793ed95354 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js @@ -21,6 +21,10 @@ frappe.ui.form.on('BOM Update Tool', { refresh: function(frm) { frm.disable_save(); frm.events.disable_button(frm, "replace"); + + frm.add_custom_button(__("View BOM Update Log"), () => { + frappe.set_route("List", "BOM Update Log"); + }); }, disable_button: (frm, field, disable=true) => { 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 55674890b15..56512526065 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -8,10 +8,12 @@ if TYPE_CHECKING: from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog import frappe -from frappe import _ from frappe.model.document import Document +<<<<<<< HEAD from frappe.utils import cstr, flt from six import string_types +======= +>>>>>>> f3715ab382 (fix: Test, Sider and Added button to access log from Tool) from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py index 57785e58dd0..8da5393f913 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py @@ -5,6 +5,7 @@ import frappe from frappe.tests.utils import FrappeTestCase from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost +from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import replace_bom from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.stock.doctype.item.test_item import create_item @@ -19,18 +20,19 @@ class TestBOMUpdateTool(FrappeTestCase): bom_doc.items[1].item_code = "_Test Item" bom_doc.insert() - update_tool = frappe.get_doc("BOM Update Tool") - update_tool.current_bom = current_bom - update_tool.new_bom = bom_doc.name - update_tool.replace_bom() + boms = frappe._dict( + current_bom=current_bom, + new_bom=bom_doc.name + ) + replace_bom(boms) self.assertFalse(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", current_bom)) self.assertTrue(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", bom_doc.name)) # reverse, as it affects other testcases - update_tool.current_bom = bom_doc.name - update_tool.new_bom = current_bom - update_tool.replace_bom() + boms.current_bom = bom_doc.name + boms.new_bom = current_bom + replace_bom(boms) def test_bom_cost(self): for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]: From 9b069ed04b59eb51368d076307dfa1eda0daef88 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 30 Mar 2022 13:01:01 +0530 Subject: [PATCH 07/15] test: API hit via BOM Update Tool - test creation of log and it's impact (cherry picked from commit 1d1e925bcf6066cac03abfb60510e76d0f97f9be) --- .../bom_update_log/test_bom_update_log.py | 83 ++++++++++++++++++- .../bom_update_tool/test_bom_update_tool.py | 2 + 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py index f74bdc356a7..52ca9cde1bd 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py @@ -1,9 +1,88 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe +import frappe from frappe.tests.utils import FrappeTestCase +from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import ( + BOMMissingError, + run_bom_job, +) +from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import enqueue_replace_bom + +test_records = frappe.get_test_records("BOM") + class TestBOMUpdateLog(FrappeTestCase): - pass + "Test BOM Update Tool Operations via BOM Update Log." + + def setUp(self): + bom_doc = frappe.copy_doc(test_records[0]) + bom_doc.items[1].item_code = "_Test Item" + bom_doc.insert() + + self.boms = frappe._dict( + current_bom="BOM-_Test Item Home Desktop Manufactured-001", + new_bom=bom_doc.name, + ) + + self.new_bom_doc = bom_doc + + def tearDown(self): + frappe.db.rollback() + + if self._testMethodName == "test_bom_update_log_completion": + # clear logs and delete BOM created via setUp + frappe.db.delete("BOM Update Log") + self.new_bom_doc.cancel() + self.new_bom_doc.delete() + frappe.db.commit() # explicitly commit and restore to original state + + def test_bom_update_log_validate(self): + "Test if BOM presence is validated." + + with self.assertRaises(BOMMissingError): + enqueue_replace_bom(boms={}) + + def test_bom_update_log_queueing(self): + "Test if BOM Update Log is created and queued." + + log = enqueue_replace_bom( + boms=self.boms, + ) + + self.assertEqual(log.docstatus, 1) + self.assertEqual(log.status, "Queued") + + def test_bom_update_log_completion(self): + "Test if BOM Update Log handles job completion correctly." + + log = enqueue_replace_bom( + boms=self.boms, + ) + + # Explicitly commits log, new bom (setUp) and replacement impact. + # Is run via background jobs IRL + run_bom_job( + doc=log, + boms=self.boms, + update_type="Replace BOM", + ) + log.reload() + + self.assertEqual(log.status, "Completed") + + # teardown (undo replace impact) due to commit + boms = frappe._dict( + current_bom=self.boms.new_bom, + new_bom=self.boms.current_bom, + ) + log2 = enqueue_replace_bom( + boms=self.boms, + ) + run_bom_job( # Explicitly commits + doc=log2, + boms=boms, + update_type="Replace BOM", + ) + self.assertEqual(log2.status, "Completed") diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py index 8da5393f913..36bcd9dcd09 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py @@ -13,6 +13,8 @@ test_records = frappe.get_test_records("BOM") class TestBOMUpdateTool(FrappeTestCase): + "Test major functions run via BOM Update Tool." + def test_replace_bom(self): current_bom = "BOM-_Test Item Home Desktop Manufactured-001" From 1f9ecb33979d3f9a6fd8d2cfce7d067ea403ef75 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 30 Mar 2022 13:08:58 +0530 Subject: [PATCH 08/15] fix: Auto format `bom_update_log.py` (cherry picked from commit 79495679e209a31a1865b7d4bd1bfc42c4813403) --- .../doctype/bom_update_log/bom_update_log.py | 68 +++++++++---------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index 7f60d8fc7df..172f38d250f 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -15,6 +15,7 @@ from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update class BOMMissingError(frappe.ValidationError): pass + class BOMUpdateLog(Document): def validate(self): if self.update_type == "Replace BOM": @@ -28,7 +29,8 @@ class BOMUpdateLog(Document): 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 + title=_("Mandatory"), + exc=BOMMissingError, ) def validate_same_bom(self): @@ -47,20 +49,22 @@ class BOMUpdateLog(Document): return if self.update_type == "Replace BOM": - boms = { - "current_bom": self.current_bom, - "new_bom": self.new_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 + 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 + 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") @@ -69,13 +73,13 @@ def replace_bom(boms: Dict) -> None: unit_cost = get_new_bom_unit_cost(new_bom) update_new_bom(unit_cost, current_bom, new_bom) - frappe.cache().delete_key('bom_children') + frappe.cache().delete_key("bom_children") parent_boms = get_parent_boms(new_bom) with click.progressbar(parent_boms) as parent_boms: pass for bom in parent_boms: - bom_obj = frappe.get_cached_doc('BOM', bom) + bom_obj = frappe.get_cached_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 @@ -85,34 +89,29 @@ def replace_bom(boms: Dict) -> None: 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: + if bom_obj.meta.get("track_changes") and not bom_obj.flags.ignore_version: bom_obj.save_version() + def update_new_bom(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( + 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") + (bom_item.bom_no == current_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM") ).run() + 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) + 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: @@ -123,17 +122,19 @@ def get_parent_boms(new_bom: str, bom_list: Optional[List] = None) -> 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() + 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] = None, update_type: Optional[str] = "Replace BOM") -> None: + +def run_bom_job( + doc: "BOMUpdateLog", boms: Optional[Dict] = None, update_type: Optional[str] = "Replace BOM" +) -> None: try: doc.db_set("status", "In Progress") if not frappe.flags.in_test: @@ -152,10 +153,7 @@ def run_bom_job(doc: "BOMUpdateLog", boms: Optional[Dict] = None, update_type: O except (Exception, JobTimeoutException): frappe.db.rollback() - error_log = frappe.log_error( - message=frappe.get_traceback(), - title=_("BOM Update Tool Error") - ) + 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) From c0c39f8c795ca8ca155ab52acfee9cb677de2958 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 30 Mar 2022 13:22:29 +0530 Subject: [PATCH 09/15] fix: Semgrep - Explain explicit commits and skip semgrep - Format client side translated string correctly (cherry picked from commit ebf00946c91bf03105533d46c85e9b405cc7d62a) --- .../manufacturing/doctype/bom_update_log/bom_update_log.py | 2 +- .../doctype/bom_update_log/test_bom_update_log.py | 4 +++- .../manufacturing/doctype/bom_update_tool/bom_update_tool.js | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index 172f38d250f..ce2774347b2 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -160,4 +160,4 @@ def run_bom_job( finally: frappe.db.auto_commit_on_many_writes = 0 - frappe.db.commit() + frappe.db.commit() # nosemgrep diff --git a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py index 52ca9cde1bd..d1da18d0ab8 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py @@ -36,7 +36,9 @@ class TestBOMUpdateLog(FrappeTestCase): frappe.db.delete("BOM Update Log") self.new_bom_doc.cancel() self.new_bom_doc.delete() - frappe.db.commit() # explicitly commit and restore to original state + + # explicitly commit and restore to original state + frappe.db.commit() # nosemgrep def test_bom_update_log_validate(self): "Test if BOM presence is validated." diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js index a793ed95354..7ba6517a4fb 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.js @@ -78,7 +78,7 @@ frappe.ui.form.on('BOM Update Tool', { confirm_job_start: (frm, log_data) => { let log_link = frappe.utils.get_form_link("BOM Update Log", log_data.name, true); frappe.msgprint({ - "message": __(`BOM Updation is queued and may take a few minutes. Check ${log_link} for progress.`), + "message": __("BOM Updation is queued and may take a few minutes. Check {0} for progress.", [log_link]), "title": __("BOM Update Initiated"), "indicator": "blue" }); From 0d3c8e4d7458d6763115af7dbd262a0864ad991d Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 30 Mar 2022 18:03:52 +0530 Subject: [PATCH 10/15] fix: Type Annotations, Redundancy, etc. - Renamed public function`update_new_bom` to `update_new_bom_in_bom_items` - Replaced `get_cached_doc` with `get_doc` - Removed click progress bar (drive through update log) - Removed `bom_obj.update_new_bom()`, was redundant. Did same job as `update_new_bom_in_bom_items` - Removed `update_new_bom()` in `bom.py`, unused. - Prettier query formatting - `update_type` annotated as non optional Literal - Removed redundant use of JobTimeoutException - Corrected type annotations in `create_bom_update_log()` (cherry picked from commit 620575a9012a9759c6285558ac25c6709c4e92cc) # Conflicts: # erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py --- erpnext/manufacturing/doctype/bom/bom.py | 9 ------ .../doctype/bom_update_log/bom_update_log.py | 31 ++++++++++--------- .../bom_update_tool/bom_update_tool.py | 11 ++++++- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8fd6050b4f9..f8fcd073951 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -687,15 +687,6 @@ class BOM(WebsiteGenerator): 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() diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index ce2774347b2..139dcbcdd90 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -1,13 +1,11 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from typing import Dict, List, Optional +from typing import Dict, List, Literal, Optional -import click import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import cstr, flt -from rq.timeouts import JobTimeoutException from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost @@ -71,20 +69,17 @@ def replace_bom(boms: Dict) -> None: new_bom = boms.get("new_bom") unit_cost = get_new_bom_unit_cost(new_bom) - update_new_bom(unit_cost, current_bom, 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) - with click.progressbar(parent_boms) as parent_boms: - pass for bom in parent_boms: - bom_obj = frappe.get_cached_doc("BOM", bom) + 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_new_bom(unit_cost, current_bom, new_bom) bom_obj.update_exploded_items() bom_obj.calculate_cost() bom_obj.update_parent_cost() @@ -93,12 +88,16 @@ def replace_bom(boms: Dict) -> None: bom_obj.save_version() -def update_new_bom(unit_cost: float, current_bom: str, new_bom: str) -> None: +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") + ( + 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() @@ -133,7 +132,9 @@ def get_new_bom_unit_cost(new_bom: str) -> float: def run_bom_job( - doc: "BOMUpdateLog", boms: Optional[Dict] = None, update_type: Optional[str] = "Replace BOM" + 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") @@ -151,7 +152,7 @@ def run_bom_job( doc.db_set("status", "Completed") - except (Exception, JobTimeoutException): + except Exception: frappe.db.rollback() error_log = frappe.log_error(message=frappe.get_traceback(), title=_("BOM Update Tool Error")) 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 56512526065..a7573902d78 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,7 @@ # For license information, please see license.txt import json -from typing import TYPE_CHECKING, Dict, Optional, Union +from typing import TYPE_CHECKING, Dict, Literal, Optional, Union if TYPE_CHECKING: from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog @@ -205,8 +205,17 @@ def update_cost() -> None: for bom in bom_list: frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True) +<<<<<<< HEAD def create_bom_update_log(boms: Optional[Dict] = None, update_type: str = "Replace BOM") -> "BOMUpdateLog": +======= + +def create_bom_update_log( + boms: Optional[Dict[str, str]] = None, + update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM", +) -> "BOMUpdateLog": +>>>>>>> 620575a901 (fix: Type Annotations, Redundancy, etc.) """Creates a BOM Update Log that handles the background job.""" + boms = boms or {} current_bom = boms.get("current_bom") new_bom = boms.get("new_bom") From 770f8da792b0baa32be0b0909e300d513dd0ed49 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 30 Mar 2022 18:20:54 +0530 Subject: [PATCH 11/15] test: Added test for 2 more validations - Covers full validate function (cherry picked from commit a945484af4f69c8b698a2283f4078b99c38df039) --- .../doctype/bom_update_log/test_bom_update_log.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py index d1da18d0ab8..47efea961b4 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py @@ -46,6 +46,12 @@ class TestBOMUpdateLog(FrappeTestCase): with self.assertRaises(BOMMissingError): enqueue_replace_bom(boms={}) + with self.assertRaises(frappe.ValidationError): + enqueue_replace_bom(boms=frappe._dict(current_bom=self.boms.new_bom, new_bom=self.boms.new_bom)) + + with self.assertRaises(frappe.ValidationError): + enqueue_replace_bom(boms=frappe._dict(current_bom=self.boms.new_bom, new_bom="Dummy BOM")) + def test_bom_update_log_queueing(self): "Test if BOM Update Log is created and queued." From a9ec72d83320bdd8d1be2d5253523d2ff6873b6d Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 31 Mar 2022 12:55:48 +0530 Subject: [PATCH 12/15] chore: Added BOM std filters and update type in List View (cherry picked from commit 2fece523f6c0cda8025334e4680794b963fb6914) --- .../manufacturing/doctype/bom_update_log/bom_update_log.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json index 38c685a64f1..98c1acb71ce 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json @@ -20,6 +20,7 @@ "fieldname": "current_bom", "fieldtype": "Link", "in_list_view": 1, + "in_standard_filter": 1, "label": "Current BOM", "options": "BOM" }, @@ -27,6 +28,7 @@ "fieldname": "new_bom", "fieldtype": "Link", "in_list_view": 1, + "in_standard_filter": 1, "label": "New BOM", "options": "BOM" }, @@ -37,6 +39,7 @@ { "fieldname": "update_type", "fieldtype": "Select", + "in_list_view": 1, "label": "Update Type", "options": "Replace BOM\nUpdate Cost" }, @@ -66,7 +69,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-03-17 12:51:28.067900", + "modified": "2022-03-31 12:51:44.885102", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Update Log", From e8f3e23008b9899297ead7951cb2c57ccdffb545 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 5 Apr 2022 18:22:35 +0530 Subject: [PATCH 13/15] fix: Merge Conflicts --- .../bom_update_tool/bom_update_tool.py | 183 ++---------------- 1 file changed, 12 insertions(+), 171 deletions(-) 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 a7573902d78..b0e7da12017 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -9,142 +9,32 @@ if TYPE_CHECKING: import frappe from frappe.model.document import Document -<<<<<<< HEAD -from frappe.utils import cstr, flt -from six import string_types -======= ->>>>>>> f3715ab382 (fix: Test, Sider and Added button to access log from Tool) from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order class BOMUpdateTool(Document): -<<<<<<< HEAD - def replace_bom(self): - unit_cost = get_new_bom_unit_cost(self.new_bom) - self.update_new_bom(unit_cost) - - frappe.cache().delete_key("bom_children") - bom_list = self.get_parent_boms(self.new_bom) - - with click.progressbar(bom_list) as bom_list: - pass - for bom in bom_list: - try: - bom_obj = frappe.get_cached_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_new_bom(self.current_bom, self.new_bom, unit_cost) - 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() - except Exception: - frappe.log_error(frappe.get_traceback()) - -<<<<<<< HEAD - def validate_bom(self): - if cstr(self.current_bom) == cstr(self.new_bom): - frappe.throw(_("Current BOM and New BOM can not be same")) - - if frappe.db.get_value("BOM", self.current_bom, "item") != frappe.db.get_value( - "BOM", self.new_bom, "item" - ): - frappe.throw(_("The selected BOMs are not for the same item")) - -======= ->>>>>>> 4283a13e5a (feat: BOM Update Log) - def update_new_bom(self, unit_cost): - frappe.db.sql( - """update `tabBOM Item` set bom_no=%s, - rate=%s, amount=stock_qty*%s where bom_no = %s and docstatus < 2 and parenttype='BOM'""", - (self.new_bom, unit_cost, unit_cost, self.current_bom), - ) - - def get_parent_boms(self, bom, bom_list=None): - if bom_list is None: - bom_list = [] - data = frappe.db.sql( - """SELECT DISTINCT parent FROM `tabBOM Item` - WHERE bom_no = %s AND docstatus < 2 AND parenttype='BOM'""", - bom, - ) - - for d in data: - if self.new_bom == d[0]: - frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(bom, self.new_bom)) - - bom_list.append(d[0]) - self.get_parent_boms(d[0], bom_list) - - return list(set(bom_list)) - - -def get_new_bom_unit_cost(bom): - new_bom_unitcost = frappe.db.sql( - """SELECT `total_cost`/`quantity` - FROM `tabBOM` WHERE name = %s""", - bom, - ) - - return flt(new_bom_unitcost[0][0]) if new_bom_unitcost else 0 -======= pass ->>>>>>> cff91558d4 (chore: Polish error handling and code sepration) @frappe.whitelist() -<<<<<<< HEAD -def enqueue_replace_bom(args): - if isinstance(args, string_types): - args = json.loads(args) - -<<<<<<< HEAD - frappe.enqueue( - "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.replace_bom", - args=args, - timeout=40000, - ) -======= - create_bom_update_log(boms=args) ->>>>>>> 4283a13e5a (feat: BOM Update Log) - frappe.msgprint(_("Queued for replacing the BOM. It may take a few minutes.")) -======= -def enqueue_replace_bom(boms: Optional[Union[Dict, str]] = None, args: Optional[Union[Dict, str]] = None) -> "BOMUpdateLog": +def enqueue_replace_bom( + boms: Optional[Union[Dict, str]] = None, args: Optional[Union[Dict, str]] = None +) -> "BOMUpdateLog": """Returns a BOM Update Log (that queues a job) for BOM Replacement.""" boms = boms or args if isinstance(boms, str): boms = json.loads(boms) ->>>>>>> cff91558d4 (chore: Polish error handling and code sepration) update_log = create_bom_update_log(boms=boms) return update_log -@frappe.whitelist() -<<<<<<< HEAD -def enqueue_update_cost(): -<<<<<<< HEAD - frappe.enqueue( - "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_cost", timeout=40000 - ) - frappe.msgprint( - _("Queued for updating latest price in all Bill of Materials. It may take a few minutes.") - ) -======= - create_bom_update_log(update_type="Update Cost") - frappe.msgprint(_("Queued for updating latest price in all Bill of Materials. It may take a few minutes.")) ->>>>>>> 4283a13e5a (feat: BOM Update Log) -======= +@frappe.whitelist() def enqueue_update_cost() -> "BOMUpdateLog": """Returns a BOM Update Log (that queues a job) for BOM Cost Updation.""" update_log = create_bom_update_log(update_type="Update Cost") return update_log ->>>>>>> cff91558d4 (chore: Polish error handling and code sepration) def auto_update_latest_price_in_all_boms() -> None: @@ -152,77 +42,28 @@ def auto_update_latest_price_in_all_boms() -> None: if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"): update_cost() -<<<<<<< HEAD -<<<<<<< HEAD -def replace_bom(args): - try: - frappe.db.auto_commit_on_many_writes = 1 - args = frappe._dict(args) - doc = frappe.get_doc("BOM Update Tool") - doc.current_bom = args.current_bom - doc.new_bom = args.new_bom - doc.replace_bom() - except Exception: - frappe.log_error( - msg=frappe.get_traceback(), - title=_("BOM Update Tool Error") - ) - finally: - frappe.db.auto_commit_on_many_writes = 0 - - -def update_cost(): - try: - frappe.db.auto_commit_on_many_writes = 1 - 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) - except Exception: - frappe.log_error( - msg=frappe.get_traceback(), - title=_("BOM Update Tool Error") - ) - finally: - frappe.db.auto_commit_on_many_writes = 0 -======= -def create_bom_update_log(boms=None, update_type="Replace BOM"): - "Creates a BOM Update Log that handles the background job." - current_bom = boms.get("current_bom") if boms else None - new_bom = boms.get("new_bom") if boms else None - log_doc = frappe.get_doc({ - "doctype": "BOM Update Log", - "current_bom": current_bom, - "new_bom": new_bom, - "update_type": update_type - }) - log_doc.submit() ->>>>>>> 4283a13e5a (feat: BOM Update Log) -======= 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) -<<<<<<< HEAD -def create_bom_update_log(boms: Optional[Dict] = None, update_type: str = "Replace BOM") -> "BOMUpdateLog": -======= def create_bom_update_log( boms: Optional[Dict[str, str]] = None, update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM", ) -> "BOMUpdateLog": ->>>>>>> 620575a901 (fix: Type Annotations, Redundancy, etc.) """Creates a BOM Update Log that handles the background job.""" boms = boms or {} current_bom = boms.get("current_bom") new_bom = boms.get("new_bom") - return frappe.get_doc({ - "doctype": "BOM Update Log", - "current_bom": current_bom, - "new_bom": new_bom, - "update_type": update_type, - }).submit() ->>>>>>> cff91558d4 (chore: Polish error handling and code sepration) + return frappe.get_doc( + { + "doctype": "BOM Update Log", + "current_bom": current_bom, + "new_bom": new_bom, + "update_type": update_type, + } + ).submit() From ec646a1a44d993f5d6b36333e3133bf1850853dc Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 5 Apr 2022 18:36:18 +0530 Subject: [PATCH 14/15] fix: User Literal from `typing_extensions` as its not supported in `typing` in py 3.7 - https://mypy.readthedocs.io/en/stable/literal_types.html --- .../manufacturing/doctype/bom_update_log/bom_update_log.py | 3 ++- .../manufacturing/doctype/bom_update_tool/bom_update_tool.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index 139dcbcdd90..c3df96c99b1 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -1,11 +1,12 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from typing import Dict, List, Literal, Optional +from typing import Dict, List, Optional import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import cstr, flt +from typing_extensions import Literal from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost 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 b0e7da12017..4061c5af7c2 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,9 @@ # For license information, please see license.txt import json -from typing import TYPE_CHECKING, Dict, Literal, Optional, Union +from typing import TYPE_CHECKING, Dict, Optional, Union + +from typing_extensions import Literal if TYPE_CHECKING: from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog From e186d7633711ab562eeb7369b7d65aabaca21ae1 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 5 Apr 2022 19:05:30 +0530 Subject: [PATCH 15/15] fix: Linter --- .../doctype/bom_update_tool/test_bom_update_tool.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py index 36bcd9dcd09..fae72a0f6f7 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py @@ -4,8 +4,8 @@ import frappe from frappe.tests.utils import FrappeTestCase -from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import replace_bom +from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.stock.doctype.item.test_item import create_item @@ -22,10 +22,7 @@ class TestBOMUpdateTool(FrappeTestCase): bom_doc.items[1].item_code = "_Test Item" bom_doc.insert() - boms = frappe._dict( - current_bom=current_bom, - new_bom=bom_doc.name - ) + boms = frappe._dict(current_bom=current_bom, new_bom=bom_doc.name) replace_bom(boms) self.assertFalse(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", current_bom))