From 0f4350dd613cce2855c76aa2c944aa79b8d0456f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 19 Dec 2014 10:57:46 +0530 Subject: [PATCH] capacity planning fixes and cleanup --- erpnext/manufacturing/doctype/bom/bom.js | 28 ++- erpnext/manufacturing/doctype/bom/bom.json | 64 ++++--- erpnext/manufacturing/doctype/bom/bom.py | 20 ++- .../doctype/bom_operation/bom_operation.json | 162 ++++++++--------- .../manufacturing_settings.json | 4 +- .../doctype/operation/operation.json | 23 ++- .../production_order/production_order.js | 14 +- .../production_order/production_order.json | 79 +++++---- .../production_order/production_order.py | 92 ++++++---- .../production_order_calendar.js | 8 +- .../production_order_operation.json | 45 +++-- .../doctype/workstation/workstation.json | 28 +-- .../doctype/workstation/workstation.py | 88 ++++----- erpnext/patches.txt | 1 + erpnext/patches/v5_0/capacity_planning.py | 23 +++ erpnext/projects/doctype/time_log/time_log.js | 19 +- .../projects/doctype/time_log/time_log.json | 23 +-- erpnext/projects/doctype/time_log/time_log.py | 167 +++++++----------- 18 files changed, 464 insertions(+), 424 deletions(-) create mode 100644 erpnext/patches/v5_0/capacity_planning.py diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index ce9ac0e35de..1fef5045923 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -114,23 +114,16 @@ cur_frm.cscript.rate = function(doc, cdt, cdn) { erpnext.bom.calculate_op_cost = function(doc) { var op = doc.bom_operations || []; - total_op_cost = 0; + doc.total_variable_cost, doc.total_fixed_cost = 0.0, 0.0; for(var i=0;i 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") - start_date = datetime.datetime.strptime(from_time,'%Y-%m-%d %H:%M:%S').strftime('%Y-%m-%d') - end_date = datetime.datetime.strptime(to_time,'%Y-%m-%d %H:%M:%S').strftime('%Y-%m-%d') - msg = _("Workstation is closed on the following dates as per Holiday List:") - flag = 0 - for d in frappe.db.sql("""select holiday_date from `tabHoliday` where parent = %s and holiday_date between - %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 @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 + return frappe.db.get_value("Company", frappe.defaults.get_user_default("company"), "default_holiday_list") + +def check_if_within_operating_hours(workstation, from_time, to_time): + if check_workstation_for_operation_time(workstation, from_time, to_time): + frappe.throw(_("Time Log timings outside workstation Operating Hours !"), WorkstationIsClosedError) + + if frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays") == "No": + msg = check_workstation_for_holiday(workstation, from_time, to_time) + if msg != None: + frappe.throw(msg, WorkstationHolidayError) + +def check_workstation_for_operation_time(workstation, 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") + + 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, workstation, 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(workstation, from_time, to_time): + holiday_list = frappe.db.get_value("Workstation", workstation, "holiday_list") + start_date = datetime.datetime.strptime(from_time,'%Y-%m-%d %H:%M:%S').strftime('%Y-%m-%d') + end_date = datetime.datetime.strptime(to_time,'%Y-%m-%d %H:%M:%S').strftime('%Y-%m-%d') + msg = _("Workstation is closed on the following dates as per Holiday List:") + flag = 0 + for d in frappe.db.sql("""select holiday_date from `tabHoliday` where parent = %s and holiday_date between + %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 diff --git a/erpnext/patches.txt b/erpnext/patches.txt index a81527ec797..07cfdbf040f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -98,3 +98,4 @@ execute:frappe.reload_doc('stock', 'doctype', 'item') execute:frappe.db.sql("update `tabItem` i set apply_warehouse_wise_reorder_level=1, re_order_level=0, re_order_qty=0 where exists(select name from `tabItem Reorder` where parent=i.name)") execute:frappe.rename_doc("DocType", "Support Ticket", "Issue", force=True) erpnext.patches.v5_0.set_default_company_in_bom +erpnext.patches.v5_0.capacity_planning diff --git a/erpnext/patches/v5_0/capacity_planning.py b/erpnext/patches/v5_0/capacity_planning.py new file mode 100644 index 00000000000..ce964353b02 --- /dev/null +++ b/erpnext/patches/v5_0/capacity_planning.py @@ -0,0 +1,23 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe + +def execute(): + for dt in ["workstation", "bom", "bom_operation"]: + frappe.reload_doc("manufacturing", "doctype", dt) + + frappe.db.sql("update `tabWorkstation` set fixed_cost = fixed_cycle_cost, total_variable_cost = overhead") + + frappe.db.sql("update `tabBOM Operation` set fixed_cost = fixed_cycle_cost") + + for d in frappe.db.sql("select name from `tabBOM` where docstatus < 2"): + try: + bom = frappe.get_doc('BOM', d[0]) + if bom.docstatus == 1: + bom.ignore_validate_update_after_submit = True + bom.calculate_cost() + bom.save() + except: + print "error", frappe.get_traceback() + pass diff --git a/erpnext/projects/doctype/time_log/time_log.js b/erpnext/projects/doctype/time_log/time_log.js index 36ca2d2ba8f..2c74bb328e2 100644 --- a/erpnext/projects/doctype/time_log/time_log.js +++ b/erpnext/projects/doctype/time_log/time_log.js @@ -26,10 +26,18 @@ frappe.ui.form.on("Time Log", "to_time", function(frm) { "hours")); }); +cur_frm.set_query("production_order", function(doc) { + return { + "filters": { + "docstatus": 1 + } + }; +}); + cur_frm.add_fetch('task','project','project'); $.extend(cur_frm.cscript, { - production_order: function(doc) { + production_order: function(doc) { if (doc.production_order){ var operations = []; frappe.model.with_doc("Production Order", doc.production_order, function(pro) { @@ -44,13 +52,16 @@ $.extend(cur_frm.cscript, { }, operation: function(doc) { - return cur_frm.call({ + return frappe.call({ method: "erpnext.projects.doctype.time_log.time_log.get_workstation", - args: { + args: { "production_order": doc.production_order, "operation": doc.operation }, callback: function(r) { + if(!r.exc) { + cur_frm.set_value("workstation", r.message) + } doc.workstation = r.workstation; } }); @@ -59,4 +70,4 @@ $.extend(cur_frm.cscript, { if (cur_frm.doc.time_log_for == "Manufacturing") { cur_frm.cscript.onload = cur_frm.cscript.production_order; -} \ No newline at end of file +} diff --git a/erpnext/projects/doctype/time_log/time_log.json b/erpnext/projects/doctype/time_log/time_log.json index 6e2706de2c6..4afc0a04696 100644 --- a/erpnext/projects/doctype/time_log/time_log.json +++ b/erpnext/projects/doctype/time_log/time_log.json @@ -20,10 +20,10 @@ "fieldname": "time_log_for", "fieldtype": "Select", "label": "Time Log For", - "options": "Project\nManufacturing", + "options": "\nProject\nManufacturing", "permlevel": 0, "precision": "", - "reqd": 1 + "reqd": 0 }, { "fieldname": "from_time", @@ -68,7 +68,7 @@ "reqd": 0 }, { - "depends_on": "eval:doc.time_log_for == 'Project'", + "depends_on": "eval:doc.time_log_for != 'Manufacturing'", "fieldname": "activity_type", "fieldtype": "Link", "in_list_view": 1, @@ -79,7 +79,7 @@ "reqd": 0 }, { - "depends_on": "eval:doc.time_log_for == 'Project'", + "depends_on": "eval:doc.time_log_for != 'Manufacturing'", "fieldname": "task", "fieldtype": "Link", "label": "Task", @@ -112,13 +112,15 @@ "label": "Workstation", "options": "Workstation", "permlevel": 0, - "precision": "" + "precision": "", + "read_only": 1 }, { - "depends_on": "eval:doc.time_log_for == 'Manufacturing'", - "fieldname": "qty", - "fieldtype": "Float", - "label": "Quantity", + "default": "Work in Progress", + "fieldname": "operation_status", + "fieldtype": "Select", + "label": "Operation Status", + "options": "\nWork in Progress\nCompleted", "permlevel": 0, "precision": "" }, @@ -150,6 +152,7 @@ "read_only": 0 }, { + "depends_on": "eval:doc.time_log_for == 'Project'", "fieldname": "project", "fieldtype": "Link", "in_list_view": 1, @@ -197,7 +200,7 @@ "icon": "icon-time", "idx": 1, "is_submittable": 1, - "modified": "2014-11-19 11:39:02.633802", + "modified": "2014-12-18 17:21:01.520646", "modified_by": "Administrator", "module": "Projects", "name": "Time Log", diff --git a/erpnext/projects/doctype/time_log/time_log.py b/erpnext/projects/doctype/time_log/time_log.py index 650996bb342..db08abc8522 100644 --- a/erpnext/projects/doctype/time_log/time_log.py +++ b/erpnext/projects/doctype/time_log/time_log.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, json from frappe import _ -from frappe.utils import cstr, cint, comma_and +from frappe.utils import cstr, comma_and, flt class OverlapError(frappe.ValidationError): pass @@ -16,25 +16,25 @@ class NotSubmittedError(frappe.ValidationError): pass from frappe.model.document import Document 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() def on_cancel(self): - self.update_production_order_on_cancel() + self.update_production_order() - def calculate_total_hours(self): - from frappe.utils import time_diff_in_hours - self.hours = time_diff_in_hours(self.to_time, self.from_time) + def before_update_after_submit(self): + self.set_status() + + def before_cancel(self): + self.set_status() def set_status(self): self.status = { @@ -65,127 +65,84 @@ 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() + def calculate_total_hours(self): + from frappe.utils import time_diff_in_seconds + self.hours = flt(time_diff_in_seconds(self.to_time, self.from_time)) / 3600 - def before_update_after_submit(self): - 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")) - if d.get('qty') == required_qty: - d['status'] = "Completed" - - dates = self.get_production_dates() - if self.from_time < dates.start_date: - dates.start_date = self.from_time - if self.to_time > dates.end_date: - dates.end_date = self.to_time - - 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() - dates = self.get_production_dates() - 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) - if qty == 0: - status = "Pending" - return { - "qty": qty, - "status": status - } - - 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) - 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) + from erpnext.manufacturing.doctype.workstation.workstation import check_if_within_operating_hours + check_if_within_operating_hours(self.workstation, 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), 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.throw(_("You can make a time log only against a submitted production order"), NotSubmittedError) + + def update_production_order(self): + """Updates `start_date`, `end_date`, `status` for operation in Production Order.""" + + if self.time_log_for=="Manufacturing" and self.operation: + dates = self.get_operation_start_end_time() + op_status = self.get_op_status() + actual_op_time = self.get_actual_op_time() + + d = self.operation.split('. ',1) + + frappe.db.sql("""update `tabProduction Order Operation` + set actual_start_time = %s, actual_end_time = %s, status = %s, actual_operation_time = %s + where parent=%s and idx=%s and operation = %s""", + (dates.start_date, dates.end_date, op_status, actual_op_time, + self.production_order, d[0], d[1])) + + frappe.get_doc("Production Order", self.production_order).save() + + def get_operation_start_end_time(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 get_actual_op_time(self): + """Returns 'Actual Operating Time'. """ + actual_time = frappe.db.sql("""select sum(hours*60) as time_diff from + `tabTime Log` where production_order = %s and operation = %s and docstatus=1""", + (self.production_order, self.operation)) + return actual_time[0][0] if actual_time else 0 + + def get_op_status(self): + status = frappe.db.sql("""select operation_status from `tabTime Log` + where production_order=%s and operation=%s and docstatus=1 + order by to_time desc limit 1""", (self.production_order, self.operation)) + + return status if status else self.status + @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] - operation = d[1] + idx, operation = operation.split('. ',1) - return frappe.db.sql("""select workstation from `tabProduction Order Operation` where idx=%s and - parent=%s and operation = %s""", (idx, production_order, operation), as_dict=1)[0] + workstation = frappe.db.sql("""select workstation from `tabProduction Order Operation` where idx=%s and + parent=%s and operation = %s""", (idx, production_order, operation)) + return workstation[0][0] if workstation else "" @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. @@ -194,8 +151,6 @@ def get_events(start, end, filters=None): if not frappe.has_permission("Time Log"): frappe.msgprint(_("No Permission"), raise_exception=1) - match = build_match_conditions("Time Log") - conditions = build_match_conditions("Time Log") conditions = conditions and (" and " + conditions) or "" if filters: @@ -211,7 +166,7 @@ def get_events(start, end, filters=None): "start": start, "end": end }, as_dict=True, update={"allDay": 0}) - + for d in data: d.title = d.name + ": " + (d.activity_type or d.production_order or "") if d.task: