From aa0bed16f6a8cc311c8c870d9691e8988bb2c7fd Mon Sep 17 00:00:00 2001 From: Pawan Mehta Date: Mon, 12 Nov 2018 16:40:07 +0530 Subject: [PATCH] [fix] #14038-Std hours at company level to calculate timesheet hours (#15819) * [fix] #14038 * codacy fixes * add end time calc method * test case and rename function * Update timesheet.py --- .../doctype/timesheet/test_timesheet.py | 44 +++++++ .../projects/doctype/timesheet/timesheet.js | 45 +++++-- .../projects/doctype/timesheet/timesheet.py | 14 +- erpnext/setup/doctype/company/company.json | 123 +++++++++++------- 4 files changed, 169 insertions(+), 57 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index 9f1c58601d..8c84c11ae9 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -128,6 +128,50 @@ class TestTimesheet(unittest.TestCase): settings.ignore_employee_time_overlap = initial_setting settings.save() + def test_timesheet_std_working_hours(self): + company = frappe.get_doc('Company', "_Test Company") + company.standard_working_hours = 8 + company.save() + + timesheet = frappe.new_doc("Timesheet") + timesheet.employee = "_T-Employee-00001" + timesheet.company = '_Test Company' + timesheet.append( + 'time_logs', + { + "activity_type": "_Test Activity Type", + "from_time": now_datetime(), + "to_time": now_datetime() + datetime.timedelta(days= 4) + } + ) + timesheet.save() + + ts = frappe.get_doc('Timesheet', timesheet.name) + self.assertEqual(ts.total_hours, 32) + ts.submit() + ts.cancel() + + company = frappe.get_doc('Company', "_Test Company") + company.standard_working_hours = 0 + company.save() + + timesheet = frappe.new_doc("Timesheet") + timesheet.employee = "_T-Employee-00001" + timesheet.company = '_Test Company' + timesheet.append( + 'time_logs', + { + "activity_type": "_Test Activity Type", + "from_time": now_datetime(), + "to_time": now_datetime() + datetime.timedelta(days= 4) + } + ) + timesheet.save() + + ts = frappe.get_doc('Timesheet', timesheet.name) + self.assertEqual(ts.total_hours, 96) + ts.submit() + ts.cancel() def make_salary_structure_for_timesheet(employee): salary_structure_name = "Timesheet Salary Structure Test" diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index 5234df67ff..e890befd1a 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -90,6 +90,13 @@ frappe.ui.form.on("Timesheet", { } }, + company: function(frm) { + frappe.db.get_value('Company', { 'company_name' : frm.doc.company }, 'standard_working_hours') + .then(({ message }) => { + (frappe.working_hours = message.standard_working_hours || 0); + }); + }, + make_invoice: function(frm) { let dialog = new frappe.ui.Dialog({ title: __("Select Item (optional)"), @@ -142,11 +149,21 @@ frappe.ui.form.on("Timesheet Detail", { to_time: function(frm, cdt, cdn) { var child = locals[cdt][cdn]; + var time_diff = (moment(child.to_time).diff(moment(child.from_time),"seconds")) / ( 60 * 60 * 24); + var std_working_hours = 0; if(frm._setting_hours) return; - frappe.model.set_value(cdt, cdn, "hours", moment(child.to_time).diff(moment(child.from_time), - "seconds") / 3600); + + var hours = moment(child.to_time).diff(moment(child.from_time), "seconds") / 3600; + std_working_hours = time_diff * frappe.working_hours; + + if (std_working_hours < hours && std_working_hours > 0) { + frappe.model.set_value(cdt, cdn, "hours", std_working_hours); + } else { + frappe.model.set_value(cdt, cdn, "hours", hours); + } }, + time_logs_add: function(frm) { var $trigger_again = $('.form-grid').find('.grid-row').find('.btn-open-row'); $trigger_again.on('click', () => { @@ -209,17 +226,23 @@ var calculate_end_time = function(frm, cdt, cdn) { let d = moment(child.from_time); if(child.hours) { - d.add(child.hours, "hours"); - frm._setting_hours = true; - frappe.model.set_value(cdt, cdn, "to_time", - d.format(frappe.defaultDatetimeFormat)).then(() => { - frm._setting_hours = false; - }); - } + var time_diff = (moment(child.to_time).diff(moment(child.from_time),"seconds")) / (60 * 60 * 24); + var std_working_hours = 0; + var hours = moment(child.to_time).diff(moment(child.from_time), "seconds") / 3600; + std_working_hours = time_diff * frappe.working_hours; - if((frm.doc.__islocal || frm.doc.__onload.maintain_bill_work_hours_same) && child.hours){ - frappe.model.set_value(cdt, cdn, "billing_hours", child.hours); + if (std_working_hours < hours && std_working_hours > 0) { + frappe.model.set_value(cdt, cdn, "hours", std_working_hours); + frappe.model.set_value(cdt, cdn, "to_time", d.add(hours, "hours").format(frappe.defaultDatetimeFormat)); + } else { + d.add(child.hours, "hours"); + frm._setting_hours = true; + frappe.model.set_value(cdt, cdn, "to_time", + d.format(frappe.defaultDatetimeFormat)).then(() => { + frm._setting_hours = false; + }); + } } } diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index f48c0c634b..4b466d2630 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -9,7 +9,7 @@ from frappe import _ import json from datetime import timedelta from erpnext.controllers.queries import get_match_cond -from frappe.utils import flt, time_diff_in_hours, get_datetime, getdate, cint +from frappe.utils import flt, time_diff_in_hours, get_datetime, getdate, cint, date_diff, add_to_date from frappe.model.document import Document from erpnext.manufacturing.doctype.workstation.workstation import (check_if_within_operating_hours, WorkstationHolidayError) @@ -27,6 +27,7 @@ class Timesheet(Document): self.set_status() self.validate_dates() self.validate_time_logs() + self.calculate_std_hours() self.update_cost() self.calculate_total_amounts() self.calculate_percentage_billed() @@ -93,6 +94,17 @@ class Timesheet(Document): self.start_date = getdate(start_date) self.end_date = getdate(end_date) + def calculate_std_hours(self): + std_working_hours = frappe.get_value("Company", self.company, 'standard_working_hours') + + for time in self.time_logs: + if time.from_time and time.to_time: + if flt(std_working_hours) > 0: + time.hours = flt(std_working_hours) * date_diff(time.to_time, time.from_time) + else: + if not time.hours: + time.hours = time_diff_in_hours(time.to_time, time.from_time) + def before_cancel(self): self.set_status() diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 9377cad143..01f8956a82 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 1, "allow_rename": 1, @@ -723,6 +724,38 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "standard_working_hours", + "fieldtype": "Float", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Standard Working Hours", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -837,7 +870,7 @@ "label": "Create Chart Of Accounts Based On", "length": 0, "no_copy": 0, - "options": "\nStandard Template\nExisting Company", + "options": "\nStandard Template\nExisting Company", "permlevel": 0, "precision": "", "print_hide": 0, @@ -871,7 +904,7 @@ "label": "Chart Of Accounts Template", "length": 0, "no_copy": 1, - "options": "", + "options": "", "permlevel": 0, "precision": "", "print_hide": 0, @@ -1158,39 +1191,39 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "round_off_cost_center", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Round Off Cost Center", - "length": 0, - "no_copy": 0, - "options": "Cost Center", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "fieldname": "round_off_cost_center", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Round Off Cost Center", + "length": 0, + "no_copy": 0, + "options": "Cost Center", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, "fieldname": "write_off_account", "fieldtype": "Link", "hidden": 0, @@ -1491,7 +1524,7 @@ "collapsible": 0, "columns": 0, "depends_on": "eval:!doc.__islocal", - "fieldname": "default_deferred_expense_account", + "fieldname": "default_deferred_expense_account", "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 1, @@ -1500,7 +1533,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Default Deferred Expense Account", + "label": "Default Deferred Expense Account", "length": 0, "no_copy": 1, "options": "Account", @@ -1525,7 +1558,7 @@ "collapsible": 0, "columns": 0, "depends_on": "eval:!doc.__islocal", - "fieldname": "default_payroll_payable_account", + "fieldname": "default_payroll_payable_account", "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 1, @@ -1534,7 +1567,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Default Payroll Payable Account", + "label": "Default Payroll Payable Account", "length": 0, "no_copy": 1, "options": "Account", @@ -1558,20 +1591,20 @@ "bold": 0, "collapsible": 0, "columns": 0, - "depends_on": "eval:!doc.__islocal", - "fieldname": "default_expense_claim_payable_account", + "depends_on": "eval:!doc.__islocal", + "fieldname": "default_expense_claim_payable_account", "fieldtype": "Link", "hidden": 0, - "ignore_user_permissions": 1, + "ignore_user_permissions": 1, "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Default Expense Claim Payable Account", + "label": "Default Expense Claim Payable Account", "length": 0, - "no_copy": 1, - "options": "Account", + "no_copy": 1, + "options": "Account", "permlevel": 0, "precision": "", "print_hide": 0, @@ -2870,8 +2903,8 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2018-09-13 10:00:47.915706", - "modified_by": "Administrator", + "modified": "2018-10-24 12:57:46.776452", + "modified_by": "Administrator", "module": "Setup", "name": "Company", "owner": "Administrator",