capacity planning fixes and cleanup

This commit is contained in:
Nabin Hait
2014-12-19 10:57:46 +05:30
parent 1fa7171bfa
commit 0f4350dd61
18 changed files with 464 additions and 424 deletions

View File

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

View File

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

View File

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