[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",
"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",

View File

@ -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",

View File

@ -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

View File

@ -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",

View File

@ -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")

View File

@ -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",

View File

@ -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':

View File

@ -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", {