Browse Source

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
develop
Noah Jacob 3 years ago
committed by GitHub
parent
commit
da82bd4b51
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 45
      erpnext/manufacturing/doctype/bom/bom.py
  2. 2
      erpnext/manufacturing/doctype/bom/test_bom.py
  3. 14
      erpnext/manufacturing/doctype/routing/routing.py
  4. 58
      erpnext/manufacturing/doctype/routing/test_routing.py
  5. 62
      erpnext/manufacturing/doctype/workstation/test_workstation.py
  6. 5
      erpnext/manufacturing/doctype/workstation/workstation.py

45
erpnext/manufacturing/doctype/bom/bom.py

@ -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,

2
erpnext/manufacturing/doctype/bom/test_bom.py

@ -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)

14
erpnext/manufacturing/doctype/routing/routing.py

@ -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

58
erpnext/manufacturing/doctype/routing/test_routing.py

@ -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)

62
erpnext/manufacturing/doctype/workstation/test_workstation.py

@ -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)

5
erpnext/manufacturing/doctype/workstation/workstation.py

@ -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…
Cancel
Save