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:
parent
d2da7b673b
commit
f1b4ce7430
@ -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"),
|
||||
|
@ -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'))
|
||||
|
@ -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
|
||||
}
|
||||
]
|
Loading…
x
Reference in New Issue
Block a user