From 05eff5b2b4627f9dbbc32eef8637ba0c650e6449 Mon Sep 17 00:00:00 2001 From: Ishwarya Date: Fri, 5 Jun 2026 19:42:54 +0530 Subject: [PATCH 1/2] fix:a BOM cost update should re-calculate operating costs off a workstation vs using cached hour_rate field --- inventory_tools/hooks.py | 5 +++++ .../inventory_tools/overrides/workstation.py | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/inventory_tools/hooks.py b/inventory_tools/hooks.py index 924a675c..e74ab6c8 100644 --- a/inventory_tools/hooks.py +++ b/inventory_tools/hooks.py @@ -206,6 +206,11 @@ "inventory_tools.inventory_tools.doctype.workstation_operating_cost.workstation_operating_cost.validate_dates", ] }, + "BOM Update Log": { + "before_submit": [ + "inventory_tools.inventory_tools.overrides.workstation.refresh_all_workstation_hour_rates" + ] + }, } # Scheduled Tasks diff --git a/inventory_tools/inventory_tools/overrides/workstation.py b/inventory_tools/inventory_tools/overrides/workstation.py index 91255293..682d7d60 100644 --- a/inventory_tools/inventory_tools/overrides/workstation.py +++ b/inventory_tools/inventory_tools/overrides/workstation.py @@ -202,3 +202,14 @@ def get_alternative_workstations(doctype, txt, searchfield, start, page_len, fil workstation.insert(0, _default) return workstation + + +def refresh_all_workstation_hour_rates(doc, method=None): + if doc.update_type != "Update Cost": + return + for ws_name in frappe.get_all("Workstation", pluck="name"): + ws = frappe.get_doc("Workstation", ws_name) + old_rate = flt(ws.hour_rate) + ws.set_hour_rate() + if ws.hour_rate != old_rate: + frappe.db.set_value("Workstation", ws_name, "hour_rate", ws.hour_rate) From d99e51044881852e186ee0f0ff763fcf84b16328 Mon Sep 17 00:00:00 2001 From: Ishwarya Date: Wed, 24 Jun 2026 17:45:18 +0530 Subject: [PATCH 2/2] Fixed BOM cost update --- inventory_tools/hooks.py | 7 +- .../inventory_tools/overrides/bom.py | 77 +++++++++++++++++++ .../inventory_tools/overrides/workstation.py | 11 --- .../public/js/custom/bom_custom.js | 47 +++++++++++ 4 files changed, 126 insertions(+), 16 deletions(-) create mode 100644 inventory_tools/inventory_tools/overrides/bom.py create mode 100644 inventory_tools/public/js/custom/bom_custom.js diff --git a/inventory_tools/hooks.py b/inventory_tools/hooks.py index e74ab6c8..1bc6d05a 100644 --- a/inventory_tools/hooks.py +++ b/inventory_tools/hooks.py @@ -42,6 +42,7 @@ # include js in doctype views doctype_js = { + "BOM": "public/js/custom/bom_custom.js", "Item": "public/js/custom/item_custom.js", "Job Card": "public/js/custom/job_card_custom.js", "Operation": "public/js/custom/operation_custom.js", @@ -125,6 +126,7 @@ # Override standard doctype classes override_doctype_class = { + "BOM": "inventory_tools.inventory_tools.overrides.bom.InventoryToolsBOM", "Delivery Note": "inventory_tools.inventory_tools.overrides.delivery_note.InventoryToolsDeliveryNote", "Pick List": "inventory_tools.inventory_tools.overrides.pick_list.InventoryToolsPickList", "Quality Inspection": "inventory_tools.inventory_tools.overrides.quality_inspection.InventoryToolsQualityInspection", @@ -206,11 +208,6 @@ "inventory_tools.inventory_tools.doctype.workstation_operating_cost.workstation_operating_cost.validate_dates", ] }, - "BOM Update Log": { - "before_submit": [ - "inventory_tools.inventory_tools.overrides.workstation.refresh_all_workstation_hour_rates" - ] - }, } # Scheduled Tasks diff --git a/inventory_tools/inventory_tools/overrides/bom.py b/inventory_tools/inventory_tools/overrides/bom.py new file mode 100644 index 00000000..3b642145 --- /dev/null +++ b/inventory_tools/inventory_tools/overrides/bom.py @@ -0,0 +1,77 @@ +# Copyright (c) 2025, AgriTheory and contributors +# For license information, please see license.txt + +import frappe +from erpnext.manufacturing.doctype.bom.bom import BOM +from frappe.utils import flt, getdate, nowdate + + +class InventoryToolsBOM(BOM): + @frappe.whitelist() + def update_cost( + self, + update_parent=True, + from_child_bom=False, + update_hour_rate=True, + save=True, + as_of_date=None, + ): + self._as_of_date = getdate(as_of_date) if as_of_date else getdate(nowdate()) + return super().update_cost( + update_parent=update_parent, + from_child_bom=from_child_bom, + update_hour_rate=update_hour_rate, + save=save, + ) + + def update_rate_and_time(self, row, update_hour_rate=False): + as_of_date = getattr(self, "_as_of_date", getdate(nowdate())) + + if not row.hour_rate or update_hour_rate: + hour_rate = self._get_workstation_hour_rate(row.workstation, as_of_date) + if hour_rate is not None: + row.hour_rate = ( + hour_rate / flt(self.conversion_rate) if self.conversion_rate and hour_rate else hour_rate + ) + else: + cached_rate = flt(frappe.get_cached_value("Workstation", row.workstation, "hour_rate")) + if cached_rate: + row.hour_rate = ( + cached_rate / flt(self.conversion_rate) + if self.conversion_rate and cached_rate + else cached_rate + ) + + if row.hour_rate and row.time_in_mins: + row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate) + row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0 + row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate) + row.cost_per_unit = row.operating_cost / (row.batch_size or 1.0) + row.base_cost_per_unit = row.base_operating_cost / (row.batch_size or 1.0) + + if update_hour_rate: + row.db_update() + + def _get_workstation_hour_rate(self, workstation_name, as_of_date): + """Sum costs from workstation_operating_cost table that cover the given date.""" + rows = frappe.get_all( + "Workstation Operating Cost", + filters={"parent": workstation_name}, + fields=["from_date", "to_date", "qty"], + ) + + if not rows: + return None + + net_hour_rate = 0.0 + has_match = False + for row in rows: + from_date = getdate(row.from_date) if row.from_date else None + to_date = getdate(row.to_date) if row.to_date else None + + if from_date and from_date <= as_of_date: + if to_date is None or as_of_date <= to_date: + net_hour_rate += flt(row.qty) + has_match = True + + return net_hour_rate if has_match else None diff --git a/inventory_tools/inventory_tools/overrides/workstation.py b/inventory_tools/inventory_tools/overrides/workstation.py index 682d7d60..91255293 100644 --- a/inventory_tools/inventory_tools/overrides/workstation.py +++ b/inventory_tools/inventory_tools/overrides/workstation.py @@ -202,14 +202,3 @@ def get_alternative_workstations(doctype, txt, searchfield, start, page_len, fil workstation.insert(0, _default) return workstation - - -def refresh_all_workstation_hour_rates(doc, method=None): - if doc.update_type != "Update Cost": - return - for ws_name in frappe.get_all("Workstation", pluck="name"): - ws = frappe.get_doc("Workstation", ws_name) - old_rate = flt(ws.hour_rate) - ws.set_hour_rate() - if ws.hour_rate != old_rate: - frappe.db.set_value("Workstation", ws_name, "hour_rate", ws.hour_rate) diff --git a/inventory_tools/public/js/custom/bom_custom.js b/inventory_tools/public/js/custom/bom_custom.js new file mode 100644 index 00000000..285e1ad1 --- /dev/null +++ b/inventory_tools/public/js/custom/bom_custom.js @@ -0,0 +1,47 @@ +// Copyright (c) 2025, AgriTheory and contributors +// For license information, please see license.txt + +frappe.ui.form.on('BOM', { + refresh(frm) { + if (!frm.is_new() && frm.doc.docstatus < 2) { + frm.remove_custom_button(__('Update Cost')) + frm.add_custom_button(__('Update Cost'), function () { + let dialog = new frappe.ui.Dialog({ + title: __('Update Cost'), + fields: [ + { + fieldname: 'as_of_date', + fieldtype: 'Date', + label: __('As of Date'), + default: frappe.datetime.get_today(), + reqd: 1, + description: __( + 'Operating costs from the Workstation Operating Cost table will be applied based on this date.' + ), + }, + ], + primary_action_label: __('Update'), + primary_action(values) { + dialog.hide() + frappe.call({ + doc: frm.doc, + method: 'update_cost', + freeze: true, + args: { + update_parent: true, + save: true, + from_child_bom: false, + as_of_date: values.as_of_date, + }, + callback(r) { + refresh_field('items') + if (!r.exc) frm.refresh_fields() + }, + }) + }, + }) + dialog.show() + }) + } + }, +})