mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-12 17:51:20 +00:00
Merge pull request #49213 from rohitwaghchaure/feat-workstation-operating-component
refactor: workstation operating component
This commit is contained in:
@@ -1421,11 +1421,11 @@ def add_additional_cost(stock_entry, work_order):
|
|||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
expecnse_account = (
|
expense_account = (
|
||||||
company_account.default_operating_cost_account or company_account.default_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_non_stock_items_cost(stock_entry, work_order, expense_account)
|
||||||
add_operations_cost(stock_entry, work_order, expecnse_account)
|
add_operations_cost(stock_entry, work_order, expense_account)
|
||||||
|
|
||||||
|
|
||||||
def add_non_stock_items_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):
|
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
|
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)
|
operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no)
|
||||||
|
|
||||||
if operating_cost_per_unit:
|
if operating_cost_per_unit:
|
||||||
stock_entry.append(
|
cost_added = add_operating_cost_component_wise(
|
||||||
"additional_costs",
|
stock_entry, work_order, operating_cost_per_unit, expense_account
|
||||||
{
|
|
||||||
"expense_account": expense_account,
|
|
||||||
"description": _("Operating Cost as per Work Order / BOM"),
|
|
||||||
"amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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:
|
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)
|
additional_operating_cost_per_unit = flt(work_order.additional_operating_cost) / flt(work_order.qty)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import _
|
||||||
from frappe.tests import IntegrationTestCase
|
from frappe.tests import IntegrationTestCase
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
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")
|
bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency="INR")
|
||||||
w1 = frappe.get_doc("Workstation", "_Test Workstation A")
|
w1 = frappe.get_doc("Workstation", "_Test Workstation A")
|
||||||
# resets values
|
# resets values
|
||||||
w1.hour_rate_rent = 300
|
for row in w1.workstation_costs:
|
||||||
w1.hour_rate_labour = 0
|
if row.operating_component == _("Rent"):
|
||||||
|
row.operating_cost = 300
|
||||||
|
break
|
||||||
|
|
||||||
w1.save()
|
w1.save()
|
||||||
bom_doc.update_cost()
|
bom_doc.update_cost()
|
||||||
bom_doc.reload()
|
bom_doc.reload()
|
||||||
self.assertEqual(w1.hour_rate, 300)
|
self.assertEqual(w1.hour_rate, 300)
|
||||||
self.assertEqual(bom_doc.operations[0].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()
|
w1.save()
|
||||||
# updating after setting new rates in workstations
|
# updating after setting new rates in workstations
|
||||||
bom_doc.update_cost()
|
bom_doc.update_cost()
|
||||||
@@ -102,8 +111,24 @@ def make_workstation(*args, **kwargs):
|
|||||||
workstation_name = args.workstation_name or args.workstation
|
workstation_name = args.workstation_name or args.workstation
|
||||||
if not frappe.db.exists("Workstation", workstation_name):
|
if not frappe.db.exists("Workstation", workstation_name):
|
||||||
doc = frappe.get_doc({"doctype": "Workstation", "workstation_name": workstation_name})
|
doc = frappe.get_doc({"doctype": "Workstation", "workstation_name": workstation_name})
|
||||||
doc.hour_rate_rent = args.get("hour_rate_rent")
|
if args.get("hour_rate_rent"):
|
||||||
doc.hour_rate_labour = args.get("hour_rate_labour")
|
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.workstation_type = args.get("workstation_type")
|
||||||
doc.insert()
|
doc.insert()
|
||||||
|
|
||||||
|
|||||||
@@ -27,11 +27,8 @@
|
|||||||
"column_break_etmc",
|
"column_break_etmc",
|
||||||
"off_status_image",
|
"off_status_image",
|
||||||
"over_heads",
|
"over_heads",
|
||||||
"hour_rate_electricity",
|
"section_break_auzm",
|
||||||
"hour_rate_consumable",
|
"workstation_costs",
|
||||||
"column_break_11",
|
|
||||||
"hour_rate_rent",
|
|
||||||
"hour_rate_labour",
|
|
||||||
"section_break_11",
|
"section_break_11",
|
||||||
"hour_rate",
|
"hour_rate",
|
||||||
"workstaion_description",
|
"workstaion_description",
|
||||||
@@ -68,50 +65,6 @@
|
|||||||
"label": "Operating Costs",
|
"label": "Operating Costs",
|
||||||
"oldfieldtype": "Section Break"
|
"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",
|
"description": "per hour",
|
||||||
"fieldname": "hour_rate",
|
"fieldname": "hour_rate",
|
||||||
@@ -252,6 +205,17 @@
|
|||||||
"fieldname": "disabled",
|
"fieldname": "disabled",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Disabled"
|
"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,
|
"hide_toolbar": 1,
|
||||||
@@ -259,7 +223,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"image_field": "on_status_image",
|
"image_field": "on_status_image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-07-13 16:02:13.615001",
|
"modified": "2025-08-19 12:07:05.374386",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Workstation",
|
"name": "Workstation",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _, bold
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
add_days,
|
add_days,
|
||||||
@@ -44,6 +44,7 @@ class Workstation(Document):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from frappe.types import DF
|
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 (
|
from erpnext.manufacturing.doctype.workstation_working_hour.workstation_working_hour import (
|
||||||
WorkstationWorkingHour,
|
WorkstationWorkingHour,
|
||||||
)
|
)
|
||||||
@@ -52,10 +53,6 @@ class Workstation(Document):
|
|||||||
disabled: DF.Check
|
disabled: DF.Check
|
||||||
holiday_list: DF.Link | None
|
holiday_list: DF.Link | None
|
||||||
hour_rate: DF.Currency
|
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
|
off_status_image: DF.AttachImage | None
|
||||||
on_status_image: DF.AttachImage | None
|
on_status_image: DF.AttachImage | None
|
||||||
plant_floor: DF.Link | None
|
plant_floor: DF.Link | None
|
||||||
@@ -64,10 +61,26 @@ class Workstation(Document):
|
|||||||
total_working_hours: DF.Float
|
total_working_hours: DF.Float
|
||||||
warehouse: DF.Link | None
|
warehouse: DF.Link | None
|
||||||
working_hours: DF.Table[WorkstationWorkingHour]
|
working_hours: DF.Table[WorkstationWorkingHour]
|
||||||
|
workstation_costs: DF.Table[WorkstationCost]
|
||||||
workstation_name: DF.Data
|
workstation_name: DF.Data
|
||||||
workstation_type: DF.Link | None
|
workstation_type: DF.Link | None
|
||||||
# end: auto-generated types
|
# 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):
|
def before_save(self):
|
||||||
self.set_data_based_on_workstation_type()
|
self.set_data_based_on_workstation_type()
|
||||||
self.set_hour_rate()
|
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))
|
frappe.throw(_("Row #{0}: Start Time must be before End Time").format(row.idx))
|
||||||
|
|
||||||
def set_hour_rate(self):
|
def set_hour_rate(self):
|
||||||
self.hour_rate = (
|
self.hour_rate = 0.0
|
||||||
flt(self.hour_rate_labour)
|
for row in self.workstation_costs:
|
||||||
+ flt(self.hour_rate_electricity)
|
if row.operating_cost:
|
||||||
+ flt(self.hour_rate_consumable)
|
self.hour_rate += flt(row.operating_cost)
|
||||||
+ flt(self.hour_rate_rent)
|
|
||||||
)
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def set_data_based_on_workstation_type(self):
|
def set_data_based_on_workstation_type(self):
|
||||||
|
if self.workstation_costs:
|
||||||
|
return
|
||||||
|
|
||||||
if self.workstation_type:
|
if self.workstation_type:
|
||||||
fields = [
|
data = frappe.get_all(
|
||||||
"hour_rate_labour",
|
"Workstation Cost",
|
||||||
"hour_rate_electricity",
|
fields=["operating_component", "operating_cost", "idx"],
|
||||||
"hour_rate_consumable",
|
filters={"parent": self.workstation_type, "parenttype": "Workstation Type"},
|
||||||
"hour_rate_rent",
|
order_by="idx",
|
||||||
"hour_rate",
|
)
|
||||||
"description",
|
|
||||||
]
|
|
||||||
|
|
||||||
data = frappe.get_cached_value("Workstation Type", self.workstation_type, fields, as_dict=True)
|
for row in data:
|
||||||
|
self.append(
|
||||||
if not data:
|
"workstation_costs",
|
||||||
return
|
{
|
||||||
|
"operating_component": row.operating_component,
|
||||||
for field in fields:
|
"operating_cost": row.operating_cost,
|
||||||
if self.get(field):
|
"idx": row.idx,
|
||||||
continue
|
},
|
||||||
|
)
|
||||||
if value := data.get(field):
|
|
||||||
self.set(field, value)
|
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
self.validate_overlap_for_operation_timings()
|
self.validate_overlap_for_operation_timings()
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
@@ -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": []
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
@@ -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": []
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
@@ -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": []
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -10,12 +10,8 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"workstation_type",
|
"workstation_type",
|
||||||
"over_heads",
|
"section_break_auzm",
|
||||||
"hour_rate_electricity",
|
"workstation_costs",
|
||||||
"hour_rate_consumable",
|
|
||||||
"column_break_5",
|
|
||||||
"hour_rate_rent",
|
|
||||||
"hour_rate_labour",
|
|
||||||
"section_break_8",
|
"section_break_8",
|
||||||
"hour_rate",
|
"hour_rate",
|
||||||
"description_tab",
|
"description_tab",
|
||||||
@@ -32,44 +28,6 @@
|
|||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"unique": 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",
|
"description": "per hour",
|
||||||
"fieldname": "hour_rate",
|
"fieldname": "hour_rate",
|
||||||
@@ -88,10 +46,6 @@
|
|||||||
"oldfieldtype": "Text",
|
"oldfieldtype": "Text",
|
||||||
"width": "300px"
|
"width": "300px"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "column_break_5",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "description_tab",
|
"fieldname": "description_tab",
|
||||||
@@ -101,11 +55,22 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "section_break_8",
|
"fieldname": "section_break_8",
|
||||||
"fieldtype": "Section Break"
|
"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",
|
"icon": "icon-wrench",
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-27 13:11:00.946367",
|
"modified": "2025-08-19 12:06:56.683558",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Workstation Type",
|
"name": "Workstation Type",
|
||||||
@@ -125,9 +90,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
|
"row_format": "Dynamic",
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import _, bold
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
|
|
||||||
@@ -15,25 +16,38 @@ class WorkstationType(Document):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
|
from erpnext.manufacturing.doctype.workstation_cost.workstation_cost import WorkstationCost
|
||||||
|
|
||||||
description: DF.SmallText | None
|
description: DF.SmallText | None
|
||||||
hour_rate: DF.Currency
|
hour_rate: DF.Currency
|
||||||
hour_rate_consumable: DF.Currency
|
workstation_costs: DF.Table[WorkstationCost]
|
||||||
hour_rate_electricity: DF.Currency
|
|
||||||
hour_rate_labour: DF.Currency
|
|
||||||
hour_rate_rent: DF.Currency
|
|
||||||
workstation_type: DF.Data
|
workstation_type: DF.Data
|
||||||
# end: auto-generated types
|
# 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):
|
def before_save(self):
|
||||||
self.set_hour_rate()
|
self.set_hour_rate()
|
||||||
|
|
||||||
def set_hour_rate(self):
|
def set_hour_rate(self):
|
||||||
self.hour_rate = (
|
self.hour_rate = 0.0
|
||||||
flt(self.hour_rate_labour)
|
|
||||||
+ flt(self.hour_rate_electricity)
|
for row in self.workstation_costs:
|
||||||
+ flt(self.hour_rate_consumable)
|
if row.operating_cost:
|
||||||
+ flt(self.hour_rate_rent)
|
self.hour_rate += flt(row.operating_cost)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_workstations(workstation_type):
|
def get_workstations(workstation_type):
|
||||||
|
|||||||
@@ -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.update_serial_no_reference_name
|
||||||
erpnext.patches.v16_0.set_invoice_type_in_pos_settings
|
erpnext.patches.v16_0.set_invoice_type_in_pos_settings
|
||||||
erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter
|
erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter
|
||||||
|
erpnext.patches.v16_0.make_workstation_operating_components #1
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -294,6 +294,10 @@ def install(country=None):
|
|||||||
{"doctype": "Market Segment", "market_segment": _("Upper Income")},
|
{"doctype": "Market Segment", "market_segment": _("Upper Income")},
|
||||||
# Warehouse Type
|
# Warehouse Type
|
||||||
{"doctype": "Warehouse Type", "name": "Transit"},
|
{"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 (
|
for doctype, title_field, filename in (
|
||||||
|
|||||||
Reference in New Issue
Block a user