From e84fa67f3099d9a0c9915ae079e8090955778523 Mon Sep 17 00:00:00 2001 From: Neil Trini Lasrado Date: Thu, 27 Nov 2014 10:49:07 +0530 Subject: [PATCH] Actual operating cost, Actal operating time added to 'production order operations' - auto fetched based on 'time log' creation 'make time log' button appears only if 'production order' document is submitted server side validation added to check if 'Production Order' mentioned on 'Time Log' is in submit state test cases added to check all of the above Time Log Bug Fixed Manufacturing seetings doctype added. prod order holiday list time calculation added --- erpnext/config/manufacturing.py | 7 +- .../hr/doctype/holiday_list/test_records.json | 3 +- .../manufacturing_settings/__init__.py | 0 .../manufacturing_settings.json | 90 ++++++++++ .../manufacturing_settings.py | 9 + .../doctype/operation/test_operation.py | 2 +- .../production_order/production_order.js | 9 + .../production_order/production_order.json | 11 +- .../production_order/production_order.py | 38 ++++- .../production_order/test_production_order.py | 32 +++- .../production_order_operation.json | 156 +++++++++++------- .../doctype/workstation/test_records.json | 1 + .../doctype/workstation/workstation.js | 10 +- .../doctype/workstation/workstation.json | 3 +- .../doctype/workstation/workstation.py | 33 ++-- .../doctype/time_log/test_time_log.py | 60 +++++++ erpnext/projects/doctype/time_log/time_log.py | 66 +++++++- .../doctype/time_log/time_log_list.html | 20 ++- .../doctype/time_log/time_log_list.js | 2 +- erpnext/setup/doctype/company/company.json | 10 +- .../form_grid/production_order_grid.html | 34 ++++ .../templates/form_grid/stock_entry_grid.html | 3 +- 22 files changed, 497 insertions(+), 102 deletions(-) create mode 100644 erpnext/manufacturing/doctype/manufacturing_settings/__init__.py create mode 100644 erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json create mode 100644 erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py create mode 100644 erpnext/templates/form_grid/production_order_grid.html diff --git a/erpnext/config/manufacturing.py b/erpnext/config/manufacturing.py index 43b46381a5..6c915b7a81 100644 --- a/erpnext/config/manufacturing.py +++ b/erpnext/config/manufacturing.py @@ -25,13 +25,18 @@ def get_data(): { "type": "doctype", "name": "Workstation", - "description": _("Where manufacturing operations are carried out."), + "description": _("Where manufacturing operations are carried."), }, { "type": "doctype", "name": "Operation", "description": _("Details of the operations carried out."), }, + { + "type": "doctype", + "name": "Manufacturing Settings", + "description": _("Global settings for all manufacturing processes."), + }, ] }, diff --git a/erpnext/hr/doctype/holiday_list/test_records.json b/erpnext/hr/doctype/holiday_list/test_records.json index 1c4abe7862..34a4894947 100644 --- a/erpnext/hr/doctype/holiday_list/test_records.json +++ b/erpnext/hr/doctype/holiday_list/test_records.json @@ -1,6 +1,7 @@ [ { - "doctype": "Holiday List", + "doctype": "Holiday List", + "name": "_Test Holiday List 1", "fiscal_year": "_Test Fiscal Year 2013", "holiday_list_details": [ { diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/__init__.py b/erpnext/manufacturing/doctype/manufacturing_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json new file mode 100644 index 0000000000..48db7bc0c2 --- /dev/null +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -0,0 +1,90 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "creation": "2014-11-27 14:12:07.542534", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Master", + "fields": [ + { + "allow_on_submit": 0, + "default": "30", + "description": "Maximum Overtime allowed against an workstation.\n( in mins )", + "fieldname": "max_overtime", + "fieldtype": "Float", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Maximum Overtime", + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "default": "No", + "fieldname": "allow_production_on_holidays", + "fieldtype": "Select", + "label": "Allow Production on Holidays", + "options": "Yes\nNo", + "permlevel": 0, + "precision": "" + }, + { + "default": "30", + "description": "Delay in start time of production order operations if automatically make time logs is used.\n(in mins)", + "fieldname": "operations_start_delay", + "fieldtype": "Float", + "label": "Operations Start Delay", + "permlevel": 0, + "precision": "" + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "icon": "icon-wrench", + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "modified": "2014-12-01 15:33:00.905276", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Manufacturing Settings", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 0, + "export": 0, + "import": 0, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 0, + "role": "Manufacturing Manager", + "set_user_permissions": 0, + "submit": 0, + "write": 1 + } + ], + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py new file mode 100644 index 0000000000..d40c736fd1 --- /dev/null +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class ManufacturingSettings(Document): + pass diff --git a/erpnext/manufacturing/doctype/operation/test_operation.py b/erpnext/manufacturing/doctype/operation/test_operation.py index 5823f7cac5..daa450d89c 100644 --- a/erpnext/manufacturing/doctype/operation/test_operation.py +++ b/erpnext/manufacturing/doctype/operation/test_operation.py @@ -1,4 +1,4 @@ -# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors and Contributors +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors # See license.txt import frappe diff --git a/erpnext/manufacturing/doctype/production_order/production_order.js b/erpnext/manufacturing/doctype/production_order/production_order.js index fbc3cc9c2d..b8a2f3cb49 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.js +++ b/erpnext/manufacturing/doctype/production_order/production_order.js @@ -83,6 +83,15 @@ $.extend(cur_frm.cscript, { frappe.set_route("Form", doclist[0].doctype, doclist[0].name); } }); + }, + + auto_time_log: function(doc){ + frappe.call({ + method:"erpnext.manufacturing.doctype.production_order.production_order.auto_make_time_log", + args: { + "production_order_id": doc.name + } + }); } }); diff --git a/erpnext/manufacturing/doctype/production_order/production_order.json b/erpnext/manufacturing/doctype/production_order/production_order.json index df89a46041..6d0ce9d492 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.json +++ b/erpnext/manufacturing/doctype/production_order/production_order.json @@ -207,6 +207,15 @@ "precision": "", "read_only": 1 }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.docstatus==1", + "fieldname": "auto_time_log", + "fieldtype": "Button", + "label": "Automatically Make Time logs", + "permlevel": 0, + "precision": "" + }, { "fieldname": "more_info", "fieldtype": "Section Break", @@ -279,7 +288,7 @@ "idx": 1, "in_create": 0, "is_submittable": 1, - "modified": "2014-11-24 11:13:09.639253", + "modified": "2014-12-01 11:36:56.832268", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Order", diff --git a/erpnext/manufacturing/doctype/production_order/production_order.py b/erpnext/manufacturing/doctype/production_order/production_order.py index aa204f0e06..5e4139e77e 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.py +++ b/erpnext/manufacturing/doctype/production_order/production_order.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe, json, time, datetime -from frappe.utils import flt, nowdate +from frappe.utils import flt, nowdate, now, cint, cstr from frappe import _ from frappe.model.document import Document from erpnext.manufacturing.doctype.bom.bom import validate_bom_no @@ -12,6 +12,10 @@ from erpnext.manufacturing.doctype.bom.bom import validate_bom_no class OverProductionError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass +form_grid_templates = { + "production_order_operations": "templates/form_grid/production_order_grid.html" +} + class ProductionOrder(Document): def validate(self): if self.docstatus == 0: @@ -146,6 +150,7 @@ class ProductionOrder(Document): update_bin(args) def set_production_order_operations(self): + """Sets operations table in 'Production Order'. """ self.set('production_order_operations', []) operations = frappe.db.sql("""select operation, opn_description, workstation, hour_rate, time_in_mins, operating_cost, fixed_cycle_cost from `tabBOM Operation` where parent = %s""", self.bom_no, as_dict=1) @@ -154,9 +159,24 @@ class ProductionOrder(Document): for d in self.get('production_order_operations'): d.status = "Pending" d.qty_completed=0 + + self.auto_caluclate_production_dates() def auto_caluclate_production_dates(self): - pass + start_delay = cint(frappe.db.get_value("Manufacturing Settings", "None", "operations_start_delay")) * 60 + time = datetime.datetime.now() + datetime.timedelta(seconds= start_delay) + for d in self.get('production_order_operations'): + holiday_list = frappe.db.get_value("Workstation", d.workstation, "holiday_list") + for d in frappe.db.sql("""select holiday_date from `tabHoliday` where parent = %s + order by holiday_date""", holiday_list, as_dict=1): + print "time date", time.date() + print "holiday ", d.holiday_date + if d.holiday_date == time.date(): + print "time IN ", time + time = time + datetime.timedelta(seconds= 24*60*60) + d.planned_start_time = time.strftime('%Y-%m-%d %H:%M:%S') + time = time + datetime.timedelta(seconds= (cint(d.time_in_mins) * 60)) + d.planned_end_time = time.strftime('%Y-%m-%d %H:%M:%S') @frappe.whitelist() def get_item_details(item): @@ -220,7 +240,7 @@ def get_events(start, end, filters=None): return data @frappe.whitelist() -def make_time_log(name, operation, from_time=None, to_time=None, qty=None, project=None, workstation=None): +def make_time_log(name, operation, from_time, to_time, qty=None, project=None, workstation=None): time_log = frappe.new_doc("Time Log") time_log.time_log_for = 'Manufacturing' time_log.from_time = from_time @@ -232,4 +252,14 @@ def make_time_log(name, operation, from_time=None, to_time=None, qty=None, proje time_log.workstation= workstation if from_time and to_time : time_log.calculate_total_hours() - return time_log \ No newline at end of file + return time_log + +@frappe.whitelist() +def auto_make_time_log(production_order_id): + prod_order = frappe.get_doc("Production Order", production_order_id) + for d in prod_order.production_order_operations: + operation = cstr(d.idx) + ". " + d.operation + time_log = make_time_log(prod_order.name, operation, d.planned_start_time, d.planned_end_time, + prod_order.qty, prod_order.project_name, d.workstation) + time_log.save() + frappe.msgprint(_("Time Logs created.")) diff --git a/erpnext/manufacturing/doctype/production_order/test_production_order.py b/erpnext/manufacturing/doctype/production_order/test_production_order.py index 799cfacfae..945e986a3f 100644 --- a/erpnext/manufacturing/doctype/production_order/test_production_order.py +++ b/erpnext/manufacturing/doctype/production_order/test_production_order.py @@ -8,6 +8,7 @@ import frappe from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory from erpnext.manufacturing.doctype.production_order.production_order import make_stock_entry from erpnext.stock.doctype.stock_entry import test_stock_entry +from erpnext.projects.doctype.time_log.time_log import OverProductionError class TestProductionOrder(unittest.TestCase): def test_planned_qty(self): @@ -60,17 +61,20 @@ class TestProductionOrder(unittest.TestCase): def test_make_time_log(self): prod_order = frappe.get_doc({ - "doctype":"Production Order", + "doctype": "Production Order", "production_item": "_Test FG Item 2", "bom_no": "BOM/_Test FG Item 2/002", - "qty": 1 + "qty": 1, + "wip_warehouse": "_Test Warehouse - _TC", + "fg_warehouse": "_Test Warehouse 1 - _TC" }) prod_order.set_production_order_operations() prod_order.production_order_operations[0].update({ "planned_start_time": "2014-11-25 00:00:00", - "planned_end_time": "2014-11-25 10:00:00" + "planned_end_time": "2014-11-25 10:00:00", + "hour_rate": 10 }) prod_order.insert() @@ -81,6 +85,8 @@ class TestProductionOrder(unittest.TestCase): from frappe.utils import cstr from frappe.utils import time_diff_in_hours + prod_order.submit() + time_log = make_time_log( prod_order.name, cstr(d.idx) + ". " + d.operation, \ d.planned_start_time, d.planned_end_time, prod_order.qty - d.qty_completed) @@ -91,6 +97,14 @@ class TestProductionOrder(unittest.TestCase): time_log.save() time_log.submit() + manufacturing_settings = frappe.get_doc({ + "doctype": "Manufacturing Settings", + "maximum_overtime": 30, + "allow_production_on_holidays": "No" + }) + + manufacturing_settings.save() + prod_order.load_from_db() self.assertEqual(prod_order.production_order_operations[0].status, "Completed") self.assertEqual(prod_order.production_order_operations[0].qty_completed, prod_order.qty) @@ -98,11 +112,17 @@ class TestProductionOrder(unittest.TestCase): self.assertEqual(prod_order.production_order_operations[0].actual_start_time, time_log.from_time) self.assertEqual(prod_order.production_order_operations[0].actual_end_time, time_log.to_time) + self.assertEqual(prod_order.production_order_operations[0].actual_operation_time, 600) + self.assertEqual(prod_order.production_order_operations[0].actual_operating_cost, 6000) + time_log.cancel() prod_order.load_from_db() - self.assertEqual(prod_order.production_order_operations[0].status,"Pending") - self.assertEqual(prod_order.production_order_operations[0].qty_completed,0) + self.assertEqual(prod_order.production_order_operations[0].status, "Pending") + self.assertEqual(prod_order.production_order_operations[0].qty_completed, 0) + + self.assertEqual(prod_order.production_order_operations[0].actual_operation_time, 0) + self.assertEqual(prod_order.production_order_operations[0].actual_operating_cost, 0) time_log2 = frappe.copy_doc(time_log) time_log2.update({ @@ -111,6 +131,6 @@ class TestProductionOrder(unittest.TestCase): "to_time": "2014-11-26 00:00:00", "docstatus": 0 }) - self.assertRaises(frappe.ValidationError, time_log2.save) + self.assertRaises(OverProductionError, time_log2.save) test_records = frappe.get_test_records('Production Order') diff --git a/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json b/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json index 5e12c80f88..5b186b7e88 100644 --- a/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json +++ b/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json @@ -45,7 +45,7 @@ "hidden": 0, "ignore_user_permissions": 0, "in_filter": 0, - "in_list_view": 1, + "in_list_view": 0, "label": "Operation Description", "no_copy": 0, "oldfieldname": "opn_description", @@ -83,19 +83,22 @@ "default": "Pending", "fieldname": "status", "fieldtype": "Select", - "in_list_view": 1, + "in_list_view": 0, "label": "Status", "options": "Pending\nWork in Progress\nCompleted", "permlevel": 0, - "precision": "" + "precision": "", + "read_only": 1 }, { "default": "0", "fieldname": "qty_completed", "fieldtype": "Float", + "in_list_view": 1, "label": "Qty Completed", "permlevel": 0, - "precision": "" + "precision": "", + "read_only": 1 }, { "allow_on_submit": 0, @@ -121,9 +124,9 @@ "unique": 0 }, { - "fieldname": "cost", + "fieldname": "estimated_time_and_cost", "fieldtype": "Section Break", - "label": "Cost", + "label": "Estimated Time and Cost", "permlevel": 0, "precision": "" }, @@ -149,57 +152,6 @@ "set_only_once": 0, "unique": 0 }, - { - "allow_on_submit": 0, - "fieldname": "time_in_mins", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Operation Time (mins)", - "no_copy": 0, - "oldfieldname": "time_in_mins", - "oldfieldtype": "Currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "fieldname": "column_break_10", - "fieldtype": "Column Break", - "permlevel": 0, - "precision": "" - }, - { - "allow_on_submit": 0, - "description": "Hour rate * hours", - "fieldname": "operating_cost", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Operating Cost", - "no_copy": 0, - "oldfieldname": "operating_cost", - "oldfieldtype": "Currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, { "allow_on_submit": 0, "fieldname": "fixed_cycle_cost", @@ -221,26 +173,98 @@ "unique": 0 }, { - "fieldname": "section_break_9", - "fieldtype": "Section Break", - "label": "Time", + "allow_on_submit": 0, + "description": "Hour Rate * Operating Time", + "fieldname": "operating_cost", + "fieldtype": "Float", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Operating Cost", + "no_copy": 0, + "oldfieldname": "operating_cost", + "oldfieldtype": "Currency", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break", "permlevel": 0, "precision": "" }, + { + "allow_on_submit": 0, + "description": "in Minutes", + "fieldname": "time_in_mins", + "fieldtype": "Float", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Operation Time", + "no_copy": 0, + "oldfieldname": "time_in_mins", + "oldfieldtype": "Currency", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 1, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "fieldname": "planned_start_time", "fieldtype": "Datetime", "label": "Planned Start Time", "permlevel": 0, - "precision": "" + "precision": "", + "reqd": 1 }, { "fieldname": "planned_end_time", "fieldtype": "Datetime", "label": "Planned End Time", "permlevel": 0, + "precision": "", + "reqd": 1 + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "label": "Actual Time and Cost", + "permlevel": 0, "precision": "" }, + { + "description": "in Minutes\nUpdated via 'Time Log'", + "fieldname": "actual_operation_time", + "fieldtype": "Float", + "label": "Actual Operation Time", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, + { + "description": "Hour Rate * Actual Operating Cost", + "fieldname": "actual_operating_cost", + "fieldtype": "Float", + "label": "Actual Operating Cost", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, { "fieldname": "column_break_11", "fieldtype": "Column Break", @@ -252,17 +276,21 @@ "fieldtype": "Datetime", "label": "Actual Start Time", "permlevel": 0, - "precision": "" + "precision": "", + "read_only": 1 }, { + "description": "Updated via 'Time Log'", "fieldname": "actual_end_time", "fieldtype": "Datetime", "label": "Actual End Time", "permlevel": 0, - "precision": "" + "precision": "", + "read_only": 1 }, { "allow_on_submit": 1, + "depends_on": "eval:doc.docstatus==1", "fieldname": "make_time_log", "fieldtype": "Button", "label": "Make Time Log", @@ -277,7 +305,7 @@ "is_submittable": 0, "issingle": 0, "istable": 1, - "modified": "2014-11-25 13:34:10.697445", + "modified": "2014-12-01 14:06:40.068700", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Order Operation", diff --git a/erpnext/manufacturing/doctype/workstation/test_records.json b/erpnext/manufacturing/doctype/workstation/test_records.json index c9ee893e8a..685c84e2d7 100644 --- a/erpnext/manufacturing/doctype/workstation/test_records.json +++ b/erpnext/manufacturing/doctype/workstation/test_records.json @@ -6,6 +6,7 @@ "warehouse": "_Test warehouse - _TC", "fixed_cycle_cost": 1000, "hour_rate":100, + "holiday_list": "_Test Holiday List", "workstation_operation_hours": [ { "start_time": "10:00:00", diff --git a/erpnext/manufacturing/doctype/workstation/workstation.js b/erpnext/manufacturing/doctype/workstation/workstation.js index 6271a163cb..d3c7b56e8a 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.js +++ b/erpnext/manufacturing/doctype/workstation/workstation.js @@ -5,7 +5,15 @@ //--------- ONLOAD ------------- cur_frm.cscript.onload = function(doc, cdt, cdn) { - + frappe.call({ + type:"GET", + method:"erpnext.manufacturing.doctype.workstation.workstation.get_default_holiday_list", + callback: function(r) { + if(!r.exe && r.message){ + cur_frm.set_value("holiday_list", r.message); + } + } + }) } cur_frm.cscript.refresh = function(doc, cdt, cdn) { diff --git a/erpnext/manufacturing/doctype/workstation/workstation.json b/erpnext/manufacturing/doctype/workstation/workstation.json index 45b16af85b..bde2a41187 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.json +++ b/erpnext/manufacturing/doctype/workstation/workstation.json @@ -150,6 +150,7 @@ "precision": "" }, { + "default": "", "fieldname": "holiday_list", "fieldtype": "Link", "label": "Holiday List", @@ -160,7 +161,7 @@ ], "icon": "icon-wrench", "idx": 1, - "modified": "2014-11-07 11:39:37.720913", + "modified": "2014-11-27 19:04:58.125107", "modified_by": "Administrator", "module": "Manufacturing", "name": "Workstation", diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index 52d644ca2c..6f864b49bd 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -5,10 +5,13 @@ from __future__ import unicode_literals import frappe import datetime from frappe import _ -from frappe.utils import flt +from frappe.utils import flt, cint from frappe.model.document import Document +class WorkstationHolidayError(frappe.ValidationError): pass +class WorkstationIsClosedError(frappe.ValidationError): pass + class Workstation(Document): def update_bom_operation(self): bom_list = frappe.db.sql("""select DISTINCT parent from `tabBOM Operation` @@ -26,19 +29,26 @@ class Workstation(Document): def check_if_within_operating_hours(self, from_time, to_time): if self.check_workstation_for_operation_time(from_time, to_time): - frappe.msgprint(_("Warning: Time Log timings outside workstation Operating Hours !")) + frappe.throw(_("Time Log timings outside workstation Operating Hours !"), WorkstationIsClosedError) - msg = self.check_workstation_for_holiday(from_time, to_time) - if msg != None: - frappe.msgprint(msg) + if frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays") == "No": + msg = self.check_workstation_for_holiday(from_time, to_time) + if msg != None: + frappe.throw(msg, WorkstationHolidayError) def check_workstation_for_operation_time(self, from_time, to_time): start_time = datetime.datetime.strptime(from_time,'%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S') end_time = datetime.datetime.strptime(to_time,'%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S') + max_time_diff = frappe.db.get_value("Manufacturing Settings", "None", "max_overtime") - if frappe.db.sql("""select start_time, end_time from `tabWorkstation Operation Hours` - where parent = %s and (%s end_time )""",(self.workstation_name, start_time, end_time), as_dict=1): - return 1 + for d in frappe.db.sql("""select time_to_sec(timediff( start_time, %s))/60 as st_diff , + time_to_sec(timediff( %s, end_time))/60 as et_diff from `tabWorkstation Operation Hours` + where parent = %s and (%s end_time )""", + (start_time, end_time, self.workstation_name, start_time, end_time), as_dict=1): + if cint(d.st_diff) > cint(max_time_diff): + return 1 + if cint(d.et_diff) > cint(max_time_diff): + return 1 def check_workstation_for_holiday(self, from_time, to_time): holiday_list = frappe.db.get_value("Workstation", self.workstation_name, "holiday_list") @@ -50,8 +60,11 @@ class Workstation(Document): %s and %s """,(holiday_list, start_date, end_date), as_dict=1): flag = 1 msg = msg + "\n" + d.holiday_date - if flag ==1: return msg else: - return None \ No newline at end of file + return None + +@frappe.whitelist() +def get_default_holiday_list(): + return frappe.db.get_value("Company", frappe.defaults.get_user_default("company"), "default_holiday_list") \ No newline at end of file diff --git a/erpnext/projects/doctype/time_log/test_time_log.py b/erpnext/projects/doctype/time_log/test_time_log.py index bc0a9dc24b..bfc4c05eab 100644 --- a/erpnext/projects/doctype/time_log/test_time_log.py +++ b/erpnext/projects/doctype/time_log/test_time_log.py @@ -1,10 +1,16 @@ # Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +from __future__ import unicode_literals import frappe import unittest from erpnext.projects.doctype.time_log.time_log import OverlapError +from erpnext.projects.doctype.time_log.time_log import NotSubmittedError + +from erpnext.manufacturing.doctype.workstation.workstation import WorkstationHolidayError +from erpnext.manufacturing.doctype.workstation.workstation import WorkstationIsClosedError + from erpnext.projects.doctype.time_log_batch.test_time_log_batch import * class TestTimeLog(unittest.TestCase): @@ -17,5 +23,59 @@ class TestTimeLog(unittest.TestCase): frappe.db.sql("delete from `tabTime Log`") + def test_production_order_status(self): + prod_order = make_prod_order(self) + + prod_order.save() + + time_log = frappe.get_doc({ + "doctype": "Time Log", + "time_log_for": "Manufacturing", + "production_order": prod_order.name, + "qty": 1, + "from_time": "2014-12-26 00:00:00", + "to_time": "2014-12-26 00:00:00" + }) + + self.assertRaises(NotSubmittedError, time_log.save) + + def test_time_log_on_holiday(self): + prod_order = make_prod_order(self) + + prod_order.save() + prod_order.submit() + + time_log = frappe.get_doc({ + "doctype": "Time Log", + "time_log_for": "Manufacturing", + "production_order": prod_order.name, + "qty": 1, + "from_time": "2013-02-01 10:00:00", + "to_time": "2013-02-01 20:00:00", + "workstation": "_Test Workstation 1" + }) + self.assertRaises(WorkstationHolidayError , time_log.save) + + time_log.update({ + "from_time": "2013-02-02 09:00:00", + "to_time": "2013-02-02 20:00:00" + }) + self.assertRaises(WorkstationIsClosedError , time_log.save) + + time_log.from_time= "2013-02-02 09:30:00" + time_log.save() + time_log.submit() + time_log.cancel() + +def make_prod_order(self): + return frappe.get_doc({ + "doctype":"Production Order", + "production_item": "_Test FG Item 2", + "bom_no": "BOM/_Test FG Item 2/002", + "qty": 1, + "wip_warehouse": "_Test Warehouse - _TC", + "fg_warehouse": "_Test Warehouse 1 - _TC" + }) + test_records = frappe.get_test_records('Time Log') test_ignore = ["Time Log Batch", "Sales Invoice"] diff --git a/erpnext/projects/doctype/time_log/time_log.py b/erpnext/projects/doctype/time_log/time_log.py index ffbd8f315d..650996bb34 100644 --- a/erpnext/projects/doctype/time_log/time_log.py +++ b/erpnext/projects/doctype/time_log/time_log.py @@ -9,8 +9,9 @@ from frappe import _ from frappe.utils import cstr, cint, comma_and - class OverlapError(frappe.ValidationError): pass +class OverProductionError(frappe.ValidationError): pass +class NotSubmittedError(frappe.ValidationError): pass from frappe.model.document import Document @@ -19,9 +20,11 @@ class TimeLog(Document): def validate(self): self.set_status() self.validate_overlap() + self.validate_timings() self.calculate_total_hours() self.check_workstation_timings() self.validate_qty() + self.validate_production_order() def on_submit(self): self.update_production_order() @@ -47,6 +50,7 @@ class TimeLog(Document): self.status="Billed" def validate_overlap(self): + """Checks if 'Time Log' entries overlap each other. """ existing = frappe.db.sql_list("""select name from `tabTime Log` where owner=%s and ( (from_time between %s and %s) or @@ -61,6 +65,10 @@ class TimeLog(Document): if existing: frappe.throw(_("This Time Log conflicts with {0}").format(comma_and(existing)), OverlapError) + + def validate_timings(self): + if self.to_time < self.from_time: + frappe.throw(_("From Time cannot be greater than To Time")) def before_cancel(self): self.set_status() @@ -69,6 +77,7 @@ class TimeLog(Document): self.set_status() def update_production_order(self): + """Updates `start_date`, `end_date` for operation in Production Order.""" if self.time_log_for=="Manufacturing" and self.operation: d = self.get_qty_and_status() required_qty = cint(frappe.db.get_value("Production Order" , self.production_order, "qty")) @@ -84,6 +93,7 @@ class TimeLog(Document): self.production_order_update(dates, d.get('qty'), d['status']) def update_production_order_on_cancel(self): + """Updates operations in 'Production Order' when an associated 'Time Log' is cancelled.""" if self.time_log_for=="Manufacturing" and self.operation: d = frappe._dict() d = self.get_qty_and_status() @@ -91,6 +101,7 @@ class TimeLog(Document): self.production_order_update(dates, d.get('qty'), d.get('status')) def get_qty_and_status(self): + """Returns quantity and status of Operation in 'Time Log'. """ status = "Work in Progress" qty = cint(frappe.db.sql("""select sum(qty) as qty from `tabTime Log` where production_order = %s and operation = %s and docstatus=1""", (self.production_order, self.operation),as_dict=1)[0].qty) @@ -102,30 +113,67 @@ class TimeLog(Document): } def get_production_dates(self): + """Returns Min From and Max To Dates of Time Logs against a specific Operation. """ return frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date from `tabTime Log` where production_order = %s and operation = %s and docstatus=1""", (self.production_order, self.operation), as_dict=1)[0] def production_order_update(self, dates, qty, status): + """Updates 'Produuction Order' and sets 'Actual Start Time', 'Actual End Time', 'Status', 'Compleated Qty'. """ d = self.operation.split('. ',1) - frappe.db.sql("""update `tabProduction Order Operation` set actual_start_time = %s, actual_end_time = %s, - qty_completed = %s, status = %s where idx=%s and parent=%s and operation = %s """, - (dates.start_date, dates.end_date, qty, status, d[0], self.production_order, d[1] )) + actual_op_time = self.get_actual_op_time().time_diff + if actual_op_time == None: + actual_op_time = 0 + actual_op_cost = self.get_actual_op_cost(actual_op_time) + frappe.db.sql("""update `tabProduction Order Operation` set actual_start_time = %s, actual_end_time = %s, qty_completed = %s, + status = %s, actual_operation_time = %s, actual_operating_cost = %s where idx=%s and parent=%s and operation = %s """, + (dates.start_date, dates.end_date, qty, status, actual_op_time, actual_op_cost, d[0], self.production_order, d[1] )) + + def get_actual_op_time(self): + """Returns 'Actual Operating Time'. """ + return frappe.db.sql("""select sum(time_to_sec(timediff(to_time, from_time))/60) as time_diff from + `tabTime Log` where production_order = %s and operation = %s and docstatus=1""", + (self.production_order, self.operation), as_dict = 1)[0] + + def get_actual_op_cost(self, actual_op_time): + """Returns 'Actual Operating Cost'. """ + if self.operation: + d = self.operation.split('. ',1) + idx = d[0] + operation = d[1] + hour_rate = frappe.db.sql("""select hour_rate from `tabProduction Order Operation` where idx=%s and + parent=%s and operation = %s""", (idx, self.production_order, operation), as_dict=1)[0].hour_rate + return hour_rate * actual_op_time + def check_workstation_timings(self): + """Checks if **Time Log** is between operating hours of the **Workstation**.""" if self.workstation: frappe.get_doc("Workstation", self.workstation).check_if_within_operating_hours(self.from_time, self.to_time) def validate_qty(self): + """Throws `OverProductionError` if quantity surpasses **Production Order** quantity.""" if self.qty == None: self.qty=0 required_qty = cint(frappe.db.get_value("Production Order" , self.production_order, "qty")) completed_qty = self.get_qty_and_status().get('qty') if (completed_qty + cint(self.qty)) > required_qty: - frappe.throw(_("Quantity cannot be greater than pending quantity that is {0}").format(required_qty)) - + frappe.throw(_("Quantity cannot be greater than pending quantity that is {0}").format(required_qty), OverProductionError) + + def validate_production_order(self): + """Throws 'NotSubmittedError' if **production order** is not submitted. """ + if self.production_order: + if frappe.db.get_value("Production Order", self.production_order, "docstatus") != 1 : + frappe.throw(_("You cannot make a time log against a production order that has not been submitted.") + , NotSubmittedError) + @frappe.whitelist() def get_workstation(production_order, operation): + """Returns workstation name from Production Order against an associated Operation. + + :param production_order string + :param operation string + """ if operation: d = operation.split('. ',1) idx = d[0] @@ -136,6 +184,12 @@ def get_workstation(production_order, operation): @frappe.whitelist() def get_events(start, end, filters=None): + """Returns events for Gantt / Calendar view rendering. + + :param start: Start date-time. + :param end: End date-time. + :param filters: Filters like workstation, project etc. + """ from frappe.desk.reportview import build_match_conditions if not frappe.has_permission("Time Log"): frappe.msgprint(_("No Permission"), raise_exception=1) diff --git a/erpnext/projects/doctype/time_log/time_log_list.html b/erpnext/projects/doctype/time_log/time_log_list.html index ee0b96f28c..96b8925edb 100644 --- a/erpnext/projects/doctype/time_log/time_log_list.html +++ b/erpnext/projects/doctype/time_log/time_log_list.html @@ -9,17 +9,31 @@ {% } %} + + {% if(doc.time_log_for == 'Manufacturing') { %} + + + + {% } %} + + {% if(doc.activity_type) { %} {%= doc.activity_type %} - - ({%= doc.hours + " " + __("hours") %}) - + {% } %} + {% if(doc.project) { %} {%= doc.project %} {% } %} + + + ({%= doc.hours + " " + __("hours") %}) + + diff --git a/erpnext/projects/doctype/time_log/time_log_list.js b/erpnext/projects/doctype/time_log/time_log_list.js index 664117484d..6115607ade 100644 --- a/erpnext/projects/doctype/time_log/time_log_list.js +++ b/erpnext/projects/doctype/time_log/time_log_list.js @@ -3,7 +3,7 @@ // render frappe.listview_settings['Time Log'] = { - add_fields: ["status", "billable", "activity_type", "task", "project", "hours"], + add_fields: ["status", "billable", "activity_type", "task", "project", "hours", "time_log_for"], selectable: true, onload: function(me) { me.appframe.add_primary_action(__("Make Time Log Batch"), function() { diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 3439f0a9b1..05e49ba567 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -160,6 +160,14 @@ "options": "Account", "permlevel": 0 }, + { + "fieldname": "default_holiday_list", + "fieldtype": "Link", + "label": "Default Holiday List", + "options": "Holiday List", + "permlevel": 0, + "precision": "" + }, { "fieldname": "column_break0", "fieldtype": "Column Break", @@ -356,7 +364,7 @@ ], "icon": "icon-building", "idx": 1, - "modified": "2014-08-29 15:50:18.539228", + "modified": "2014-11-27 18:15:48.909416", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/templates/form_grid/production_order_grid.html b/erpnext/templates/form_grid/production_order_grid.html new file mode 100644 index 0000000000..080f80f001 --- /dev/null +++ b/erpnext/templates/form_grid/production_order_grid.html @@ -0,0 +1,34 @@ +{% var visible_columns = row.get_visible_columns(["operation", + "opn_description", "status", "qty_completed", "workstation"]); +%} + +{% if(!doc) { %} +
+
{%= __("Operation") %}
+
{%= __("Workstation") %}
+
{%= __("Completed Qty") %}
+
+{% } else { %} +
+
+ {%= doc.operation %} + + {%= doc.status %} + + {% include "templates/form_grid/includes/visible_cols.html" %} +
+ {%= doc.get_formatted("opn_description") %} +
+
+ + +
+ {%= doc.get_formatted("workstation") %} +
+ + +
+ {%= doc.get_formatted("qty_completed") %} +
+
+{% } %} diff --git a/erpnext/templates/form_grid/stock_entry_grid.html b/erpnext/templates/form_grid/stock_entry_grid.html index c5f3ecd899..9f913087c5 100644 --- a/erpnext/templates/form_grid/stock_entry_grid.html +++ b/erpnext/templates/form_grid/stock_entry_grid.html @@ -40,7 +40,8 @@
{%= doc.get_formatted("amount") %}
- {%= doc.get_formatted("incoming_rate") %}
+ {%= doc.get_formatted("incoming_rate") %} +
{% } %}