[time logs] fixes

This commit is contained in:
Rushabh Mehta 2015-02-24 14:41:12 +05:30
parent 9524ee7ec6
commit 99d0941008
8 changed files with 135 additions and 76 deletions

View File

@ -37,11 +37,6 @@ def get_data():
"name": "Operation", "name": "Operation",
"description": _("Details of the operations carried out."), "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"), "label": _("Standard Reports"),
"icon": "icon-list", "icon": "icon-list",

View File

@ -9,10 +9,17 @@
"document_type": "Master", "document_type": "Master",
"fields": [ "fields": [
{ {
"description": "Will not allow to make time logs outside \"Workstation operation timings\"", "fieldname": "capacity_planning",
"fieldname": "dont_allow_overtime", "fieldtype": "Section Break",
"label": "Capacity Planning",
"permlevel": 0,
"precision": ""
},
{
"description": "Plan time logs outside Workstation Working Hours.",
"fieldname": "allow_overtime",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Don't allow overtime", "label": "Allow Overtime",
"permlevel": 0, "permlevel": 0,
"precision": "" "precision": ""
}, },
@ -40,6 +47,14 @@
"label": "Capacity Planning For (Days)", "label": "Capacity Planning For (Days)",
"permlevel": 0, "permlevel": 0,
"precision": "" "precision": ""
},
{
"description": "Default 10 mins",
"fieldname": "mins_between_operations",
"fieldtype": "Data",
"label": "Time Between Operations (in mins)",
"permlevel": 0,
"precision": ""
} }
], ],
"hide_heading": 0, "hide_heading": 0,
@ -50,7 +65,7 @@
"is_submittable": 0, "is_submittable": 0,
"issingle": 1, "issingle": 1,
"istable": 0, "istable": 0,
"modified": "2015-02-23 09:05:58.927098", "modified": "2015-02-23 23:44:45.917027",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Manufacturing Settings", "name": "Manufacturing Settings",

View File

@ -23,9 +23,6 @@ form_grid_templates = {
} }
class ProductionOrder(Document): class ProductionOrder(Document):
def __setup__(self):
self.holidays = frappe._dict()
def validate(self): def validate(self):
if self.docstatus == 0: if self.docstatus == 0:
self.status = "Draft" self.status = "Draft"
@ -159,6 +156,7 @@ class ProductionOrder(Document):
frappe.db.set(self,'status', 'Cancelled') frappe.db.set(self,'status', 'Cancelled')
self.update_planned_qty(-self.qty) self.update_planned_qty(-self.qty)
self.delete_time_logs()
def update_planned_qty(self, qty): def update_planned_qty(self, qty):
"""update planned qty in bin""" """update planned qty in bin"""
@ -182,47 +180,44 @@ class ProductionOrder(Document):
self.set('operations', operations) self.set('operations', operations)
self.plan_operations()
self.calculate_operating_cost() 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): def get_holidays(self, workstation):
holiday_list = frappe.db.get_value("Workstation", workstation, "holiday_list") 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"], 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): 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 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, 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) self.check_operation_fits_in_working_hours(d)
original_start_time = time_log.from_time original_start_time = time_log.from_time
while True: while True:
_from_time = time_log.from_time
try: try:
time_log.save() time_log.save()
break 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)) frappe.msgprint(_("Unable to find Time Slot in the next {0} days for Operation {1}").format(plan_days, d.operation))
break 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: if time_log.name:
time_logs.append(time_log.name) time_logs.append(time_log.name)
self.planned_end_date = self.operations[-1].planned_end_time
if time_logs: if time_logs:
frappe.msgprint(_("Time Logs created:") + "\n" + "\n".join(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): def check_operation_fits_in_working_hours(self, d):
"""Raises expection if operation is longer than working hours in the given workstation.""" """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): 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.")) 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() @frappe.whitelist()
def get_item_details(item): def get_item_details(item):
res = frappe.db.sql("""select stock_uom, description res = frappe.db.sql("""select stock_uom, description

View File

@ -148,7 +148,8 @@
"no_copy": 1, "no_copy": 1,
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"reqd": 1 "read_only": 1,
"reqd": 0
}, },
{ {
"fieldname": "planned_end_time", "fieldname": "planned_end_time",
@ -158,7 +159,8 @@
"no_copy": 1, "no_copy": 1,
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"reqd": 1 "read_only": 1,
"reqd": 0
}, },
{ {
"fieldname": "column_break_10", "fieldname": "column_break_10",
@ -290,7 +292,7 @@
"is_submittable": 0, "is_submittable": 0,
"issingle": 0, "issingle": 0,
"istable": 1, "istable": 1,
"modified": "2015-02-23 07:55:19.368919", "modified": "2015-02-24 00:27:44.651084",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Production Order Operation", "name": "Production Order Operation",

View File

@ -3,9 +3,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import datetime
from frappe import _ 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 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") 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): def check_if_within_operating_hours(workstation, from_datetime, to_datetime):
if not is_within_operating_hours(workstation, from_datetime, to_datetime): if not cint(frappe.db.get_value("Manufacturing Settings", None, "allow_overtime")):
frappe.throw(_("Time Log timings outside workstation operating hours"), NotInWorkingHoursError) is_within_operating_hours(workstation, from_datetime, to_datetime)
if not cint(frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays")): if not cint(frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays")):
check_workstation_for_holiday(workstation, from_datetime, to_datetime) check_workstation_for_holiday(workstation, from_datetime, to_datetime)
def is_within_operating_hours(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")): start_time = get_datetime(from_datetime).time()
return True end_time = get_datetime(to_datetime).time()
start_time = datetime.datetime.strptime(from_datetime,'%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S') working_hours = frappe.db.sql_list("""select idx from `tabWorkstation Working Hour`
end_time = datetime.datetime.strptime(to_datetime,'%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S') 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): def check_workstation_for_holiday(workstation, from_datetime, to_datetime):
holiday_list = frappe.db.get_value("Workstation", workstation, "holiday_list") holiday_list = frappe.db.get_value("Workstation", workstation, "holiday_list")

View File

@ -75,12 +75,14 @@
"reqd": 0 "reqd": 0
}, },
{ {
"default": "Project",
"fieldname": "time_log_for", "fieldname": "time_log_for",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Time Log For", "label": "Time Log For",
"options": "\nProject\nManufacturing", "options": "\nProject\nManufacturing",
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"read_only": 1,
"reqd": 0 "reqd": 0
}, },
{ {
@ -117,7 +119,8 @@
"label": "Production Order", "label": "Production Order",
"options": "Production Order", "options": "Production Order",
"permlevel": 0, "permlevel": 0,
"precision": "" "precision": "",
"read_only": 1
}, },
{ {
"depends_on": "eval:doc.time_log_for == 'Manufacturing'", "depends_on": "eval:doc.time_log_for == 'Manufacturing'",
@ -126,7 +129,8 @@
"label": "Operation", "label": "Operation",
"options": "", "options": "",
"permlevel": 0, "permlevel": 0,
"precision": "" "precision": "",
"read_only": 1
}, },
{ {
"fieldname": "operation_id", "fieldname": "operation_id",
@ -230,7 +234,7 @@
"icon": "icon-time", "icon": "icon-time",
"idx": 1, "idx": 1,
"is_submittable": 1, "is_submittable": 1,
"modified": "2015-02-23 08:00:48.195775", "modified": "2015-02-24 03:57:27.652685",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Time Log", "name": "Time Log",

View File

@ -6,7 +6,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, json import frappe, json
from frappe import _ 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.relativedelta import relativedelta
from dateutil.parser import parse from dateutil.parser import parse
@ -109,25 +109,28 @@ class TimeLog(Document):
def update_production_order(self): def update_production_order(self):
"""Updates `start_date`, `end_date`, `status` for operation in Production Order.""" """Updates `start_date`, `end_date`, `status` for operation in Production Order."""
if self.time_log_for=="Manufacturing" and self.operation: if self.time_log_for=="Manufacturing" and self.production_order:
operation = self.operation if not self.operation_id:
frappe.throw(_("Operation ID not set"))
dates = self.get_operation_start_end_time() 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` pro.flags.ignore_validate_update_after_submit = True
set actual_start_time = %s, actual_end_time = %s, completed_qty = %s, actual_operation_time = %s pro.update_operation_status()
where parent=%s and idx=%s and operation = %s""", pro.calculate_operating_cost()
(dates.start_date, dates.end_date, tl.completed_qty, pro.set_actual_dates()
tl.hours, self.production_order, operation[0], operation[1])) pro.save()
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()
def get_operation_start_end_time(self): def get_operation_start_end_time(self):
"""Returns Min From and Max To Dates of Time Logs against a specific Operation. """ """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): def move_to_next_day(self):
"""Move start and end time one day forward""" """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): def move_to_next_working_slot(self):
"""Move to next working slot from workstation""" """Move to next working slot from workstation"""
@ -145,13 +148,13 @@ class TimeLog(Document):
slot_found = False slot_found = False
for working_hour in workstation.working_hours: for working_hour in workstation.working_hours:
if get_datetime(self.from_time).time() < get_time(working_hour.start_time): 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 slot_found = True
break break
if not slot_found: if not slot_found:
# later than last time # 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() self.move_to_next_day()
def move_to_next_non_overlapping_slot(self): def move_to_next_non_overlapping_slot(self):
@ -160,13 +163,13 @@ class TimeLog(Document):
if overlapping: if overlapping:
self.from_time = parse(overlapping.to_time) + relativedelta(minutes=10) 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'. """ """Returns 'Actual Operating Time'. """
return frappe.db.sql("""select 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` from `tabTime Log`
where production_order = %s and operation = %s and docstatus=1""", where production_order = %s and operation_id = %s and docstatus=1""",
(self.production_order, self.operation), as_dict=1)[0] (self.production_order, self.operation_id), as_dict=1)[0]
def validate_project(self): def validate_project(self):
if self.time_log_for == 'Project': if self.time_log_for == 'Project':

View File

@ -19,6 +19,7 @@ class NotUpdateStockError(frappe.ValidationError): pass
class StockOverReturnError(frappe.ValidationError): pass class StockOverReturnError(frappe.ValidationError): pass
class IncorrectValuationRateError(frappe.ValidationError): pass class IncorrectValuationRateError(frappe.ValidationError): pass
class DuplicateEntryForProductionOrderError(frappe.ValidationError): pass class DuplicateEntryForProductionOrderError(frappe.ValidationError): pass
class OperationsNotCompleteError(frappe.ValidationError): pass
from erpnext.controllers.stock_controller import StockController 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) total_completed_qty = flt(self.fg_completed_qty) + flt(prod_order.produced_qty)
if total_completed_qty > flt(d.completed_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") 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): def check_duplicate_entry_for_production_order(self):
other_ste = [t[0] for t in frappe.db.get_values("Stock Entry", { other_ste = [t[0] for t in frappe.db.get_values("Stock Entry", {