diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index d1f63854c7..3f109d91b5 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/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, diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 42b23f223d..1f443fb95a 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/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) diff --git a/erpnext/manufacturing/doctype/routing/routing.py b/erpnext/manufacturing/doctype/routing/routing.py index 8312d7436c..ece0db717a 100644 --- a/erpnext/manufacturing/doctype/routing/routing.py +++ b/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 \ No newline at end of file + sequence_id = row.sequence_id diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py index 6a38dcfa03..92f26946ab 100644 --- a/erpnext/manufacturing/doctype/routing/test_routing.py +++ b/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) diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index c6699bee48..9b73aca601 100644 --- a/erpnext/manufacturing/doctype/workstation/test_workstation.py +++ b/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) \ No newline at end of file + return frappe.get_doc("Workstation", workstation_name) diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index 3512e59045..f4483f7547 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/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