diff --git a/erpnext/config/projects.py b/erpnext/config/projects.py index d591c9e2a8..07149e38b6 100644 --- a/erpnext/config/projects.py +++ b/erpnext/config/projects.py @@ -32,6 +32,11 @@ def get_data(): "name": "Activity Type", "description": _("Types of activities for Time Sheets"), }, + { + "type": "doctype", + "name": "Activity Cost", + "description": _("Cost of various activities"), + }, ] }, { diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index 8076bf7b60..d279074f84 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -133,12 +133,14 @@ cur_frm.cscript.calculate_total = function(doc,cdt,cdn){ cur_frm.cscript.calculate_total_amount = function(doc,cdt,cdn){ cur_frm.cscript.calculate_total(doc,cdt,cdn); } + cur_frm.cscript.claim_amount = function(doc,cdt,cdn){ cur_frm.cscript.calculate_total(doc,cdt,cdn); var child = locals[cdt][cdn]; refresh_field("sanctioned_amount", child.name, child.parentfield); } + cur_frm.cscript.sanctioned_amount = function(doc,cdt,cdn){ cur_frm.cscript.calculate_total(doc,cdt,cdn); } @@ -148,3 +150,30 @@ cur_frm.cscript.on_submit = function(doc, cdt, cdn) { cur_frm.email_doc(frappe.boot.notification_settings.expense_claim_message); } } + +erpnext.expense_claim = { + set_title :function(frm) { + if (!frm.doc.task) { + frm.set_value("title", frm.doc.employee_name); + } + else { + frm.set_value("title", frm.doc.employee_name + " for "+ frm.doc.task); + } + } +} + +frappe.ui.form.on("Expense Claim", "employee_name", function(frm) { + erpnext.expense_claim.set_title(frm); +}); + +frappe.ui.form.on("Expense Claim", "task", function(frm) { + erpnext.expense_claim.set_title(frm); +}); + +cur_frm.fields_dict['task'].get_query = function(doc) { + return { + filters:{ + 'project': doc.project + } + } +} \ No newline at end of file diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.json b/erpnext/hr/doctype/expense_claim/expense_claim.json index ef3d6170bf..e08856a3bb 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.json +++ b/erpnext/hr/doctype/expense_claim/expense_claim.json @@ -93,7 +93,8 @@ "oldfieldname": "expense_voucher_details", "oldfieldtype": "Table", "options": "Expense Claim Detail", - "permlevel": 0 + "permlevel": 0, + "reqd": 1 }, { "fieldname": "sb1", @@ -102,6 +103,7 @@ "permlevel": 0 }, { + "default": "Today", "fieldname": "posting_date", "fieldtype": "Date", "in_filter": 1, @@ -191,6 +193,21 @@ "permlevel": 0, "precision": "" }, + { + "fieldname": "task", + "fieldtype": "Link", + "label": "Task", + "options": "Task", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "permlevel": 0, + "precision": "" + }, { "fieldname": "email_id", "fieldtype": "Data", @@ -220,7 +237,7 @@ "icon": "icon-money", "idx": 1, "is_submittable": 1, - "modified": "2015-03-26 04:41:50.473196", + "modified": "2015-04-22 01:51:24.782515", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim", @@ -294,5 +311,5 @@ "search_fields": "approval_status,employee,employee_name", "sort_field": "modified", "sort_order": "DESC", - "title_field": "employee_name" + "title_field": "title" } \ No newline at end of file diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 76606e5b8a..6687399f37 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import get_fullname +from frappe.utils import get_fullname, flt from frappe.model.document import Document from erpnext.hr.utils import set_employee_name from erpnext.accounts.utils import validate_fiscal_year @@ -18,19 +18,44 @@ class ExpenseClaim(Document): def validate(self): validate_fiscal_year(self.posting_date, self.fiscal_year, _("Posting Date"), self) - self.validate_exp_details() + self.validate_sanctioned_amount() self.validate_expense_approver() + self.validate_task() + self.calculate_total_amount() set_employee_name(self) def on_submit(self): if self.approval_status=="Draft": frappe.throw(_("""Approval Status must be 'Approved' or 'Rejected'""")) - - def validate_exp_details(self): - if not self.get('expenses'): - frappe.throw(_("Please add expense voucher details")) + if self.task: + self.update_task() + + def on_cancel(self): + if self.task: + self.update_task() + + def calculate_total_amount(self): + self.total_claimed_amount = 0 + self.total_sanctioned_amount = 0 + for d in self.get('expenses'): + self.total_claimed_amount += flt(d.claim_amount) + self.total_sanctioned_amount += flt(d.sanctioned_amount) def validate_expense_approver(self): 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)) \ No newline at end of file diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index f1c657adb8..a9091fba6a 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -8,4 +8,47 @@ import unittest test_records = frappe.get_test_records('Expense Claim') class TestExpenseClaim(unittest.TestCase): - pass + 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.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"}) + expense_claim = frappe.get_doc({ + "doctype": "Expense Claim", + "employee": "_T-Employee-0001", + "approval_status": "Approved", + "project": "_Test Project 1", + "task": task_name, + "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": + [{ "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) diff --git a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json index d97518941e..c6123eeeff 100644 --- a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json +++ b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json @@ -15,6 +15,12 @@ "reqd": 0, "width": "150px" }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break", + "permlevel": 0, + "precision": "" + }, { "fieldname": "expense_type", "fieldtype": "Link", @@ -28,6 +34,12 @@ "reqd": 1, "width": "150px" }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "permlevel": 0, + "precision": "" + }, { "fieldname": "description", "fieldtype": "Small Text", @@ -39,6 +51,12 @@ "print_width": "300px", "width": "300px" }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "permlevel": 0, + "precision": "" + }, { "fieldname": "claim_amount", "fieldtype": "Currency", @@ -52,6 +70,12 @@ "reqd": 1, "width": "150px" }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break", + "permlevel": 0, + "precision": "" + }, { "allow_on_submit": 0, "fieldname": "sanctioned_amount", @@ -69,7 +93,7 @@ ], "idx": 1, "istable": 1, - "modified": "2014-05-09 02:16:38.529082", + "modified": "2015-04-08 06:18:47.539134", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim Detail", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0e0fda9311..0665df4ab1 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -143,3 +143,4 @@ execute:frappe.permissions.reset_perms("Expense Claim Type") #2014-06-19 erpnext.patches.v5_0.execute_on_doctype_update erpnext.patches.v4_2.fix_recurring_orders erpnext.patches.v4_2.delete_gl_entries_for_cancelled_invoices +erpnext.patches.v5_0.project_costing diff --git a/erpnext/patches/v5_0/project_costing.py b/erpnext/patches/v5_0/project_costing.py new file mode 100644 index 0000000000..33bb9c1630 --- /dev/null +++ b/erpnext/patches/v5_0/project_costing.py @@ -0,0 +1,7 @@ +import frappe + +def execute(): + frappe.reload_doctype("Project") + frappe.db.sql("update `tabProject` set expected_start_date = project_start_date, \ + expected_end_date = completion_date, actual_end_date = act_completion_date, \ + estimated_costing = project_value, gross_margin = gross_margin_value") \ No newline at end of file diff --git a/erpnext/projects/doctype/activity_cost/__init__.py b/erpnext/projects/doctype/activity_cost/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/projects/doctype/activity_cost/activity_cost.json b/erpnext/projects/doctype/activity_cost/activity_cost.json new file mode 100644 index 0000000000..7f7720acbb --- /dev/null +++ b/erpnext/projects/doctype/activity_cost/activity_cost.json @@ -0,0 +1,170 @@ +{ + "allow_copy": 0, + "allow_import": 1, + "allow_rename": 1, + "autoname": "Activity Cost - .#", + "creation": "2015-03-23 02:00:21.861546", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Master", + "fields": [ + { + "allow_on_submit": 0, + "fieldname": "employee", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "Employee", + "no_copy": 0, + "options": "Employee", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0 + }, + { + "fieldname": "employee_name", + "fieldtype": "Read Only", + "label": "Employee Name", + "options": "employee.employee_name", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break", + "permlevel": 0, + "precision": "" + }, + { + "allow_on_submit": 0, + "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, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0 + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "permlevel": 0, + "precision": "" + }, + { + "allow_on_submit": 0, + "default": "0", + "description": "per hour", + "fieldname": "billing_rate", + "fieldtype": "Currency", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "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 + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break", + "permlevel": 0, + "precision": "" + }, + { + "allow_on_submit": 0, + "default": "0", + "description": "per hour", + "fieldname": "costing_rate", + "fieldtype": "Currency", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 0, + "label": "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 + }, + { + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "title", + "no_copy": 1, + "permlevel": 0, + "precision": "", + "read_only": 1 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "modified": "2015-04-14 02:08:33.690406", + "modified_by": "Administrator", + "module": "Projects", + "name": "Activity Cost", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Projects User", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "title" +} \ No newline at end of file diff --git a/erpnext/projects/doctype/activity_cost/activity_cost.py b/erpnext/projects/doctype/activity_cost/activity_cost.py new file mode 100644 index 0000000000..121e6508f8 --- /dev/null +++ b/erpnext/projects/doctype/activity_cost/activity_cost.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.model.document import Document + +class DuplicationError(frappe.ValidationError): pass + +class ActivityCost(Document): + def validate(self): + self.set_title() + self.check_unique() + + def set_title(self): + self.title = _("{0} for {1}").format(self.employee_name, self.activity_type) + + def check_unique(self): + if frappe.db.sql("""select name from `tabActivity Cost` where employee_name= %s and activity_type= %s and name != %s""", + (self.employee_name, self.activity_type, self.name)): + frappe.throw(_("Activity Cost exists for Employee {0} against Activity Type - {1}") + .format(self.employee, self.activity_type), DuplicationError) diff --git a/erpnext/projects/doctype/activity_cost/test_activity_cost.py b/erpnext/projects/doctype/activity_cost/test_activity_cost.py new file mode 100644 index 0000000000..5afd97f96c --- /dev/null +++ b/erpnext/projects/doctype/activity_cost/test_activity_cost.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +from erpnext.projects.doctype.activity_cost.activity_cost import DuplicationError + +class TestActivityCost(unittest.TestCase): + def test_duplication(self): + frappe.db.sql("delete from `tabActivity Cost`") + activity_cost1 = frappe.new_doc('Activity Cost') + activity_cost1.update({ + "employee": "_T-Employee-0001", + "employee_name": "_Test Employee", + "activity_type": "_Test Activity Type", + "billing_rate": 100, + "costing_rate": 50 + }) + activity_cost1.insert() + activity_cost2 = frappe.copy_doc(activity_cost1) + self.assertRaises(DuplicationError, activity_cost2.insert ) diff --git a/erpnext/projects/doctype/activity_cost/test_records.json b/erpnext/projects/doctype/activity_cost/test_records.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/erpnext/projects/doctype/activity_cost/test_records.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index eeedeb0147..aa55876654 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -1,6 +1,19 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt +frappe.ui.form.on("Project", { + onload: function(frm) { + var so = frappe.meta.get_docfield("Project", "sales_order"); + so.get_route_options_for_new_doc = function(field) { + if(frm.is_new()) return; + return { + "customer": frm.doc.customer, + "project_name": frm.doc.name + } + } + } +}); + frappe.ui.form.on("Project Task", "edit_task", function(frm, doctype, name) { var doc = frappe.get_doc(doctype, name); if(doc.task_id) { @@ -14,13 +27,21 @@ frappe.ui.form.on("Project Task", "edit_task", function(frm, doctype, name) { cur_frm.cscript.refresh = function(doc) { if(!doc.__islocal) { cur_frm.add_custom_button(__("Gantt Chart"), function() { - frappe.route_options = {"project": doc.name, "start": doc.project_start_date, "end": doc.completion_date}; + frappe.route_options = {"project": doc.name, "start": doc.expected_start_date, "end": doc.expected_end_date}; frappe.set_route("Gantt", "Task"); }, "icon-tasks", true); cur_frm.add_custom_button(__("Tasks"), function() { frappe.route_options = {"project": doc.name} frappe.set_route("List", "Task"); }, "icon-list", true); + cur_frm.add_custom_button(__("Time Logs"), function() { + frappe.route_options = {"project": doc.name} + frappe.set_route("List", "Time Log"); + }, "icon-list", true); + cur_frm.add_custom_button(__("Expense Claims"), function() { + frappe.route_options = {"project": doc.name} + frappe.set_route("List", "Expense Claim"); + }, "icon-list", true); } } @@ -29,3 +50,11 @@ cur_frm.fields_dict.customer.get_query = function(doc,cdt,cdn) { query: "erpnext.controllers.queries.customer_query" } } + +cur_frm.fields_dict['sales_order'].get_query = function(doc) { + return { + filters:{ + 'project_name': doc.name + } + } +} diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 6a143ab083..5c4f7d1719 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -7,19 +7,6 @@ "doctype": "DocType", "document_type": "Master", "fields": [ - { - "fieldname": "overview", - "fieldtype": "Section Break", - "label": "Overview", - "options": "icon-file", - "permlevel": 0 - }, - { - "fieldname": "cb_project_status", - "fieldtype": "Column Break", - "label": "Status", - "permlevel": 0 - }, { "description": "", "fieldname": "project_name", @@ -45,6 +32,23 @@ "reqd": 1, "search_index": 1 }, + { + "fieldname": "project_type", + "fieldtype": "Select", + "label": "Project Type", + "no_copy": 0, + "oldfieldname": "project_type", + "oldfieldtype": "Data", + "options": "Internal\nExternal\nOther", + "permlevel": 0, + "search_index": 0 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break", + "permlevel": 0, + "precision": "" + }, { "fieldname": "is_active", "fieldtype": "Select", @@ -69,16 +73,16 @@ "search_index": 0 }, { - "fieldname": "cb_project_dates", - "fieldtype": "Column Break", - "label": "Dates", - "permlevel": 0 + "fieldname": "section_break_12", + "fieldtype": "Section Break", + "permlevel": 0, + "precision": "" }, { - "fieldname": "project_start_date", + "fieldname": "expected_start_date", "fieldtype": "Date", "in_filter": 1, - "label": "Project Start Date", + "label": "Expected Start Date", "no_copy": 0, "oldfieldname": "project_start_date", "oldfieldtype": "Date", @@ -86,9 +90,15 @@ "search_index": 0 }, { - "fieldname": "completion_date", + "fieldname": "column_break_11", + "fieldtype": "Column Break", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "expected_end_date", "fieldtype": "Date", - "label": "Completion Date", + "label": "Expected End Date", "no_copy": 0, "oldfieldname": "completion_date", "oldfieldtype": "Date", @@ -96,25 +106,47 @@ "search_index": 0 }, { - "fieldname": "act_completion_date", - "fieldtype": "Date", - "label": "Actual Completion Date", - "no_copy": 0, - "oldfieldname": "act_completion_date", - "oldfieldtype": "Date", - "permlevel": 0, - "search_index": 0 + "fieldname": "customer_details", + "fieldtype": "Section Break", + "label": "", + "oldfieldtype": "Section Break", + "options": "icon-user", + "permlevel": 0 }, { - "fieldname": "project_type", - "fieldtype": "Select", - "label": "Project Type", + "fieldname": "customer", + "fieldtype": "Link", + "in_filter": 1, + "label": "Customer", "no_copy": 0, - "oldfieldname": "project_type", - "oldfieldtype": "Data", - "options": "Internal\nExternal\nOther", + "oldfieldname": "customer", + "oldfieldtype": "Link", + "options": "Customer", "permlevel": 0, - "search_index": 0 + "print_hide": 1, + "reqd": 0, + "search_index": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "permlevel": 0 + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "sales_order", + "fieldtype": "Link", + "label": "Sales Order", + "options": "Sales Order", + "permlevel": 0, + "precision": "" }, { "fieldname": "sb_milestones", @@ -130,7 +162,8 @@ "label": "Tasks", "options": "Project Task", "permlevel": 0, - "precision": "" + "precision": "", + "reqd": 0 }, { "fieldname": "percent_complete", @@ -159,25 +192,55 @@ "search_index": 0 }, { - "fieldname": "company", - "fieldtype": "Link", - "label": "Company", - "options": "Company", - "permlevel": 0 - }, - { - "fieldname": "project_details", + "fieldname": "section_break_18", "fieldtype": "Section Break", - "label": "Project Costing", - "oldfieldtype": "Section Break", - "options": "icon-money", - "permlevel": 0 + "permlevel": 0, + "precision": "" }, { - "fieldname": "project_value", + "fieldname": "actual_start_date", + "fieldtype": "Data", + "label": "Actual Start Date", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, + { + "fieldname": "actual_time", + "fieldtype": "Float", + "label": "Actual Time (in Hours)", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, + { + "fieldname": "column_break_20", + "fieldtype": "Column Break", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "actual_end_date", + "fieldtype": "Date", + "label": "Actual End Date", + "no_copy": 0, + "oldfieldname": "act_completion_date", + "oldfieldtype": "Date", + "permlevel": 0, + "read_only": 1, + "search_index": 0 + }, + { + "fieldname": "section_break_26", + "fieldtype": "Section Break", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "estimated_costing", "fieldtype": "Currency", "in_list_view": 1, - "label": "Project Value", + "label": "Estimated Costing", "no_copy": 0, "oldfieldname": "project_value", "oldfieldtype": "Currency", @@ -187,15 +250,10 @@ "search_index": 0 }, { - "fieldname": "est_material_cost", - "fieldtype": "Currency", - "label": "Estimated Material Cost", - "no_copy": 0, - "oldfieldname": "est_material_cost", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", + "fieldname": "column_break_22", + "fieldtype": "Column Break", "permlevel": 0, - "search_index": 0 + "precision": "" }, { "fieldname": "cost_center", @@ -205,64 +263,91 @@ "permlevel": 0 }, { - "fieldname": "column_break0", + "fieldname": "project_details", + "fieldtype": "Section Break", + "label": "", + "oldfieldtype": "Section Break", + "options": "icon-money", + "permlevel": 0 + }, + { + "description": "", + "fieldname": "total_costing_amount", + "fieldtype": "Currency", + "label": "Total Costing Amount (via Time Logs)", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, + { + "description": "", + "fieldname": "total_expense_claim", + "fieldtype": "Currency", + "label": "Total Expense Claim (via Expense Claims)", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, + { + "fieldname": "column_break_28", "fieldtype": "Column Break", - "label": "Margin", + "permlevel": 0, + "precision": "" + }, + { + "description": "", + "fieldname": "total_billing_amount", + "fieldtype": "Currency", + "label": "Total Billing Amount (via Time Logs)", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, + { + "fieldname": "margin", + "fieldtype": "Section Break", + "label": "", "oldfieldtype": "Column Break", "permlevel": 0, "width": "50%" }, { - "fieldname": "gross_margin_value", + "fieldname": "gross_margin", "fieldtype": "Currency", - "label": "Gross Margin Value", + "label": "Gross Margin", "no_copy": 0, "oldfieldname": "gross_margin_value", "oldfieldtype": "Currency", "options": "Company:company:default_currency", "permlevel": 0, + "read_only": 1, "reqd": 0, "search_index": 0 }, + { + "fieldname": "column_break_37", + "fieldtype": "Column Break", + "permlevel": 0, + "precision": "" + }, { "fieldname": "per_gross_margin", - "fieldtype": "Currency", + "fieldtype": "Percent", "label": "Gross Margin %", "no_copy": 0, "oldfieldname": "per_gross_margin", "oldfieldtype": "Currency", - "options": "Company:company:default_currency", + "options": "", "permlevel": 0, + "read_only": 1, "reqd": 0, "search_index": 0 - }, - { - "fieldname": "customer_details", - "fieldtype": "Section Break", - "label": "Customer Details", - "oldfieldtype": "Section Break", - "options": "icon-user", - "permlevel": 0 - }, - { - "fieldname": "customer", - "fieldtype": "Link", - "in_filter": 1, - "label": "Customer", - "no_copy": 0, - "oldfieldname": "customer", - "oldfieldtype": "Link", - "options": "Customer", - "permlevel": 0, - "print_hide": 1, - "reqd": 0, - "search_index": 1 } ], "icon": "icon-puzzle-piece", "idx": 1, "max_attachments": 4, - "modified": "2015-02-22 11:17:49.051755", + "modified": "2015-04-14 07:37:56.810833", "modified_by": "Administrator", "module": "Projects", "name": "Project", diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index c142180fa8..89cf523c1e 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -25,23 +25,19 @@ class Project(Document): "task_id": task.name }) - def get_gross_profit(self): - pft, per_pft =0, 0 - pft = flt(self.project_value) - flt(self.est_material_cost) - #if pft > 0: - per_pft = (flt(pft) / flt(self.project_value)) * 100 - ret = {'gross_margin_value': pft, 'per_gross_margin': per_pft} - return ret - def validate(self): - if self.project_start_date and self.completion_date: - if getdate(self.completion_date) < getdate(self.project_start_date): - frappe.throw(_("Expected Completion Date can not be less than Project Start Date")) - + self.validate_dates() self.sync_tasks() + def validate_dates(self): + if self.expected_start_date and self.expected_end_date: + if getdate(self.expected_end_date) < getdate(self.expected_start_date): + frappe.throw(_("Expected End Date can not be less than Expected Start Date")) + def sync_tasks(self): """sync tasks and remove table""" + if self.flags.dont_sync_tasks: return + task_names = [] for t in self.tasks: if t.task_id: @@ -66,7 +62,7 @@ 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) - + self.tasks = [] def update_percent_complete(self): @@ -77,7 +73,22 @@ class Project(Document): project=%s and status in ('Closed', 'Cancelled')""", self.name)[0][0] frappe.db.set_value("Project", self.name, "percent_complete", int(float(completed) / total * 100)) - + + def update_costing(self): + total_cost = frappe.db.sql("""select sum(total_costing_amount) as costing_amount, + sum(total_billing_amount) as billing_amount, sum(total_expense_claim) 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] + + 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 @frappe.whitelist() def get_cost_center_name(project_name): diff --git a/erpnext/projects/doctype/project/project_list.js b/erpnext/projects/doctype/project/project_list.js index 8281c7dbb9..b0d1ae8673 100644 --- a/erpnext/projects/doctype/project/project_list.js +++ b/erpnext/projects/doctype/project/project_list.js @@ -1,5 +1,5 @@ frappe.listview_settings['Project'] = { - add_fields: ["status", "priority", "is_active", "percent_complete", "completion_date"], + add_fields: ["status", "priority", "is_active", "percent_complete", "expected_end_date"], filters:[["status","=", "Open"]], get_indicator: function(doc) { if(doc.status=="Open" && doc.percent_complete) { diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 744d6b427b..f69ce80824 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -5,4 +5,3 @@ from __future__ import unicode_literals import frappe test_records = frappe.get_test_records('Project') -test_ignore = ["Task"] diff --git a/erpnext/projects/doctype/project/test_records.json b/erpnext/projects/doctype/project/test_records.json index 69226f0cc9..9379c22b5c 100644 --- a/erpnext/projects/doctype/project/test_records.json +++ b/erpnext/projects/doctype/project/test_records.json @@ -1,10 +1,12 @@ [ { - "project_name": "_Test Project", - "status": "Open" - }, - { - "project_name": "_Test Project 1", - "status": "Open" + "project_name": "_Test Project", + "status": "Open", + "tasks":[ + { + "title": "_Test Task", + "status": "Open" + } + ] } ] \ No newline at end of file diff --git a/erpnext/projects/doctype/project_task/project_task.json b/erpnext/projects/doctype/project_task/project_task.json index c29dcc05d6..f3d2c66d9b 100644 --- a/erpnext/projects/doctype/project_task/project_task.json +++ b/erpnext/projects/doctype/project_task/project_task.json @@ -37,7 +37,7 @@ "in_filter": 0, "in_list_view": 1, "label": "Status", - "no_copy": 0, + "no_copy": 1, "options": "Open\nWorking\nPending Review\nClosed\nCancelled", "permlevel": 0, "precision": "", @@ -51,7 +51,7 @@ { "fieldname": "edit_task", "fieldtype": "Button", - "label": "Edit Task", + "label": "View Task", "permlevel": 0, "precision": "" }, @@ -143,7 +143,7 @@ "is_submittable": 0, "issingle": 0, "istable": 1, - "modified": "2015-02-23 01:55:18.865117", + "modified": "2015-04-13 04:56:18.766659", "modified_by": "Administrator", "module": "Projects", "name": "Project Task", diff --git a/erpnext/projects/doctype/task/task.js b/erpnext/projects/doctype/task/task.js index 51aabca93c..975633186b 100644 --- a/erpnext/projects/doctype/task/task.js +++ b/erpnext/projects/doctype/task/task.js @@ -25,6 +25,19 @@ erpnext.projects.Task = frappe.ui.form.Controller.extend({ this.frm.doc.project && frappe.model.remove_from_locals("Project", this.frm.doc.project); }, + + refresh: function(doc) { + if(!doc.__islocal) { + cur_frm.add_custom_button(__("Time Logs"), function() { + frappe.route_options = {"project": doc.project, "task": doc.name} + frappe.set_route("List", "Time Log"); + }, "icon-list", true); + cur_frm.add_custom_button(__("Expense Claims"), function() { + frappe.route_options = {"project": doc.project, "task": doc.name} + frappe.set_route("List", "Expense Claim"); + }, "icon-list", true); + } + } }); diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json index e54783406c..fc604c264c 100644 --- a/erpnext/projects/doctype/task/task.json +++ b/erpnext/projects/doctype/task/task.json @@ -6,16 +6,6 @@ "doctype": "DocType", "document_type": "Master", "fields": [ - { - "fieldname": "task_details", - "fieldtype": "Section Break", - "label": "", - "oldfieldtype": "Section Break", - "permlevel": 0, - "print_width": "50%", - "search_index": 0, - "width": "50%" - }, { "fieldname": "subject", "fieldtype": "Data", @@ -28,24 +18,14 @@ "reqd": 1 }, { - "fieldname": "exp_start_date", - "fieldtype": "Date", - "label": "Expected Start Date", - "oldfieldname": "exp_start_date", - "oldfieldtype": "Date", - "permlevel": 0, - "reqd": 0 - }, - { - "fieldname": "exp_end_date", - "fieldtype": "Date", - "in_filter": 1, - "label": "Expected End Date", - "oldfieldname": "exp_end_date", - "oldfieldtype": "Date", - "permlevel": 0, - "reqd": 0, - "search_index": 1 + "fieldname": "project", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Project", + "oldfieldname": "project", + "oldfieldtype": "Link", + "options": "Project", + "permlevel": 0 }, { "fieldname": "column_break0", @@ -66,16 +46,6 @@ "options": "Open\nWorking\nPending Review\nClosed\nCancelled", "permlevel": 0 }, - { - "fieldname": "project", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Project", - "oldfieldname": "project", - "oldfieldtype": "Link", - "options": "Project", - "permlevel": 0 - }, { "fieldname": "priority", "fieldtype": "Select", @@ -110,41 +80,52 @@ { "fieldname": "time_and_budget", "fieldtype": "Section Break", - "label": "Time and Budget", + "label": "", "oldfieldtype": "Section Break", "permlevel": 0 }, { - "fieldname": "expected", - "fieldtype": "Column Break", - "label": "Expected", - "oldfieldtype": "Column Break", + "fieldname": "exp_start_date", + "fieldtype": "Date", + "label": "Expected Start Date", + "oldfieldname": "exp_start_date", + "oldfieldtype": "Date", "permlevel": 0, - "print_width": "50%", - "width": "50%" + "reqd": 0 }, { - "fieldname": "exp_total_hrs", - "fieldtype": "Data", - "label": "Total Hours (Expected)", + "default": "0", + "description": "", + "fieldname": "expected_time", + "fieldtype": "Float", + "label": "Expected Time (in hours)", "oldfieldname": "exp_total_hrs", "oldfieldtype": "Data", "permlevel": 0, "reqd": 0 }, { - "fieldname": "allocated_budget", - "fieldtype": "Currency", - "label": "Allocated Budget", - "oldfieldname": "allocated_budget", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "permlevel": 0 + "fieldname": "column_break_11", + "fieldtype": "Column Break", + "permlevel": 0, + "precision": "" }, { + "fieldname": "exp_end_date", + "fieldtype": "Date", + "in_filter": 1, + "label": "Expected End Date", + "oldfieldname": "exp_end_date", + "oldfieldtype": "Date", + "permlevel": 0, + "reqd": 0, + "search_index": 1 + }, + { + "description": "", "fieldname": "actual", - "fieldtype": "Column Break", - "label": "Actual", + "fieldtype": "Section Break", + "label": "", "oldfieldtype": "Column Break", "permlevel": 0, "print_width": "50%", @@ -153,27 +134,77 @@ { "fieldname": "act_start_date", "fieldtype": "Date", - "label": "Actual Start Date", + "label": "Actual Start Date (via Time Logs)", "oldfieldname": "act_start_date", "oldfieldtype": "Date", - "permlevel": 0 + "permlevel": 0, + "read_only": 1 + }, + { + "default": "", + "description": "", + "fieldname": "actual_time", + "fieldtype": "Float", + "label": "Actual Time (in hours)", + "options": "", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, + { + "fieldname": "column_break_15", + "fieldtype": "Column Break", + "permlevel": 0, + "precision": "" }, { "fieldname": "act_end_date", "fieldtype": "Date", - "label": "Actual End Date", + "label": "Actual End Date (via Time Logs)", "oldfieldname": "act_end_date", "oldfieldtype": "Date", - "permlevel": 0 + "permlevel": 0, + "read_only": 1 }, { - "fieldname": "actual_budget", + "fieldname": "section_break_17", + "fieldtype": "Section Break", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "total_costing_amount", "fieldtype": "Currency", - "label": "Actual Budget", + "label": "Total Costing Amount (via Time Logs)", "oldfieldname": "actual_budget", "oldfieldtype": "Currency", "options": "Company:company:default_currency", - "permlevel": 0 + "permlevel": 0, + "read_only": 1 + }, + { + "fieldname": "total_expense_claim", + "fieldtype": "Currency", + "label": "Total Expense Claim (via Expense Claim)", + "options": "Company:company:default_currency", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, + { + "fieldname": "column_break_20", + "fieldtype": "Column Break", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "total_billing_amount", + "fieldtype": "Currency", + "hidden": 0, + "label": "Total Billing Amount (via Time Logs)", + "permlevel": 0, + "precision": "", + "read_only": 1 }, { "fieldname": "more_details", @@ -216,8 +247,9 @@ ], "icon": "icon-check", "idx": 1, + "istable": 0, "max_attachments": 5, - "modified": "2015-02-20 05:09:27.295024", + "modified": "2015-04-14 07:56:24.481667", "modified_by": "Administrator", "module": "Projects", "name": "Task", diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index acd087768d..a03340f7de 100644 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe, json -from frappe.utils import getdate, today +from frappe.utils import getdate from frappe import _ @@ -26,27 +26,48 @@ class Task(Document): return ret def validate(self): + self.validate_dates() + + def validate_dates(self): if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date): frappe.throw(_("'Expected Start Date' can not be greater than 'Expected End Date'")) if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date): frappe.throw(_("'Actual Start Date' can not be greater than 'Actual End Date'")) - self.update_status() - - def update_status(self): - status = frappe.db.get_value("Task", self.name, "status") - if self.status=="Working" and status !="Working" and not self.act_start_date: - self.act_start_date = today() - - if self.status=="Closed" and status != "Closed" and not self.act_end_date: - self.act_end_date = today() - def on_update(self): + self.update_percentage() + self.update_project() + + def update_percentage(self): """update percent complete in project""" if self.project and not self.flags.from_project: project = frappe.get_doc("Project", self.project) project.run_method("update_percent_complete") + + def update_total_expense_claim(self): + self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim` + where project = %s and task = %s and approval_status = "Approved" and docstatus=1""",(self.project, self.name)) + + 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(hours) as time from `tabTime Log` where project = %s and task = %s and docstatus=1""", + (self.project, self.name),as_dict=1)[0] + if self.status == "Open": + self.status = "Working" + self.total_costing_amount= tl.total_costing_amount + self.total_billing_amount= tl.total_billing_amount + self.actual_time= tl.time + self.act_start_date= tl.start_date + self.act_end_date= tl.end_date + + def update_project(self): + if self.project and frappe.db.exists("Project", self.project): + project = frappe.get_doc("Project", self.project) + project.flags.dont_sync_tasks = True + project.update_costing() + project.save() @frappe.whitelist() def get_events(start, end, filters=None): diff --git a/erpnext/projects/doctype/task/test_records.json b/erpnext/projects/doctype/task/test_records.json index 1f98172b70..42ca0e77ea 100644 --- a/erpnext/projects/doctype/task/test_records.json +++ b/erpnext/projects/doctype/task/test_records.json @@ -1,8 +1,8 @@ [ { - "project": "_Test Project", "status": "Open", - "subject": "_Test Task" + "subject": "_Test Task", + "name": "task001" }, { "status": "Open", diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index 6f75e0deb8..62e560fbe7 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -5,5 +5,3 @@ from __future__ import unicode_literals import frappe test_records = frappe.get_test_records('Task') -test_dependencies = ["Project"] -test_ignore = ["Customer"] diff --git a/erpnext/projects/doctype/time_log/test_records.json b/erpnext/projects/doctype/time_log/test_records.json index d9e67e954a..568c0121a7 100644 --- a/erpnext/projects/doctype/time_log/test_records.json +++ b/erpnext/projects/doctype/time_log/test_records.json @@ -5,7 +5,6 @@ "doctype": "Time Log", "from_time": "2013-01-01 10:00:00.000000", "note": "_Test Note", - "to_time": "2013-01-01 11:00:00.000000", - "project": "_Test Project" + "to_time": "2013-01-01 11:00:00.000000" } ] diff --git a/erpnext/projects/doctype/time_log/test_time_log.py b/erpnext/projects/doctype/time_log/test_time_log.py index 8f8e31d01a..3d9e0be5f9 100644 --- a/erpnext/projects/doctype/time_log/test_time_log.py +++ b/erpnext/projects/doctype/time_log/test_time_log.py @@ -10,7 +10,6 @@ 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 NotInWorkingHoursError -from erpnext.projects.doctype.time_log_batch.test_time_log_batch import * from erpnext.manufacturing.doctype.production_order.test_production_order import make_prod_order_test_record @@ -85,6 +84,72 @@ class TestTimeLog(unittest.TestCase): test_time_log.to_time = "2013-01-01 10:00:00.000000" self.assertRaises(frappe.ValidationError, test_time_log.save) frappe.db.sql("delete from `tabTime Log`") - + + 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 `tabActivity Cost` where employee = "_T-Employee-0001" and activity_type = "_Test Activity Type" """) + + activity_cost = frappe.new_doc('Activity Cost') + activity_cost.update({ + "employee": "_T-Employee-0001", + "employee_name": "_Test Employee", + "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 = frappe.get_doc({ + "activity_type": "_Test Activity Type", + "docstatus": 1, + "doctype": "Time Log", + "from_time": "2013-02-02 09:00:00.000000", + "to_time": "2013-02-02 11:00:00.000000", + "employee": "_T-Employee-0001", + "project": "_Test Project 1", + "task": task_name, + "billable": 1 + }) + time_log.save() + 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) + time_log.submit() + + 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 = frappe.get_doc({ + "activity_type": "_Test Activity Type", + "docstatus": 1, + "doctype": "Time Log", + "from_time": "2013-02-03 09:00:00.000000", + "to_time": "2013-02-03 11:00:00.000000", + "employee": "_T-Employee-0001", + "project": "_Test Project 1", + "task": task_name, + "billable": 1 + }) + time_log2.save() + + 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) + test_records = frappe.get_test_records('Time Log') test_ignore = ["Time Log Batch", "Sales Invoice"] diff --git a/erpnext/projects/doctype/time_log/time_log.js b/erpnext/projects/doctype/time_log/time_log.js index a49b1f1c04..ba933018ef 100644 --- a/erpnext/projects/doctype/time_log/time_log.js +++ b/erpnext/projects/doctype/time_log/time_log.js @@ -4,7 +4,6 @@ frappe.provide("erpnext.projects"); frappe.ui.form.on("Time Log", "onload", function(frm) { - frm.set_query("task", erpnext.queries.task); if (frm.doc.for_manufacturing) { frappe.ui.form.trigger("Time Log", "production_order"); } @@ -41,4 +40,60 @@ frappe.ui.form.on("Time Log", "to_time", function(frm) { if(frm._setting_hours) return; frm.set_value("hours", moment(cur_frm.doc.to_time).diff(moment(cur_frm.doc.from_time), "hours")); -}); \ No newline at end of file + +}); + +var calculate_cost = function(frm) { + frm.set_value("costing_amount", frm.doc.costing_rate * frm.doc.hours); + if (frm.doc.billable==1){ + frm.set_value("billing_amount", frm.doc.billing_rate * frm.doc.hours); + } +} + +var get_activity_cost = function(frm) { + if (frm.doc.employee && frm.doc.activity_type){ + return frappe.call({ + method: "erpnext.projects.doctype.time_log.time_log.get_activity_cost", + args: { + "employee": frm.doc.employee, + "activity_type": frm.doc.activity_type + }, + callback: function(r) { + if(!r.exc && r.message) { + frm.set_value("costing_rate", r.message.costing_rate); + frm.set_value("billing_rate", r.message.billing_rate); + calculate_cost(frm); + } + } + }); + } +} + +frappe.ui.form.on("Time Log", "hours", function(frm) { + calculate_cost(frm); +}); + +frappe.ui.form.on("Time Log", "activity_type", function(frm) { + get_activity_cost(frm); +}); + +frappe.ui.form.on("Time Log", "employee", function(frm) { + get_activity_cost(frm); +}); + +frappe.ui.form.on("Time Log", "billable", function(frm) { + if (frm.doc.billable==1) { + calculate_cost(frm); + } + else { + frm.set_value("billing_amount", 0); + } +}); + +cur_frm.fields_dict['task'].get_query = function(doc) { + return { + filters:{ + 'project': doc.project + } + } +} diff --git a/erpnext/projects/doctype/time_log/time_log.json b/erpnext/projects/doctype/time_log/time_log.json index 20b5c0c296..e9e2077225 100644 --- a/erpnext/projects/doctype/time_log/time_log.json +++ b/erpnext/projects/doctype/time_log/time_log.json @@ -17,44 +17,10 @@ "reqd": 1 }, { - "fieldname": "from_time", - "fieldtype": "Datetime", - "in_list_view": 0, - "label": "From Time", - "permlevel": 0, - "read_only": 0, - "reqd": 1 - }, - { - "fieldname": "to_time", - "fieldtype": "Datetime", - "in_list_view": 0, - "label": "To Time", - "permlevel": 0, - "read_only": 0, - "reqd": 1 - }, - { - "fieldname": "hours", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Hours", - "permlevel": 0, - "read_only": 0 - }, - { - "fieldname": "user", - "fieldtype": "Link", - "label": "User", - "options": "User", - "permlevel": 0, - "precision": "" - }, - { - "fieldname": "column_break_3", + "fieldname": "column_break_2", "fieldtype": "Column Break", "permlevel": 0, - "read_only": 0 + "precision": "" }, { "fieldname": "status", @@ -67,20 +33,62 @@ "reqd": 0 }, { - "fieldname": "billable", - "fieldtype": "Check", + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "from_time", + "fieldtype": "Datetime", "in_list_view": 0, - "label": "Billable", + "label": "From Time", + "permlevel": 0, + "read_only": 0, + "reqd": 1 + }, + { + "default": "0", + "fieldname": "hours", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Hours", "permlevel": 0, "read_only": 0 }, { - "fieldname": "for_manufacturing", - "fieldtype": "Check", - "label": "For Manufacturing", + "fieldname": "to_time", + "fieldtype": "Datetime", + "in_list_view": 0, + "label": "To Time", "permlevel": 0, - "precision": "", - "read_only": 1 + "read_only": 0, + "reqd": 1 + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break", + "permlevel": 0, + "precision": "" + }, + { + "depends_on": "", + "fieldname": "project", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Project", + "options": "Project", + "permlevel": 0, + "read_only": 0 + }, + { + "depends_on": "", + "fieldname": "task", + "fieldtype": "Link", + "label": "Task", + "options": "Task", + "permlevel": 0, + "read_only": 0 }, { "depends_on": "eval:!doc.for_manufacturing", @@ -94,11 +102,47 @@ "reqd": 0 }, { - "depends_on": "eval:!doc.for_manufacturing", - "fieldname": "task", + "fieldname": "section_break_12", + "fieldtype": "Section Break", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "user", "fieldtype": "Link", - "label": "Task", - "options": "Task", + "label": "User", + "options": "User", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "employee", + "fieldtype": "Link", + "label": "Employee", + "options": "Employee", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break", + "permlevel": 0, + "read_only": 0 + }, + { + "fieldname": "for_manufacturing", + "fieldtype": "Check", + "hidden": 1, + "label": "For Manufacturing", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, + { + "fieldname": "billable", + "fieldtype": "Check", + "in_list_view": 0, + "label": "Billable", "permlevel": 0, "read_only": 0 }, @@ -180,20 +224,62 @@ "read_only": 0 }, { - "fieldname": "section_break_9", + "depends_on": "", + "fieldname": "section_break_24", "fieldtype": "Section Break", "permlevel": 0, - "read_only": 0 + "precision": "" }, { - "depends_on": "", - "fieldname": "project", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Project", - "options": "Project", + "default": "0", + "description": "", + "fieldname": "costing_rate", + "fieldtype": "Currency", + "label": "Costing Rate (per hour)", "permlevel": 0, - "read_only": 0 + "precision": "", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "costing_amount", + "fieldtype": "Currency", + "label": "Costing Amount", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, + { + "fieldname": "column_break_25", + "fieldtype": "Column Break", + "permlevel": 0, + "precision": "" + }, + { + "default": "0", + "description": "", + "fieldname": "billing_rate", + "fieldtype": "Currency", + "label": "Billing Rate (per hour)", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, + { + "default": "0", + "description": "Will be updated only if Time Log is 'Billable'", + "fieldname": "billing_amount", + "fieldtype": "Currency", + "label": "Billing Amount", + "permlevel": 0, + "precision": "", + "read_only": 1 + }, + { + "fieldname": "section_break_29", + "fieldtype": "Section Break", + "permlevel": 0, + "precision": "" }, { "description": "Will be updated when batched.", @@ -213,12 +299,6 @@ "permlevel": 0, "read_only": 1 }, - { - "fieldname": "column_break_16", - "fieldtype": "Column Break", - "permlevel": 0, - "read_only": 0 - }, { "fieldname": "amended_from", "fieldtype": "Link", @@ -242,7 +322,7 @@ "icon": "icon-time", "idx": 1, "is_submittable": 1, - "modified": "2015-04-06 02:47:16.187046", + "modified": "2015-04-14 09:07:28.468792", "modified_by": "Administrator", "module": "Projects", "name": "Time Log", diff --git a/erpnext/projects/doctype/time_log/time_log.py b/erpnext/projects/doctype/time_log/time_log.py index c385c0935b..f7b501d451 100644 --- a/erpnext/projects/doctype/time_log/time_log.py +++ b/erpnext/projects/doctype/time_log/time_log.py @@ -24,12 +24,16 @@ class TimeLog(Document): self.check_workstation_timings() self.validate_production_order() self.validate_manufacturing() + self.validate_task() + self.update_cost() def on_submit(self): self.update_production_order() + self.update_task() def on_cancel(self): self.update_production_order() + self.update_task() def before_update_after_submit(self): self.set_status() @@ -63,6 +67,7 @@ class TimeLog(Document): def validate_overlap(self): """Checks if 'Time Log' entries overlap for a user, workstation. """ self.validate_overlap_for("user") + self.validate_overlap_for("employee") self.validate_overlap_for("workstation") def validate_overlap_for(self, fieldname): @@ -123,8 +128,8 @@ class TimeLog(Document): def update_production_order(self): """Updates `start_date`, `end_date`, `status` for operation in Production Order.""" - - if self.for_manufacturing and self.production_order: + + if self.production_order and self.for_manufacturing: if not self.operation_id: frappe.throw(_("Operation ID not set")) @@ -205,6 +210,27 @@ class TimeLog(Document): self.production_order = None self.operation = None self.quantity = None + + 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_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 self.project and not self.task: + frappe.throw(_("Task is Mandatory if Time Log is against a project")) + + def update_task(self): + if self.task and frappe.db.exists("Task", self.task): + task = frappe.get_doc("Task", self.task) + task.update_time_and_costing() + task.save() @frappe.whitelist() def get_events(start, end, filters=None): @@ -242,3 +268,9 @@ def get_events(start, end, filters=None): d.title += " for Project: " + d.project return data + +@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) + return rate[0] if rate else {} diff --git a/erpnext/projects/doctype/time_log/time_log_list.js b/erpnext/projects/doctype/time_log/time_log_list.js index d4448068fa..a2eb05cfc9 100644 --- a/erpnext/projects/doctype/time_log/time_log_list.js +++ b/erpnext/projects/doctype/time_log/time_log_list.js @@ -3,7 +3,7 @@ // render frappe.listview_settings['Time Log'] = { - add_fields: ["status", "billable", "activity_type", "task", "project", "hours", "for_manufacturing"], + add_fields: ["status", "billable", "activity_type", "task", "project", "hours", "for_manufacturing", "billing_amount"], selectable: true, onload: function(me) { me.page.add_menu_item(__("Make Time Log Batch"), function() { @@ -37,7 +37,7 @@ frappe.listview_settings['Time Log'] = { $.extend(detail, { "time_log": d.name, "activity_type": d.activity_type, - "created_by": d.owner, + "billing_amount": d.billing_amount, "hours": d.hours }); }) diff --git a/erpnext/projects/doctype/time_log_batch/test_time_log_batch.py b/erpnext/projects/doctype/time_log_batch/test_time_log_batch.py index 240ca97926..faa0a601e3 100644 --- a/erpnext/projects/doctype/time_log_batch/test_time_log_batch.py +++ b/erpnext/projects/doctype/time_log_batch/test_time_log_batch.py @@ -44,7 +44,6 @@ def create_time_log(): def create_time_log_batch(time_log): tlb = frappe.get_doc({ "doctype": "Time Log Batch", - "rate": "500", "time_logs": [ { "doctype": "Time Log Batch Detail", diff --git a/erpnext/projects/doctype/time_log_batch/time_log_batch.js b/erpnext/projects/doctype/time_log_batch/time_log_batch.js index 0635db09b9..6b5f08094d 100644 --- a/erpnext/projects/doctype/time_log_batch/time_log_batch.js +++ b/erpnext/projects/doctype/time_log_batch/time_log_batch.js @@ -2,7 +2,7 @@ // License: GNU General Public License v3. See license.txt 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", "billing_amount", "billing_amount"); cur_frm.add_fetch("time_log", "hours", "hours"); cur_frm.set_query("time_log", "time_logs", function(doc) { @@ -36,3 +36,15 @@ $.extend(cur_frm.cscript, { }); } }); + +frappe.ui.form.on("Time Log Batch Detail", "time_log", function(frm, cdt, cdn) { + var tl = frm.doc.time_logs || []; + total_hr = 0; + total_amt = 0; + for(var i=0; i