completed Time Log / Time Log Batch

This commit is contained in:
Rushabh Mehta 2013-03-01 18:24:52 +05:30
parent b21eb9ac09
commit fee642d547
27 changed files with 402 additions and 42 deletions

View File

@ -75,6 +75,7 @@ class DocType(SellingController):
self.set_aging_date()
self.set_against_income_account()
self.validate_c_form()
self.validate_time_logs_are_submitted()
self.validate_recurring_invoice()
def on_submit(self):
@ -104,7 +105,7 @@ class DocType(SellingController):
self.update_against_document_in_jv()
self.update_c_form()
self.update_time_log_batch(self.doc.name)
self.convert_to_recurring()
@ -122,12 +123,28 @@ class DocType(SellingController):
self.check_next_docstatus()
sales_com_obj.update_prevdoc_detail(0, self)
self.update_time_log_batch(None)
self.make_gl_entries(is_cancel=1)
def on_update_after_submit(self):
self.validate_recurring_invoice()
self.convert_to_recurring()
def update_time_log_batch(self, sales_invoice):
for d in self.doclist.get({"doctype":"Sales Invoice Item"}):
if d.time_log_batch:
tlb = webnotes.bean("Time Log Batch", d.time_log_batch)
tlb.doc.sales_invoice = sales_invoice
tlb.update_after_submit()
def validate_time_logs_are_submitted(self):
for d in self.doclist.get({"doctype":"Sales Invoice Item"}):
if d.time_log_batch:
status = webnotes.conn.get_value("Time Log Batch", d.time_log_batch, "status")
if status!="Submitted":
webnotes.msgprint(_("Time Log Batch status must be 'Submitted'") + ":" + d.time_log_batch,
raise_exception=True)
def set_pos_fields(self):
"""Set retail related fields from pos settings"""
pos = webnotes.conn.sql("select * from `tabPOS Setting` where ifnull(user,'') = '%s' and company = '%s'" % (session['user'], self.doc.company), as_dict=1)

View File

@ -0,0 +1,16 @@
wn.model.map_info["Sales Invoice"] = {
"Time Log Batch": {
table_map: {
"Sales Invoice Item": "Time Log Batch",
},
field_map: {
"Sales Invoice Item": {
"basic_rate": "rate",
"time_log_batch": "name",
"qty": "total_hours",
"stock_uom": "=Hour",
"description": "=via Time Logs"
}
},
}
}

View File

@ -29,6 +29,31 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEquals(webnotes.conn.get_value("Sales Invoice", w.doc.name, "outstanding_amount"),
561.8)
def test_time_log_batch(self):
tlb = webnotes.bean("Time Log Batch", "_T-Time Log Batch-00001")
tlb.submit()
w = webnotes.bean(webnotes.copy_doclist(test_records[0]))
w.doclist[1].time_log_batch = "_T-Time Log Batch-00001"
w.insert()
w.submit()
self.assertEquals(webnotes.conn.get_value("Time Log Batch", "_T-Time Log Batch-00001", "status"),
"Billed")
self.assertEquals(webnotes.conn.get_value("Time Log", "_T-Time Log-00001", "status"),
"Billed")
w.cancel()
self.assertEquals(webnotes.conn.get_value("Time Log Batch", "_T-Time Log Batch-00001", "status"),
"Submitted")
self.assertEquals(webnotes.conn.get_value("Time Log", "_T-Time Log-00001", "status"),
"Batched for Billing")
test_dependencies = ["Journal Voucher"]
test_records = [[

View File

@ -1,8 +1,8 @@
[
{
"creation": "2013-01-10 16:34:09",
"creation": "2013-01-29 19:25:49",
"docstatus": 0,
"modified": "2013-01-29 16:27:51",
"modified": "2013-03-01 13:41:51",
"modified_by": "Administrator",
"owner": "Administrator"
},
@ -320,6 +320,13 @@
"read_only": 1,
"search_index": 1
},
{
"doctype": "DocField",
"fieldname": "time_log_batch",
"fieldtype": "Link",
"label": "Time Log Batch",
"options": "Time Log Batch"
},
{
"doctype": "DocField",
"fieldname": "item_tax_rate",

View File

@ -22,6 +22,7 @@ from webnotes.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_
from webnotes import msgprint
class LeaveDayBlockedError(Exception): pass
class OverlapError(Exception): pass
from webnotes.model.controller import DocListController
class DocType(DocListController):
@ -129,17 +130,22 @@ class DocType(DocListController):
(self.doc.leave_type,), raise_exception=1)
def validate_leave_overlap(self):
if not self.doc.name:
self.doc.name = "New Leave Application"
for d in webnotes.conn.sql("""select name, leave_type, posting_date,
from_date, to_date
from `tabLeave Application`
where
(from_date <= %(to_date)s and to_date >= %(from_date)s)
and employee = %(employee)s
employee = %(employee)s
and docstatus < 2
and status in ("Open", "Approved")
and (from_date between %(from_date)s and %(to_date)s
or to_date between %(from_date)s and %(to_date)s
or %(from_date)s between from_date and to_date)
and name != %(name)s""", self.doc.fields, as_dict = 1):
msgprint("Employee : %s has already applied for %s between %s and %s on %s. Please refer Leave Application : <a href=\"#Form/Leave Application/%s\">%s</a>" % (self.doc.employee, cstr(d['leave_type']), formatdate(d['from_date']), formatdate(d['to_date']), formatdate(d['posting_date']), d['name'], d['name']), raise_exception = 1)
msgprint("Employee : %s has already applied for %s between %s and %s on %s. Please refer Leave Application : <a href=\"#Form/Leave Application/%s\">%s</a>" % (self.doc.employee, cstr(d['leave_type']), formatdate(d['from_date']), formatdate(d['to_date']), formatdate(d['posting_date']), d['name'], d['name']), raise_exception = OverlapError)
def validate_max_days(self):
max_days = webnotes.conn.sql("select max_days_allowed from `tabLeave Type` where name = '%s'" %(self.doc.leave_type))

View File

@ -1,7 +1,7 @@
import webnotes
import unittest
from hr.doctype.leave_application.leave_application import LeaveDayBlockedError
from hr.doctype.leave_application.leave_application import LeaveDayBlockedError, OverlapError
class TestLeaveApplication(unittest.TestCase):
def get_application(self, doclist):
@ -24,12 +24,21 @@ class TestLeaveApplication(unittest.TestCase):
from webnotes.profile import add_role
add_role("test1@example.com", "HR User")
# clear other applications
webnotes.conn.sql("delete from `tabLeave Application`")
application = self.get_application(test_records[1])
self.assertTrue(application.insert())
def test_overlap(self):
application = self.get_application(test_records[1])
self.assertRaises(OverlapError, application.insert)
def test_global_block_list(self):
application = self.get_application(test_records[3])
application.doc.leave_approver = "test@example.com"
webnotes.conn.set_value("Leave Block List", "_Test Leave Block List",
"applies_to_all_departments", 1)
webnotes.conn.set_value("Employee", "_T-Employee-0002", "department",

View File

@ -200,4 +200,5 @@ patch_list = [
'execute:webnotes.reload_doc("accounts", "Print Format", "Sales Invoice Modern") # 2013-02-26',
'execute:webnotes.reload_doc("accounts", "Print Format", "Sales Invoice Spartan") # 2013-02-26',
"execute:(not webnotes.conn.exists('Role', 'Projects Manager')) and webnotes.doc({'doctype':'Role', 'role_name':'Projects Manager'}).insert()",
"execute:(not webnotes.conn.exists('UOM', 'Hour')) and webnotes.doc({'uom_name': 'Unit', 'doctype': 'UOM', 'name': 'Hour'}).insert()",
]

View File

@ -0,0 +1,5 @@
test_records = [
[{"activity_type":"_Test Activity Type"}],
[{"activity_type":"_Test Activity Type 1"}],
[{"activity_type":"_Test Activity Type 2"}]
]

View File

@ -0,0 +1,8 @@
test_records = [[{
"project_name": "_Test Project",
"status": "Open"
}],
[{
"project_name": "_Test Project 1",
"status": "Open"
}]]

View File

@ -0,0 +1,7 @@
test_records = [
[{"subject": "_Test Task", "project":"_Test Project", "status":"Open"}],
[{"subject": "_Test Task 1", "status":"Open"}],
[{"subject": "_Test Task 2", "status":"Open"}]
]
test_ignore = ["Customer"]

View File

@ -0,0 +1,19 @@
import webnotes
import unittest
from projects.doctype.time_log.time_log import OverlapError
class TestTimeLog(unittest.TestCase):
def test_duplication(self):
ts = webnotes.bean(webnotes.copy_doclist(test_records[0]))
self.assertRaises(OverlapError, ts.insert)
test_records = [[{
"from_time": "2013-01-01 10:00:00",
"to_time": "2013-01-01 11:00:00",
"activity_type": "_Test Activity Type",
"note": "_Test Note",
"docstatus": 1
}]]
test_ignore = ["Sales Invoice", "Time Log Batch"]

View File

@ -0,0 +1,5 @@
$.extend(cur_frm.cscript, {
refresh: function(doc) {
}
});

View File

@ -6,6 +6,8 @@ from webnotes import _
from webnotes.widgets.reportview import build_match_conditions
class OverlapError(webnotes.ValidationError): pass
class DocType:
def __init__(self, d, dl):
self.doc, self.doclist = d, dl
@ -13,27 +15,45 @@ class DocType:
def validate(self):
self.set_status()
self.validate_overlap()
self.calculate_total_hours()
def calculate_total_hours(self):
from webnotes.utils import time_diff_in_hours
self.doc.hours = time_diff_in_hours(self.doc.to_time, self.doc.from_time)
def set_status(self):
if self.doc.docstatus==0:
self.doc.status = "Draft"
elif self.doc.docstatus==1:
self.doc.status = "Submitted"
elif self.doc.docstatus==2:
self.doc.status = "Cancelled"
self.doc.status = {
"0": "Draft",
"1": "Submitted",
"2": "Cancelled"
}[str(self.doc.docstatus or 0)]
# billed will be set directly
if self.doc.time_log_batch:
self.doc.status="Batched for Billing"
if self.doc.sales_invoice:
self.doc.status="Billed"
def validate_overlap(self):
existing = webnotes.conn.sql_list("""select name from `tabTime Log` where owner=%s and
((from_time between %s and %s) or (to_time between %s and %s)) and name!=%s""",
(
(from_time between %s and %s) or
(to_time between %s and %s) or
(%s between from_time and to_time))
and name!=%s
and docstatus < 2""",
(self.doc.owner, self.doc.from_time, self.doc.to_time, self.doc.from_time,
self.doc.to_time, self.doc.name))
self.doc.to_time, self.doc.from_time, self.doc.name or "No Name"))
if existing:
webnotes.msgprint(_("This Time Log conflicts with") + ":" + ', '.join(existing),
raise_exception=True)
raise_exception=OverlapError)
def before_cancel(self):
self.set_status()
def before_update_after_submit(self):
self.set_status()
@webnotes.whitelist()
def get_events(start, end):

View File

@ -2,13 +2,13 @@
{
"creation": "2013-02-26 14:58:28",
"docstatus": 0,
"modified": "2013-02-28 18:41:40",
"modified": "2013-03-01 17:48:09",
"modified_by": "Administrator",
"owner": "Administrator"
},
{
"allow_attach": 1,
"autoname": "TL-.######",
"autoname": "naming_series:",
"description": "Log of Activities performed by users against Tasks that can be used for tracking time, billing.",
"doctype": "DocType",
"document_type": "Master",
@ -40,14 +40,12 @@
},
{
"doctype": "DocField",
"fieldname": "status",
"fieldname": "naming_series",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Draft\nSubmitted\nBatched for Billing\nBilled\nCancelled",
"label": "Naming Series",
"options": "TL-",
"permlevel": 0,
"read_only": 1,
"reqd": 0
"reqd": 1
},
{
"doctype": "DocField",
@ -67,12 +65,31 @@
"permlevel": 0,
"reqd": 1
},
{
"doctype": "DocField",
"fieldname": "hours",
"fieldtype": "Float",
"label": "Hours",
"permlevel": 0,
"read_only": 1
},
{
"doctype": "DocField",
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"permlevel": 0
},
{
"doctype": "DocField",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Draft\nSubmitted\nBatched for Billing\nBilled\nCancelled",
"permlevel": 0,
"read_only": 1,
"reqd": 0
},
{
"doctype": "DocField",
"fieldname": "activity_type",
@ -127,6 +144,26 @@
"options": "Project",
"permlevel": 0
},
{
"description": "Will be updated when batched.",
"doctype": "DocField",
"fieldname": "time_log_batch",
"fieldtype": "Link",
"label": "Time Log Batch",
"options": "Time Log Batch",
"permlevel": 0,
"read_only": 1
},
{
"description": "Will be updated when billed.",
"doctype": "DocField",
"fieldname": "sales_invoice",
"fieldtype": "Link",
"label": "Sales Invoice",
"options": "Sales Invoice",
"permlevel": 0,
"read_only": 1
},
{
"doctype": "DocField",
"fieldname": "file_list",
@ -143,7 +180,7 @@
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Sales Invoice",
"options": "Time Log",
"permlevel": 1,
"print_hide": 1
},

View File

@ -1,5 +1,6 @@
// render
wn.listview_settings['Time Log'] = {
add_fields: ["`tabTime Log`.`status`", "`tabTime Log`.`billable`", "`tabTime Log`.`activity_type`"],
selectable: true,
onload: function(me) {
me.appframe.add_button(wn._("Make Time Log Batch"), function() {
@ -16,12 +17,28 @@ wn.listview_settings['Time Log'] = {
msgprint(wn._("Time Log is not billable") + ": " + d.name);
return;
}
if(d.sales_invoice) {
msgprint(wn._("Time Log has been Invoiced") + ": " + d.name);
if(d.status!="Submitted") {
msgprint(wn._("Time Log Status must be Submitted."));
}
}
//
// make batch
wn.model.with_doctype("Time Log Batch", function() {
var tlb = wn.model.get_new_doc("Time Log Batch");
$.each(selected, function(i, d) {
var detail = wn.model.get_new_doc("Time Log Batch Detail");
$.extend(detail, {
"parenttype": "Time Log Batch",
"parentfield": "time_log_batch_details",
"parent": tlb.name,
"time_log": d.name,
"activity_type": d.activity_type,
"created_by": d.owner,
"idx": i+1
});
})
wn.set_route("Form", "Time Log Batch", tlb.name);
})
}, "icon-file-alt");
}

View File

@ -0,0 +1,20 @@
import webnotes, unittest
class TimeLogBatchTest(unittest.TestCase):
def test_time_log_status(self):
self.assertEquals(webnotes.conn.get_value("Time Log", "_T-Time Log-00001", "status"), "Submitted")
tlb = webnotes.bean("Time Log Batch", "_T-Time Log Batch-00001")
tlb.submit()
self.assertEquals(webnotes.conn.get_value("Time Log", "_T-Time Log-00001", "status"), "Batched for Billing")
tlb.cancel()
self.assertEquals(webnotes.conn.get_value("Time Log", "_T-Time Log-00001", "status"), "Submitted")
test_records = [[
{"rate": "500"},
{
"doctype": "Time Log Batch Detail",
"parenttype": "Time Log Batch",
"parentfield": "time_log_batch_details",
"time_log": "_T-Time Log-00001",
}
]]

View File

@ -1,5 +1,6 @@
cur_frm.add_fetch("time_log", "activity_type", "activity_type");
cur_frm.add_fetch("time_log", "owner", "created_by");
cur_frm.add_fetch("time_log", "hours", "hours");
cur_frm.set_query("time_log", "time_log_batch_details", function(doc) {
return {
@ -12,7 +13,24 @@ cur_frm.set_query("time_log", "time_log_batch_details", function(doc) {
});
$.extend(cur_frm.cscript, {
refresh: function() {
refresh: function(doc) {
cur_frm.set_intro({
"Draft": wn._("Select Time Logs and Submit to create a new Sales Invoice."),
"Submitted": wn._("Click on 'Make Sales Invoice' button to create a new Sales Invoice."),
"Billed": wn._("This Time Log Batch has been billed."),
"Cancelled": wn._("This Time Log Batch has been cancelled.")
}[doc.status]);
if(doc.status=="Submitted") {
cur_frm.add_custom_button("Make Sales Invoice", function() { cur_frm.cscript.make_invoice() },
"icon-file-alt");
}
})
},
make_invoice: function() {
var doc = cur_frm.doc;
wn.model.map({
source: wn.model.get_doclist(doc.doctype, doc.name),
target: "Sales Invoice"
});
}
});

View File

@ -2,10 +2,59 @@
from __future__ import unicode_literals
import webnotes
from webnotes import _
class DocType:
def __init__(self, d, dl):
self.doc, self.doclist = d, dl
def validate(self):
self.set_status()
self.doc.total_hours = 0.0
for d in self.doclist.get({"doctype":"Time Log Batch Detail"}):
tl = webnotes.doc("Time Log", d.time_log)
self.update_time_log_values(d, tl)
self.validate_time_log_is_submitted(tl)
self.doc.total_hours += float(tl.hours or 0.0)
def update_time_log_values(self, d, tl):
d.fields.update({
"hours": tl.hours,
"activity_type": tl.activity_type,
"created_by": tl.owner
})
def validate_time_log_is_submitted(self, tl):
if tl.status != "Submitted":
webnotes.msgprint(_("Time Log must have status 'Submitted'") + \
" :" + tl.name + " (" + _(tl.status) + ")", raise_exception=True)
def set_status(self):
self.doc.status = {
"0": "Draft",
"1": "Submitted",
"2": "Cancelled"
}[str(self.doc.docstatus or 0)]
if self.doc.sales_invoice:
self.doc.status = "Billed"
def on_submit(self):
# update time logs as batched
self.update_status(self.doc.name)
def before_cancel(self):
self.update_status(None)
def before_update_after_submit(self):
self.update_status(self.doc.name)
def update_status(self, time_log_batch):
self.set_status()
for d in self.doclist.get({"doctype":"Time Log Batch Detail"}):
tl = webnotes.bean("Time Log", d.time_log)
tl.doc.time_log_batch = time_log_batch
tl.doc.sales_invoice = self.doc.sales_invoice
tl.update_after_submit()

View File

@ -2,7 +2,7 @@
{
"creation": "2013-02-28 17:57:33",
"docstatus": 0,
"modified": "2013-02-28 18:36:28",
"modified": "2013-03-01 17:54:57",
"modified_by": "Administrator",
"owner": "Administrator"
},
@ -24,6 +24,7 @@
"permlevel": 0
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"doctype": "DocPerm",
@ -57,19 +58,51 @@
"fieldtype": "Currency",
"label": "Rate"
},
{
"doctype": "DocField",
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"default": "Draft",
"doctype": "DocField",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Draft\nSubmitted\nBilled\nCancelled",
"read_only": 1
},
{
"description": "Will be updated after Sales Invoice is Submitted.",
"doctype": "DocField",
"fieldname": "sales_invoice",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Sales Invoice",
"options": "Sales Invoice",
"read_only": 1
},
{
"doctype": "DocField",
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"doctype": "DocField",
"fieldname": "time_log_batch_details",
"fieldtype": "Table",
"label": "Time Log Batch Details",
"options": "Time Log Batch Detail"
"options": "Time Log Batch Detail",
"reqd": 1
},
{
"description": "In Hours",
"doctype": "DocField",
"fieldname": "total_time",
"fieldname": "total_hours",
"fieldtype": "Float",
"label": "Total Time",
"in_list_view": 1,
"label": "Total Hours",
"read_only": 1
},
{

View File

@ -2,7 +2,7 @@
{
"creation": "2013-02-28 17:56:12",
"docstatus": 0,
"modified": "2013-02-28 17:56:12",
"modified": "2013-03-01 15:20:17",
"modified_by": "Administrator",
"owner": "Administrator"
},
@ -47,5 +47,11 @@
"fieldtype": "Data",
"label": "Activity Type",
"read_only": 1
},
{
"doctype": "DocField",
"fieldname": "hours",
"fieldtype": "Float",
"label": "Hours"
}
]

View File

@ -56,7 +56,18 @@ wn.module_page["Projects"] = [
},
]
},
{
title: wn._("Reports"),
right: true,
icon: "icon-list",
items: [
{
"label":wn._("Time Log Summary"),
route: "Report2/Time Log/Time Log Summary",
doctype: "Time Log"
},
]
}]
pscript['onload_projects-home'] = function(wrapper) {
wn.views.moduleview.make(wrapper, "Projects");

View File

View File

@ -0,0 +1,22 @@
[
{
"creation": "2013-03-01 17:36:35",
"docstatus": 0,
"modified": "2013-03-01 18:17:13",
"modified_by": "Administrator",
"owner": "Administrator"
},
{
"doctype": "Report",
"is_standard": "Yes",
"json": "{\"filters\":[],\"columns\":[[\"name\",\"Time Log\"],[\"status\",\"Time Log\"],[\"from_time\",\"Time Log\"],[\"hours\",\"Time Log\"],[\"activity_type\",\"Time Log\"],[\"owner\",\"Time Log\"],[\"billable\",\"Time Log\"],[\"time_log_batch\",\"Time Log\"],[\"sales_invoice\",\"Time Log\"]],\"sort_by\":\"Time Log.name\",\"sort_order\":\"desc\",\"sort_by_next\":\"\",\"sort_order_next\":\"desc\"}",
"name": "__common__",
"ref_doctype": "Time Log",
"report_name": "Time Log Summary",
"report_type": "Report Builder"
},
{
"doctype": "Report",
"name": "Time Log Summary"
}
]

View File

@ -162,6 +162,7 @@ def import_defaults():
# UOM
{'uom_name': 'Unit', 'doctype': 'UOM', 'name': 'Unit'},
{'uom_name': 'Unit', 'doctype': 'UOM', 'name': 'Hour'},
{'uom_name': 'Box', 'doctype': 'UOM', 'name': 'Box'},
{'uom_name': 'Ft', 'doctype': 'UOM', 'name': 'Ft'},
{'uom_name': 'Kg', 'doctype': 'UOM', 'name': 'Kg'},

View File

@ -19,5 +19,4 @@ observer_map = {
"*:on_submit": "home.update_feed",
"Stock Entry:on_submit": "stock.doctype.material_request.material_request.update_completed_qty",
"Stock Entry:on_cancel": "stock.doctype.material_request.material_request.update_completed_qty",
# "*:on_update": "webnotes.widgets.moduleview.update_count"
}

View File

@ -27,4 +27,6 @@ queries = {
"Production Order": {"docstatus":0},
"BOM": {"docstatus":0},
"Timesheet": {"docstatus":0},
"Time Log": {"status":"Draft"},
"Time Log Batch": {"status":"Draft"},
}