[time logs] fixes
This commit is contained in:
parent
9524ee7ec6
commit
99d0941008
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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")
|
||||
|
@ -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",
|
||||
|
@ -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':
|
||||
|
@ -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", {
|
||||
|
Loading…
Reference in New Issue
Block a user