diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 732084192a1..7115d309ffc 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1421,11 +1421,11 @@ def add_additional_cost(stock_entry, work_order): as_dict=1, ) - expecnse_account = ( + expense_account = ( company_account.default_operating_cost_account or company_account.default_expense_account ) - add_non_stock_items_cost(stock_entry, work_order, expecnse_account) - add_operations_cost(stock_entry, work_order, expecnse_account) + add_non_stock_items_cost(stock_entry, work_order, expense_account) + add_operations_cost(stock_entry, work_order, expense_account) def add_non_stock_items_cost(stock_entry, work_order, expense_account): @@ -1460,21 +1460,74 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account): ) +def add_operating_cost_component_wise( + stock_entry, work_order=None, operating_cost_per_unit=None, op_expense_account=None +): + if not work_order: + return False + + cost_added = False + for row in work_order.operations: + workstation_cost = frappe.get_all( + "Workstation Cost", + fields=["operating_component", "operating_cost"], + filters={ + "parent": row.workstation, + "parenttype": "Workstation", + }, + ) + + for wc in workstation_cost: + expense_account = get_component_account(wc.operating_component) or op_expense_account + actual_cp_operating_cost = flt( + flt(wc.operating_cost) * flt(flt(row.actual_operation_time) / 60.0), + row.precision("actual_operating_cost"), + ) + + per_unit_cost = flt(actual_cp_operating_cost) / flt(row.completed_qty) + + if per_unit_cost and expense_account: + stock_entry.append( + "additional_costs", + { + "expense_account": expense_account, + "description": _("{0} Operating Cost for operation {1}").format( + wc.operating_component, row.operation + ), + "amount": per_unit_cost * flt(stock_entry.fg_completed_qty), + }, + ) + + cost_added = True + + return cost_added + + +@frappe.request_cache +def get_component_account(parent): + return frappe.db.get_value("Workstation Operating Component Account", parent, "expense_account") + + def add_operations_cost(stock_entry, work_order=None, expense_account=None): from erpnext.stock.doctype.stock_entry.stock_entry import get_operating_cost_per_unit operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no) if operating_cost_per_unit: - stock_entry.append( - "additional_costs", - { - "expense_account": expense_account, - "description": _("Operating Cost as per Work Order / BOM"), - "amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty), - }, + cost_added = add_operating_cost_component_wise( + stock_entry, work_order, operating_cost_per_unit, expense_account ) + if not cost_added: + stock_entry.append( + "additional_costs", + { + "expense_account": expense_account, + "description": _("Operating Cost as per Work Order / BOM"), + "amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty), + }, + ) + if work_order and work_order.additional_operating_cost and work_order.qty: additional_operating_cost_per_unit = flt(work_order.additional_operating_cost) / flt(work_order.qty) diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index 51c31f029b3..6a407c22b8c 100644 --- a/erpnext/manufacturing/doctype/workstation/test_workstation.py +++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py @@ -1,6 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors # See license.txt import frappe +from frappe import _ from frappe.tests import IntegrationTestCase from erpnext.manufacturing.doctype.operation.test_operation import make_operation @@ -75,14 +76,22 @@ class TestWorkstation(IntegrationTestCase): bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency="INR") w1 = frappe.get_doc("Workstation", "_Test Workstation A") # resets values - w1.hour_rate_rent = 300 - w1.hour_rate_labour = 0 + for row in w1.workstation_costs: + if row.operating_component == _("Rent"): + row.operating_cost = 300 + break + w1.save() bom_doc.update_cost() bom_doc.reload() self.assertEqual(w1.hour_rate, 300) self.assertEqual(bom_doc.operations[0].hour_rate, 300) - w1.hour_rate_rent = 250 + + for row in w1.workstation_costs: + if row.operating_component == _("Rent"): + row.operating_cost = 250 + break + w1.save() # updating after setting new rates in workstations bom_doc.update_cost() @@ -102,8 +111,24 @@ def make_workstation(*args, **kwargs): workstation_name = args.workstation_name or args.workstation if not frappe.db.exists("Workstation", workstation_name): doc = frappe.get_doc({"doctype": "Workstation", "workstation_name": workstation_name}) - doc.hour_rate_rent = args.get("hour_rate_rent") - doc.hour_rate_labour = args.get("hour_rate_labour") + if args.get("hour_rate_rent"): + doc.append( + "workstation_costs", + { + "operating_component": _("Rent"), + "operating_cost": args.get("hour_rate_rent"), + }, + ) + + if args.get("hour_rate_labour"): + doc.append( + "workstation_costs", + { + "operating_component": _("Wages"), + "operating_cost": args.get("hour_rate_labour"), + }, + ) + doc.workstation_type = args.get("workstation_type") doc.insert() diff --git a/erpnext/manufacturing/doctype/workstation/workstation.json b/erpnext/manufacturing/doctype/workstation/workstation.json index bf31d7ddc39..81b83d9066b 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.json +++ b/erpnext/manufacturing/doctype/workstation/workstation.json @@ -27,11 +27,8 @@ "column_break_etmc", "off_status_image", "over_heads", - "hour_rate_electricity", - "hour_rate_consumable", - "column_break_11", - "hour_rate_rent", - "hour_rate_labour", + "section_break_auzm", + "workstation_costs", "section_break_11", "hour_rate", "workstaion_description", @@ -68,50 +65,6 @@ "label": "Operating Costs", "oldfieldtype": "Section Break" }, - { - "bold": 1, - "description": "per hour", - "fieldname": "hour_rate_electricity", - "fieldtype": "Currency", - "label": "Electricity Cost", - "non_negative": 1, - "oldfieldname": "hour_rate_electricity", - "oldfieldtype": "Currency" - }, - { - "bold": 1, - "description": "per hour", - "fieldname": "hour_rate_consumable", - "fieldtype": "Currency", - "label": "Consumable Cost", - "non_negative": 1, - "oldfieldname": "hour_rate_consumable", - "oldfieldtype": "Currency" - }, - { - "fieldname": "column_break_11", - "fieldtype": "Column Break" - }, - { - "bold": 1, - "description": "per hour", - "fieldname": "hour_rate_rent", - "fieldtype": "Currency", - "label": "Rent Cost", - "non_negative": 1, - "oldfieldname": "hour_rate_rent", - "oldfieldtype": "Currency" - }, - { - "bold": 1, - "description": "Wages per hour", - "fieldname": "hour_rate_labour", - "fieldtype": "Currency", - "label": "Wages", - "non_negative": 1, - "oldfieldname": "hour_rate_labour", - "oldfieldtype": "Currency" - }, { "description": "per hour", "fieldname": "hour_rate", @@ -252,6 +205,17 @@ "fieldname": "disabled", "fieldtype": "Check", "label": "Disabled" + }, + { + "fieldname": "section_break_auzm", + "fieldtype": "Section Break", + "label": "Operating Costs (Per Hour)" + }, + { + "fieldname": "workstation_costs", + "fieldtype": "Table", + "label": "Operating Components Cost", + "options": "Workstation Cost" } ], "hide_toolbar": 1, @@ -259,7 +223,7 @@ "idx": 1, "image_field": "on_status_image", "links": [], - "modified": "2025-07-13 16:02:13.615001", + "modified": "2025-08-19 12:07:05.374386", "modified_by": "Administrator", "module": "Manufacturing", "name": "Workstation", diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index 0eb0a4f72d4..a740c13a461 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -3,7 +3,7 @@ import frappe -from frappe import _ +from frappe import _, bold from frappe.model.document import Document from frappe.utils import ( add_days, @@ -44,6 +44,7 @@ class Workstation(Document): if TYPE_CHECKING: from frappe.types import DF + from erpnext.manufacturing.doctype.workstation_cost.workstation_cost import WorkstationCost from erpnext.manufacturing.doctype.workstation_working_hour.workstation_working_hour import ( WorkstationWorkingHour, ) @@ -52,10 +53,6 @@ class Workstation(Document): disabled: DF.Check holiday_list: DF.Link | None hour_rate: DF.Currency - hour_rate_consumable: DF.Currency - hour_rate_electricity: DF.Currency - hour_rate_labour: DF.Currency - hour_rate_rent: DF.Currency off_status_image: DF.AttachImage | None on_status_image: DF.AttachImage | None plant_floor: DF.Link | None @@ -64,10 +61,26 @@ class Workstation(Document): total_working_hours: DF.Float warehouse: DF.Link | None working_hours: DF.Table[WorkstationWorkingHour] + workstation_costs: DF.Table[WorkstationCost] workstation_name: DF.Data workstation_type: DF.Link | None # end: auto-generated types + def validate(self): + self.validate_duplicate_operating_component() + + def validate_duplicate_operating_component(self): + components = [] + for row in self.workstation_costs: + if row.operating_component not in components: + components.append(row.operating_component) + else: + frappe.throw( + _("Duplicate Operating Component {0} found in Operating Components").format( + bold(row.operating_component) + ) + ) + def before_save(self): self.set_data_based_on_workstation_type() self.set_hour_rate() @@ -95,36 +108,33 @@ class Workstation(Document): frappe.throw(_("Row #{0}: Start Time must be before End Time").format(row.idx)) def set_hour_rate(self): - self.hour_rate = ( - flt(self.hour_rate_labour) - + flt(self.hour_rate_electricity) - + flt(self.hour_rate_consumable) - + flt(self.hour_rate_rent) - ) + self.hour_rate = 0.0 + for row in self.workstation_costs: + if row.operating_cost: + self.hour_rate += flt(row.operating_cost) @frappe.whitelist() def set_data_based_on_workstation_type(self): + if self.workstation_costs: + return + if self.workstation_type: - fields = [ - "hour_rate_labour", - "hour_rate_electricity", - "hour_rate_consumable", - "hour_rate_rent", - "hour_rate", - "description", - ] + data = frappe.get_all( + "Workstation Cost", + fields=["operating_component", "operating_cost", "idx"], + filters={"parent": self.workstation_type, "parenttype": "Workstation Type"}, + order_by="idx", + ) - data = frappe.get_cached_value("Workstation Type", self.workstation_type, fields, as_dict=True) - - if not data: - return - - for field in fields: - if self.get(field): - continue - - if value := data.get(field): - self.set(field, value) + for row in data: + self.append( + "workstation_costs", + { + "operating_component": row.operating_component, + "operating_cost": row.operating_cost, + "idx": row.idx, + }, + ) def on_update(self): self.validate_overlap_for_operation_timings() diff --git a/erpnext/manufacturing/doctype/workstation_cost/__init__.py b/erpnext/manufacturing/doctype/workstation_cost/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/doctype/workstation_cost/test_workstation_cost.py b/erpnext/manufacturing/doctype/workstation_cost/test_workstation_cost.py new file mode 100644 index 00000000000..ea948f62abc --- /dev/null +++ b/erpnext/manufacturing/doctype/workstation_cost/test_workstation_cost.py @@ -0,0 +1,20 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class IntegrationTestWorkstationCost(IntegrationTestCase): + """ + Integration tests for WorkstationCost. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/erpnext/manufacturing/doctype/workstation_cost/workstation_cost.js b/erpnext/manufacturing/doctype/workstation_cost/workstation_cost.js new file mode 100644 index 00000000000..ea6577fa905 --- /dev/null +++ b/erpnext/manufacturing/doctype/workstation_cost/workstation_cost.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Workstation Cost", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/manufacturing/doctype/workstation_cost/workstation_cost.json b/erpnext/manufacturing/doctype/workstation_cost/workstation_cost.json new file mode 100644 index 00000000000..fa5fe337a1c --- /dev/null +++ b/erpnext/manufacturing/doctype/workstation_cost/workstation_cost.json @@ -0,0 +1,43 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-08-17 16:43:13.542333", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "operating_component", + "operating_cost" + ], + "fields": [ + { + "fieldname": "operating_cost", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Operating Cost", + "reqd": 1 + }, + { + "fieldname": "operating_component", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Operating Component", + "options": "Workstation Operating Component", + "reqd": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2025-08-17 19:21:02.725365", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Workstation Cost", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/erpnext/manufacturing/doctype/workstation_cost/workstation_cost.py b/erpnext/manufacturing/doctype/workstation_cost/workstation_cost.py new file mode 100644 index 00000000000..7ab2ff9ac77 --- /dev/null +++ b/erpnext/manufacturing/doctype/workstation_cost/workstation_cost.py @@ -0,0 +1,24 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class WorkstationCost(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + operating_component: DF.Link + operating_cost: DF.Currency + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + # end: auto-generated types + + pass diff --git a/erpnext/manufacturing/doctype/workstation_operating_component/__init__.py b/erpnext/manufacturing/doctype/workstation_operating_component/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/doctype/workstation_operating_component/test_workstation_operating_component.py b/erpnext/manufacturing/doctype/workstation_operating_component/test_workstation_operating_component.py new file mode 100644 index 00000000000..0d327c6473d --- /dev/null +++ b/erpnext/manufacturing/doctype/workstation_operating_component/test_workstation_operating_component.py @@ -0,0 +1,20 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class IntegrationTestWorkstationOperatingComponent(IntegrationTestCase): + """ + Integration tests for WorkstationOperatingComponent. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/erpnext/manufacturing/doctype/workstation_operating_component/workstation_operating_component.js b/erpnext/manufacturing/doctype/workstation_operating_component/workstation_operating_component.js new file mode 100644 index 00000000000..6182af1eece --- /dev/null +++ b/erpnext/manufacturing/doctype/workstation_operating_component/workstation_operating_component.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Workstation Operating Component", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/manufacturing/doctype/workstation_operating_component/workstation_operating_component.json b/erpnext/manufacturing/doctype/workstation_operating_component/workstation_operating_component.json new file mode 100644 index 00000000000..dd1fe88bf57 --- /dev/null +++ b/erpnext/manufacturing/doctype/workstation_operating_component/workstation_operating_component.json @@ -0,0 +1,84 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:component_name", + "creation": "2025-08-17 16:49:30.711201", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "component_name", + "section_break_ewdg", + "accounts" + ], + "fields": [ + { + "fieldname": "component_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Component Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "section_break_ewdg", + "fieldtype": "Section Break" + }, + { + "fieldname": "accounts", + "fieldtype": "Table", + "label": "Component Expense Account", + "options": "Workstation Operating Component Account" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-08-17 19:23:47.510540", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Workstation Operating Component", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/erpnext/manufacturing/doctype/workstation_operating_component/workstation_operating_component.py b/erpnext/manufacturing/doctype/workstation_operating_component/workstation_operating_component.py new file mode 100644 index 00000000000..d8572f908b1 --- /dev/null +++ b/erpnext/manufacturing/doctype/workstation_operating_component/workstation_operating_component.py @@ -0,0 +1,25 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class WorkstationOperatingComponent(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from erpnext.manufacturing.doctype.workstation_operating_component_account.workstation_operating_component_account import ( + WorkstationOperatingComponentAccount, + ) + + accounts: DF.Table[WorkstationOperatingComponentAccount] + component_name: DF.Data + # end: auto-generated types + + pass diff --git a/erpnext/manufacturing/doctype/workstation_operating_component_account/__init__.py b/erpnext/manufacturing/doctype/workstation_operating_component_account/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/manufacturing/doctype/workstation_operating_component_account/test_workstation_operating_component_account.py b/erpnext/manufacturing/doctype/workstation_operating_component_account/test_workstation_operating_component_account.py new file mode 100644 index 00000000000..3bf919b8df0 --- /dev/null +++ b/erpnext/manufacturing/doctype/workstation_operating_component_account/test_workstation_operating_component_account.py @@ -0,0 +1,20 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class IntegrationTestWorkstationOperatingComponentAccount(IntegrationTestCase): + """ + Integration tests for WorkstationOperatingComponentAccount. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/erpnext/manufacturing/doctype/workstation_operating_component_account/workstation_operating_component_account.js b/erpnext/manufacturing/doctype/workstation_operating_component_account/workstation_operating_component_account.js new file mode 100644 index 00000000000..10cba973862 --- /dev/null +++ b/erpnext/manufacturing/doctype/workstation_operating_component_account/workstation_operating_component_account.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Workstation Operating Component Account", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/manufacturing/doctype/workstation_operating_component_account/workstation_operating_component_account.json b/erpnext/manufacturing/doctype/workstation_operating_component_account/workstation_operating_component_account.json new file mode 100644 index 00000000000..31f04fa3d5d --- /dev/null +++ b/erpnext/manufacturing/doctype/workstation_operating_component_account/workstation_operating_component_account.json @@ -0,0 +1,43 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-08-17 19:21:36.356779", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "expense_account" + ], + "fields": [ + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "expense_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Expense Account", + "options": "Account" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2025-08-17 19:24:01.487406", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Workstation Operating Component Account", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/erpnext/manufacturing/doctype/workstation_operating_component_account/workstation_operating_component_account.py b/erpnext/manufacturing/doctype/workstation_operating_component_account/workstation_operating_component_account.py new file mode 100644 index 00000000000..5de55bc1c2c --- /dev/null +++ b/erpnext/manufacturing/doctype/workstation_operating_component_account/workstation_operating_component_account.py @@ -0,0 +1,24 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class WorkstationOperatingComponentAccount(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + company: DF.Link + expense_account: DF.Link | None + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + # end: auto-generated types + + pass diff --git a/erpnext/manufacturing/doctype/workstation_type/workstation_type.json b/erpnext/manufacturing/doctype/workstation_type/workstation_type.json index 5d5776ab77d..75a0482e43f 100644 --- a/erpnext/manufacturing/doctype/workstation_type/workstation_type.json +++ b/erpnext/manufacturing/doctype/workstation_type/workstation_type.json @@ -10,12 +10,8 @@ "engine": "InnoDB", "field_order": [ "workstation_type", - "over_heads", - "hour_rate_electricity", - "hour_rate_consumable", - "column_break_5", - "hour_rate_rent", - "hour_rate_labour", + "section_break_auzm", + "workstation_costs", "section_break_8", "hour_rate", "description_tab", @@ -32,44 +28,6 @@ "reqd": 1, "unique": 1 }, - { - "fieldname": "over_heads", - "fieldtype": "Section Break", - "label": "Operating Costs", - "oldfieldtype": "Section Break" - }, - { - "description": "per hour", - "fieldname": "hour_rate_electricity", - "fieldtype": "Currency", - "label": "Electricity Cost", - "oldfieldname": "hour_rate_electricity", - "oldfieldtype": "Currency" - }, - { - "description": "per hour", - "fieldname": "hour_rate_consumable", - "fieldtype": "Currency", - "label": "Consumable Cost", - "oldfieldname": "hour_rate_consumable", - "oldfieldtype": "Currency" - }, - { - "description": "per hour", - "fieldname": "hour_rate_rent", - "fieldtype": "Currency", - "label": "Rent Cost", - "oldfieldname": "hour_rate_rent", - "oldfieldtype": "Currency" - }, - { - "description": "Wages per hour", - "fieldname": "hour_rate_labour", - "fieldtype": "Currency", - "label": "Wages", - "oldfieldname": "hour_rate_labour", - "oldfieldtype": "Currency" - }, { "description": "per hour", "fieldname": "hour_rate", @@ -88,10 +46,6 @@ "oldfieldtype": "Text", "width": "300px" }, - { - "fieldname": "column_break_5", - "fieldtype": "Column Break" - }, { "collapsible": 1, "fieldname": "description_tab", @@ -101,11 +55,22 @@ { "fieldname": "section_break_8", "fieldtype": "Section Break" + }, + { + "fieldname": "section_break_auzm", + "fieldtype": "Section Break", + "label": "Operating Costs (Per Hour)" + }, + { + "fieldname": "workstation_costs", + "fieldtype": "Table", + "label": "Operating Components Cost", + "options": "Workstation Cost" } ], "icon": "icon-wrench", "links": [], - "modified": "2024-03-27 13:11:00.946367", + "modified": "2025-08-19 12:06:56.683558", "modified_by": "Administrator", "module": "Manufacturing", "name": "Workstation Type", @@ -125,9 +90,10 @@ } ], "quick_entry": 1, + "row_format": "Dynamic", "show_name_in_global_search": 1, "sort_field": "creation", "sort_order": "ASC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/workstation_type/workstation_type.py b/erpnext/manufacturing/doctype/workstation_type/workstation_type.py index 0f151a2fb2b..838eea46469 100644 --- a/erpnext/manufacturing/doctype/workstation_type/workstation_type.py +++ b/erpnext/manufacturing/doctype/workstation_type/workstation_type.py @@ -2,6 +2,7 @@ # For license information, please see license.txt import frappe +from frappe import _, bold from frappe.model.document import Document from frappe.utils import flt @@ -15,25 +16,38 @@ class WorkstationType(Document): if TYPE_CHECKING: from frappe.types import DF + from erpnext.manufacturing.doctype.workstation_cost.workstation_cost import WorkstationCost + description: DF.SmallText | None hour_rate: DF.Currency - hour_rate_consumable: DF.Currency - hour_rate_electricity: DF.Currency - hour_rate_labour: DF.Currency - hour_rate_rent: DF.Currency + workstation_costs: DF.Table[WorkstationCost] workstation_type: DF.Data # end: auto-generated types + def validate(self): + self.validate_duplicate_operating_component() + + def validate_duplicate_operating_component(self): + components = [] + for row in self.workstation_costs: + if row.operating_component not in components: + components.append(row.operating_component) + else: + frappe.throw( + _("Duplicate Operating Component {0} found in Operating Components").format( + bold(row.operating_component) + ) + ) + def before_save(self): self.set_hour_rate() def set_hour_rate(self): - self.hour_rate = ( - flt(self.hour_rate_labour) - + flt(self.hour_rate_electricity) - + flt(self.hour_rate_consumable) - + flt(self.hour_rate_rent) - ) + self.hour_rate = 0.0 + + for row in self.workstation_costs: + if row.operating_cost: + self.hour_rate += flt(row.operating_cost) def get_workstations(workstation_type): diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e1d7e1f899e..37da137476a 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -435,3 +435,4 @@ erpnext.patches.v15_0.add_company_payment_gateway_account erpnext.patches.v16_0.update_serial_no_reference_name erpnext.patches.v16_0.set_invoice_type_in_pos_settings erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter +erpnext.patches.v16_0.make_workstation_operating_components #1 diff --git a/erpnext/patches/v16_0/make_workstation_operating_components.py b/erpnext/patches/v16_0/make_workstation_operating_components.py new file mode 100644 index 00000000000..0ee876fd1bc --- /dev/null +++ b/erpnext/patches/v16_0/make_workstation_operating_components.py @@ -0,0 +1,79 @@ +import frappe +from frappe import _ + + +def get_operating_cost_account(company): + company_details = frappe.db.get_value( + "Company", company, ["default_operating_cost_account", "default_expense_account"], as_dict=True + ) + + return company_details.get("default_operating_cost_account") or company_details.get( + "default_expense_account" + ) + + +def execute(): + components = [ + "Electricity", + "Consumables", + "Rent", + "Wages", + ] + + companies = frappe.get_all("Company", filters={"is_group": 0}, pluck="name") + + for component in components: + component = _(component) + if not frappe.db.exists("Workstation Operating Component", component): + doc = frappe.new_doc("Workstation Operating Component") + doc.component_name = component + + for company in companies: + operating_cost_account = get_operating_cost_account(company) + + doc.append("accounts", {"company": company, "expense_account": operating_cost_account}) + + doc.insert() + + workstations = frappe.get_all("Workstation", filters={"hour_rate": (">", 0.0)}, pluck="name") or [] + workstation_types = ( + frappe.get_all("Workstation Type", filters={"hour_rate": (">", 0.0)}, pluck="name") or [] + ) + + if not workstations and not workstation_types: + return + + components_map = { + "hour_rate_electricity": _("Electricity"), + "hour_rate_consumable": _("Consumables"), + "hour_rate_rent": _("Rent"), + "hour_rate_labour": _("Wages"), + } + + for workstation in workstations: + doc = frappe.get_doc("Workstation", workstation) + for field, component in components_map.items(): + if doc.get(field): + doc.append( + "workstation_costs", + { + "operating_component": component, + "operating_cost": doc.get(field), + }, + ) + + doc.save() + + for workstation_type in workstation_types: + doc = frappe.get_doc("Workstation Type", workstation_type) + for field, component in components_map.items(): + if doc.get(field): + doc.append( + "workstation_costs", + { + "operating_component": component, + "operating_cost": doc.get(field), + }, + ) + + doc.save() diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index ee37399e583..360cd6f4c5f 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -294,6 +294,10 @@ def install(country=None): {"doctype": "Market Segment", "market_segment": _("Upper Income")}, # Warehouse Type {"doctype": "Warehouse Type", "name": "Transit"}, + {"doctype": "Workstation Operating Component", "component_name": _("Electricity")}, + {"doctype": "Workstation Operating Component", "component_name": _("Consumables")}, + {"doctype": "Workstation Operating Component", "component_name": _("Rent")}, + {"doctype": "Workstation Operating Component", "component_name": _("Wages")}, ] for doctype, title_field, filename in (