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