diff --git a/erpnext/change_log/current/activity_type.md b/erpnext/change_log/current/activity_type.md new file mode 100644 index 0000000000..1fa158f918 --- /dev/null +++ b/erpnext/change_log/current/activity_type.md @@ -0,0 +1,2 @@ +- Set default costing rate and billing rate in **Activity Type** +- Task not mandatory in **Time Log** and **Expense Claim** diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.json b/erpnext/hr/doctype/expense_claim/expense_claim.json index 2bd20b88bc..f4e4bae683 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.json +++ b/erpnext/hr/doctype/expense_claim/expense_claim.json @@ -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,7 +536,7 @@ "is_submittable": 1, "issingle": 0, "istable": 0, - "modified": "2015-05-02 07:42:25.202983", + "modified": "2015-08-31 08:48:32.488942", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim", diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 112a626bf6..64be65e4dd 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -20,22 +20,25 @@ 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) 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 +48,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 + "%")) \ No newline at end of file + """, ("%" + txt + "%")) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5f7dd91a50..0941b4fc1f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -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 diff --git a/erpnext/patches/v6_0/default_activity_rate.py b/erpnext/patches/v6_0/default_activity_rate.py new file mode 100644 index 0000000000..90a7a8c338 --- /dev/null +++ b/erpnext/patches/v6_0/default_activity_rate.py @@ -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) diff --git a/erpnext/projects/doctype/activity_cost/activity_cost.json b/erpnext/projects/doctype/activity_cost/activity_cost.json index f6898f84dc..88f91162eb 100644 --- a/erpnext/projects/doctype/activity_cost/activity_cost.json +++ b/erpnext/projects/doctype/activity_cost/activity_cost.json @@ -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", diff --git a/erpnext/projects/doctype/activity_type/activity_type.js b/erpnext/projects/doctype/activity_type/activity_type.js new file mode 100644 index 0000000000..f7bb9a5222 --- /dev/null +++ b/erpnext/projects/doctype/activity_type/activity_type.js @@ -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"); + }); + } +}); diff --git a/erpnext/projects/doctype/activity_type/activity_type.json b/erpnext/projects/doctype/activity_type/activity_type.json index 63aa0886e2..4cb6c43db9 100644 --- a/erpnext/projects/doctype/activity_type/activity_type.json +++ b/erpnext/projects/doctype/activity_type/activity_type.json @@ -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", diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index de1c3f83f4..277ffdb982 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -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,28 @@ 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)) + from `tabExpense Claim` where project = %s""", 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 diff --git a/erpnext/projects/doctype/project/project_list.js b/erpnext/projects/doctype/project/project_list.js index b0d1ae8673..437bf607ed 100644 --- a/erpnext/projects/doctype/project/project_list.js +++ b/erpnext/projects/doctype/project/project_list.js @@ -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]; } diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 28cfcd3050..e5b3355db4 100644 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -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 diff --git a/erpnext/projects/doctype/time_log/test_time_log.py b/erpnext/projects/doctype/time_log/test_time_log.py index 9bdf5886f7..1894e37763 100644 --- a/erpnext/projects/doctype/time_log/test_time_log.py +++ b/erpnext/projects/doctype/time_log/test_time_log.py @@ -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 \ No newline at end of file + return time_log diff --git a/erpnext/projects/doctype/time_log/time_log.js b/erpnext/projects/doctype/time_log/time_log.js index 776a75b3cc..5fce97009d 100644 --- a/erpnext/projects/doctype/time_log/time_log.js +++ b/erpnext/projects/doctype/time_log/time_log.js @@ -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); }); diff --git a/erpnext/projects/doctype/time_log/time_log.json b/erpnext/projects/doctype/time_log/time_log.json index e349859dbc..76bc0ef12a 100644 --- a/erpnext/projects/doctype/time_log/time_log.json +++ b/erpnext/projects/doctype/time_log/time_log.json @@ -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", diff --git a/erpnext/projects/doctype/time_log/time_log.py b/erpnext/projects/doctype/time_log/time_log.py index ed8930701b..cde733d2ff 100644 --- a/erpnext/projects/doctype/time_log/time_log.py +++ b/erpnext/projects/doctype/time_log/time_log.py @@ -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 {}