refactor: update cost updates operation time and hour rates in BOM (#25891)
* refactor: updates hour_rate and operation time on update cost * refactor: hour_rates are updated in routing when updated in workstations * test: test cases for updating hour_rates and operation time in linked bom
This commit is contained in:
parent
c1954ec72a
commit
da82bd4b51
@ -81,7 +81,7 @@ class BOM(WebsiteGenerator):
|
||||
self.validate_operations()
|
||||
self.calculate_cost()
|
||||
self.update_stock_qty()
|
||||
self.update_cost(update_parent=False, from_child_bom=True, save=False)
|
||||
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
|
||||
|
||||
def get_context(self, context):
|
||||
context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
|
||||
@ -213,7 +213,7 @@ class BOM(WebsiteGenerator):
|
||||
return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1)
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_cost(self, update_parent=True, from_child_bom=False, save=True):
|
||||
def update_cost(self, update_parent=True, from_child_bom=False, update_hour_rate = True, save=True):
|
||||
if self.docstatus == 2:
|
||||
return
|
||||
|
||||
@ -242,7 +242,7 @@ class BOM(WebsiteGenerator):
|
||||
|
||||
if self.docstatus == 1:
|
||||
self.flags.ignore_validate_update_after_submit = True
|
||||
self.calculate_cost()
|
||||
self.calculate_cost(update_hour_rate)
|
||||
if save:
|
||||
self.db_update()
|
||||
|
||||
@ -403,32 +403,47 @@ class BOM(WebsiteGenerator):
|
||||
bom_list.reverse()
|
||||
return bom_list
|
||||
|
||||
def calculate_cost(self):
|
||||
def calculate_cost(self, update_hour_rate = False):
|
||||
"""Calculate bom totals"""
|
||||
self.calculate_op_cost()
|
||||
self.calculate_op_cost(update_hour_rate)
|
||||
self.calculate_rm_cost()
|
||||
self.calculate_sm_cost()
|
||||
self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
|
||||
self.base_total_cost = self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
|
||||
|
||||
def calculate_op_cost(self):
|
||||
def calculate_op_cost(self, update_hour_rate = False):
|
||||
"""Update workstation rate and calculates totals"""
|
||||
self.operating_cost = 0
|
||||
self.base_operating_cost = 0
|
||||
for d in self.get('operations'):
|
||||
if d.workstation:
|
||||
if not d.hour_rate:
|
||||
hour_rate = flt(frappe.db.get_value("Workstation", d.workstation, "hour_rate"))
|
||||
d.hour_rate = hour_rate / flt(self.conversion_rate) if self.conversion_rate else hour_rate
|
||||
|
||||
if d.hour_rate and d.time_in_mins:
|
||||
d.base_hour_rate = flt(d.hour_rate) * flt(self.conversion_rate)
|
||||
d.operating_cost = flt(d.hour_rate) * flt(d.time_in_mins) / 60.0
|
||||
d.base_operating_cost = flt(d.operating_cost) * flt(self.conversion_rate)
|
||||
self.update_rate_and_time(d, update_hour_rate)
|
||||
|
||||
self.operating_cost += flt(d.operating_cost)
|
||||
self.base_operating_cost += flt(d.base_operating_cost)
|
||||
|
||||
def update_rate_and_time(self, row, update_hour_rate = False):
|
||||
if not row.hour_rate or update_hour_rate:
|
||||
hour_rate = flt(frappe.get_cached_value("Workstation", row.workstation, "hour_rate"))
|
||||
row.hour_rate = (hour_rate / flt(self.conversion_rate)
|
||||
if self.conversion_rate and hour_rate else hour_rate)
|
||||
|
||||
if self.routing:
|
||||
row.time_in_mins = flt(frappe.db.get_value("BOM Operation", {
|
||||
"workstation": row.workstation,
|
||||
"operation": row.operation,
|
||||
"sequence_id": row.sequence_id,
|
||||
"parent": self.routing
|
||||
}, ["time_in_mins"]))
|
||||
|
||||
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)
|
||||
|
||||
if update_hour_rate:
|
||||
row.db_update()
|
||||
|
||||
def calculate_rm_cost(self):
|
||||
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
||||
total_rm_cost = 0
|
||||
@ -975,7 +990,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
if filters and filters.get("is_stock_item"):
|
||||
query_filters["is_stock_item"] = 1
|
||||
|
||||
|
||||
return frappe.get_all("Item",
|
||||
fields = fields, filters=query_filters,
|
||||
or_filters = or_cond_filters, order_by=order_by,
|
||||
|
@ -123,7 +123,7 @@ class TestBOM(unittest.TestCase):
|
||||
bom.items[0].conversion_factor = 5
|
||||
bom.insert()
|
||||
|
||||
bom.update_cost()
|
||||
bom.update_cost(update_hour_rate = False)
|
||||
|
||||
# test amounts in selected currency
|
||||
self.assertEqual(bom.items[0].rate, 300)
|
||||
|
@ -4,14 +4,24 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.utils import cint
|
||||
from frappe.utils import cint, flt
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
class Routing(Document):
|
||||
def validate(self):
|
||||
self.calculate_operating_cost()
|
||||
self.set_routing_id()
|
||||
|
||||
def on_update(self):
|
||||
self.calculate_operating_cost()
|
||||
|
||||
def calculate_operating_cost(self):
|
||||
for operation in self.operations:
|
||||
if not operation.hour_rate:
|
||||
operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate')
|
||||
operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, 2)
|
||||
|
||||
def set_routing_id(self):
|
||||
sequence_id = 0
|
||||
for row in self.operations:
|
||||
@ -21,4 +31,4 @@ class Routing(Document):
|
||||
frappe.throw(_("At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}")
|
||||
.format(row.idx, row.sequence_id, sequence_id))
|
||||
|
||||
sequence_id = row.sequence_id
|
||||
sequence_id = row.sequence_id
|
||||
|
@ -7,9 +7,7 @@ import unittest
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError
|
||||
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
|
||||
class TestRouting(unittest.TestCase):
|
||||
@ -48,7 +46,53 @@ class TestRouting(unittest.TestCase):
|
||||
wo_doc.cancel()
|
||||
wo_doc.delete()
|
||||
|
||||
def test_update_bom_operation_time(self):
|
||||
operations = [
|
||||
{
|
||||
"operation": "Test Operation A",
|
||||
"workstation": "_Test Workstation A",
|
||||
"hour_rate_rent": 300,
|
||||
"hour_rate_labour": 750 ,
|
||||
"time_in_mins": 30
|
||||
},
|
||||
{
|
||||
"operation": "Test Operation B",
|
||||
"workstation": "_Test Workstation B",
|
||||
"hour_rate_labour": 200,
|
||||
"hour_rate_rent": 1000,
|
||||
"time_in_mins": 20
|
||||
}
|
||||
]
|
||||
|
||||
test_routing_operations = [
|
||||
{
|
||||
"operation": "Test Operation A",
|
||||
"workstation": "_Test Workstation A",
|
||||
"time_in_mins": 30
|
||||
},
|
||||
{
|
||||
"operation": "Test Operation B",
|
||||
"workstation": "_Test Workstation A",
|
||||
"time_in_mins": 20
|
||||
}
|
||||
]
|
||||
setup_operations(operations)
|
||||
routing_doc = create_routing(routing_name="Routing Test", operations=test_routing_operations)
|
||||
bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency = 'INR')
|
||||
self.assertEqual(routing_doc.operations[0].time_in_mins, 30)
|
||||
self.assertEqual(routing_doc.operations[1].time_in_mins, 20)
|
||||
routing_doc.operations[0].time_in_mins = 90
|
||||
routing_doc.operations[1].time_in_mins = 42.2
|
||||
routing_doc.save()
|
||||
bom_doc.update_cost()
|
||||
bom_doc.reload()
|
||||
self.assertEqual(bom_doc.operations[0].time_in_mins, 90)
|
||||
self.assertEqual(bom_doc.operations[1].time_in_mins, 42.2)
|
||||
|
||||
|
||||
def setup_operations(rows):
|
||||
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
||||
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
||||
for row in rows:
|
||||
make_workstation(row)
|
||||
make_operation(row)
|
||||
@ -61,12 +105,14 @@ def create_routing(**args):
|
||||
|
||||
if not args.do_not_save:
|
||||
try:
|
||||
for operation in args.operations:
|
||||
doc.append("operations", operation)
|
||||
|
||||
doc.insert()
|
||||
except frappe.DuplicateEntryError:
|
||||
doc = frappe.get_doc("Routing", args.routing_name)
|
||||
doc.delete_key('operations')
|
||||
for operation in args.operations:
|
||||
doc.append("operations", operation)
|
||||
|
||||
doc.save()
|
||||
|
||||
return doc
|
||||
|
||||
@ -91,7 +137,7 @@ def setup_bom(**args):
|
||||
name = frappe.db.get_value('BOM', {'item': args.item_code}, 'name')
|
||||
if not name:
|
||||
bom_doc = make_bom(item = args.item_code, raw_materials = args.get("raw_materials"),
|
||||
routing = args.routing, with_operations=1)
|
||||
routing = args.routing, with_operations=1, currency = args.currency)
|
||||
else:
|
||||
bom_doc = frappe.get_doc("BOM", name)
|
||||
|
||||
|
@ -1,16 +1,19 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
||||
|
||||
import frappe
|
||||
import unittest
|
||||
from erpnext.manufacturing.doctype.workstation.workstation import check_if_within_operating_hours, NotInWorkingHoursError, WorkstationHolidayError
|
||||
from erpnext.manufacturing.doctype.routing.test_routing import setup_bom, create_routing
|
||||
from frappe.test_runner import make_test_records
|
||||
|
||||
test_dependencies = ["Warehouse"]
|
||||
test_records = frappe.get_test_records('Workstation')
|
||||
make_test_records('Workstation')
|
||||
|
||||
class TestWorkstation(unittest.TestCase):
|
||||
|
||||
def test_validate_timings(self):
|
||||
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00")
|
||||
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00")
|
||||
@ -21,6 +24,58 @@ class TestWorkstation(unittest.TestCase):
|
||||
self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours,
|
||||
"_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00")
|
||||
|
||||
def test_update_bom_operation_rate(self):
|
||||
operations = [
|
||||
{
|
||||
"operation": "Test Operation A",
|
||||
"workstation": "_Test Workstation A",
|
||||
"hour_rate_rent": 300,
|
||||
"time_in_mins": 60
|
||||
},
|
||||
{
|
||||
"operation": "Test Operation B",
|
||||
"workstation": "_Test Workstation B",
|
||||
"hour_rate_rent": 1000,
|
||||
"time_in_mins": 60
|
||||
}
|
||||
]
|
||||
|
||||
for row in operations:
|
||||
make_workstation(row)
|
||||
make_operation(row)
|
||||
|
||||
test_routing_operations = [
|
||||
{
|
||||
"operation": "Test Operation A",
|
||||
"workstation": "_Test Workstation A",
|
||||
"time_in_mins": 60
|
||||
},
|
||||
{
|
||||
"operation": "Test Operation B",
|
||||
"workstation": "_Test Workstation A",
|
||||
"time_in_mins": 60
|
||||
}
|
||||
]
|
||||
routing_doc = create_routing(routing_name = "Routing Test", operations=test_routing_operations)
|
||||
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
|
||||
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
|
||||
w1.save()
|
||||
#updating after setting new rates in workstations
|
||||
bom_doc.update_cost()
|
||||
bom_doc.reload()
|
||||
self.assertEqual(w1.hour_rate, 250)
|
||||
self.assertEqual(bom_doc.operations[0].hour_rate, 250)
|
||||
self.assertEqual(bom_doc.operations[1].hour_rate, 250)
|
||||
|
||||
def make_workstation(*args, **kwargs):
|
||||
args = args if args else kwargs
|
||||
if isinstance(args, tuple):
|
||||
@ -34,9 +89,10 @@ def make_workstation(*args, **kwargs):
|
||||
"doctype": "Workstation",
|
||||
"workstation_name": workstation_name
|
||||
})
|
||||
|
||||
doc.hour_rate_rent = args.get("hour_rate_rent")
|
||||
doc.hour_rate_labour = args.get("hour_rate_labour")
|
||||
doc.insert()
|
||||
|
||||
return doc
|
||||
except frappe.DuplicateEntryError:
|
||||
return frappe.get_doc("Workstation", workstation_name)
|
||||
return frappe.get_doc("Workstation", workstation_name)
|
||||
|
@ -39,7 +39,8 @@ class Workstation(Document):
|
||||
|
||||
def update_bom_operation(self):
|
||||
bom_list = frappe.db.sql("""select DISTINCT parent from `tabBOM Operation`
|
||||
where workstation = %s""", self.name)
|
||||
where workstation = %s and parenttype = 'routing' """, self.name)
|
||||
|
||||
for bom_no in bom_list:
|
||||
frappe.db.sql("""update `tabBOM Operation` set hour_rate = %s
|
||||
where parent = %s and workstation = %s""",
|
||||
@ -71,7 +72,7 @@ def check_if_within_operating_hours(workstation, operation, from_datetime, to_da
|
||||
def is_within_operating_hours(workstation, operation, from_datetime, to_datetime):
|
||||
operation_length = time_diff_in_seconds(to_datetime, from_datetime)
|
||||
workstation = frappe.get_doc("Workstation", workstation)
|
||||
|
||||
|
||||
if not workstation.working_hours:
|
||||
return
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user