diff --git a/erpnext/hr/report/employee_exits/__init__.py b/erpnext/hr/report/employee_exits/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/report/employee_exits/employee_exits.js b/erpnext/hr/report/employee_exits/employee_exits.js new file mode 100644 index 00000000000..ac677d87e78 --- /dev/null +++ b/erpnext/hr/report/employee_exits/employee_exits.js @@ -0,0 +1,77 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Employee Exits"] = { + filters: [ + { + "fieldname": "from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12) + }, + { + "fieldname": "to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.nowdate() + }, + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company" + }, + { + "fieldname": "department", + "label": __("Department"), + "fieldtype": "Link", + "options": "Department" + }, + { + "fieldname": "designation", + "label": __("Designation"), + "fieldtype": "Link", + "options": "Designation" + }, + { + "fieldname": "employee", + "label": __("Employee"), + "fieldtype": "Link", + "options": "Employee" + }, + { + "fieldname": "reports_to", + "label": __("Reports To"), + "fieldtype": "Link", + "options": "Employee" + }, + { + "fieldname": "interview_status", + "label": __("Interview Status"), + "fieldtype": "Select", + "options": ["", "Pending", "Scheduled", "Completed"] + }, + { + "fieldname": "final_decision", + "label": __("Final Decision"), + "fieldtype": "Select", + "options": ["", "Employee Retained", "Exit Confirmed"] + }, + { + "fieldname": "exit_interview_pending", + "label": __("Exit Interview Pending"), + "fieldtype": "Check" + }, + { + "fieldname": "questionnaire_pending", + "label": __("Exit Questionnaire Pending"), + "fieldtype": "Check" + }, + { + "fieldname": "fnf_pending", + "label": __("FnF Pending"), + "fieldtype": "Check" + } + ] +}; diff --git a/erpnext/hr/report/employee_exits/employee_exits.json b/erpnext/hr/report/employee_exits/employee_exits.json new file mode 100644 index 00000000000..4fe9a853c0c --- /dev/null +++ b/erpnext/hr/report/employee_exits/employee_exits.json @@ -0,0 +1,33 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-12-05 19:47:18.332319", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letter_head": "Test", + "modified": "2021-12-05 19:47:18.332319", + "modified_by": "Administrator", + "module": "HR", + "name": "Employee Exits", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Exit Interview", + "report_name": "Employee Exits", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "HR Manager" + }, + { + "role": "HR User" + } + ] +} \ No newline at end of file diff --git a/erpnext/hr/report/employee_exits/employee_exits.py b/erpnext/hr/report/employee_exits/employee_exits.py new file mode 100644 index 00000000000..fd49543d084 --- /dev/null +++ b/erpnext/hr/report/employee_exits/employee_exits.py @@ -0,0 +1,231 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# License: MIT. See LICENSE + +import frappe +from frappe import _ +from frappe.utils import getdate + + +def execute(filters=None): + columns = get_columns() + data = get_data(filters) + chart = get_chart_data(data) + report_summary = get_report_summary(data) + + return columns, data, None, chart, report_summary + +def get_columns(): + return [ + { + 'label': _('Employee'), + 'fieldname': 'employee', + 'fieldtype': 'Link', + 'options': 'Employee', + 'width': 150 + }, + { + 'label': _('Employee Name'), + 'fieldname': 'employee_name', + 'fieldtype': 'Data', + 'width': 150 + }, + { + 'label': _('Date of Joining'), + 'fieldname': 'date_of_joining', + 'fieldtype': 'Date', + 'width': 100 + }, + { + 'label': _('Relieving Date'), + 'fieldname': 'relieving_date', + 'fieldtype': 'Date', + 'width': 100 + }, + { + 'label': _('Department'), + 'fieldname': 'department', + 'fieldtype': 'Link', + 'options': 'Department', + 'width': 120 + }, + { + 'label': _('Designation'), + 'fieldname': 'designation', + 'fieldtype': 'Link', + 'options': 'Designation', + 'width': 120 + }, + { + 'label': _('Reports To'), + 'fieldname': 'reports_to', + 'fieldtype': 'Link', + 'options': 'Employee', + 'width': 120 + }, + { + 'label': _('Exit Interview'), + 'fieldname': 'exit_interview', + 'fieldtype': 'Link', + 'options': 'Exit Interview', + 'width': 98 + }, + { + 'label': _('Interview Status'), + 'fieldname': 'interview_status', + 'fieldtype': 'Data', + 'width': 98 + }, + { + 'label': _('Final Decision'), + 'fieldname': 'employee_status', + 'fieldtype': 'Data', + 'width': 98 + }, + { + 'label': _('Full and Final Statement'), + 'fieldname': 'full_and_final_statement', + 'fieldtype': 'Link', + 'options': 'Full and Final Statement', + 'width': 150 + } + ] + +def get_data(filters): + data = [] + + employee = frappe.qb.DocType('Employee') + interview = frappe.qb.DocType('Exit Interview') + fnf = frappe.qb.DocType('Full and Final Statement') + + query = ( + frappe.qb.from_(employee) + .left_join(interview).on(interview.employee == employee.name) + .left_join(fnf).on(fnf.employee == employee.name) + .select( + employee.name.as_('employee'), employee.employee_name.as_('employee_name'), + employee.date_of_joining.as_('date_of_joining'), employee.relieving_date.as_('relieving_date'), + employee.department.as_('department'), employee.designation.as_('designation'), + employee.reports_to.as_('reports_to'), interview.name.as_('exit_interview'), + interview.status.as_('interview_status'), interview.employee_status.as_('employee_status'), + interview.reference_document_name.as_('questionnaire'), fnf.name.as_('full_and_final_statement') + ).distinct() + .where( + ((employee.relieving_date.isnotnull()) | (employee.relieving_date != '')) + & ((interview.name.isnull()) | ((interview.name.isnotnull()) & (interview.docstatus != 2))) + & ((fnf.name.isnull()) | ((fnf.name.isnotnull()) & (fnf.docstatus != 2))) + ) + ) + + query = get_conditions(filters, query, employee, interview, fnf) + result = query.run(as_dict=True) + + return result + + +def get_conditions(filters, query, employee, interview, fnf): + if filters.get('from_date') and filters.get('to_date'): + query = query.where(employee.relieving_date[getdate(filters.get('from_date')):getdate(filters.get('to_date'))]) + + elif filters.get('from_date'): + query = query.where(employee.relieving_date >= filters.get('from_date')) + + elif filters.get('to_date'): + query = query.where(employee.relieving_date <= filters.get('to_date')) + + if filters.get('company'): + query = query.where(employee.company == filters.get('company')) + + if filters.get('department'): + query = query.where(employee.department == filters.get('department')) + + if filters.get('designation'): + query = query.where(employee.designation == filters.get('designation')) + + if filters.get('employee'): + query = query.where(employee.name == filters.get('employee')) + + if filters.get('reports_to'): + query = query.where(employee.reports_to == filters.get('reports_to')) + + if filters.get('interview_status'): + query = query.where(interview.status == filters.get('interview_status')) + + if filters.get('final_decision'): + query = query.where(interview.employee_status == filters.get('final_decision')) + + if filters.get('exit_interview_pending'): + query = query.where((interview.name == '') | (interview.name.isnull())) + + if filters.get('questionnaire_pending'): + query = query.where((interview.reference_document_name == '') | (interview.reference_document_name.isnull())) + + if filters.get('fnf_pending'): + query = query.where((fnf.name == '') | (interview.name.isnull())) + + return query + + +def get_chart_data(data): + if not data: + return None + + retained = 0 + exit_confirmed = 0 + pending = 0 + + for entry in data: + if entry.employee_status == 'Employee Retained': + retained += 1 + elif entry.employee_status == 'Exit Confirmed': + exit_confirmed += 1 + else: + pending += 1 + + chart = { + 'data': { + 'labels': [_('Retained'), _('Exit Confirmed'), _('Decision Pending')], + 'datasets': [{'name': _('Employee Status'), 'values': [retained, exit_confirmed, pending]}] + }, + 'type': 'donut', + 'colors': ['green', 'red', 'blue'], + } + + return chart + + +def get_report_summary(data): + if not data: + return None + + total_resignations = len(data) + interviews_pending = len([entry.name for entry in data if not entry.exit_interview]) + fnf_pending = len([entry.name for entry in data if not entry.full_and_final_statement]) + questionnaires_pending = len([entry.name for entry in data if not entry.questionnaire]) + + return [ + { + 'value': total_resignations, + 'label': _('Total Resignations'), + 'indicator': 'Red' if total_resignations > 0 else 'Green', + 'datatype': 'Int', + }, + { + 'value': interviews_pending, + 'label': _('Pending Interviews'), + 'indicator': 'Blue' if interviews_pending > 0 else 'Green', + 'datatype': 'Int', + }, + { + 'value': fnf_pending, + 'label': _('Pending FnF'), + 'indicator': 'Blue' if fnf_pending > 0 else 'Green', + 'datatype': 'Int', + }, + { + 'value': questionnaires_pending, + 'label': _('Pending Questionnaires'), + 'indicator': 'Blue' if questionnaires_pending > 0 else 'Green', + 'datatype': 'Int' + }, + ] +