Merge pull request #3968 from rmehta/task-not-mandatory
[minor] task not mandatory for Time Log and Expense Claim #3904
This commit is contained in:
commit
43888546f6
@ -42,13 +42,13 @@ frappe.pages["Accounts Browser"].on_page_load = function(wrapper){
|
||||
wrapper.page.add_menu_item(__('New Company'), function() { newdoc('Company'); }, true);
|
||||
}
|
||||
|
||||
wrapper.page.set_secondary_action(__('Refresh'), function() {
|
||||
wrapper.page.add_menu_item(__('Refresh'), function() {
|
||||
wrapper.$company_select.change();
|
||||
});
|
||||
|
||||
wrapper.page.set_primary_action(__('New'), function() {
|
||||
erpnext.account_chart && erpnext.account_chart.make_new();
|
||||
});
|
||||
}, "octicon octicon-plus");
|
||||
|
||||
// company-select
|
||||
wrapper.$company_select = wrapper.page.add_select("Company", [])
|
||||
@ -121,7 +121,8 @@ erpnext.AccountsChart = Class.extend({
|
||||
label: __("Add Child"),
|
||||
click: function() {
|
||||
me.make_new()
|
||||
}
|
||||
},
|
||||
btnClass: "hidden-xs"
|
||||
},
|
||||
{
|
||||
condition: function(node) {
|
||||
@ -137,8 +138,8 @@ erpnext.AccountsChart = Class.extend({
|
||||
"company": me.company
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
}
|
||||
|
||||
},
|
||||
btnClass: "hidden-xs"
|
||||
},
|
||||
{
|
||||
condition: function(node) { return !node.root && me.can_write },
|
||||
@ -147,7 +148,8 @@ erpnext.AccountsChart = Class.extend({
|
||||
frappe.model.rename_doc(me.ctype, node.label, function(new_name) {
|
||||
node.reload();
|
||||
});
|
||||
}
|
||||
},
|
||||
btnClass: "hidden-xs"
|
||||
},
|
||||
{
|
||||
condition: function(node) { return !node.root && me.can_delete },
|
||||
@ -156,7 +158,8 @@ erpnext.AccountsChart = Class.extend({
|
||||
frappe.model.delete_doc(me.ctype, node.label, function() {
|
||||
node.parent.remove();
|
||||
});
|
||||
}
|
||||
},
|
||||
btnClass: "hidden-xs"
|
||||
}
|
||||
],
|
||||
onrender: function(node) {
|
||||
|
2
erpnext/change_log/current/activity_type.md
Normal file
2
erpnext/change_log/current/activity_type.md
Normal file
@ -0,0 +1,2 @@
|
||||
- Set default costing rate and billing rate in **Activity Type**
|
||||
- Task not mandatory in **Time Log** and **Expense Claim**
|
@ -456,12 +456,13 @@
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"allow_on_submit": 1,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"default": "{employee_name}",
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
@ -535,12 +536,32 @@
|
||||
"is_submittable": 1,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"modified": "2015-05-02 07:42:25.202983",
|
||||
"modified": "2015-09-01 07:11:25.759637",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Expense Claim",
|
||||
"owner": "harshada@webnotestech.com",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 1,
|
||||
|
@ -20,22 +20,27 @@ class ExpenseClaim(Document):
|
||||
validate_fiscal_year(self.posting_date, self.fiscal_year, _("Posting Date"), self)
|
||||
self.validate_sanctioned_amount()
|
||||
self.validate_expense_approver()
|
||||
self.validate_task()
|
||||
self.calculate_total_amount()
|
||||
set_employee_name(self)
|
||||
if self.task and not self.project:
|
||||
self.project = frappe.db.get_value("Task", self.task, "project")
|
||||
|
||||
def on_submit(self):
|
||||
if self.approval_status=="Draft":
|
||||
frappe.throw(_("""Approval Status must be 'Approved' or 'Rejected'"""))
|
||||
if self.task:
|
||||
self.update_task()
|
||||
|
||||
self.update_task_and_project()
|
||||
|
||||
def on_cancel(self):
|
||||
self.update_task_and_project()
|
||||
|
||||
def update_task_and_project(self):
|
||||
if self.task:
|
||||
self.update_task()
|
||||
|
||||
elif self.project:
|
||||
frappe.get_doc("Project", self.project).update_project()
|
||||
|
||||
def calculate_total_amount(self):
|
||||
self.total_claimed_amount = 0
|
||||
self.total_claimed_amount = 0
|
||||
self.total_sanctioned_amount = 0
|
||||
for d in self.get('expenses'):
|
||||
self.total_claimed_amount += flt(d.claim_amount)
|
||||
@ -45,26 +50,22 @@ class ExpenseClaim(Document):
|
||||
if self.exp_approver and "Expense Approver" not in frappe.get_roles(self.exp_approver):
|
||||
frappe.throw(_("{0} ({1}) must have role 'Expense Approver'")\
|
||||
.format(get_fullname(self.exp_approver), self.exp_approver), InvalidExpenseApproverError)
|
||||
|
||||
|
||||
def update_task(self):
|
||||
task = frappe.get_doc("Task", self.task)
|
||||
task.update_total_expense_claim()
|
||||
task.save()
|
||||
|
||||
def validate_task(self):
|
||||
if self.project and not self.task:
|
||||
frappe.throw(_("Task is mandatory if Expense Claim is against a Project"))
|
||||
|
||||
def validate_sanctioned_amount(self):
|
||||
for d in self.get('expenses'):
|
||||
if flt(d.sanctioned_amount) > flt(d.claim_amount):
|
||||
frappe.throw(_("Sanctioned Amount cannot be greater than Claim Amount in Row {0}.").format(d.idx))
|
||||
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_expense_approver(doctype, txt, searchfield, start, page_len, filters):
|
||||
return frappe.db.sql("""
|
||||
select u.name, concat(u.first_name, ' ', u.last_name)
|
||||
select u.name, concat(u.first_name, ' ', u.last_name)
|
||||
from tabUser u, tabUserRole r
|
||||
where u.name = r.parent and r.role = 'Expense Approver' and u.name like %s
|
||||
""", ("%" + txt + "%"))
|
||||
""", ("%" + txt + "%"))
|
||||
|
@ -11,44 +11,47 @@ class TestExpenseClaim(unittest.TestCase):
|
||||
def test_total_expense_claim_for_project(self):
|
||||
frappe.db.sql("""delete from `tabTask` where project = "_Test Project 1" """)
|
||||
frappe.db.sql("""delete from `tabProject` where name = "_Test Project 1" """)
|
||||
|
||||
frappe.db.sql("""delete from `tabExpense Claim`""")
|
||||
frappe.db.sql("""delete from `tabExpense Claim Detail`""")
|
||||
|
||||
frappe.get_doc({
|
||||
"project_name": "_Test Project 1",
|
||||
"doctype": "Project",
|
||||
"tasks" :
|
||||
[{ "title": "_Test Project Task 1", "status": "Open" }]
|
||||
}).save()
|
||||
|
||||
task_name = frappe.db.get_value("Task",{"project": "_Test Project 1"})
|
||||
|
||||
task_name = frappe.db.get_value("Task", {"project": "_Test Project 1"})
|
||||
|
||||
expense_claim = frappe.get_doc({
|
||||
"doctype": "Expense Claim",
|
||||
"employee": "_T-Employee-0001",
|
||||
"approval_status": "Approved",
|
||||
"project": "_Test Project 1",
|
||||
"task": task_name,
|
||||
"expenses":
|
||||
"expenses":
|
||||
[{ "expense_type": "Food", "claim_amount": 300, "sanctioned_amount": 200 }]
|
||||
})
|
||||
expense_claim.submit()
|
||||
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200)
|
||||
self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 200)
|
||||
|
||||
|
||||
expense_claim2 = frappe.get_doc({
|
||||
"doctype": "Expense Claim",
|
||||
"employee": "_T-Employee-0001",
|
||||
"approval_status": "Approved",
|
||||
"project": "_Test Project 1",
|
||||
"task": task_name,
|
||||
"expenses":
|
||||
"expenses":
|
||||
[{ "expense_type": "Food", "claim_amount": 600, "sanctioned_amount": 500 }]
|
||||
})
|
||||
expense_claim2.submit()
|
||||
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 700)
|
||||
self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 700)
|
||||
|
||||
|
||||
expense_claim2.cancel()
|
||||
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200)
|
||||
self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 200)
|
||||
|
@ -201,3 +201,4 @@ execute:frappe.delete_doc_if_exists("Print Format", "Credit Note - Negative Invo
|
||||
|
||||
# V6.0
|
||||
erpnext.patches.v6_0.set_default_title # 2015-09-03
|
||||
erpnext.patches.v6_0.default_activity_rate
|
||||
|
11
erpnext/patches/v6_0/default_activity_rate.py
Normal file
11
erpnext/patches/v6_0/default_activity_rate.py
Normal file
@ -0,0 +1,11 @@
|
||||
import frappe
|
||||
|
||||
def execute():
|
||||
for cost in frappe.db.get_list("Activity Cost", filters = {"employee": ""},
|
||||
fields = ("name", "activity_type", "costing_rate", "billing_rate")):
|
||||
activity_type = frappe.get_doc("Activity Type", cost.activity_type)
|
||||
activity_type.costing_rate = cost.costing_rate
|
||||
activity_type.billing_rate = cost.billing_rate
|
||||
activity_type.save()
|
||||
|
||||
frappe.delete_doc("Activity Cost", cost.name)
|
@ -7,7 +7,7 @@
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Master",
|
||||
"document_type": "Setup",
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
@ -219,7 +219,7 @@
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"modified": "2015-06-16 03:12:25.644839",
|
||||
"modified": "2015-08-31 06:43:42.804365",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Activity Cost",
|
||||
|
8
erpnext/projects/doctype/activity_type/activity_type.js
Normal file
8
erpnext/projects/doctype/activity_type/activity_type.js
Normal file
@ -0,0 +1,8 @@
|
||||
frappe.ui.form.on("Activity Type", {
|
||||
refresh: function(frm) {
|
||||
frm.add_custom_button("Activity Cost per Employee", function() {
|
||||
frappe.route_options = {"activity_type": frm.doc.name};
|
||||
frappe.set_route("List", "Activity Cost");
|
||||
});
|
||||
}
|
||||
});
|
@ -7,7 +7,7 @@
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Master",
|
||||
"document_type": "Setup",
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
@ -29,6 +29,71 @@
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "costing_rate",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"label": "Default Costing Rate",
|
||||
"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
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"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
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "billing_rate",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"label": "Default Billing Rate",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"hide_heading": 0,
|
||||
@ -40,7 +105,7 @@
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"modified": "2015-02-18 13:05:23.608066",
|
||||
"modified": "2015-08-31 06:39:04.527080",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Activity Type",
|
||||
|
@ -46,8 +46,6 @@ class Project(Document):
|
||||
"""sync tasks and remove table"""
|
||||
if self.flags.dont_sync_tasks: return
|
||||
|
||||
|
||||
task_added_or_deleted = False
|
||||
task_names = []
|
||||
for t in self.tasks:
|
||||
if t.task_id:
|
||||
@ -55,7 +53,6 @@ class Project(Document):
|
||||
else:
|
||||
task = frappe.new_doc("Task")
|
||||
task.project = self.name
|
||||
task_added_or_deleted = True
|
||||
|
||||
task.update({
|
||||
"subject": t.title,
|
||||
@ -73,14 +70,15 @@ class Project(Document):
|
||||
# delete
|
||||
for t in frappe.get_all("Task", ["name"], {"project": self.name, "name": ("not in", task_names)}):
|
||||
frappe.delete_doc("Task", t.name)
|
||||
task_added_or_deleted = True
|
||||
|
||||
if task_added_or_deleted:
|
||||
self.update_project()
|
||||
self.update_percent_complete()
|
||||
self.update_costing()
|
||||
|
||||
def update_project(self):
|
||||
self.update_percent_complete()
|
||||
self.update_costing()
|
||||
self.flags.dont_sync_tasks = True
|
||||
self.save()
|
||||
|
||||
def update_percent_complete(self):
|
||||
total = frappe.db.sql("""select count(*) from tabTask where project=%s""", self.name)[0][0]
|
||||
@ -91,18 +89,31 @@ class Project(Document):
|
||||
self.percent_complete = flt(completed) / total * 100
|
||||
|
||||
def update_costing(self):
|
||||
total_cost = frappe.db.sql("""select sum(ifnull(total_costing_amount, 0)) as costing_amount,
|
||||
sum(ifnull(total_billing_amount, 0)) as billing_amount, sum(ifnull(total_expense_claim, 0)) as expense_claim,
|
||||
min(act_start_date) as start_date, max(act_end_date) as end_date, sum(actual_time) as time
|
||||
from `tabTask` where project = %s""", self.name, as_dict=1)[0]
|
||||
from_time_log = frappe.db.sql("""select
|
||||
sum(ifnull(costing_amount, 0)) as costing_amount,
|
||||
sum(ifnull(billing_amount, 0)) as billing_amount,
|
||||
min(from_time) as start_date,
|
||||
max(to_time) as end_date,
|
||||
sum(hours) as time
|
||||
from `tabTime Log` where project = %s and docstatus = 1""", self.name, as_dict=1)[0]
|
||||
|
||||
from_expense_claim = frappe.db.sql("""select
|
||||
sum(ifnull(total_sanctioned_amount, 0)) as total_sanctioned_amount
|
||||
from `tabExpense Claim` where project = %s and approval_status='Approved'
|
||||
and docstatus = 1""",
|
||||
self.name, as_dict=1)[0]
|
||||
|
||||
self.actual_start_date = from_time_log.start_date
|
||||
self.actual_end_date = from_time_log.end_date
|
||||
|
||||
self.total_costing_amount = from_time_log.costing_amount
|
||||
self.total_billing_amount = from_time_log.billing_amount
|
||||
self.actual_time = from_time_log.time
|
||||
|
||||
self.total_expense_claim = from_expense_claim.total_sanctioned_amount
|
||||
|
||||
self.gross_margin = flt(self.total_billing_amount) - flt(self.total_costing_amount)
|
||||
|
||||
self.total_costing_amount = total_cost.costing_amount
|
||||
self.total_billing_amount = total_cost.billing_amount
|
||||
self.total_expense_claim = total_cost.expense_claim
|
||||
self.actual_start_date = total_cost.start_date
|
||||
self.actual_end_date = total_cost.end_date
|
||||
self.actual_time = total_cost.time
|
||||
self.gross_margin = flt(total_cost.billing_amount) - flt(total_cost.costing_amount)
|
||||
if self.total_billing_amount:
|
||||
self.per_gross_margin = (self.gross_margin / flt(self.total_billing_amount)) *100
|
||||
|
||||
|
@ -3,7 +3,7 @@ frappe.listview_settings['Project'] = {
|
||||
filters:[["status","=", "Open"]],
|
||||
get_indicator: function(doc) {
|
||||
if(doc.status=="Open" && doc.percent_complete) {
|
||||
return [__("{0}% Complete", [doc.percent_complete]), "orange", "percent_complete,>,0|status,=,Open"];
|
||||
return [__("{0}% Complete", [cint(doc.percent_complete)]), "orange", "percent_complete,>,0|status,=,Open"];
|
||||
} else {
|
||||
return [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status];
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ class Task(Document):
|
||||
|
||||
def update_time_and_costing(self):
|
||||
tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date,
|
||||
sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
|
||||
sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
|
||||
sum(hours) as time from `tabTime Log` where task = %s and docstatus=1"""
|
||||
,self.name, as_dict=1)[0]
|
||||
if self.status == "Open":
|
||||
@ -70,10 +70,7 @@ class Task(Document):
|
||||
|
||||
def update_project(self):
|
||||
if self.project and not self.flags.from_project:
|
||||
project = frappe.get_doc("Project", self.project)
|
||||
project.flags.dont_sync_tasks = True
|
||||
project.update_project()
|
||||
project.save()
|
||||
frappe.get_doc("Project", self.project).update_project()
|
||||
|
||||
def check_recursion(self):
|
||||
if self.flags.ignore_recursion_check: return
|
||||
|
@ -26,21 +26,21 @@ class TestTimeLog(unittest.TestCase):
|
||||
prod_order.set_production_order_operations()
|
||||
prod_order.save()
|
||||
|
||||
time_log = make_time_log_test_record(for_manufacturing= 1, production_order= prod_order.name, qty= 1,
|
||||
time_log = make_time_log_test_record(for_manufacturing= 1, production_order= prod_order.name, qty= 1,
|
||||
employee= "_T-Employee-0003", do_not_save= True, simulate=1)
|
||||
|
||||
self.assertRaises(NotSubmittedError, time_log.save)
|
||||
|
||||
def test_time_log_on_holiday(self):
|
||||
prod_order = make_prod_order_test_record(item= "_Test FG Item 2", qty= 1,
|
||||
prod_order = make_prod_order_test_record(item= "_Test FG Item 2", qty= 1,
|
||||
planned_start_date= now(), do_not_save= True)
|
||||
prod_order.set_production_order_operations()
|
||||
prod_order.save()
|
||||
prod_order.submit()
|
||||
|
||||
time_log = make_time_log_test_record(from_time= "2013-02-01 10:00:00", to_time= "2013-02-01 20:00:00",
|
||||
for_manufacturing= 1, production_order= prod_order.name, qty= 1,
|
||||
operation= prod_order.operations[0].operation, operation_id= prod_order.operations[0].name,
|
||||
for_manufacturing= 1, production_order= prod_order.name, qty= 1,
|
||||
operation= prod_order.operations[0].operation, operation_id= prod_order.operations[0].name,
|
||||
workstation= "_Test Workstation 1", do_not_save= True)
|
||||
|
||||
self.assertRaises(WorkstationHolidayError , time_log.save)
|
||||
@ -61,59 +61,81 @@ class TestTimeLog(unittest.TestCase):
|
||||
employee="_T-Employee-0006",do_not_save= True)
|
||||
self.assertRaises(NegativeHoursError, time_log.save)
|
||||
|
||||
def test_default_activity_cost(self):
|
||||
activity_type = frappe.get_doc("Activity Type", "_Test Activity Type")
|
||||
activity_type.billing_rate = 20
|
||||
activity_type.costing_rate = 15
|
||||
activity_type.save()
|
||||
|
||||
project_name = "_Test Project for Activity Type"
|
||||
|
||||
frappe.db.sql("delete from `tabTime Log` where project=%s or employee='_T-Employee-0002'", project_name)
|
||||
frappe.delete_doc("Project", project_name)
|
||||
project = frappe.get_doc({"doctype": "Project", "project_name": project_name}).insert()
|
||||
|
||||
make_time_log_test_record(employee="_T-Employee-0002", hours=2,
|
||||
activity_type = "_Test Activity Type", project = project.name)
|
||||
|
||||
project = frappe.get_doc("Project", project.name)
|
||||
self.assertTrue(project.total_costing_amount, 30)
|
||||
self.assertTrue(project.total_billing_amount, 40)
|
||||
|
||||
def test_total_activity_cost_for_project(self):
|
||||
frappe.db.sql("""delete from `tabTask` where project = "_Test Project 1" """)
|
||||
frappe.db.sql("""delete from `tabProject` where name = "_Test Project 1" """)
|
||||
|
||||
frappe.db.sql("""delete from `tabTime Log` where name = "_Test Project 1" """)
|
||||
|
||||
if not frappe.db.exists('Activity Cost', {"activity_type": "_Test Activity Type"}):
|
||||
activity_cost = frappe.get_doc({
|
||||
"doctype": "Activity Cost",
|
||||
"employee": "",
|
||||
"employee": "_T-Employee-0002",
|
||||
"activity_type": "_Test Activity Type",
|
||||
"billing_rate": 100,
|
||||
"costing_rate": 50
|
||||
})
|
||||
activity_cost.insert()
|
||||
|
||||
|
||||
frappe.get_doc({
|
||||
"project_name": "_Test Project 1",
|
||||
"doctype": "Project",
|
||||
"tasks" :
|
||||
[{ "title": "_Test Project Task 1", "status": "Open" }]
|
||||
}).save()
|
||||
|
||||
|
||||
task_name = frappe.db.get_value("Task",{"project": "_Test Project 1"})
|
||||
|
||||
time_log = make_time_log_test_record(employee="_T-Employee-0002", hours=2, task= task_name)
|
||||
|
||||
time_log = make_time_log_test_record(employee="_T-Employee-0002", hours=2,
|
||||
task=task_name)
|
||||
self.assertEqual(time_log.costing_rate, 50)
|
||||
self.assertEqual(time_log.costing_amount, 100)
|
||||
self.assertEqual(time_log.billing_rate, 100)
|
||||
self.assertEqual(time_log.billing_amount, 200)
|
||||
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Task", task_name, "total_billing_amount"), 200)
|
||||
self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_billing_amount"), 200)
|
||||
|
||||
time_log2 = make_time_log_test_record(employee="_T-Employee-0003", hours=2, task= task_name)
|
||||
|
||||
time_log2 = make_time_log_test_record(employee="_T-Employee-0002",
|
||||
hours=2, task= task_name, from_time = now_datetime() + datetime.timedelta(hours= 3))
|
||||
self.assertEqual(frappe.db.get_value("Task", task_name, "total_billing_amount"), 400)
|
||||
self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_billing_amount"), 400)
|
||||
|
||||
|
||||
time_log2.cancel()
|
||||
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Task", task_name, "total_billing_amount"), 200)
|
||||
self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_billing_amount"), 200)
|
||||
time_log.cancel()
|
||||
|
||||
|
||||
test_ignore = ["Time Log Batch", "Sales Invoice"]
|
||||
|
||||
def make_time_log_test_record(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
time_log = frappe.new_doc("Time Log")
|
||||
|
||||
|
||||
time_log.from_time = args.from_time or now_datetime()
|
||||
time_log.hours = args.hours or 1
|
||||
time_log.to_time = args.to_time or time_log.from_time + datetime.timedelta(hours= time_log.hours)
|
||||
|
||||
|
||||
time_log.project = args.project
|
||||
time_log.task = args.task
|
||||
time_log.for_manufacturing = args.for_manufacturing
|
||||
@ -126,7 +148,7 @@ def make_time_log_test_record(**args):
|
||||
time_log.billable = args.billable or 1
|
||||
time_log.employee = args.employee
|
||||
time_log.user = args.user
|
||||
|
||||
|
||||
if not args.do_not_save:
|
||||
if args.simulate:
|
||||
while True:
|
||||
@ -141,4 +163,4 @@ def make_time_log_test_record(**args):
|
||||
if not args.do_not_submit:
|
||||
time_log.submit()
|
||||
|
||||
return time_log
|
||||
return time_log
|
||||
|
@ -17,6 +17,8 @@ frappe.ui.form.on("Time Log", "refresh", function(frm) {
|
||||
if (frm.doc.__islocal && !frm.doc.user) {
|
||||
frm.set_value("user", user);
|
||||
}
|
||||
|
||||
frm.toggle_reqd("activity_type", !frm.doc.for_manufacturing);
|
||||
});
|
||||
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
"description": "Log of Activities performed by users against Tasks that can be used for tracking time, billing.",
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Master",
|
||||
"document_type": "Setup",
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
@ -181,6 +181,29 @@
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"depends_on": "eval:!doc.for_manufacturing",
|
||||
"fieldname": "activity_type",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"label": "Activity Type",
|
||||
"no_copy": 0,
|
||||
"options": "Activity Type",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
@ -227,29 +250,6 @@
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"depends_on": "eval:!doc.for_manufacturing",
|
||||
"fieldname": "activity_type",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"label": "Activity Type",
|
||||
"no_copy": 0,
|
||||
"options": "Activity Type",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
@ -618,7 +618,7 @@
|
||||
"ignore_user_permissions": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"label": "Costing Rate (per hour)",
|
||||
"label": "Costing Rate based on Activity Type (per hour)",
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
@ -686,7 +686,7 @@
|
||||
"ignore_user_permissions": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 0,
|
||||
"label": "Billing Rate (per hour)",
|
||||
"label": "Billing Rate based on Activity Type (per hour)",
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
@ -812,7 +812,7 @@
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"allow_on_submit": 1,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "title",
|
||||
@ -843,7 +843,7 @@
|
||||
"is_submittable": 1,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"modified": "2015-04-14 09:07:28.468792",
|
||||
"modified": "2015-08-31 06:34:07.703583",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Time Log",
|
||||
|
@ -26,16 +26,16 @@ class TimeLog(Document):
|
||||
self.check_workstation_timings()
|
||||
self.validate_production_order()
|
||||
self.validate_manufacturing()
|
||||
self.validate_task()
|
||||
self.set_project_if_missing()
|
||||
self.update_cost()
|
||||
|
||||
def on_submit(self):
|
||||
self.update_production_order()
|
||||
self.update_task()
|
||||
self.update_task_and_project()
|
||||
|
||||
def on_cancel(self):
|
||||
self.update_production_order()
|
||||
self.update_task()
|
||||
self.update_task_and_project()
|
||||
|
||||
def before_update_after_submit(self):
|
||||
self.set_status()
|
||||
@ -57,14 +57,17 @@ class TimeLog(Document):
|
||||
self.status="Billed"
|
||||
|
||||
def set_title(self):
|
||||
"""Set default title for the Time Log"""
|
||||
if self.title:
|
||||
return
|
||||
|
||||
from frappe.utils import get_fullname
|
||||
if self.production_order:
|
||||
self.title = _("{0} for {1}").format(self.operation, self.production_order)
|
||||
elif self.task:
|
||||
self.title = _("{0} for {1}").format(self.activity_type, self.task)
|
||||
elif self.project:
|
||||
self.title = _("{0} for {1}").format(self.activity_type, self.project)
|
||||
elif self.activity_type and (self.task or self.project):
|
||||
self.title = _("{0} for {1}").format(self.activity_type, self.task or self.project)
|
||||
else:
|
||||
self.title = self.activity_type
|
||||
self.title = self.task or self.project or get_fullname(frappe.session.user)
|
||||
|
||||
def validate_overlap(self):
|
||||
"""Checks if 'Time Log' entries overlap for a user, workstation. """
|
||||
@ -111,6 +114,11 @@ class TimeLog(Document):
|
||||
from frappe.utils import time_diff_in_seconds
|
||||
self.hours = flt(time_diff_in_seconds(self.to_time, self.from_time)) / 3600
|
||||
|
||||
def set_project_if_missing(self):
|
||||
"""Set project if task is set"""
|
||||
if self.task and not self.project:
|
||||
self.project = frappe.db.get_value("Task", self.task, "project")
|
||||
|
||||
def validate_time_log_for(self):
|
||||
if not self.for_manufacturing:
|
||||
for fld in ["production_order", "operation", "workstation", "completed_qty"]:
|
||||
@ -221,25 +229,26 @@ class TimeLog(Document):
|
||||
def update_cost(self):
|
||||
rate = get_activity_cost(self.employee, self.activity_type)
|
||||
if rate:
|
||||
self.costing_rate = rate.get('costing_rate')
|
||||
self.billing_rate = rate.get('billing_rate')
|
||||
self.costing_rate = flt(rate.get('costing_rate'))
|
||||
self.billing_rate = flt(rate.get('billing_rate'))
|
||||
self.costing_amount = self.costing_rate * self.hours
|
||||
if self.billable:
|
||||
self.billing_amount = self.billing_rate * self.hours
|
||||
else:
|
||||
self.billing_amount = 0
|
||||
|
||||
def validate_task(self):
|
||||
# if a time log is being created against a project without production order
|
||||
if (self.project and not self.production_order) and not self.task:
|
||||
frappe.throw(_("Task is Mandatory if Time Log is against a project"))
|
||||
def update_task_and_project(self):
|
||||
"""Update costing rate in Task or Project if either is set"""
|
||||
|
||||
def update_task(self):
|
||||
if self.task and frappe.db.exists("Task", self.task):
|
||||
if self.task:
|
||||
task = frappe.get_doc("Task", self.task)
|
||||
task.update_time_and_costing()
|
||||
task.save()
|
||||
|
||||
elif self.project:
|
||||
frappe.get_doc("Project", self.project).update_project()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_events(start, end, filters=None):
|
||||
"""Returns events for Gantt / Calendar view rendering.
|
||||
@ -270,9 +279,10 @@ def get_events(start, end, filters=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_activity_cost(employee=None, activity_type=None):
|
||||
rate = frappe.db.sql("""select costing_rate, billing_rate from `tabActivity Cost` where employee= %s
|
||||
and activity_type= %s""", (employee, activity_type), as_dict=1)
|
||||
rate = frappe.db.get_values("Activity Cost", {"employee": employee,
|
||||
"activity_type": activity_type}, ["costing_rate", "billing_rate"], as_dict=True)
|
||||
if not rate:
|
||||
rate = frappe.db.sql("""select costing_rate, billing_rate from `tabActivity Cost` where ifnull(employee, '')=''
|
||||
and activity_type= %s""", (activity_type), as_dict=1)
|
||||
rate = frappe.db.get_values("Activity Type", {"activity_type": activity_type},
|
||||
["costing_rate", "billing_rate"], as_dict=True)
|
||||
|
||||
return rate[0] if rate else {}
|
||||
|
@ -7,7 +7,7 @@ frappe.pages["Sales Browser"].on_page_load = function(wrapper){
|
||||
single_column: true,
|
||||
});
|
||||
|
||||
wrapper.page.set_secondary_action(__('Refresh'), function() {
|
||||
wrapper.page.add_menu_item(__('Refresh'), function() {
|
||||
wrapper.make_tree();
|
||||
});
|
||||
|
||||
@ -57,7 +57,7 @@ erpnext.SalesChart = Class.extend({
|
||||
|
||||
me.page.set_primary_action(__("New"), function() {
|
||||
me.new_node();
|
||||
});
|
||||
}, "octicon octicon-plus");
|
||||
|
||||
this.tree = new frappe.ui.Tree({
|
||||
parent: $(parent),
|
||||
@ -80,7 +80,8 @@ erpnext.SalesChart = Class.extend({
|
||||
condition: function(node) { return me.can_create && node.expandable; },
|
||||
click: function(node) {
|
||||
me.new_node();
|
||||
}
|
||||
},
|
||||
btnClass: "hidden-xs"
|
||||
},
|
||||
{
|
||||
label:__("Rename"),
|
||||
@ -89,7 +90,8 @@ erpnext.SalesChart = Class.extend({
|
||||
frappe.model.rename_doc(me.ctype, node.label, function(new_name) {
|
||||
node.$a.html(new_name);
|
||||
});
|
||||
}
|
||||
},
|
||||
btnClass: "hidden-xs"
|
||||
},
|
||||
{
|
||||
label:__("Delete"),
|
||||
@ -98,7 +100,8 @@ erpnext.SalesChart = Class.extend({
|
||||
frappe.model.delete_doc(me.ctype, node.label, function() {
|
||||
node.parent.remove();
|
||||
});
|
||||
}
|
||||
},
|
||||
btnClass: "hidden-xs"
|
||||
}
|
||||
|
||||
]
|
||||
|
Loading…
x
Reference in New Issue
Block a user