From b7f2ba1556fc800526cc90e9b5a0f277441d39d9 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Tue, 30 Mar 2021 12:19:16 +0530 Subject: [PATCH 01/20] feat: Add Report boilerplate --- .../__init__.py | 0 ...ee_hours_utilisation_based_on_timesheet.js | 9 ++++++++ ..._hours_utilisation_based_on_timesheet.json | 22 +++++++++++++++++++ ...ee_hours_utilisation_based_on_timesheet.py | 9 ++++++++ 4 files changed, 40 insertions(+) create mode 100644 erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/__init__.py create mode 100644 erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.js create mode 100644 erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.json create mode 100644 erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py diff --git a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/__init__.py b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.js b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.js new file mode 100644 index 00000000000..f800877e38e --- /dev/null +++ b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.js @@ -0,0 +1,9 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Employee Hours Utilisation Based On Timesheet"] = { + "filters": [ + + ] +}; diff --git a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.json b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.json new file mode 100644 index 00000000000..1b504e011d2 --- /dev/null +++ b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.json @@ -0,0 +1,22 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-03-30 12:18:02.090630", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-03-30 12:18:02.090630", + "modified_by": "Administrator", + "module": "Projects", + "name": "Employee Hours Utilisation Based On Timesheet", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Timesheet", + "report_name": "Employee Hours Utilisation Based On Timesheet", + "report_type": "Script Report", + "roles": [] +} \ No newline at end of file diff --git a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py new file mode 100644 index 00000000000..de2aa1ff416 --- /dev/null +++ b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py @@ -0,0 +1,9 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe + +def execute(filters=None): + columns, data = [], [] + return columns, data From 2b85a81a1ca8d42b3ef9fd5e85aeab4d7d566f50 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Tue, 30 Mar 2021 13:57:44 +0530 Subject: [PATCH 02/20] feat: Add filters to front end --- ...ee_hours_utilisation_based_on_timesheet.js | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.js b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.js index f800877e38e..e9b817ab673 100644 --- a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.js +++ b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.js @@ -4,6 +4,39 @@ frappe.query_reports["Employee Hours Utilisation Based On Timesheet"] = { "filters": [ - + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1 + }, + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.defaults.get_global_default("year_start_date"), + reqd: 1 + }, + { + fieldname:"to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.defaults.get_global_default("year_end_date"), + reqd: 1 + }, + { + fieldname: "employee", + label: __("Employee"), + fieldtype: "Link", + options: "Employee" + }, + { + fieldname: "project", + label: __("Project"), + fieldtype: "Link", + options: "Project" + } ] }; From 1a0229eb36ad0af47a7471718aeaf1493c3af701 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Tue, 30 Mar 2021 18:20:22 +0530 Subject: [PATCH 03/20] feat: Add report table data generation --- ...ee_hours_utilisation_based_on_timesheet.py | 126 +++++++++++++++++- 1 file changed, 123 insertions(+), 3 deletions(-) diff --git a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py index de2aa1ff416..71ed2e79eba 100644 --- a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py +++ b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py @@ -2,8 +2,128 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe +from frappe import _ +from frappe.utils import flt, getdate +from six import iteritems def execute(filters=None): - columns, data = [], [] - return columns, data + return EmployeeHoursReport(filters).run() + +class EmployeeHoursReport: + '''Employee Hours Utilisation Report Based On Timesheet''' + def __init__(self, filters=None): + self.filters = frappe._dict(filters or {}) + + self.from_date = getdate(self.filters.from_date) + self.to_date = getdate(self.filters.to_date) + + self.validate_dates() + + def validate_dates(self): + self.day_span = (self.to_date - self.from_date).days + + if self.day_span < 0: + frappe.throw(_('From Date must come before To Date')) + + def run(self): + self.generate_columns() + self.generate_data() + + return self.columns, self.data + + def generate_columns(self): + self.columns = [ + { + 'label': _('Employee'), + 'options': 'Employee', + 'fieldname': 'employee', + 'fieldtype': 'Link', + 'width': 200 + }, + { + 'label': _('Total Hours'), + 'fieldname': 'total_hours', + 'fieldtype': 'Float', + 'width': 150 + }, + { + 'label': _('Billed Hours'), + 'fieldname': 'billed_hours', + 'fieldtype': 'Float', + 'width': 150 + }, + { + 'label': _('Non-Billed Hours'), + 'fieldname': 'non_billed_hours', + 'fieldtype': 'Float', + 'width': 150 + }, + { + 'label': _('Untracked Hours'), + 'fieldname': 'untracked_hours', + 'fieldtype': 'Float', + 'width': 150 + }, + { + 'label': _('% Utilization'), + 'fieldname': 'per_util', + 'fieldtype': 'Percentage', + 'width': 200 + } + ] + + def generate_data(self): + self.generate_filtered_time_logs() + self.generate_stats_by_employee() + self.calculate_utilisations() + + self.data = [] + + for emp, data in iteritems(self.stats_by_employee): + row = frappe._dict() + row['employee'] = emp + row.update(data) + self.data.append(row) + + def generate_filtered_time_logs(self): + additional_filters = '' + + if self.filters.employee: + additional_filters += f'AND tt.employee = {self.filters.employee}' + + if self.filters.project: + additional_filters += f'AND ttd.project = {self.filters.project}' + + self.filtered_time_logs = frappe.db.sql(''' + SELECT tt.employee AS employee, ttd.hours AS hours, ttd.billable AS billable, ttd.project AS project + FROM `tabTimesheet Detail` AS ttd + JOIN `tabTimesheet` AS tt + ON ttd.parent = tt.name + WHERE tt.start_date BETWEEN '{0}' AND '{1}' + AND tt.end_date BETWEEN '{0}' AND '{1}' + {2}; + '''.format(self.filters.from_date, self.filters.to_date, additional_filters)) + + def generate_stats_by_employee(self): + self.stats_by_employee = frappe._dict() + + for emp, hours, billable, project in self.filtered_time_logs: + self.stats_by_employee.setdefault( + emp, frappe._dict() + ).setdefault('billed_hours', 0.0) + + self.stats_by_employee[emp].setdefault('non_billed_hours', 0.0) + + if billable: + self.stats_by_employee[emp]['billed_hours'] += flt(hours, 2) + else: + self.stats_by_employee[emp]['non_billed_hours'] += flt(hours, 2) + + def calculate_utilisations(self): + # (9.0) Will be fetched from HR settings + TOTAL_HOURS = flt(9.0 * self.day_span, 2) + for emp, data in iteritems(self.stats_by_employee): + data['total_hours'] = TOTAL_HOURS + data['untracked_hours'] = flt(TOTAL_HOURS - data['billed_hours'] - data['non_billed_hours'], 2) + data['per_util'] = flt(((data['billed_hours'] + data['non_billed_hours']) / TOTAL_HOURS) * 100, 2) \ No newline at end of file From f5576d67dd4723d5f8be4aa83aa6a70174ccd2f5 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Wed, 31 Mar 2021 12:25:36 +0530 Subject: [PATCH 04/20] feat: Add Report Summary generation --- ...ee_hours_utilisation_based_on_timesheet.py | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py index 71ed2e79eba..702c85a824b 100644 --- a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py +++ b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py @@ -29,8 +29,9 @@ class EmployeeHoursReport: def run(self): self.generate_columns() self.generate_data() + self.generate_report_summary() - return self.columns, self.data + return self.columns, self.data, None, None, self.report_summary def generate_columns(self): self.columns = [ @@ -126,4 +127,25 @@ class EmployeeHoursReport: for emp, data in iteritems(self.stats_by_employee): data['total_hours'] = TOTAL_HOURS data['untracked_hours'] = flt(TOTAL_HOURS - data['billed_hours'] - data['non_billed_hours'], 2) - data['per_util'] = flt(((data['billed_hours'] + data['non_billed_hours']) / TOTAL_HOURS) * 100, 2) \ No newline at end of file + data['per_util'] = flt(((data['billed_hours'] + data['non_billed_hours']) / TOTAL_HOURS) * 100, 2) + + def generate_report_summary(self): + if not self.data: + return + + avg_utilisation = 0.0 + for row in self.data: + avg_utilisation += row['per_util'] + + avg_utilisation /= len(self.data) + avg_utilisation = flt(avg_utilisation, 2) + + THRESHOLD_PERCENTAGE = 70.0 + self.report_summary = [ + { + 'value': f'{avg_utilisation}%', + 'indicator': 'Red' if avg_utilisation < THRESHOLD_PERCENTAGE else 'Green', + 'label': _('Average Utilisation'), + 'datatype': 'Percentage' + } + ] From 92c768972ff4cdce051328b1c66a8ff10bff5168 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Wed, 31 Mar 2021 12:26:37 +0530 Subject: [PATCH 05/20] fix: Missing quotation marks in SQL query --- .../employee_hours_utilisation_based_on_timesheet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py index 702c85a824b..e3d6b9b7f28 100644 --- a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py +++ b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py @@ -91,10 +91,10 @@ class EmployeeHoursReport: additional_filters = '' if self.filters.employee: - additional_filters += f'AND tt.employee = {self.filters.employee}' + additional_filters += f"AND tt.employee = '{self.filters.employee}'" if self.filters.project: - additional_filters += f'AND ttd.project = {self.filters.project}' + additional_filters += f"AND ttd.project = '{self.filters.project}'" self.filtered_time_logs = frappe.db.sql(''' SELECT tt.employee AS employee, ttd.hours AS hours, ttd.billable AS billable, ttd.project AS project From 39b75c63ad053f9fc6b7315f75e86cef9d39edc9 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Wed, 31 Mar 2021 13:13:25 +0530 Subject: [PATCH 06/20] feat: Add chart generation --- ...ee_hours_utilisation_based_on_timesheet.py | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py index e3d6b9b7f28..8124c7a1be7 100644 --- a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py +++ b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py @@ -30,8 +30,9 @@ class EmployeeHoursReport: self.generate_columns() self.generate_data() self.generate_report_summary() + self.generate_chart_data() - return self.columns, self.data, None, None, self.report_summary + return self.columns, self.data, None, self.chart, self.report_summary def generate_columns(self): self.columns = [ @@ -87,6 +88,9 @@ class EmployeeHoursReport: row.update(data) self.data.append(row) + # Sort by descending order of percentage utilisation + self.data.sort(key=lambda x: x['per_util'], reverse=True) + def generate_filtered_time_logs(self): additional_filters = '' @@ -149,3 +153,45 @@ class EmployeeHoursReport: 'datatype': 'Percentage' } ] + + def generate_chart_data(self): + self.chart = {} + + labels = [] + billed_hours = [] + non_billed_hours = [] + untracked_hours = [] + + + for row in self.data: + emp_name = frappe.db.get_value( + 'Employee', row['employee'], 'employee_name' + ) + labels.append(emp_name) + billed_hours.append(row.get('billed_hours')) + non_billed_hours.append(row.get('non_billed_hours')) + untracked_hours.append(row.get('untracked_hours')) + + self.chart = { + 'data': { + 'labels': labels[:30], + 'datasets': [ + { + 'name': _('Billed Hours'), + 'values': billed_hours[:30] + }, + { + 'name': _('Non-Billed Hours'), + 'values': non_billed_hours[:30] + }, + { + 'name': _('Untracked Hours'), + 'values': untracked_hours[:30] + } + ] + }, + 'type': 'bar', + 'barOptions': { + 'stacked': True + } + } From 1e772bb9a4718e8c5f707f378f77e12ed38626e8 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Thu, 1 Apr 2021 00:44:35 +0530 Subject: [PATCH 07/20] test: Basic report data testing --- ...ee_hours_utilisation_based_on_timesheet.py | 7 +- .../test_employee_util.py | 147 ++++++++++++++++++ 2 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/test_employee_util.py diff --git a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py index 8124c7a1be7..19793917ce5 100644 --- a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py +++ b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py @@ -99,7 +99,10 @@ class EmployeeHoursReport: if self.filters.project: additional_filters += f"AND ttd.project = '{self.filters.project}'" - + + if self.filters.company: + additional_filters += f"AND tt.company = '{self.filters.company}'" + self.filtered_time_logs = frappe.db.sql(''' SELECT tt.employee AS employee, ttd.hours AS hours, ttd.billable AS billable, ttd.project AS project FROM `tabTimesheet Detail` AS ttd @@ -134,6 +137,8 @@ class EmployeeHoursReport: data['per_util'] = flt(((data['billed_hours'] + data['non_billed_hours']) / TOTAL_HOURS) * 100, 2) def generate_report_summary(self): + self.report_summary = [] + if not self.data: return diff --git a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/test_employee_util.py b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/test_employee_util.py new file mode 100644 index 00000000000..7b310fa1fe9 --- /dev/null +++ b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/test_employee_util.py @@ -0,0 +1,147 @@ +from __future__ import unicode_literals +import unittest +import frappe + +from frappe.utils.make_random import get_random +from frappe.utils import get_timestamp +from erpnext.projects.report.employee_hours_utilisation_based_on_timesheet.employee_hours_utilisation_based_on_timesheet import execute +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.projects.doctype.project.test_project import make_project + +class TestEmployeeUtilisation(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Create test employee + cls.test_emp1 = make_employee("test@example.com", "_Test Company") + cls.test_emp2 = make_employee("test1@example.com", "_Test Company") + + # Create test project + cls.test_project = make_project({"project_name": "_Test Project"}) + + # Create test timesheets + cls.create_test_timesheets() + + @classmethod + def create_test_timesheets(cls): + timesheet1 = frappe.new_doc("Timesheet") + timesheet1.employee = cls.test_emp1 + timesheet1.company = '_Test Company' + + timesheet1.append("time_logs", { + "activity_type": get_random("Activity Type"), + "hours": 5, + "billable": 1, + "from_time": '2021-04-01 13:30:00.000000', + "to_time": '2021-04-01 18:30:00.000000' + }) + + timesheet1.save() + timesheet1.submit() + + timesheet2 = frappe.new_doc("Timesheet") + timesheet2.employee = cls.test_emp2 + timesheet2.company = '_Test Company' + + timesheet2.append("time_logs", { + "activity_type": get_random("Activity Type"), + "hours": 10, + "billable": 0, + "from_time": '2021-04-01 13:30:00.000000', + "to_time": '2021-04-01 23:30:00.000000', + "project": cls.test_project.name + }) + + timesheet2.save() + timesheet2.submit() + + @classmethod + def tearDownClass(cls): + # Delete time logs + frappe.db.sql(""" + DELETE FROM `tabTimesheet Detail` + WHERE parent IN ( + SELECT name + FROM `tabTimesheet` + WHERE company = '_Test Company' + ) + """) + + frappe.db.sql("DELETE FROM `tabTimesheet` WHERE company='_Test Company'") + frappe.db.sql(f"DELETE FROM `tabProject` WHERE name='{cls.test_project.name}'") + print(f"DELETE FROM `tabProject` WHERE name='{cls.test_project.name}'") + + def test_utilisation_report_with_required_filters_only(self): + filters = { + "company": "_Test Company", + "from_date": "2021-04-01", + "to_date": "2021-04-03" + } + + report = execute(filters) + + expected_data = [ + { + 'employee': 'EMP-00002', + 'billed_hours': 0.0, + 'non_billed_hours': 10.0, + 'total_hours': 18.0, + 'untracked_hours': 8.0, + 'per_util': 55.56 + }, + { + 'employee': 'EMP-00001', + 'billed_hours': 5.0, + 'non_billed_hours': 0.0, + 'total_hours': 18.0, + 'untracked_hours': 13.0, + 'per_util': 27.78 + } + ] + + self.assertEqual(report[1], expected_data) + + def test_utilisation_report_for_single_employee(self): + filters = { + "company": "_Test Company", + "from_date": "2021-04-01", + "to_date": "2021-04-03", + "employee": self.test_emp1 + } + + report = execute(filters) + + expected_data = [ + { + 'employee': 'EMP-00001', + 'billed_hours': 5.0, + 'non_billed_hours': 0.0, + 'total_hours': 18.0, + 'untracked_hours': 13.0, + 'per_util': 27.78 + } + ] + + self.assertEqual(report[1], expected_data) + + def test_utilisation_report_for_project(self): + filters = { + "company": "_Test Company", + "from_date": "2021-04-01", + "to_date": "2021-04-03", + "project": self.test_project.name + } + + report = execute(filters) + + expected_data = [ + { + 'employee': 'EMP-00002', + 'billed_hours': 0.0, + 'non_billed_hours': 10.0, + 'total_hours': 18.0, + 'untracked_hours': 8.0, + 'per_util': 55.56 + } + ] + + self.assertEqual(report[1], expected_data) \ No newline at end of file From d27fc93eddf3d04fec977464d698c5ecec9c1e6b Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Fri, 2 Apr 2021 19:54:58 +0530 Subject: [PATCH 08/20] feat: Add additional stats to report summary --- ...ee_hours_utilisation_based_on_timesheet.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py index 19793917ce5..c27efc96fb2 100644 --- a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py +++ b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py @@ -143,8 +143,14 @@ class EmployeeHoursReport: return avg_utilisation = 0.0 + total_billed, total_non_billed = 0.0, 0.0 + total_untracked = 0.0 + for row in self.data: avg_utilisation += row['per_util'] + total_billed += row['billed_hours'] + total_non_billed += row['non_billed_hours'] + total_untracked += row['untracked_hours'] avg_utilisation /= len(self.data) avg_utilisation = flt(avg_utilisation, 2) @@ -156,6 +162,21 @@ class EmployeeHoursReport: 'indicator': 'Red' if avg_utilisation < THRESHOLD_PERCENTAGE else 'Green', 'label': _('Average Utilisation'), 'datatype': 'Percentage' + }, + { + 'value': total_billed, + 'label': _('Total Billed Hours'), + 'datatype': 'Float' + }, + { + 'value': total_non_billed, + 'label': _('Total Non-Billed Hours'), + 'datatype': 'Float' + }, + { + 'value': total_untracked, + 'label': _('Total Untracked Hours'), + 'datatype': 'Float' } ] From 40abd2a2e12b52a814dd414115e7b29e8729a0dd Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Fri, 2 Apr 2021 19:55:54 +0530 Subject: [PATCH 09/20] test: Add test case for summary data --- .../test_employee_util.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/test_employee_util.py b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/test_employee_util.py index 7b310fa1fe9..adf55afc14a 100644 --- a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/test_employee_util.py +++ b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/test_employee_util.py @@ -68,7 +68,6 @@ class TestEmployeeUtilisation(unittest.TestCase): frappe.db.sql("DELETE FROM `tabTimesheet` WHERE company='_Test Company'") frappe.db.sql(f"DELETE FROM `tabProject` WHERE name='{cls.test_project.name}'") - print(f"DELETE FROM `tabProject` WHERE name='{cls.test_project.name}'") def test_utilisation_report_with_required_filters_only(self): filters = { @@ -144,4 +143,22 @@ class TestEmployeeUtilisation(unittest.TestCase): } ] - self.assertEqual(report[1], expected_data) \ No newline at end of file + self.assertEqual(report[1], expected_data) + + def test_report_summary_data(self): + filters = { + "company": "_Test Company", + "from_date": "2021-04-01", + "to_date": "2021-04-03" + } + + report = execute(filters) + summary = report[4] + expected_summary_values = ['41.67%', 5.0, 10.0, 21.0] + + self.assertEqual(len(summary), 4) + + for i in range(4): + self.assertEqual( + summary[i]['value'], expected_summary_values[i] + ) From ccb15de1c9e4c23487787b3f8be3bf3e8d7dfb86 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Mon, 5 Apr 2021 11:23:06 +0530 Subject: [PATCH 10/20] fix: Handle overtime edge case --- .../employee_hours_utilisation_based_on_timesheet.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py index c27efc96fb2..19cb1c911dc 100644 --- a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py +++ b/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py @@ -134,6 +134,11 @@ class EmployeeHoursReport: for emp, data in iteritems(self.stats_by_employee): data['total_hours'] = TOTAL_HOURS data['untracked_hours'] = flt(TOTAL_HOURS - data['billed_hours'] - data['non_billed_hours'], 2) + + # To handle overtime edge-case + if data['untracked_hours'] < 0: + data['untracked_hours'] = 0.0 + data['per_util'] = flt(((data['billed_hours'] + data['non_billed_hours']) / TOTAL_HOURS) * 100, 2) def generate_report_summary(self): From bb32f5a92a0f20dbd0f673c3b3beab69f47dd892 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Mon, 5 Apr 2021 19:32:10 +0530 Subject: [PATCH 11/20] fix: Rename utilisation to utilization --- .../__init__.py | 0 ...e_hours_utilization_based_on_timesheet.js} | 2 +- ...hours_utilization_based_on_timesheet.json} | 8 +++---- ...e_hours_utilization_based_on_timesheet.py} | 22 +++++++++---------- .../test_employee_util.py | 10 ++++----- 5 files changed, 21 insertions(+), 21 deletions(-) rename erpnext/projects/report/{employee_hours_utilisation_based_on_timesheet => employee_hours_utilization_based_on_timesheet}/__init__.py (100%) rename erpnext/projects/report/{employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.js => employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.js} (93%) rename erpnext/projects/report/{employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.json => employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.json} (63%) rename erpnext/projects/report/{employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py => employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py} (92%) rename erpnext/projects/report/{employee_hours_utilisation_based_on_timesheet => employee_hours_utilization_based_on_timesheet}/test_employee_util.py (92%) diff --git a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/__init__.py b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/__init__.py similarity index 100% rename from erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/__init__.py rename to erpnext/projects/report/employee_hours_utilization_based_on_timesheet/__init__.py diff --git a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.js b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.js similarity index 93% rename from erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.js rename to erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.js index e9b817ab673..2a5ea3655d6 100644 --- a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.js +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.js @@ -2,7 +2,7 @@ // For license information, please see license.txt /* eslint-disable */ -frappe.query_reports["Employee Hours Utilisation Based On Timesheet"] = { +frappe.query_reports["Employee Hours Utilization Based On Timesheet"] = { "filters": [ { fieldname: "company", diff --git a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.json b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.json similarity index 63% rename from erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.json rename to erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.json index 1b504e011d2..5ff8186572e 100644 --- a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.json +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.json @@ -1,7 +1,7 @@ { "add_total_row": 0, "columns": [], - "creation": "2021-03-30 12:18:02.090630", + "creation": "2021-04-05 19:23:43.838623", "disable_prepared_report": 0, "disabled": 0, "docstatus": 0, @@ -9,14 +9,14 @@ "filters": [], "idx": 0, "is_standard": "Yes", - "modified": "2021-03-30 12:18:02.090630", + "modified": "2021-04-05 19:23:43.838623", "modified_by": "Administrator", "module": "Projects", - "name": "Employee Hours Utilisation Based On Timesheet", + "name": "Employee Hours Utilization Based On Timesheet", "owner": "Administrator", "prepared_report": 0, "ref_doctype": "Timesheet", - "report_name": "Employee Hours Utilisation Based On Timesheet", + "report_name": "Employee Hours Utilization Based On Timesheet", "report_type": "Script Report", "roles": [] } \ No newline at end of file diff --git a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py similarity index 92% rename from erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py rename to erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py index 19cb1c911dc..bc9dc7d0952 100644 --- a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/employee_hours_utilisation_based_on_timesheet.py +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py @@ -11,7 +11,7 @@ def execute(filters=None): return EmployeeHoursReport(filters).run() class EmployeeHoursReport: - '''Employee Hours Utilisation Report Based On Timesheet''' + '''Employee Hours Utilization Report Based On Timesheet''' def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -78,7 +78,7 @@ class EmployeeHoursReport: def generate_data(self): self.generate_filtered_time_logs() self.generate_stats_by_employee() - self.calculate_utilisations() + self.calculate_utilizations() self.data = [] @@ -88,7 +88,7 @@ class EmployeeHoursReport: row.update(data) self.data.append(row) - # Sort by descending order of percentage utilisation + # Sort by descending order of percentage utilization self.data.sort(key=lambda x: x['per_util'], reverse=True) def generate_filtered_time_logs(self): @@ -128,7 +128,7 @@ class EmployeeHoursReport: else: self.stats_by_employee[emp]['non_billed_hours'] += flt(hours, 2) - def calculate_utilisations(self): + def calculate_utilizations(self): # (9.0) Will be fetched from HR settings TOTAL_HOURS = flt(9.0 * self.day_span, 2) for emp, data in iteritems(self.stats_by_employee): @@ -147,25 +147,25 @@ class EmployeeHoursReport: if not self.data: return - avg_utilisation = 0.0 + avg_utilization = 0.0 total_billed, total_non_billed = 0.0, 0.0 total_untracked = 0.0 for row in self.data: - avg_utilisation += row['per_util'] + avg_utilization += row['per_util'] total_billed += row['billed_hours'] total_non_billed += row['non_billed_hours'] total_untracked += row['untracked_hours'] - avg_utilisation /= len(self.data) - avg_utilisation = flt(avg_utilisation, 2) + avg_utilization /= len(self.data) + avg_utilization = flt(avg_utilization, 2) THRESHOLD_PERCENTAGE = 70.0 self.report_summary = [ { - 'value': f'{avg_utilisation}%', - 'indicator': 'Red' if avg_utilisation < THRESHOLD_PERCENTAGE else 'Green', - 'label': _('Average Utilisation'), + 'value': f'{avg_utilization}%', + 'indicator': 'Red' if avg_utilization < THRESHOLD_PERCENTAGE else 'Green', + 'label': _('Average Utilization'), 'datatype': 'Percentage' }, { diff --git a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/test_employee_util.py b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py similarity index 92% rename from erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/test_employee_util.py rename to erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py index adf55afc14a..5306b922eb4 100644 --- a/erpnext/projects/report/employee_hours_utilisation_based_on_timesheet/test_employee_util.py +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py @@ -4,11 +4,11 @@ import frappe from frappe.utils.make_random import get_random from frappe.utils import get_timestamp -from erpnext.projects.report.employee_hours_utilisation_based_on_timesheet.employee_hours_utilisation_based_on_timesheet import execute +from erpnext.projects.report.employee_hours_utilization_based_on_timesheet.employee_hours_utilization_based_on_timesheet import execute from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.projects.doctype.project.test_project import make_project -class TestEmployeeUtilisation(unittest.TestCase): +class TestEmployeeUtilization(unittest.TestCase): @classmethod def setUpClass(cls): # Create test employee @@ -69,7 +69,7 @@ class TestEmployeeUtilisation(unittest.TestCase): frappe.db.sql("DELETE FROM `tabTimesheet` WHERE company='_Test Company'") frappe.db.sql(f"DELETE FROM `tabProject` WHERE name='{cls.test_project.name}'") - def test_utilisation_report_with_required_filters_only(self): + def test_utilization_report_with_required_filters_only(self): filters = { "company": "_Test Company", "from_date": "2021-04-01", @@ -99,7 +99,7 @@ class TestEmployeeUtilisation(unittest.TestCase): self.assertEqual(report[1], expected_data) - def test_utilisation_report_for_single_employee(self): + def test_utilization_report_for_single_employee(self): filters = { "company": "_Test Company", "from_date": "2021-04-01", @@ -122,7 +122,7 @@ class TestEmployeeUtilisation(unittest.TestCase): self.assertEqual(report[1], expected_data) - def test_utilisation_report_for_project(self): + def test_utilization_report_for_project(self): filters = { "company": "_Test Company", "from_date": "2021-04-01", From 33e0c8ed191eb08eea2a2bc7f9544f2defed84fa Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Tue, 6 Apr 2021 11:40:35 +0530 Subject: [PATCH 12/20] test: Unique Employee IDs for tests --- .../employee_hours_utilization_based_on_timesheet.py | 2 +- .../test_employee_util.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py index bc9dc7d0952..2c78b3125d7 100644 --- a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py @@ -23,7 +23,7 @@ class EmployeeHoursReport: def validate_dates(self): self.day_span = (self.to_date - self.from_date).days - if self.day_span < 0: + if self.day_span <= 0: frappe.throw(_('From Date must come before To Date')) def run(self): diff --git a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py index 5306b922eb4..e01e9b1539f 100644 --- a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py @@ -12,8 +12,8 @@ class TestEmployeeUtilization(unittest.TestCase): @classmethod def setUpClass(cls): # Create test employee - cls.test_emp1 = make_employee("test@example.com", "_Test Company") - cls.test_emp2 = make_employee("test1@example.com", "_Test Company") + cls.test_emp1 = make_employee("test1@employeeutil.com", "_Test Company") + cls.test_emp2 = make_employee("test2@employeeutil.com", "_Test Company") # Create test project cls.test_project = make_project({"project_name": "_Test Project"}) @@ -80,7 +80,7 @@ class TestEmployeeUtilization(unittest.TestCase): expected_data = [ { - 'employee': 'EMP-00002', + 'employee': self.test_emp2, 'billed_hours': 0.0, 'non_billed_hours': 10.0, 'total_hours': 18.0, @@ -88,7 +88,7 @@ class TestEmployeeUtilization(unittest.TestCase): 'per_util': 55.56 }, { - 'employee': 'EMP-00001', + 'employee': self.test_emp1, 'billed_hours': 5.0, 'non_billed_hours': 0.0, 'total_hours': 18.0, @@ -111,7 +111,7 @@ class TestEmployeeUtilization(unittest.TestCase): expected_data = [ { - 'employee': 'EMP-00001', + 'employee': self.test_emp1, 'billed_hours': 5.0, 'non_billed_hours': 0.0, 'total_hours': 18.0, @@ -134,7 +134,7 @@ class TestEmployeeUtilization(unittest.TestCase): expected_data = [ { - 'employee': 'EMP-00002', + 'employee': self.test_emp2, 'billed_hours': 0.0, 'non_billed_hours': 10.0, 'total_hours': 18.0, From d2da7b673bf7a4525819451d429eb889c227b619 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Tue, 6 Apr 2021 11:44:54 +0530 Subject: [PATCH 13/20] fix: Sider Issues --- .../test_employee_util.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py index e01e9b1539f..8b5d5be0e1f 100644 --- a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py @@ -3,7 +3,6 @@ import unittest import frappe from frappe.utils.make_random import get_random -from frappe.utils import get_timestamp from erpnext.projects.report.employee_hours_utilization_based_on_timesheet.employee_hours_utilization_based_on_timesheet import execute from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.projects.doctype.project.test_project import make_project @@ -161,4 +160,4 @@ class TestEmployeeUtilization(unittest.TestCase): for i in range(4): self.assertEqual( summary[i]['value'], expected_summary_values[i] - ) + ) From f1b4ce7430e5320ad67e4430628f83c7cdc0b873 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Fri, 16 Apr 2021 15:36:43 +0530 Subject: [PATCH 14/20] feat: Add filtering by department Also: - Add department filter to js - Add department column to report - Fetch only those timesheets which have an Employee Linked - Update unit tests --- ...ee_hours_utilization_based_on_timesheet.js | 6 ++ ...ee_hours_utilization_based_on_timesheet.py | 61 ++++++++++++---- .../test_employee_util.py | 71 +++++++++++++------ 3 files changed, 102 insertions(+), 36 deletions(-) diff --git a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.js b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.js index 2a5ea3655d6..b11a1fc97f7 100644 --- a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.js +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.js @@ -32,6 +32,12 @@ frappe.query_reports["Employee Hours Utilization Based On Timesheet"] = { fieldtype: "Link", options: "Employee" }, + { + fieldname: "department", + label: __("Department"), + fieldtype: "Link", + options: "Department" + }, { fieldname: "project", label: __("Project"), diff --git a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py index 2c78b3125d7..48eb7b4bb0f 100644 --- a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py @@ -41,7 +41,14 @@ class EmployeeHoursReport: 'options': 'Employee', 'fieldname': 'employee', 'fieldtype': 'Link', - 'width': 200 + 'width': 230 + }, + { + 'label': _('Department'), + 'options': 'Department', + 'fieldname': 'department', + 'fieldtype': 'Link', + 'width': 170 }, { 'label': _('Total Hours'), @@ -68,7 +75,7 @@ class EmployeeHoursReport: 'width': 150 }, { - 'label': _('% Utilization'), + 'label': _('% Utilization (Billed Hours + Non-Billed Hours / Total Hours)'), 'fieldname': 'per_util', 'fieldtype': 'Percentage', 'width': 200 @@ -78,6 +85,11 @@ class EmployeeHoursReport: def generate_data(self): self.generate_filtered_time_logs() self.generate_stats_by_employee() + self.set_employee_department_and_name() + + if self.filters.department: + self.filter_stats_by_department() + self.calculate_utilizations() self.data = [] @@ -91,26 +103,36 @@ class EmployeeHoursReport: # Sort by descending order of percentage utilization self.data.sort(key=lambda x: x['per_util'], reverse=True) + def filter_stats_by_department(self): + filtered_data = frappe._dict() + for emp, data in self.stats_by_employee.items(): + if data['department'] == self.filters.department: + filtered_data[emp] = data + + # Update stats + self.stats_by_employee = filtered_data + def generate_filtered_time_logs(self): additional_filters = '' - if self.filters.employee: - additional_filters += f"AND tt.employee = '{self.filters.employee}'" - - if self.filters.project: - additional_filters += f"AND ttd.project = '{self.filters.project}'" + filter_fields = ['employee', 'project', 'company'] - if self.filters.company: - additional_filters += f"AND tt.company = '{self.filters.company}'" + for field in filter_fields: + if self.filters.get(field): + if field == 'project': + additional_filters += f"AND ttd.{field} = '{self.filters.get(field)}'" + else: + additional_filters += f"AND tt.{field} = '{self.filters.get(field)}'" self.filtered_time_logs = frappe.db.sql(''' SELECT tt.employee AS employee, ttd.hours AS hours, ttd.billable AS billable, ttd.project AS project FROM `tabTimesheet Detail` AS ttd JOIN `tabTimesheet` AS tt ON ttd.parent = tt.name - WHERE tt.start_date BETWEEN '{0}' AND '{1}' + WHERE tt.employee IS NOT NULL + AND tt.start_date BETWEEN '{0}' AND '{1}' AND tt.end_date BETWEEN '{0}' AND '{1}' - {2}; + {2} '''.format(self.filters.from_date, self.filters.to_date, additional_filters)) def generate_stats_by_employee(self): @@ -128,6 +150,18 @@ class EmployeeHoursReport: else: self.stats_by_employee[emp]['non_billed_hours'] += flt(hours, 2) + def set_employee_department_and_name(self): + for emp in self.stats_by_employee: + emp_name = frappe.db.get_value( + 'Employee', emp, 'employee_name' + ) + emp_dept = frappe.db.get_value( + 'Employee', emp, 'department' + ) + + self.stats_by_employee[emp]['department'] = emp_dept + self.stats_by_employee[emp]['employee_name'] = emp_name + def calculate_utilizations(self): # (9.0) Will be fetched from HR settings TOTAL_HOURS = flt(9.0 * self.day_span, 2) @@ -195,10 +229,7 @@ class EmployeeHoursReport: for row in self.data: - emp_name = frappe.db.get_value( - 'Employee', row['employee'], 'employee_name' - ) - labels.append(emp_name) + labels.append(row.get('employee_name')) billed_hours.append(row.get('billed_hours')) non_billed_hours.append(row.get('non_billed_hours')) untracked_hours.append(row.get('untracked_hours')) diff --git a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py index 8b5d5be0e1f..977a10d1d9f 100644 --- a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py @@ -77,25 +77,7 @@ class TestEmployeeUtilization(unittest.TestCase): report = execute(filters) - expected_data = [ - { - 'employee': self.test_emp2, - 'billed_hours': 0.0, - 'non_billed_hours': 10.0, - 'total_hours': 18.0, - 'untracked_hours': 8.0, - 'per_util': 55.56 - }, - { - 'employee': self.test_emp1, - 'billed_hours': 5.0, - 'non_billed_hours': 0.0, - 'total_hours': 18.0, - 'untracked_hours': 13.0, - 'per_util': 27.78 - } - ] - + expected_data = self.get_expected_data_for_test_employees() self.assertEqual(report[1], expected_data) def test_utilization_report_for_single_employee(self): @@ -108,9 +90,12 @@ class TestEmployeeUtilization(unittest.TestCase): report = execute(filters) + emp1_data = frappe.get_doc('Employee', self.test_emp1) expected_data = [ { 'employee': self.test_emp1, + 'employee_name': emp1_data.employee_name, + 'department': emp1_data.department, 'billed_hours': 5.0, 'non_billed_hours': 0.0, 'total_hours': 18.0, @@ -130,10 +115,13 @@ class TestEmployeeUtilization(unittest.TestCase): } report = execute(filters) - + + emp2_data = frappe.get_doc('Employee', self.test_emp2) expected_data = [ { - 'employee': self.test_emp2, + 'employee': self.test_emp2, + 'employee_name': emp2_data.employee_name, + 'department': emp2_data.department, 'billed_hours': 0.0, 'non_billed_hours': 10.0, 'total_hours': 18.0, @@ -144,6 +132,20 @@ class TestEmployeeUtilization(unittest.TestCase): self.assertEqual(report[1], expected_data) + def test_utilization_report_for_department(self): + emp1_data = frappe.get_doc('Employee', self.test_emp1) + filters = { + "company": "_Test Company", + "from_date": "2021-04-01", + "to_date": "2021-04-03", + "department": emp1_data.department + } + + report = execute(filters) + + expected_data = self.get_expected_data_for_test_employees() + self.assertEqual(report[1], expected_data) + def test_report_summary_data(self): filters = { "company": "_Test Company", @@ -161,3 +163,30 @@ class TestEmployeeUtilization(unittest.TestCase): self.assertEqual( summary[i]['value'], expected_summary_values[i] ) + + def get_expected_data_for_test_employees(self): + emp1_data = frappe.get_doc('Employee', self.test_emp1) + emp2_data = frappe.get_doc('Employee', self.test_emp2) + + return [ + { + 'employee': self.test_emp2, + 'employee_name': emp2_data.employee_name, + 'department': emp2_data.department, + 'billed_hours': 0.0, + 'non_billed_hours': 10.0, + 'total_hours': 18.0, + 'untracked_hours': 8.0, + 'per_util': 55.56 + }, + { + 'employee': self.test_emp1, + 'employee_name': emp1_data.employee_name, + 'department': emp1_data.department, + 'billed_hours': 5.0, + 'non_billed_hours': 0.0, + 'total_hours': 18.0, + 'untracked_hours': 13.0, + 'per_util': 27.78 + } + ] \ No newline at end of file From 1f8326bb1f8884b8efabb40c38d744ec03b35374 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Mon, 19 Apr 2021 16:19:08 +0530 Subject: [PATCH 15/20] chore: Add Report to Dashboard --- erpnext/projects/workspace/projects/projects.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/erpnext/projects/workspace/projects/projects.json b/erpnext/projects/workspace/projects/projects.json index dbbd7e1458e..bc2e71097f1 100644 --- a/erpnext/projects/workspace/projects/projects.json +++ b/erpnext/projects/workspace/projects/projects.json @@ -15,6 +15,7 @@ "hide_custom": 0, "icon": "project", "idx": 0, + "is_default": 0, "is_standard": 1, "label": "Projects", "links": [ @@ -129,6 +130,16 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "Timesheet", + "hidden": 0, + "is_query_report": 1, + "label": "Employee Hours Utilization", + "link_to": "Employee Hours Utilization Based On Timesheet", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, { "dependencies": "Project", "hidden": 0, @@ -150,7 +161,7 @@ "type": "Link" } ], - "modified": "2020-12-01 13:38:37.856224", + "modified": "2021-04-19 16:17:30.142545", "modified_by": "Administrator", "module": "Projects", "name": "Projects", From 0decc2b66a060586e4a88a5356bf89754e196c1f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 26 Apr 2021 11:00:54 +0530 Subject: [PATCH 16/20] fix: validate Standard Working Hours - use standard working hours in metrics calculation --- .../hr/doctype/hr_settings/hr_settings.json | 3 +- ...ee_hours_utilization_based_on_timesheet.py | 34 ++++++++++++------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index 35532291a5f..3db6c239ef0 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -146,7 +146,6 @@ "label": "Send Leave Notification" }, { - "default": "8", "fieldname": "standard_working_hours", "fieldtype": "Int", "label": "Standard Working Hours" @@ -156,7 +155,7 @@ "idx": 1, "issingle": 1, "links": [], - "modified": "2021-04-16 15:45:18.467699", + "modified": "2021-04-26 10:52:56.192773", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", diff --git a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py index 48eb7b4bb0f..7337a03eb4d 100644 --- a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py @@ -19,13 +19,22 @@ class EmployeeHoursReport: self.to_date = getdate(self.filters.to_date) self.validate_dates() - + self.validate_standard_working_hours() + def validate_dates(self): self.day_span = (self.to_date - self.from_date).days if self.day_span <= 0: frappe.throw(_('From Date must come before To Date')) + def validate_standard_working_hours(self): + self.standard_working_hours = frappe.db.get_single_value('HR Settings', 'standard_working_hours') + if not self.standard_working_hours: + msg = _('The metrics for this report are calculated based on the Standard Working Hours. Please set {0} in {1}.').format( + frappe.bold('Standard Working Hours'), frappe.utils.get_link_to_form('HR Settings', 'HR Settings')) + + frappe.throw(msg) + def run(self): self.generate_columns() self.generate_data() @@ -47,7 +56,7 @@ class EmployeeHoursReport: 'label': _('Department'), 'options': 'Department', 'fieldname': 'department', - 'fieldtype': 'Link', + 'fieldtype': 'Link', 'width': 170 }, { @@ -81,7 +90,7 @@ class EmployeeHoursReport: 'width': 200 } ] - + def generate_data(self): self.generate_filtered_time_logs() self.generate_stats_by_employee() @@ -108,7 +117,7 @@ class EmployeeHoursReport: for emp, data in self.stats_by_employee.items(): if data['department'] == self.filters.department: filtered_data[emp] = data - + # Update stats self.stats_by_employee = filtered_data @@ -126,8 +135,8 @@ class EmployeeHoursReport: self.filtered_time_logs = frappe.db.sql(''' SELECT tt.employee AS employee, ttd.hours AS hours, ttd.billable AS billable, ttd.project AS project - FROM `tabTimesheet Detail` AS ttd - JOIN `tabTimesheet` AS tt + FROM `tabTimesheet Detail` AS ttd + JOIN `tabTimesheet` AS tt ON ttd.parent = tt.name WHERE tt.employee IS NOT NULL AND tt.start_date BETWEEN '{0}' AND '{1}' @@ -161,10 +170,9 @@ class EmployeeHoursReport: self.stats_by_employee[emp]['department'] = emp_dept self.stats_by_employee[emp]['employee_name'] = emp_name - + def calculate_utilizations(self): - # (9.0) Will be fetched from HR settings - TOTAL_HOURS = flt(9.0 * self.day_span, 2) + TOTAL_HOURS = flt(self.standard_working_hours * self.day_span, 2) for emp, data in iteritems(self.stats_by_employee): data['total_hours'] = TOTAL_HOURS data['untracked_hours'] = flt(TOTAL_HOURS - data['billed_hours'] - data['non_billed_hours'], 2) @@ -172,14 +180,14 @@ class EmployeeHoursReport: # To handle overtime edge-case if data['untracked_hours'] < 0: data['untracked_hours'] = 0.0 - + data['per_util'] = flt(((data['billed_hours'] + data['non_billed_hours']) / TOTAL_HOURS) * 100, 2) - + def generate_report_summary(self): self.report_summary = [] if not self.data: - return + return avg_utilization = 0.0 total_billed, total_non_billed = 0.0, 0.0 @@ -233,7 +241,7 @@ class EmployeeHoursReport: billed_hours.append(row.get('billed_hours')) non_billed_hours.append(row.get('non_billed_hours')) untracked_hours.append(row.get('untracked_hours')) - + self.chart = { 'data': { 'labels': labels[:30], From 2a1a4723712b6cae1c15c88c3a1e3bff4f6ea7ad Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 26 Apr 2021 11:18:32 +0530 Subject: [PATCH 17/20] chore: clean-up - set default date filters for last month instead of year - added abbreviations for columns to use them in the metrics labels --- ...ee_hours_utilization_based_on_timesheet.js | 4 +-- ...ee_hours_utilization_based_on_timesheet.py | 34 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.js b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.js index b11a1fc97f7..9a30b99f9ba 100644 --- a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.js +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.js @@ -16,14 +16,14 @@ frappe.query_reports["Employee Hours Utilization Based On Timesheet"] = { fieldname: "from_date", label: __("From Date"), fieldtype: "Date", - default: frappe.defaults.get_global_default("year_start_date"), + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), reqd: 1 }, { fieldname:"to_date", label: __("To Date"), fieldtype: "Date", - default: frappe.defaults.get_global_default("year_end_date"), + default: frappe.datetime.now_date(), reqd: 1 }, { diff --git a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py index 7337a03eb4d..b38da1416f3 100644 --- a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py @@ -57,34 +57,34 @@ class EmployeeHoursReport: 'options': 'Department', 'fieldname': 'department', 'fieldtype': 'Link', + 'width': 120 + }, + { + 'label': _('Total Hours (T)'), + 'fieldname': 'total_hours', + 'fieldtype': 'Float', + 'width': 120 + }, + { + 'label': _('Billed Hours (B)'), + 'fieldname': 'billed_hours', + 'fieldtype': 'Float', 'width': 170 }, { - 'label': _('Total Hours'), - 'fieldname': 'total_hours', - 'fieldtype': 'Float', - 'width': 150 - }, - { - 'label': _('Billed Hours'), - 'fieldname': 'billed_hours', - 'fieldtype': 'Float', - 'width': 150 - }, - { - 'label': _('Non-Billed Hours'), + 'label': _('Non-Billed Hours (NB)'), 'fieldname': 'non_billed_hours', 'fieldtype': 'Float', - 'width': 150 + 'width': 170 }, { - 'label': _('Untracked Hours'), + 'label': _('Untracked Hours (U)'), 'fieldname': 'untracked_hours', 'fieldtype': 'Float', - 'width': 150 + 'width': 170 }, { - 'label': _('% Utilization (Billed Hours + Non-Billed Hours / Total Hours)'), + 'label': _('% Utilization (B + NB) / T'), 'fieldname': 'per_util', 'fieldtype': 'Percentage', 'width': 200 From 4716ebea5a5e7588f4d0d0440dbefaf7a342cab9 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 26 Apr 2021 11:40:26 +0530 Subject: [PATCH 18/20] feat: Add % Utilization (Billed Only) - Added billed utilization metric in report summary --- ...ee_hours_utilization_based_on_timesheet.py | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py index b38da1416f3..842fd4d3148 100644 --- a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py @@ -88,6 +88,12 @@ class EmployeeHoursReport: 'fieldname': 'per_util', 'fieldtype': 'Percentage', 'width': 200 + }, + { + 'label': _('% Utilization (B / T)'), + 'fieldname': 'per_util_billed_only', + 'fieldtype': 'Percentage', + 'width': 200 } ] @@ -182,6 +188,7 @@ class EmployeeHoursReport: data['untracked_hours'] = 0.0 data['per_util'] = flt(((data['billed_hours'] + data['non_billed_hours']) / TOTAL_HOURS) * 100, 2) + data['per_util_billed_only'] = flt((data['billed_hours'] / TOTAL_HOURS) * 100, 2) def generate_report_summary(self): self.report_summary = [] @@ -190,11 +197,13 @@ class EmployeeHoursReport: return avg_utilization = 0.0 + avg_utilization_billed_only = 0.0 total_billed, total_non_billed = 0.0, 0.0 total_untracked = 0.0 for row in self.data: avg_utilization += row['per_util'] + avg_utilization_billed_only += row['per_util_billed_only'] total_billed += row['billed_hours'] total_non_billed += row['non_billed_hours'] total_untracked += row['untracked_hours'] @@ -202,12 +211,21 @@ class EmployeeHoursReport: avg_utilization /= len(self.data) avg_utilization = flt(avg_utilization, 2) + avg_utilization_billed_only /= len(self.data) + avg_utilization_billed_only = flt(avg_utilization_billed_only, 2) + THRESHOLD_PERCENTAGE = 70.0 self.report_summary = [ { 'value': f'{avg_utilization}%', 'indicator': 'Red' if avg_utilization < THRESHOLD_PERCENTAGE else 'Green', - 'label': _('Average Utilization'), + 'label': _('Avg Utilization'), + 'datatype': 'Percentage' + }, + { + 'value': f'{avg_utilization_billed_only}%', + 'indicator': 'Red' if avg_utilization_billed_only < THRESHOLD_PERCENTAGE else 'Green', + 'label': _('Avg Utilization (Billed Only)'), 'datatype': 'Percentage' }, { @@ -219,11 +237,6 @@ class EmployeeHoursReport: 'value': total_non_billed, 'label': _('Total Non-Billed Hours'), 'datatype': 'Float' - }, - { - 'value': total_untracked, - 'label': _('Total Untracked Hours'), - 'datatype': 'Float' } ] From 7a84d76489c9b621c1ddf755112990088595707f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 26 Apr 2021 16:03:10 +0530 Subject: [PATCH 19/20] fix: tests --- ...ee_hours_utilization_based_on_timesheet.py | 4 - .../test_employee_util.py | 78 ++++++++++--------- .../test_project_profitability.py | 6 +- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py index 842fd4d3148..87bac02f9a8 100644 --- a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py @@ -167,15 +167,11 @@ class EmployeeHoursReport: def set_employee_department_and_name(self): for emp in self.stats_by_employee: - emp_name = frappe.db.get_value( - 'Employee', emp, 'employee_name' - ) emp_dept = frappe.db.get_value( 'Employee', emp, 'department' ) self.stats_by_employee[emp]['department'] = emp_dept - self.stats_by_employee[emp]['employee_name'] = emp_name def calculate_utilizations(self): TOTAL_HOURS = flt(self.standard_working_hours * self.day_span, 2) diff --git a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py index 977a10d1d9f..6d955450ecb 100644 --- a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py @@ -20,6 +20,8 @@ class TestEmployeeUtilization(unittest.TestCase): # Create test timesheets cls.create_test_timesheets() + frappe.db.set_value("HR Settings", "HR Settings", "standard_working_hours", 9) + @classmethod def create_test_timesheets(cls): timesheet1 = frappe.new_doc("Timesheet") @@ -51,7 +53,7 @@ class TestEmployeeUtilization(unittest.TestCase): }) timesheet2.save() - timesheet2.submit() + timesheet2.submit() @classmethod def tearDownClass(cls): @@ -59,8 +61,8 @@ class TestEmployeeUtilization(unittest.TestCase): frappe.db.sql(""" DELETE FROM `tabTimesheet Detail` WHERE parent IN ( - SELECT name - FROM `tabTimesheet` + SELECT name + FROM `tabTimesheet` WHERE company = '_Test Company' ) """) @@ -72,14 +74,14 @@ class TestEmployeeUtilization(unittest.TestCase): filters = { "company": "_Test Company", "from_date": "2021-04-01", - "to_date": "2021-04-03" + "to_date": "2021-04-03" } report = execute(filters) expected_data = self.get_expected_data_for_test_employees() self.assertEqual(report[1], expected_data) - + def test_utilization_report_for_single_employee(self): filters = { "company": "_Test Company", @@ -93,14 +95,14 @@ class TestEmployeeUtilization(unittest.TestCase): emp1_data = frappe.get_doc('Employee', self.test_emp1) expected_data = [ { - 'employee': self.test_emp1, - 'employee_name': emp1_data.employee_name, + 'employee': self.test_emp1, + 'billed_hours': 5.0, + 'non_billed_hours': 0.0, 'department': emp1_data.department, - 'billed_hours': 5.0, - 'non_billed_hours': 0.0, - 'total_hours': 18.0, - 'untracked_hours': 13.0, - 'per_util': 27.78 + 'total_hours': 18.0, + 'untracked_hours': 13.0, + 'per_util': 27.78, + 'per_util_billed_only': 27.78 } ] @@ -115,18 +117,18 @@ class TestEmployeeUtilization(unittest.TestCase): } report = execute(filters) - + emp2_data = frappe.get_doc('Employee', self.test_emp2) expected_data = [ { 'employee': self.test_emp2, - 'employee_name': emp2_data.employee_name, + 'billed_hours': 0.0, + 'non_billed_hours': 10.0, 'department': emp2_data.department, - 'billed_hours': 0.0, - 'non_billed_hours': 10.0, - 'total_hours': 18.0, - 'untracked_hours': 8.0, - 'per_util': 55.56 + 'total_hours': 18.0, + 'untracked_hours': 8.0, + 'per_util': 55.56, + 'per_util_billed_only': 0.0 } ] @@ -155,7 +157,7 @@ class TestEmployeeUtilization(unittest.TestCase): report = execute(filters) summary = report[4] - expected_summary_values = ['41.67%', 5.0, 10.0, 21.0] + expected_summary_values = ['41.67%', '13.89%', 5.0, 10.0] self.assertEqual(len(summary), 4) @@ -167,26 +169,26 @@ class TestEmployeeUtilization(unittest.TestCase): def get_expected_data_for_test_employees(self): emp1_data = frappe.get_doc('Employee', self.test_emp1) emp2_data = frappe.get_doc('Employee', self.test_emp2) - + return [ { - 'employee': self.test_emp2, - 'employee_name': emp2_data.employee_name, - 'department': emp2_data.department, - 'billed_hours': 0.0, - 'non_billed_hours': 10.0, - 'total_hours': 18.0, - 'untracked_hours': 8.0, - 'per_util': 55.56 - }, + 'employee': self.test_emp2, + 'billed_hours': 0.0, + 'non_billed_hours': 10.0, + 'department': emp2_data.department, + 'total_hours': 18.0, + 'untracked_hours': 8.0, + 'per_util': 55.56, + 'per_util_billed_only': 0.0 + }, { - 'employee': self.test_emp1, - 'employee_name': emp1_data.employee_name, - 'department': emp1_data.department, - 'billed_hours': 5.0, - 'non_billed_hours': 0.0, - 'total_hours': 18.0, - 'untracked_hours': 13.0, - 'per_util': 27.78 + 'employee': self.test_emp1, + 'billed_hours': 5.0, + 'non_billed_hours': 0.0, + 'department': emp1_data.department, + 'total_hours': 18.0, + 'untracked_hours': 13.0, + 'per_util': 27.78, + 'per_util_billed_only': 27.78 } ] \ No newline at end of file diff --git a/erpnext/projects/report/project_profitability/test_project_profitability.py b/erpnext/projects/report/project_profitability/test_project_profitability.py index 251b71da598..7fe28b10746 100644 --- a/erpnext/projects/report/project_profitability/test_project_profitability.py +++ b/erpnext/projects/report/project_profitability/test_project_profitability.py @@ -3,7 +3,7 @@ import unittest import frappe from frappe.utils import getdate, nowdate from erpnext.hr.doctype.employee.test_employee import make_employee -from erpnext.projects.doctype.timesheet.test_timesheet import make_salary_structure_for_timesheet, make_timesheet +from erpnext.projects.doctype.timesheet.test_timesheet import make_salary_structure_for_timesheet, make_timesheet from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_sales_invoice from erpnext.projects.report.project_profitability.project_profitability import execute @@ -21,6 +21,8 @@ class TestProjectProfitability(unittest.TestCase): self.sales_invoice.due_date = nowdate() self.sales_invoice.submit() + frappe.db.set_value("HR Settings", "HR Settings", "standard_working_hours", 8) + def test_project_profitability(self): filters = { 'company': '_Test Company', @@ -43,7 +45,7 @@ class TestProjectProfitability(unittest.TestCase): standard_working_hours = frappe.db.get_single_value("HR Settings", "standard_working_hours") utilization = timesheet.total_billed_hours/(self.salary_slip.total_working_days * standard_working_hours) self.assertEqual(utilization, row.utilization) - + profit = self.sales_invoice.base_grand_total - self.salary_slip.base_gross_pay * utilization self.assertEqual(profit, row.profit) From d5ca9474ce792f26e327745cd798fe7006e65262 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 26 Apr 2021 16:10:06 +0530 Subject: [PATCH 20/20] fix: add employee name in report data --- .../employee_hours_utilization_based_on_timesheet.py | 4 ++++ .../test_employee_util.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py index 87bac02f9a8..842fd4d3148 100644 --- a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/employee_hours_utilization_based_on_timesheet.py @@ -167,11 +167,15 @@ class EmployeeHoursReport: def set_employee_department_and_name(self): for emp in self.stats_by_employee: + emp_name = frappe.db.get_value( + 'Employee', emp, 'employee_name' + ) emp_dept = frappe.db.get_value( 'Employee', emp, 'department' ) self.stats_by_employee[emp]['department'] = emp_dept + self.stats_by_employee[emp]['employee_name'] = emp_name def calculate_utilizations(self): TOTAL_HOURS = flt(self.standard_working_hours * self.day_span, 2) diff --git a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py index 6d955450ecb..fa8782733fe 100644 --- a/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py +++ b/erpnext/projects/report/employee_hours_utilization_based_on_timesheet/test_employee_util.py @@ -96,6 +96,7 @@ class TestEmployeeUtilization(unittest.TestCase): expected_data = [ { 'employee': self.test_emp1, + 'employee_name': 'test1@employeeutil.com', 'billed_hours': 5.0, 'non_billed_hours': 0.0, 'department': emp1_data.department, @@ -122,6 +123,7 @@ class TestEmployeeUtilization(unittest.TestCase): expected_data = [ { 'employee': self.test_emp2, + 'employee_name': 'test2@employeeutil.com', 'billed_hours': 0.0, 'non_billed_hours': 10.0, 'department': emp2_data.department, @@ -173,6 +175,7 @@ class TestEmployeeUtilization(unittest.TestCase): return [ { 'employee': self.test_emp2, + 'employee_name': 'test2@employeeutil.com', 'billed_hours': 0.0, 'non_billed_hours': 10.0, 'department': emp2_data.department, @@ -183,6 +186,7 @@ class TestEmployeeUtilization(unittest.TestCase): }, { 'employee': self.test_emp1, + 'employee_name': 'test1@employeeutil.com', 'billed_hours': 5.0, 'non_billed_hours': 0.0, 'department': emp1_data.department,