mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-18 02:55:20 +00:00
capacity planning fixes and cleanup
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user