mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-17 18:45:20 +00:00
Actual operating cost, Actal operating time added to 'production order operations' - auto fetched based on 'time log' creation 'make time log' button appears only if 'production order' document is submitted server side validation added to check if 'Production Order' mentioned on 'Time Log' is in submit state test cases added to check all of the above
Time Log Bug Fixed Manufacturing seetings doctype added. prod order holiday list time calculation added
This commit is contained in:
committed by
Nabin Hait
parent
5ec7542519
commit
e84fa67f30
@@ -1,10 +1,16 @@
|
||||
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import unittest
|
||||
|
||||
from erpnext.projects.doctype.time_log.time_log import OverlapError
|
||||
from erpnext.projects.doctype.time_log.time_log import NotSubmittedError
|
||||
|
||||
from erpnext.manufacturing.doctype.workstation.workstation import WorkstationHolidayError
|
||||
from erpnext.manufacturing.doctype.workstation.workstation import WorkstationIsClosedError
|
||||
|
||||
from erpnext.projects.doctype.time_log_batch.test_time_log_batch import *
|
||||
|
||||
class TestTimeLog(unittest.TestCase):
|
||||
@@ -17,5 +23,59 @@ class TestTimeLog(unittest.TestCase):
|
||||
|
||||
frappe.db.sql("delete from `tabTime Log`")
|
||||
|
||||
def test_production_order_status(self):
|
||||
prod_order = make_prod_order(self)
|
||||
|
||||
prod_order.save()
|
||||
|
||||
time_log = frappe.get_doc({
|
||||
"doctype": "Time Log",
|
||||
"time_log_for": "Manufacturing",
|
||||
"production_order": prod_order.name,
|
||||
"qty": 1,
|
||||
"from_time": "2014-12-26 00:00:00",
|
||||
"to_time": "2014-12-26 00:00:00"
|
||||
})
|
||||
|
||||
self.assertRaises(NotSubmittedError, time_log.save)
|
||||
|
||||
def test_time_log_on_holiday(self):
|
||||
prod_order = make_prod_order(self)
|
||||
|
||||
prod_order.save()
|
||||
prod_order.submit()
|
||||
|
||||
time_log = frappe.get_doc({
|
||||
"doctype": "Time Log",
|
||||
"time_log_for": "Manufacturing",
|
||||
"production_order": prod_order.name,
|
||||
"qty": 1,
|
||||
"from_time": "2013-02-01 10:00:00",
|
||||
"to_time": "2013-02-01 20:00:00",
|
||||
"workstation": "_Test Workstation 1"
|
||||
})
|
||||
self.assertRaises(WorkstationHolidayError , time_log.save)
|
||||
|
||||
time_log.update({
|
||||
"from_time": "2013-02-02 09:00:00",
|
||||
"to_time": "2013-02-02 20:00:00"
|
||||
})
|
||||
self.assertRaises(WorkstationIsClosedError , time_log.save)
|
||||
|
||||
time_log.from_time= "2013-02-02 09:30:00"
|
||||
time_log.save()
|
||||
time_log.submit()
|
||||
time_log.cancel()
|
||||
|
||||
def make_prod_order(self):
|
||||
return frappe.get_doc({
|
||||
"doctype":"Production Order",
|
||||
"production_item": "_Test FG Item 2",
|
||||
"bom_no": "BOM/_Test FG Item 2/002",
|
||||
"qty": 1,
|
||||
"wip_warehouse": "_Test Warehouse - _TC",
|
||||
"fg_warehouse": "_Test Warehouse 1 - _TC"
|
||||
})
|
||||
|
||||
test_records = frappe.get_test_records('Time Log')
|
||||
test_ignore = ["Time Log Batch", "Sales Invoice"]
|
||||
|
||||
@@ -9,8 +9,9 @@ from frappe import _
|
||||
from frappe.utils import cstr, cint, comma_and
|
||||
|
||||
|
||||
|
||||
class OverlapError(frappe.ValidationError): pass
|
||||
class OverProductionError(frappe.ValidationError): pass
|
||||
class NotSubmittedError(frappe.ValidationError): pass
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
@@ -19,9 +20,11 @@ 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()
|
||||
@@ -47,6 +50,7 @@ class TimeLog(Document):
|
||||
self.status="Billed"
|
||||
|
||||
def validate_overlap(self):
|
||||
"""Checks if 'Time Log' entries overlap each other. """
|
||||
existing = frappe.db.sql_list("""select name from `tabTime Log` where owner=%s and
|
||||
(
|
||||
(from_time between %s and %s) or
|
||||
@@ -61,6 +65,10 @@ 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()
|
||||
@@ -69,6 +77,7 @@ class TimeLog(Document):
|
||||
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"))
|
||||
@@ -84,6 +93,7 @@ class TimeLog(Document):
|
||||
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()
|
||||
@@ -91,6 +101,7 @@ class TimeLog(Document):
|
||||
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)
|
||||
@@ -102,30 +113,67 @@ class TimeLog(Document):
|
||||
}
|
||||
|
||||
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)
|
||||
frappe.db.sql("""update `tabProduction Order Operation` set actual_start_time = %s, actual_end_time = %s,
|
||||
qty_completed = %s, status = %s where idx=%s and parent=%s and operation = %s """,
|
||||
(dates.start_date, dates.end_date, qty, status, d[0], self.production_order, d[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)
|
||||
|
||||
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))
|
||||
|
||||
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.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]
|
||||
@@ -136,6 +184,12 @@ def get_workstation(production_order, operation):
|
||||
|
||||
@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.
|
||||
"""
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
if not frappe.has_permission("Time Log"):
|
||||
frappe.msgprint(_("No Permission"), raise_exception=1)
|
||||
|
||||
@@ -9,17 +9,31 @@
|
||||
<i class="icon-money text-muted"></i>
|
||||
</span>
|
||||
{% } %}
|
||||
|
||||
{% if(doc.time_log_for == 'Manufacturing') { %}
|
||||
<span style="margin-right: 8px;"
|
||||
title="{%= __("Manufacturing") %}" class="filterable"
|
||||
data-filter="time_log_for,=,Manufacturing">
|
||||
<i class="icon-cogs text-muted"></i>
|
||||
</span>
|
||||
{% } %}
|
||||
|
||||
{% if(doc.activity_type) { %}
|
||||
<span class="label label-info filterable" style="margin-right: 8px;"
|
||||
data-filter="activity_type,=,{%= doc.activity_type %}">
|
||||
{%= doc.activity_type %}</span>
|
||||
<span style="margin-right: 8px;" class="text-muted">
|
||||
({%= doc.hours + " " + __("hours") %})
|
||||
</span>
|
||||
{% } %}
|
||||
|
||||
{% if(doc.project) { %}
|
||||
<span class="filterable" style="margin-right: 8px;"
|
||||
data-filter="project,=,{%= doc.project %}">
|
||||
{%= doc.project %}</span>
|
||||
{% } %}
|
||||
|
||||
<span style="margin-right: 8px;" class="text-muted">
|
||||
({%= doc.hours + " " + __("hours") %})
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
// render
|
||||
frappe.listview_settings['Time Log'] = {
|
||||
add_fields: ["status", "billable", "activity_type", "task", "project", "hours"],
|
||||
add_fields: ["status", "billable", "activity_type", "task", "project", "hours", "time_log_for"],
|
||||
selectable: true,
|
||||
onload: function(me) {
|
||||
me.appframe.add_primary_action(__("Make Time Log Batch"), function() {
|
||||
|
||||
Reference in New Issue
Block a user