diff --git a/erpnext/config/manufacturing.py b/erpnext/config/manufacturing.py index 43b46381a59..6c915b7a81b 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 1c4abe78628..34a48949473 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 00000000000..e69de29bb2d 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 00000000000..48db7bc0c2f --- /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 00000000000..d40c736fd19 --- /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 5823f7cac50..daa450d89c4 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 fbc3cc9c2de..b8a2f3cb49b 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 df89a46041a..6d0ce9d492c 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 aa204f0e06f..5e4139e77e5 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 799cfacfaea..945e986a3ff 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 5e12c80f885..5b186b7e882 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 c9ee893e8a6..685c84e2d7a 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 6271a163cb9..d3c7b56e8a9 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 45b16af85ba..bde2a411872 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 52d644ca2c1..6f864b49bd9 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 bc0a9dc24bd..bfc4c05eabd 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 ffbd8f315dc..650996bb342 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 ee0b96f28cf..96b8925edb5 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 664117484d0..6115607adee 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 3439f0a9b1d..05e49ba5672 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 00000000000..080f80f001d --- /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 c5f3ecd8995..9f913087c56 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") %} +
{% } %}