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",
|
fieldtype: "Link",
|
||||||
options: "Employee"
|
options: "Employee"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldname: "department",
|
||||||
|
label: __("Department"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Department"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fieldname: "project",
|
fieldname: "project",
|
||||||
label: __("Project"),
|
label: __("Project"),
|
||||||
|
@ -41,7 +41,14 @@ class EmployeeHoursReport:
|
|||||||
'options': 'Employee',
|
'options': 'Employee',
|
||||||
'fieldname': 'employee',
|
'fieldname': 'employee',
|
||||||
'fieldtype': 'Link',
|
'fieldtype': 'Link',
|
||||||
'width': 200
|
'width': 230
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': _('Department'),
|
||||||
|
'options': 'Department',
|
||||||
|
'fieldname': 'department',
|
||||||
|
'fieldtype': 'Link',
|
||||||
|
'width': 170
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': _('Total Hours'),
|
'label': _('Total Hours'),
|
||||||
@ -68,7 +75,7 @@ class EmployeeHoursReport:
|
|||||||
'width': 150
|
'width': 150
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': _('% Utilization'),
|
'label': _('% Utilization (Billed Hours + Non-Billed Hours / Total Hours)'),
|
||||||
'fieldname': 'per_util',
|
'fieldname': 'per_util',
|
||||||
'fieldtype': 'Percentage',
|
'fieldtype': 'Percentage',
|
||||||
'width': 200
|
'width': 200
|
||||||
@ -78,6 +85,11 @@ class EmployeeHoursReport:
|
|||||||
def generate_data(self):
|
def generate_data(self):
|
||||||
self.generate_filtered_time_logs()
|
self.generate_filtered_time_logs()
|
||||||
self.generate_stats_by_employee()
|
self.generate_stats_by_employee()
|
||||||
|
self.set_employee_department_and_name()
|
||||||
|
|
||||||
|
if self.filters.department:
|
||||||
|
self.filter_stats_by_department()
|
||||||
|
|
||||||
self.calculate_utilizations()
|
self.calculate_utilizations()
|
||||||
|
|
||||||
self.data = []
|
self.data = []
|
||||||
@ -91,26 +103,36 @@ class EmployeeHoursReport:
|
|||||||
# Sort by descending order of percentage utilization
|
# Sort by descending order of percentage utilization
|
||||||
self.data.sort(key=lambda x: x['per_util'], reverse=True)
|
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):
|
def generate_filtered_time_logs(self):
|
||||||
additional_filters = ''
|
additional_filters = ''
|
||||||
|
|
||||||
if self.filters.employee:
|
filter_fields = ['employee', 'project', 'company']
|
||||||
additional_filters += f"AND tt.employee = '{self.filters.employee}'"
|
|
||||||
|
|
||||||
if self.filters.project:
|
|
||||||
additional_filters += f"AND ttd.project = '{self.filters.project}'"
|
|
||||||
|
|
||||||
if self.filters.company:
|
for field in filter_fields:
|
||||||
additional_filters += f"AND tt.company = '{self.filters.company}'"
|
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('''
|
self.filtered_time_logs = frappe.db.sql('''
|
||||||
SELECT tt.employee AS employee, ttd.hours AS hours, ttd.billable AS billable, ttd.project AS project
|
SELECT tt.employee AS employee, ttd.hours AS hours, ttd.billable AS billable, ttd.project AS project
|
||||||
FROM `tabTimesheet Detail` AS ttd
|
FROM `tabTimesheet Detail` AS ttd
|
||||||
JOIN `tabTimesheet` AS tt
|
JOIN `tabTimesheet` AS tt
|
||||||
ON ttd.parent = tt.name
|
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}'
|
AND tt.end_date BETWEEN '{0}' AND '{1}'
|
||||||
{2};
|
{2}
|
||||||
'''.format(self.filters.from_date, self.filters.to_date, additional_filters))
|
'''.format(self.filters.from_date, self.filters.to_date, additional_filters))
|
||||||
|
|
||||||
def generate_stats_by_employee(self):
|
def generate_stats_by_employee(self):
|
||||||
@ -128,6 +150,18 @@ class EmployeeHoursReport:
|
|||||||
else:
|
else:
|
||||||
self.stats_by_employee[emp]['non_billed_hours'] += flt(hours, 2)
|
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):
|
def calculate_utilizations(self):
|
||||||
# (9.0) Will be fetched from HR settings
|
# (9.0) Will be fetched from HR settings
|
||||||
TOTAL_HOURS = flt(9.0 * self.day_span, 2)
|
TOTAL_HOURS = flt(9.0 * self.day_span, 2)
|
||||||
@ -195,10 +229,7 @@ class EmployeeHoursReport:
|
|||||||
|
|
||||||
|
|
||||||
for row in self.data:
|
for row in self.data:
|
||||||
emp_name = frappe.db.get_value(
|
labels.append(row.get('employee_name'))
|
||||||
'Employee', row['employee'], 'employee_name'
|
|
||||||
)
|
|
||||||
labels.append(emp_name)
|
|
||||||
billed_hours.append(row.get('billed_hours'))
|
billed_hours.append(row.get('billed_hours'))
|
||||||
non_billed_hours.append(row.get('non_billed_hours'))
|
non_billed_hours.append(row.get('non_billed_hours'))
|
||||||
untracked_hours.append(row.get('untracked_hours'))
|
untracked_hours.append(row.get('untracked_hours'))
|
||||||
|
@ -77,25 +77,7 @@ class TestEmployeeUtilization(unittest.TestCase):
|
|||||||
|
|
||||||
report = execute(filters)
|
report = execute(filters)
|
||||||
|
|
||||||
expected_data = [
|
expected_data = self.get_expected_data_for_test_employees()
|
||||||
{
|
|
||||||
'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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
self.assertEqual(report[1], expected_data)
|
self.assertEqual(report[1], expected_data)
|
||||||
|
|
||||||
def test_utilization_report_for_single_employee(self):
|
def test_utilization_report_for_single_employee(self):
|
||||||
@ -108,9 +90,12 @@ class TestEmployeeUtilization(unittest.TestCase):
|
|||||||
|
|
||||||
report = execute(filters)
|
report = execute(filters)
|
||||||
|
|
||||||
|
emp1_data = frappe.get_doc('Employee', self.test_emp1)
|
||||||
expected_data = [
|
expected_data = [
|
||||||
{
|
{
|
||||||
'employee': self.test_emp1,
|
'employee': self.test_emp1,
|
||||||
|
'employee_name': emp1_data.employee_name,
|
||||||
|
'department': emp1_data.department,
|
||||||
'billed_hours': 5.0,
|
'billed_hours': 5.0,
|
||||||
'non_billed_hours': 0.0,
|
'non_billed_hours': 0.0,
|
||||||
'total_hours': 18.0,
|
'total_hours': 18.0,
|
||||||
@ -130,10 +115,13 @@ class TestEmployeeUtilization(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
report = execute(filters)
|
report = execute(filters)
|
||||||
|
|
||||||
|
emp2_data = frappe.get_doc('Employee', self.test_emp2)
|
||||||
expected_data = [
|
expected_data = [
|
||||||
{
|
{
|
||||||
'employee': self.test_emp2,
|
'employee': self.test_emp2,
|
||||||
|
'employee_name': emp2_data.employee_name,
|
||||||
|
'department': emp2_data.department,
|
||||||
'billed_hours': 0.0,
|
'billed_hours': 0.0,
|
||||||
'non_billed_hours': 10.0,
|
'non_billed_hours': 10.0,
|
||||||
'total_hours': 18.0,
|
'total_hours': 18.0,
|
||||||
@ -144,6 +132,20 @@ class TestEmployeeUtilization(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(report[1], expected_data)
|
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):
|
def test_report_summary_data(self):
|
||||||
filters = {
|
filters = {
|
||||||
"company": "_Test Company",
|
"company": "_Test Company",
|
||||||
@ -161,3 +163,30 @@ class TestEmployeeUtilization(unittest.TestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
summary[i]['value'], expected_summary_values[i]
|
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