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:
Neil Trini Lasrado 2014-11-27 10:49:07 +05:30 committed by Nabin Hait
parent 5ec7542519
commit e84fa67f30
22 changed files with 497 additions and 102 deletions

View File

@ -25,13 +25,18 @@ def get_data():
{ {
"type": "doctype", "type": "doctype",
"name": "Workstation", "name": "Workstation",
"description": _("Where manufacturing operations are carried out."), "description": _("Where manufacturing operations are carried."),
}, },
{ {
"type": "doctype", "type": "doctype",
"name": "Operation", "name": "Operation",
"description": _("Details of the operations carried out."), "description": _("Details of the operations carried out."),
}, },
{
"type": "doctype",
"name": "Manufacturing Settings",
"description": _("Global settings for all manufacturing processes."),
},
] ]
}, },

View File

@ -1,6 +1,7 @@
[ [
{ {
"doctype": "Holiday List", "doctype": "Holiday List",
"name": "_Test Holiday List 1",
"fiscal_year": "_Test Fiscal Year 2013", "fiscal_year": "_Test Fiscal Year 2013",
"holiday_list_details": [ "holiday_list_details": [
{ {

View File

@ -0,0 +1,90 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"creation": "2014-11-27 14:12:07.542534",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Master",
"fields": [
{
"allow_on_submit": 0,
"default": "30",
"description": "Maximum Overtime allowed against an workstation.\n( in mins )",
"fieldname": "max_overtime",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Maximum Overtime",
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"default": "No",
"fieldname": "allow_production_on_holidays",
"fieldtype": "Select",
"label": "Allow Production on Holidays",
"options": "Yes\nNo",
"permlevel": 0,
"precision": ""
},
{
"default": "30",
"description": "Delay in start time of production order operations if automatically make time logs is used.\n(in mins)",
"fieldname": "operations_start_delay",
"fieldtype": "Float",
"label": "Operations Start Delay",
"permlevel": 0,
"precision": ""
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "icon-wrench",
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"modified": "2014-12-01 15:33:00.905276",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Manufacturing Manager",
"set_user_permissions": 0,
"submit": 0,
"write": 1
}
],
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class ManufacturingSettings(Document):
pass

View File

@ -1,4 +1,4 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors and Contributors # Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import frappe import frappe

View File

@ -83,6 +83,15 @@ $.extend(cur_frm.cscript, {
frappe.set_route("Form", doclist[0].doctype, doclist[0].name); frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
} }
}); });
},
auto_time_log: function(doc){
frappe.call({
method:"erpnext.manufacturing.doctype.production_order.production_order.auto_make_time_log",
args: {
"production_order_id": doc.name
}
});
} }
}); });

View File

@ -207,6 +207,15 @@
"precision": "", "precision": "",
"read_only": 1 "read_only": 1
}, },
{
"allow_on_submit": 1,
"depends_on": "eval:doc.docstatus==1",
"fieldname": "auto_time_log",
"fieldtype": "Button",
"label": "Automatically Make Time logs",
"permlevel": 0,
"precision": ""
},
{ {
"fieldname": "more_info", "fieldname": "more_info",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@ -279,7 +288,7 @@
"idx": 1, "idx": 1,
"in_create": 0, "in_create": 0,
"is_submittable": 1, "is_submittable": 1,
"modified": "2014-11-24 11:13:09.639253", "modified": "2014-12-01 11:36:56.832268",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Production Order", "name": "Production Order",

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, json, time, datetime import frappe, json, time, datetime
from frappe.utils import flt, nowdate from frappe.utils import flt, nowdate, now, cint, cstr
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
@ -12,6 +12,10 @@ from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
class OverProductionError(frappe.ValidationError): pass class OverProductionError(frappe.ValidationError): pass
class StockOverProductionError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass
form_grid_templates = {
"production_order_operations": "templates/form_grid/production_order_grid.html"
}
class ProductionOrder(Document): class ProductionOrder(Document):
def validate(self): def validate(self):
if self.docstatus == 0: if self.docstatus == 0:
@ -146,6 +150,7 @@ class ProductionOrder(Document):
update_bin(args) update_bin(args)
def set_production_order_operations(self): def set_production_order_operations(self):
"""Sets operations table in 'Production Order'. """
self.set('production_order_operations', []) self.set('production_order_operations', [])
operations = frappe.db.sql("""select operation, opn_description, workstation, hour_rate, time_in_mins, operations = frappe.db.sql("""select operation, opn_description, workstation, hour_rate, time_in_mins,
operating_cost, fixed_cycle_cost from `tabBOM Operation` where parent = %s""", self.bom_no, as_dict=1) operating_cost, fixed_cycle_cost from `tabBOM Operation` where parent = %s""", self.bom_no, as_dict=1)
@ -155,8 +160,23 @@ class ProductionOrder(Document):
d.status = "Pending" d.status = "Pending"
d.qty_completed=0 d.qty_completed=0
self.auto_caluclate_production_dates()
def auto_caluclate_production_dates(self): def auto_caluclate_production_dates(self):
pass start_delay = cint(frappe.db.get_value("Manufacturing Settings", "None", "operations_start_delay")) * 60
time = datetime.datetime.now() + datetime.timedelta(seconds= start_delay)
for d in self.get('production_order_operations'):
holiday_list = frappe.db.get_value("Workstation", d.workstation, "holiday_list")
for d in frappe.db.sql("""select holiday_date from `tabHoliday` where parent = %s
order by holiday_date""", holiday_list, as_dict=1):
print "time date", time.date()
print "holiday ", d.holiday_date
if d.holiday_date == time.date():
print "time IN ", time
time = time + datetime.timedelta(seconds= 24*60*60)
d.planned_start_time = time.strftime('%Y-%m-%d %H:%M:%S')
time = time + datetime.timedelta(seconds= (cint(d.time_in_mins) * 60))
d.planned_end_time = time.strftime('%Y-%m-%d %H:%M:%S')
@frappe.whitelist() @frappe.whitelist()
def get_item_details(item): def get_item_details(item):
@ -220,7 +240,7 @@ def get_events(start, end, filters=None):
return data return data
@frappe.whitelist() @frappe.whitelist()
def make_time_log(name, operation, from_time=None, to_time=None, qty=None, project=None, workstation=None): def make_time_log(name, operation, from_time, to_time, qty=None, project=None, workstation=None):
time_log = frappe.new_doc("Time Log") time_log = frappe.new_doc("Time Log")
time_log.time_log_for = 'Manufacturing' time_log.time_log_for = 'Manufacturing'
time_log.from_time = from_time time_log.from_time = from_time
@ -233,3 +253,13 @@ def make_time_log(name, operation, from_time=None, to_time=None, qty=None, proje
if from_time and to_time : if from_time and to_time :
time_log.calculate_total_hours() time_log.calculate_total_hours()
return time_log return time_log
@frappe.whitelist()
def auto_make_time_log(production_order_id):
prod_order = frappe.get_doc("Production Order", production_order_id)
for d in prod_order.production_order_operations:
operation = cstr(d.idx) + ". " + d.operation
time_log = make_time_log(prod_order.name, operation, d.planned_start_time, d.planned_end_time,
prod_order.qty, prod_order.project_name, d.workstation)
time_log.save()
frappe.msgprint(_("Time Logs created."))

View File

@ -8,6 +8,7 @@ import frappe
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.manufacturing.doctype.production_order.production_order import make_stock_entry from erpnext.manufacturing.doctype.production_order.production_order import make_stock_entry
from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.doctype.stock_entry import test_stock_entry
from erpnext.projects.doctype.time_log.time_log import OverProductionError
class TestProductionOrder(unittest.TestCase): class TestProductionOrder(unittest.TestCase):
def test_planned_qty(self): def test_planned_qty(self):
@ -60,17 +61,20 @@ class TestProductionOrder(unittest.TestCase):
def test_make_time_log(self): def test_make_time_log(self):
prod_order = frappe.get_doc({ prod_order = frappe.get_doc({
"doctype":"Production Order", "doctype": "Production Order",
"production_item": "_Test FG Item 2", "production_item": "_Test FG Item 2",
"bom_no": "BOM/_Test FG Item 2/002", "bom_no": "BOM/_Test FG Item 2/002",
"qty": 1 "qty": 1,
"wip_warehouse": "_Test Warehouse - _TC",
"fg_warehouse": "_Test Warehouse 1 - _TC"
}) })
prod_order.set_production_order_operations() prod_order.set_production_order_operations()
prod_order.production_order_operations[0].update({ prod_order.production_order_operations[0].update({
"planned_start_time": "2014-11-25 00:00:00", "planned_start_time": "2014-11-25 00:00:00",
"planned_end_time": "2014-11-25 10:00:00" "planned_end_time": "2014-11-25 10:00:00",
"hour_rate": 10
}) })
prod_order.insert() prod_order.insert()
@ -81,6 +85,8 @@ class TestProductionOrder(unittest.TestCase):
from frappe.utils import cstr from frappe.utils import cstr
from frappe.utils import time_diff_in_hours from frappe.utils import time_diff_in_hours
prod_order.submit()
time_log = make_time_log( prod_order.name, cstr(d.idx) + ". " + d.operation, \ time_log = make_time_log( prod_order.name, cstr(d.idx) + ". " + d.operation, \
d.planned_start_time, d.planned_end_time, prod_order.qty - d.qty_completed) d.planned_start_time, d.planned_end_time, prod_order.qty - d.qty_completed)
@ -91,6 +97,14 @@ class TestProductionOrder(unittest.TestCase):
time_log.save() time_log.save()
time_log.submit() time_log.submit()
manufacturing_settings = frappe.get_doc({
"doctype": "Manufacturing Settings",
"maximum_overtime": 30,
"allow_production_on_holidays": "No"
})
manufacturing_settings.save()
prod_order.load_from_db() prod_order.load_from_db()
self.assertEqual(prod_order.production_order_operations[0].status, "Completed") self.assertEqual(prod_order.production_order_operations[0].status, "Completed")
self.assertEqual(prod_order.production_order_operations[0].qty_completed, prod_order.qty) self.assertEqual(prod_order.production_order_operations[0].qty_completed, prod_order.qty)
@ -98,11 +112,17 @@ class TestProductionOrder(unittest.TestCase):
self.assertEqual(prod_order.production_order_operations[0].actual_start_time, time_log.from_time) self.assertEqual(prod_order.production_order_operations[0].actual_start_time, time_log.from_time)
self.assertEqual(prod_order.production_order_operations[0].actual_end_time, time_log.to_time) self.assertEqual(prod_order.production_order_operations[0].actual_end_time, time_log.to_time)
self.assertEqual(prod_order.production_order_operations[0].actual_operation_time, 600)
self.assertEqual(prod_order.production_order_operations[0].actual_operating_cost, 6000)
time_log.cancel() time_log.cancel()
prod_order.load_from_db() prod_order.load_from_db()
self.assertEqual(prod_order.production_order_operations[0].status,"Pending") self.assertEqual(prod_order.production_order_operations[0].status, "Pending")
self.assertEqual(prod_order.production_order_operations[0].qty_completed,0) self.assertEqual(prod_order.production_order_operations[0].qty_completed, 0)
self.assertEqual(prod_order.production_order_operations[0].actual_operation_time, 0)
self.assertEqual(prod_order.production_order_operations[0].actual_operating_cost, 0)
time_log2 = frappe.copy_doc(time_log) time_log2 = frappe.copy_doc(time_log)
time_log2.update({ time_log2.update({
@ -111,6 +131,6 @@ class TestProductionOrder(unittest.TestCase):
"to_time": "2014-11-26 00:00:00", "to_time": "2014-11-26 00:00:00",
"docstatus": 0 "docstatus": 0
}) })
self.assertRaises(frappe.ValidationError, time_log2.save) self.assertRaises(OverProductionError, time_log2.save)
test_records = frappe.get_test_records('Production Order') test_records = frappe.get_test_records('Production Order')

View File

@ -45,7 +45,7 @@
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 1, "in_list_view": 0,
"label": "Operation Description", "label": "Operation Description",
"no_copy": 0, "no_copy": 0,
"oldfieldname": "opn_description", "oldfieldname": "opn_description",
@ -83,19 +83,22 @@
"default": "Pending", "default": "Pending",
"fieldname": "status", "fieldname": "status",
"fieldtype": "Select", "fieldtype": "Select",
"in_list_view": 1, "in_list_view": 0,
"label": "Status", "label": "Status",
"options": "Pending\nWork in Progress\nCompleted", "options": "Pending\nWork in Progress\nCompleted",
"permlevel": 0, "permlevel": 0,
"precision": "" "precision": "",
"read_only": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "qty_completed", "fieldname": "qty_completed",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1,
"label": "Qty Completed", "label": "Qty Completed",
"permlevel": 0, "permlevel": 0,
"precision": "" "precision": "",
"read_only": 1
}, },
{ {
"allow_on_submit": 0, "allow_on_submit": 0,
@ -121,9 +124,9 @@
"unique": 0 "unique": 0
}, },
{ {
"fieldname": "cost", "fieldname": "estimated_time_and_cost",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Cost", "label": "Estimated Time and Cost",
"permlevel": 0, "permlevel": 0,
"precision": "" "precision": ""
}, },
@ -149,57 +152,6 @@
"set_only_once": 0, "set_only_once": 0,
"unique": 0 "unique": 0
}, },
{
"allow_on_submit": 0,
"fieldname": "time_in_mins",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Operation Time (mins)",
"no_copy": 0,
"oldfieldname": "time_in_mins",
"oldfieldtype": "Currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break",
"permlevel": 0,
"precision": ""
},
{
"allow_on_submit": 0,
"description": "Hour rate * hours",
"fieldname": "operating_cost",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Operating Cost",
"no_copy": 0,
"oldfieldname": "operating_cost",
"oldfieldtype": "Currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "allow_on_submit": 0,
"fieldname": "fixed_cycle_cost", "fieldname": "fixed_cycle_cost",
@ -221,26 +173,98 @@
"unique": 0 "unique": 0
}, },
{ {
"fieldname": "section_break_9", "allow_on_submit": 0,
"fieldtype": "Section Break", "description": "Hour Rate * Operating Time",
"label": "Time", "fieldname": "operating_cost",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Operating Cost",
"no_copy": 0,
"oldfieldname": "operating_cost",
"oldfieldtype": "Currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break",
"permlevel": 0, "permlevel": 0,
"precision": "" "precision": ""
}, },
{
"allow_on_submit": 0,
"description": "in Minutes",
"fieldname": "time_in_mins",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Operation Time",
"no_copy": 0,
"oldfieldname": "time_in_mins",
"oldfieldtype": "Currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"fieldname": "planned_start_time", "fieldname": "planned_start_time",
"fieldtype": "Datetime", "fieldtype": "Datetime",
"label": "Planned Start Time", "label": "Planned Start Time",
"permlevel": 0, "permlevel": 0,
"precision": "" "precision": "",
"reqd": 1
}, },
{ {
"fieldname": "planned_end_time", "fieldname": "planned_end_time",
"fieldtype": "Datetime", "fieldtype": "Datetime",
"label": "Planned End Time", "label": "Planned End Time",
"permlevel": 0, "permlevel": 0,
"precision": "",
"reqd": 1
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"label": "Actual Time and Cost",
"permlevel": 0,
"precision": "" "precision": ""
}, },
{
"description": "in Minutes\nUpdated via 'Time Log'",
"fieldname": "actual_operation_time",
"fieldtype": "Float",
"label": "Actual Operation Time",
"permlevel": 0,
"precision": "",
"read_only": 1
},
{
"description": "Hour Rate * Actual Operating Cost",
"fieldname": "actual_operating_cost",
"fieldtype": "Float",
"label": "Actual Operating Cost",
"permlevel": 0,
"precision": "",
"read_only": 1
},
{ {
"fieldname": "column_break_11", "fieldname": "column_break_11",
"fieldtype": "Column Break", "fieldtype": "Column Break",
@ -252,17 +276,21 @@
"fieldtype": "Datetime", "fieldtype": "Datetime",
"label": "Actual Start Time", "label": "Actual Start Time",
"permlevel": 0, "permlevel": 0,
"precision": "" "precision": "",
"read_only": 1
}, },
{ {
"description": "Updated via 'Time Log'",
"fieldname": "actual_end_time", "fieldname": "actual_end_time",
"fieldtype": "Datetime", "fieldtype": "Datetime",
"label": "Actual End Time", "label": "Actual End Time",
"permlevel": 0, "permlevel": 0,
"precision": "" "precision": "",
"read_only": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"depends_on": "eval:doc.docstatus==1",
"fieldname": "make_time_log", "fieldname": "make_time_log",
"fieldtype": "Button", "fieldtype": "Button",
"label": "Make Time Log", "label": "Make Time Log",
@ -277,7 +305,7 @@
"is_submittable": 0, "is_submittable": 0,
"issingle": 0, "issingle": 0,
"istable": 1, "istable": 1,
"modified": "2014-11-25 13:34:10.697445", "modified": "2014-12-01 14:06:40.068700",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Production Order Operation", "name": "Production Order Operation",

View File

@ -6,6 +6,7 @@
"warehouse": "_Test warehouse - _TC", "warehouse": "_Test warehouse - _TC",
"fixed_cycle_cost": 1000, "fixed_cycle_cost": 1000,
"hour_rate":100, "hour_rate":100,
"holiday_list": "_Test Holiday List",
"workstation_operation_hours": [ "workstation_operation_hours": [
{ {
"start_time": "10:00:00", "start_time": "10:00:00",

View File

@ -5,7 +5,15 @@
//--------- ONLOAD ------------- //--------- ONLOAD -------------
cur_frm.cscript.onload = function(doc, cdt, cdn) { cur_frm.cscript.onload = function(doc, cdt, cdn) {
frappe.call({
type:"GET",
method:"erpnext.manufacturing.doctype.workstation.workstation.get_default_holiday_list",
callback: function(r) {
if(!r.exe && r.message){
cur_frm.set_value("holiday_list", r.message);
}
}
})
} }
cur_frm.cscript.refresh = function(doc, cdt, cdn) { cur_frm.cscript.refresh = function(doc, cdt, cdn) {

View File

@ -150,6 +150,7 @@
"precision": "" "precision": ""
}, },
{ {
"default": "",
"fieldname": "holiday_list", "fieldname": "holiday_list",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Holiday List", "label": "Holiday List",
@ -160,7 +161,7 @@
], ],
"icon": "icon-wrench", "icon": "icon-wrench",
"idx": 1, "idx": 1,
"modified": "2014-11-07 11:39:37.720913", "modified": "2014-11-27 19:04:58.125107",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Workstation", "name": "Workstation",

View File

@ -5,10 +5,13 @@ from __future__ import unicode_literals
import frappe import frappe
import datetime import datetime
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import flt, cint
from frappe.model.document import Document from frappe.model.document import Document
class WorkstationHolidayError(frappe.ValidationError): pass
class WorkstationIsClosedError(frappe.ValidationError): pass
class Workstation(Document): class Workstation(Document):
def update_bom_operation(self): def update_bom_operation(self):
bom_list = frappe.db.sql("""select DISTINCT parent from `tabBOM Operation` bom_list = frappe.db.sql("""select DISTINCT parent from `tabBOM Operation`
@ -26,18 +29,25 @@ class Workstation(Document):
def check_if_within_operating_hours(self, from_time, to_time): def check_if_within_operating_hours(self, from_time, to_time):
if self.check_workstation_for_operation_time(from_time, to_time): if self.check_workstation_for_operation_time(from_time, to_time):
frappe.msgprint(_("Warning: Time Log timings outside workstation Operating Hours !")) frappe.throw(_("Time Log timings outside workstation Operating Hours !"), WorkstationIsClosedError)
if frappe.db.get_value("Manufacturing Settings", "None", "allow_production_on_holidays") == "No":
msg = self.check_workstation_for_holiday(from_time, to_time) msg = self.check_workstation_for_holiday(from_time, to_time)
if msg != None: if msg != None:
frappe.msgprint(msg) frappe.throw(msg, WorkstationHolidayError)
def check_workstation_for_operation_time(self, from_time, to_time): def check_workstation_for_operation_time(self, from_time, to_time):
start_time = datetime.datetime.strptime(from_time,'%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S') start_time = datetime.datetime.strptime(from_time,'%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S')
end_time = datetime.datetime.strptime(to_time,'%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S') end_time = datetime.datetime.strptime(to_time,'%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S')
max_time_diff = frappe.db.get_value("Manufacturing Settings", "None", "max_overtime")
if frappe.db.sql("""select start_time, end_time from `tabWorkstation Operation Hours` for d in frappe.db.sql("""select time_to_sec(timediff( start_time, %s))/60 as st_diff ,
where parent = %s and (%s <start_time or %s > end_time )""",(self.workstation_name, start_time, end_time), as_dict=1): time_to_sec(timediff( %s, end_time))/60 as et_diff from `tabWorkstation Operation Hours`
where parent = %s and (%s <start_time or %s > end_time )""",
(start_time, end_time, self.workstation_name, start_time, end_time), as_dict=1):
if cint(d.st_diff) > cint(max_time_diff):
return 1
if cint(d.et_diff) > cint(max_time_diff):
return 1 return 1
def check_workstation_for_holiday(self, from_time, to_time): def check_workstation_for_holiday(self, from_time, to_time):
@ -50,8 +60,11 @@ class Workstation(Document):
%s and %s """,(holiday_list, start_date, end_date), as_dict=1): %s and %s """,(holiday_list, start_date, end_date), as_dict=1):
flag = 1 flag = 1
msg = msg + "\n" + d.holiday_date msg = msg + "\n" + d.holiday_date
if flag ==1: if flag ==1:
return msg return msg
else: else:
return None return None
@frappe.whitelist()
def get_default_holiday_list():
return frappe.db.get_value("Company", frappe.defaults.get_user_default("company"), "default_holiday_list")

View File

@ -1,10 +1,16 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors # Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from erpnext.projects.doctype.time_log.time_log import OverlapError 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 * from erpnext.projects.doctype.time_log_batch.test_time_log_batch import *
class TestTimeLog(unittest.TestCase): class TestTimeLog(unittest.TestCase):
@ -17,5 +23,59 @@ class TestTimeLog(unittest.TestCase):
frappe.db.sql("delete from `tabTime Log`") 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_records = frappe.get_test_records('Time Log')
test_ignore = ["Time Log Batch", "Sales Invoice"] test_ignore = ["Time Log Batch", "Sales Invoice"]

View File

@ -9,8 +9,9 @@ from frappe import _
from frappe.utils import cstr, cint, comma_and from frappe.utils import cstr, cint, comma_and
class OverlapError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass
class OverProductionError(frappe.ValidationError): pass
class NotSubmittedError(frappe.ValidationError): pass
from frappe.model.document import Document from frappe.model.document import Document
@ -19,9 +20,11 @@ class TimeLog(Document):
def validate(self): def validate(self):
self.set_status() self.set_status()
self.validate_overlap() self.validate_overlap()
self.validate_timings()
self.calculate_total_hours() self.calculate_total_hours()
self.check_workstation_timings() self.check_workstation_timings()
self.validate_qty() self.validate_qty()
self.validate_production_order()
def on_submit(self): def on_submit(self):
self.update_production_order() self.update_production_order()
@ -47,6 +50,7 @@ class TimeLog(Document):
self.status="Billed" self.status="Billed"
def validate_overlap(self): 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 existing = frappe.db.sql_list("""select name from `tabTime Log` where owner=%s and
( (
(from_time between %s and %s) or (from_time between %s and %s) or
@ -62,6 +66,10 @@ class TimeLog(Document):
if existing: if existing:
frappe.throw(_("This Time Log conflicts with {0}").format(comma_and(existing)), OverlapError) 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): def before_cancel(self):
self.set_status() self.set_status()
@ -69,6 +77,7 @@ class TimeLog(Document):
self.set_status() self.set_status()
def update_production_order(self): def update_production_order(self):
"""Updates `start_date`, `end_date` for operation in Production Order."""
if self.time_log_for=="Manufacturing" and self.operation: if self.time_log_for=="Manufacturing" and self.operation:
d = self.get_qty_and_status() d = self.get_qty_and_status()
required_qty = cint(frappe.db.get_value("Production Order" , self.production_order, "qty")) 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']) self.production_order_update(dates, d.get('qty'), d['status'])
def update_production_order_on_cancel(self): 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: if self.time_log_for=="Manufacturing" and self.operation:
d = frappe._dict() d = frappe._dict()
d = self.get_qty_and_status() d = self.get_qty_and_status()
@ -91,6 +101,7 @@ class TimeLog(Document):
self.production_order_update(dates, d.get('qty'), d.get('status')) self.production_order_update(dates, d.get('qty'), d.get('status'))
def get_qty_and_status(self): def get_qty_and_status(self):
"""Returns quantity and status of Operation in 'Time Log'. """
status = "Work in Progress" status = "Work in Progress"
qty = cint(frappe.db.sql("""select sum(qty) as qty from `tabTime Log` where production_order = %s 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) 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): 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` 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""", where production_order = %s and operation = %s and docstatus=1""",
(self.production_order, self.operation), as_dict=1)[0] (self.production_order, self.operation), as_dict=1)[0]
def production_order_update(self, dates, qty, status): 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) d = self.operation.split('. ',1)
frappe.db.sql("""update `tabProduction Order Operation` set actual_start_time = %s, actual_end_time = %s, actual_op_time = self.get_actual_op_time().time_diff
qty_completed = %s, status = %s where idx=%s and parent=%s and operation = %s """, if actual_op_time == None:
(dates.start_date, dates.end_date, qty, status, d[0], self.production_order, d[1] )) 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): def check_workstation_timings(self):
"""Checks if **Time Log** is between operating hours of the **Workstation**."""
if self.workstation: if self.workstation:
frappe.get_doc("Workstation", self.workstation).check_if_within_operating_hours(self.from_time, self.to_time) frappe.get_doc("Workstation", self.workstation).check_if_within_operating_hours(self.from_time, self.to_time)
def validate_qty(self): def validate_qty(self):
"""Throws `OverProductionError` if quantity surpasses **Production Order** quantity."""
if self.qty == None: if self.qty == None:
self.qty=0 self.qty=0
required_qty = cint(frappe.db.get_value("Production Order" , self.production_order, "qty")) required_qty = cint(frappe.db.get_value("Production Order" , self.production_order, "qty"))
completed_qty = self.get_qty_and_status().get('qty') completed_qty = self.get_qty_and_status().get('qty')
if (completed_qty + cint(self.qty)) > required_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() @frappe.whitelist()
def get_workstation(production_order, operation): 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: if operation:
d = operation.split('. ',1) d = operation.split('. ',1)
idx = d[0] idx = d[0]
@ -136,6 +184,12 @@ def get_workstation(production_order, operation):
@frappe.whitelist() @frappe.whitelist()
def get_events(start, end, filters=None): 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 from frappe.desk.reportview import build_match_conditions
if not frappe.has_permission("Time Log"): if not frappe.has_permission("Time Log"):
frappe.msgprint(_("No Permission"), raise_exception=1) frappe.msgprint(_("No Permission"), raise_exception=1)

View File

@ -9,17 +9,31 @@
<i class="icon-money text-muted"></i> <i class="icon-money text-muted"></i>
</span> </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;" <span class="label label-info filterable" style="margin-right: 8px;"
data-filter="activity_type,=,{%= doc.activity_type %}"> data-filter="activity_type,=,{%= doc.activity_type %}">
{%= doc.activity_type %}</span> {%= doc.activity_type %}</span>
<span style="margin-right: 8px;" class="text-muted"> {% } %}
({%= doc.hours + " " + __("hours") %})
</span>
{% if(doc.project) { %} {% if(doc.project) { %}
<span class="filterable" style="margin-right: 8px;" <span class="filterable" style="margin-right: 8px;"
data-filter="project,=,{%= doc.project %}"> data-filter="project,=,{%= doc.project %}">
{%= doc.project %}</span> {%= doc.project %}</span>
{% } %} {% } %}
<span style="margin-right: 8px;" class="text-muted">
({%= doc.hours + " " + __("hours") %})
</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,7 +3,7 @@
// render // render
frappe.listview_settings['Time Log'] = { 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, selectable: true,
onload: function(me) { onload: function(me) {
me.appframe.add_primary_action(__("Make Time Log Batch"), function() { me.appframe.add_primary_action(__("Make Time Log Batch"), function() {

View File

@ -160,6 +160,14 @@
"options": "Account", "options": "Account",
"permlevel": 0 "permlevel": 0
}, },
{
"fieldname": "default_holiday_list",
"fieldtype": "Link",
"label": "Default Holiday List",
"options": "Holiday List",
"permlevel": 0,
"precision": ""
},
{ {
"fieldname": "column_break0", "fieldname": "column_break0",
"fieldtype": "Column Break", "fieldtype": "Column Break",
@ -356,7 +364,7 @@
], ],
"icon": "icon-building", "icon": "icon-building",
"idx": 1, "idx": 1,
"modified": "2014-08-29 15:50:18.539228", "modified": "2014-11-27 18:15:48.909416",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Company", "name": "Company",

View File

@ -0,0 +1,34 @@
{% var visible_columns = row.get_visible_columns(["operation",
"opn_description", "status", "qty_completed", "workstation"]);
%}
{% if(!doc) { %}
<div class="row">
<div class="col-sm-7">{%= __("Operation") %}</div>
<div class="col-sm-2 text-right">{%= __("Workstation") %}</div>
<div class="col-sm-3 text-right">{%= __("Completed Qty") %}</div>
</div>
{% } else { %}
<div class="row">
<div class="col-sm-7">
<strong>{%= doc.operation %}</strong>
<span class="label label-primary">
{%= doc.status %}
</span>
{% include "templates/form_grid/includes/visible_cols.html" %}
<div>
{%= doc.get_formatted("opn_description") %}
</div>
</div>
<!-- workstation -->
<div class="col-sm-2 text-right">
{%= doc.get_formatted("workstation") %}
</div>
<!-- qty -->
<div class="col-sm-3 text-right">
{%= doc.get_formatted("qty_completed") %}
</div>
</div>
{% } %}

View File

@ -40,7 +40,8 @@
<div class="col-sm-2 text-right"> <div class="col-sm-2 text-right">
{%= doc.get_formatted("amount") %} {%= doc.get_formatted("amount") %}
<div class="small text-muted"> <div class="small text-muted">
{%= doc.get_formatted("incoming_rate") %}</div> {%= doc.get_formatted("incoming_rate") %}
</div>
</div> </div>
</div> </div>
{% } %} {% } %}