From 99d0941008cf66338e3199934af99bedb13310ab Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 24 Feb 2015 14:41:12 +0530 Subject: [PATCH] [time logs] fixes --- erpnext/config/manufacturing.py | 15 ++-- .../manufacturing_settings.json | 23 +++++- .../production_order/production_order.py | 76 +++++++++++++------ .../production_order_operation.json | 8 +- .../doctype/workstation/workstation.py | 27 ++++--- .../projects/doctype/time_log/time_log.json | 10 ++- erpnext/projects/doctype/time_log/time_log.py | 49 ++++++------ .../stock/doctype/stock_entry/stock_entry.py | 3 +- 8 files changed, 135 insertions(+), 76 deletions(-) diff --git a/erpnext/config/manufacturing.py b/erpnext/config/manufacturing.py index bd258fbdcf..493a01197a 100644 --- a/erpnext/config/manufacturing.py +++ b/erpnext/config/manufacturing.py @@ -37,11 +37,6 @@ def get_data(): "name": "Operation", "description": _("Details of the operations carried out."), }, - { - "type": "doctype", - "name": "Manufacturing Settings", - "description": _("Global settings for all manufacturing processes."), - }, ] }, @@ -61,6 +56,16 @@ def get_data(): }, ] }, + { + "label": _("Setup"), + "items": [ + { + "type": "doctype", + "name": "Manufacturing Settings", + "description": _("Global settings for all manufacturing processes."), + } + ] + }, { "label": _("Standard Reports"), "icon": "icon-list", diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index 1fa949d4db..a7d48fc6a5 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -9,10 +9,17 @@ "document_type": "Master", "fields": [ { - "description": "Will not allow to make time logs outside \"Workstation operation timings\"", - "fieldname": "dont_allow_overtime", + "fieldname": "capacity_planning", + "fieldtype": "Section Break", + "label": "Capacity Planning", + "permlevel": 0, + "precision": "" + }, + { + "description": "Plan time logs outside Workstation Working Hours.", + "fieldname": "allow_overtime", "fieldtype": "Check", - "label": "Don't allow overtime", + "label": "Allow Overtime", "permlevel": 0, "precision": "" }, @@ -40,6 +47,14 @@ "label": "Capacity Planning For (Days)", "permlevel": 0, "precision": "" + }, + { + "description": "Default 10 mins", + "fieldname": "mins_between_operations", + "fieldtype": "Data", + "label": "Time Between Operations (in mins)", + "permlevel": 0, + "precision": "" } ], "hide_heading": 0, @@ -50,7 +65,7 @@ "is_submittable": 0, "issingle": 1, "istable": 0, - "modified": "2015-02-23 09:05:58.927098", + "modified": "2015-02-23 23:44:45.917027", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing Settings", diff --git a/erpnext/manufacturing/doctype/production_order/production_order.py b/erpnext/manufacturing/doctype/production_order/production_order.py index ba61b3c17f..5c66d40729 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.py +++ b/erpnext/manufacturing/doctype/production_order/production_order.py @@ -23,9 +23,6 @@ form_grid_templates = { } class ProductionOrder(Document): - def __setup__(self): - self.holidays = frappe._dict() - def validate(self): if self.docstatus == 0: self.status = "Draft" @@ -159,6 +156,7 @@ class ProductionOrder(Document): frappe.db.set(self,'status', 'Cancelled') self.update_planned_qty(-self.qty) + self.delete_time_logs() def update_planned_qty(self, qty): """update planned qty in bin""" @@ -182,47 +180,44 @@ class ProductionOrder(Document): self.set('operations', operations) - self.plan_operations() self.calculate_operating_cost() - def plan_operations(self): - if self.planned_start_date: - scheduled_datetime = self.planned_start_date - for d in self.get('operations'): - while getdate(scheduled_datetime) in self.get_holidays(d.workstation): - scheduled_datetime = get_datetime(scheduled_datetime) + relativedelta(days=1) - - d.planned_start_time = scheduled_datetime - scheduled_datetime = get_datetime(scheduled_datetime) + relativedelta(minutes=d.time_in_mins) - d.planned_end_time = scheduled_datetime - - self.planned_end_date = scheduled_datetime - def get_holidays(self, workstation): holiday_list = frappe.db.get_value("Workstation", workstation, "holiday_list") - if holiday_list not in self.holidays: + holidays = {} + + if holiday_list not in holidays: holiday_list_days = [getdate(d[0]) for d in frappe.get_all("Holiday", fields=["holiday_date"], - filters={"parent": holiday_list}, order_by="holiday_date", as_list=1)] + filters={"parent": holiday_list}, order_by="holiday_date", limit_page_length=0, as_list=1)] - self.holidays[holiday_list] = holiday_list_days + holidays[holiday_list] = holiday_list_days - return self.holidays[holiday_list] + return holidays[holiday_list] def make_time_logs(self): - time_logs = [] + """Capacity Planning. Plan time logs based on earliest availablity of workstation after + Planned Start Date. Time logs will be created and remain in Draft mode and must be submitted + before manufacturing entry can be made.""" + if not self.operations: + return + + time_logs = [] plan_days = frappe.db.get_single_value("Manufacturing Settings", "capacity_planning_for_days") or 30 - for d in self.operations: + for i, d in enumerate(self.operations): + self.set_operation_start_end_time(i, d) + time_log = make_time_log(self.name, d.operation, d.planned_start_time, d.planned_end_time, - flt(self.qty) - flt(d.completed_qty), self.project_name, d.workstation) + flt(self.qty) - flt(d.completed_qty), self.project_name, d.workstation, operation_id=d.name) self.check_operation_fits_in_working_hours(d) original_start_time = time_log.from_time while True: + _from_time = time_log.from_time try: time_log.save() break @@ -240,14 +235,41 @@ class ProductionOrder(Document): frappe.msgprint(_("Unable to find Time Slot in the next {0} days for Operation {1}").format(plan_days, d.operation)) break - print time_log.as_json() + if _from_time == time_log.from_time: + frappe.throw("Capacity Planning Error") + + d.planned_start_time = time_log.from_time + d.planned_end_time = time_log.to_time + d.db_update() if time_log.name: time_logs.append(time_log.name) + self.planned_end_date = self.operations[-1].planned_end_time + if time_logs: frappe.msgprint(_("Time Logs created:") + "\n" + "\n".join(time_logs)) + def set_operation_start_end_time(self, i, d): + """Set start and end time for given operation. If first operation, set start as + `planned_start_date`, else add time diff to end time of earlier operation.""" + if i==0: + # first operation at planned_start date + d.planned_start_time = self.planned_start_date + else: + d.planned_start_time = get_datetime(self.operations[i-1].planned_end_time)\ + + self.get_mins_between_operations() + + d.planned_end_time = get_datetime(d.planned_start_time) + relativedelta(minutes = d.time_in_mins) + + if d.planned_start_time == d.planned_end_time: + frappe.throw(_("Capacity Planning Error")) + + def get_mins_between_operations(self): + if not hasattr(self, "_mins_between_operations"): + self._mins_between_operations = frappe.db.get_single_value("Manufacturing Settings", + "mins_between_operations") or 10 + return relativedelta(minutes = self._mins_between_operations) def check_operation_fits_in_working_hours(self, d): """Raises expection if operation is longer than working hours in the given workstation.""" @@ -289,6 +311,10 @@ class ProductionOrder(Document): and getdate(self.expected_delivery_date) < getdate(self.planned_end_date): frappe.msgprint(_("Production might not be able to finish by the Expected Delivery Date.")) + def delete_time_logs(self): + for time_log in frappe.get_all("Time Log", ["name"], {"production_order": self.name}): + frappe.delete_doc("Time Log", time_log.name) + @frappe.whitelist() def get_item_details(item): res = frappe.db.sql("""select stock_uom, description 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 2fe6c32e7c..b1a633036b 100644 --- a/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json +++ b/erpnext/manufacturing/doctype/production_order_operation/production_order_operation.json @@ -148,7 +148,8 @@ "no_copy": 1, "permlevel": 0, "precision": "", - "reqd": 1 + "read_only": 1, + "reqd": 0 }, { "fieldname": "planned_end_time", @@ -158,7 +159,8 @@ "no_copy": 1, "permlevel": 0, "precision": "", - "reqd": 1 + "read_only": 1, + "reqd": 0 }, { "fieldname": "column_break_10", @@ -290,7 +292,7 @@ "is_submittable": 0, "issingle": 0, "istable": 1, - "modified": "2015-02-23 07:55:19.368919", + "modified": "2015-02-24 00:27:44.651084", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Order Operation", diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index d345e263a7..4c234993ca 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -3,9 +3,8 @@ from __future__ import unicode_literals import frappe -import datetime from frappe import _ -from frappe.utils import flt, cint, getdate, formatdate, comma_and +from frappe.utils import flt, cint, getdate, formatdate, comma_and, get_datetime from frappe.model.document import Document @@ -49,23 +48,27 @@ def get_default_holiday_list(): return frappe.db.get_value("Company", frappe.defaults.get_user_default("company"), "default_holiday_list") def check_if_within_operating_hours(workstation, from_datetime, to_datetime): - if not is_within_operating_hours(workstation, from_datetime, to_datetime): - frappe.throw(_("Time Log timings outside workstation operating hours"), NotInWorkingHoursError) + if not cint(frappe.db.get_value("Manufacturing Settings", None, "allow_overtime")): + is_within_operating_hours(workstation, from_datetime, to_datetime) if not cint(frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays")): check_workstation_for_holiday(workstation, from_datetime, to_datetime) def is_within_operating_hours(workstation, from_datetime, to_datetime): - if not cint(frappe.db.get_value("Manufacturing Settings", None, "dont_allow_overtime")): - return True + start_time = get_datetime(from_datetime).time() + end_time = get_datetime(to_datetime).time() - start_time = datetime.datetime.strptime(from_datetime,'%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S') - end_time = datetime.datetime.strptime(to_datetime,'%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S') + working_hours = frappe.db.sql_list("""select idx from `tabWorkstation Working Hour` + where parent = %s + and ( + (start_time between %s and %s) or + (end_time between %s and %s) or + (%s between start_time and end_time)) + """, (workstation, start_time, end_time, start_time, end_time, start_time)) + + if not working_hours: + frappe.throw(_("Time Log timings outside workstation operating hours"), NotInWorkingHoursError) - for d in frappe.db.sql("""select start_time, end_time from `tabWorkstation Operation Hours` - where parent = %s and ifnull(enabled, 0) = 1""", workstation, as_dict=1): - if d.end_time >= start_time >= d.start_time and d.end_time >= end_time >= d.start_time: - return True def check_workstation_for_holiday(workstation, from_datetime, to_datetime): holiday_list = frappe.db.get_value("Workstation", workstation, "holiday_list") diff --git a/erpnext/projects/doctype/time_log/time_log.json b/erpnext/projects/doctype/time_log/time_log.json index 603efe36f0..9194f82f14 100644 --- a/erpnext/projects/doctype/time_log/time_log.json +++ b/erpnext/projects/doctype/time_log/time_log.json @@ -75,12 +75,14 @@ "reqd": 0 }, { + "default": "Project", "fieldname": "time_log_for", "fieldtype": "Select", "label": "Time Log For", "options": "\nProject\nManufacturing", "permlevel": 0, "precision": "", + "read_only": 1, "reqd": 0 }, { @@ -117,7 +119,8 @@ "label": "Production Order", "options": "Production Order", "permlevel": 0, - "precision": "" + "precision": "", + "read_only": 1 }, { "depends_on": "eval:doc.time_log_for == 'Manufacturing'", @@ -126,7 +129,8 @@ "label": "Operation", "options": "", "permlevel": 0, - "precision": "" + "precision": "", + "read_only": 1 }, { "fieldname": "operation_id", @@ -230,7 +234,7 @@ "icon": "icon-time", "idx": 1, "is_submittable": 1, - "modified": "2015-02-23 08:00:48.195775", + "modified": "2015-02-24 03:57:27.652685", "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 3adf8793a2..ffe65bf88b 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, flt, add_days, get_datetime, get_time +from frappe.utils import cstr, flt, get_datetime, get_time, getdate from dateutil.relativedelta import relativedelta from dateutil.parser import parse @@ -109,25 +109,28 @@ class TimeLog(Document): 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: - operation = self.operation + if self.time_log_for=="Manufacturing" and self.production_order: + if not self.operation_id: + frappe.throw(_("Operation ID not set")) dates = self.get_operation_start_end_time() - tl = self.get_all_time_logs() + summary = self.get_time_log_summary() + + pro = frappe.get_doc("Production Order", self.production_order) + for o in pro.operations: + if o.name == self.operation_id: + o.actual_start_time = dates.start_date + o.actual_end_time = dates.end_date + o.completed_qty = summary.completed_qty + o.actual_operation_time = summary.mins + break - frappe.db.sql("""update `tabProduction Order Operation` - set actual_start_time = %s, actual_end_time = %s, completed_qty = %s, actual_operation_time = %s - where parent=%s and idx=%s and operation = %s""", - (dates.start_date, dates.end_date, tl.completed_qty, - tl.hours, self.production_order, operation[0], operation[1])) - - pro_order = frappe.get_doc("Production Order", self.production_order) - pro_order.flags.ignore_validate_update_after_submit = True - pro_order.update_operation_status() - pro_order.calculate_operating_cost() - pro_order.set_actual_dates() - pro_order.save() + pro.flags.ignore_validate_update_after_submit = True + pro.update_operation_status() + pro.calculate_operating_cost() + pro.set_actual_dates() + pro.save() def get_operation_start_end_time(self): """Returns Min From and Max To Dates of Time Logs against a specific Operation. """ @@ -137,7 +140,7 @@ class TimeLog(Document): def move_to_next_day(self): """Move start and end time one day forward""" - self.from_time = add_days(self.from_time, 1) + self.from_time = get_datetime(self.from_time) + relativedelta(day=1) def move_to_next_working_slot(self): """Move to next working slot from workstation""" @@ -145,13 +148,13 @@ class TimeLog(Document): slot_found = False for working_hour in workstation.working_hours: if get_datetime(self.from_time).time() < get_time(working_hour.start_time): - self.from_time = self.from_time.split()[0] + " " + working_hour.start_time + self.from_time = getdate(self.from_time).strftime("%Y-%m-%d") + " " + working_hour.start_time slot_found = True break if not slot_found: # later than last time - self.from_time = self.from_time.split()[0] + workstation.working_hours[0].start_time + self.from_time = getdate(self.from_time).strftime("%Y-%m-%d") + " " + workstation.working_hours[0].start_time self.move_to_next_day() def move_to_next_non_overlapping_slot(self): @@ -160,13 +163,13 @@ class TimeLog(Document): if overlapping: self.from_time = parse(overlapping.to_time) + relativedelta(minutes=10) - def get_all_time_logs(self): + def get_time_log_summary(self): """Returns 'Actual Operating Time'. """ return frappe.db.sql("""select - sum(hours*60) as hours, sum(ifnull(completed_qty, 0)) as completed_qty + sum(hours*60) as mins, sum(ifnull(completed_qty, 0)) as completed_qty from `tabTime Log` - where production_order = %s and operation = %s and docstatus=1""", - (self.production_order, self.operation), as_dict=1)[0] + where production_order = %s and operation_id = %s and docstatus=1""", + (self.production_order, self.operation_id), as_dict=1)[0] def validate_project(self): if self.time_log_for == 'Project': diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 4e229cbaec..a48c5f3f9c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -19,6 +19,7 @@ class NotUpdateStockError(frappe.ValidationError): pass class StockOverReturnError(frappe.ValidationError): pass class IncorrectValuationRateError(frappe.ValidationError): pass class DuplicateEntryForProductionOrderError(frappe.ValidationError): pass +class OperationsNotCompleteError(frappe.ValidationError): pass from erpnext.controllers.stock_controller import StockController @@ -185,7 +186,7 @@ class StockEntry(StockController): total_completed_qty = flt(self.fg_completed_qty) + flt(prod_order.produced_qty) if total_completed_qty > flt(d.completed_qty): frappe.throw(_("Row #{0}: Operation {1} is not completed for {2} qty of finished goods in Production Order # {3}. Please update operation status via Time Logs") - .format(d.idx, d.operation, total_completed_qty, self.production_order)) + .format(d.idx, d.operation, total_completed_qty, self.production_order), OperationsNotCompleteError) def check_duplicate_entry_for_production_order(self): other_ste = [t[0] for t in frappe.db.get_values("Stock Entry", {