diff --git a/inventory_tools/hooks.py b/inventory_tools/hooks.py index 924a675c..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", 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/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() + }) + } + }, +})