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
This commit is contained in:
Hussain Nagaria 2021-04-16 15:36:43 +05:30
parent d2da7b673b
commit f1b4ce7430
3 changed files with 102 additions and 36 deletions

View File

@ -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"),

View File

@ -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}'"
filter_fields = ['employee', 'project', 'company']
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}'"
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'))

View File

@ -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,
@ -131,9 +116,12 @@ 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,
'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
}
]