Browse Source

feat(HR): Exit Interview and Employee Exits Report (#28741)

* feat: Exit Interview

* feat: sending Exit Questionnaire

* feat: add default Exit Questionnaire email template

* feat: track status and final decision (Retained/Exit Confirmed)

* fix: make Exit Interview submittable

* feat: bulk questionnaire sending

* feat: update Exit Interview date in employee master on submission

* feat: Employee Exits report

* chore: update HR workspace

* feat: default Notification - a day before Exit Interview

* fix: email summary

* fix: show Exit Questionnaire button only to the users with write access

- fix linter issues

* test: Exit Interview

* chore: fix report column widths

* test: Employee Exits Report

* fix: tests and sider issues

* fix: missing import

* fix: tests

- specify sorting order in employee exits query

- rollback after work order tests
develop
Rucha Mahabal 3 years ago
committed by GitHub
parent
commit
6fa66a4d81
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      erpnext/hr/doctype/employee/employee_dashboard.py
  2. 0
      erpnext/hr/doctype/exit_interview/__init__.py
  3. 38
      erpnext/hr/doctype/exit_interview/exit_interview.js
  4. 246
      erpnext/hr/doctype/exit_interview/exit_interview.json
  5. 131
      erpnext/hr/doctype/exit_interview/exit_interview.py
  6. 27
      erpnext/hr/doctype/exit_interview/exit_interview_list.js
  7. 16
      erpnext/hr/doctype/exit_interview/exit_questionnaire_notification_template.html
  8. 118
      erpnext/hr/doctype/exit_interview/test_exit_interview.py
  9. 29
      erpnext/hr/doctype/hr_settings/hr_settings.json
  10. 0
      erpnext/hr/notification/exit_interview_scheduled/__init__.py
  11. 29
      erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.json
  12. 37
      erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.md
  13. 6
      erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py
  14. 0
      erpnext/hr/report/employee_exits/__init__.py
  15. 77
      erpnext/hr/report/employee_exits/employee_exits.js
  16. 33
      erpnext/hr/report/employee_exits/employee_exits.json
  17. 231
      erpnext/hr/report/employee_exits/employee_exits.py
  18. 242
      erpnext/hr/report/employee_exits/test_employee_exits.py
  19. 871
      erpnext/hr/workspace/hr/hr.json
  20. 3
      erpnext/manufacturing/doctype/work_order/test_work_order.py
  21. 1
      erpnext/patches.txt
  22. 27
      erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py
  23. 2
      erpnext/setup/setup_wizard/operations/defaults_setup.py
  24. 5
      erpnext/setup/setup_wizard/operations/install_fixtures.py

6
erpnext/hr/doctype/employee/employee_dashboard.py

@ -21,7 +21,11 @@ def get_data():
},
{
'label': _('Lifecycle'),
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Grievance']
},
{
'label': _('Exit'),
'items': ['Employee Separation', 'Exit Interview', 'Full and Final Statement']
},
{
'label': _('Shift'),

0
erpnext/hr/doctype/exit_interview/__init__.py

38
erpnext/hr/doctype/exit_interview/exit_interview.js

@ -0,0 +1,38 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Exit Interview', {
refresh: function(frm) {
if (!frm.doc.__islocal && !frm.doc.questionnaire_email_sent && frappe.boot.user.can_write.includes('Exit Interview')) {
frm.add_custom_button(__('Send Exit Questionnaire'), function () {
frm.trigger('send_exit_questionnaire');
});
}
},
employee: function(frm) {
frappe.db.get_value('Employee', frm.doc.employee, 'relieving_date', (message) => {
if (!message.relieving_date) {
frappe.throw({
message: __('Please set the relieving date for employee {0}',
['<a href="/app/employee/' + frm.doc.employee +'">' + frm.doc.employee + '</a>']),
title: __('Relieving Date Missing')
});
}
});
},
send_exit_questionnaire: function(frm) {
frappe.call({
method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire',
args: {
'interviews': [frm.doc]
},
callback: function(r) {
if (!r.exc) {
frm.refresh_field('questionnaire_email_sent');
}
}
});
}
});

246
erpnext/hr/doctype/exit_interview/exit_interview.json

@ -0,0 +1,246 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "naming_series:",
"creation": "2021-12-05 13:56:36.241690",
"doctype": "DocType",
"editable_grid": 1,
"email_append_to": 1,
"engine": "InnoDB",
"field_order": [
"naming_series",
"employee",
"employee_name",
"email",
"column_break_5",
"company",
"status",
"date",
"employee_details_section",
"department",
"designation",
"reports_to",
"column_break_9",
"date_of_joining",
"relieving_date",
"exit_questionnaire_section",
"ref_doctype",
"questionnaire_email_sent",
"column_break_10",
"reference_document_name",
"interview_summary_section",
"interviewers",
"interview_summary",
"employee_status_section",
"employee_status",
"amended_from"
],
"fields": [
{
"fieldname": "employee",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Employee",
"options": "Employee",
"reqd": 1
},
{
"fetch_from": "employee.employee_name",
"fieldname": "employee_name",
"fieldtype": "Data",
"label": "Employee Name",
"read_only": 1
},
{
"fetch_from": "employee.department",
"fieldname": "department",
"fieldtype": "Link",
"label": "Department",
"options": "Department",
"read_only": 1
},
{
"fetch_from": "employee.relieving_date",
"fieldname": "relieving_date",
"fieldtype": "Date",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Relieving Date",
"read_only": 1
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Date",
"mandatory_depends_on": "eval:doc.status==='Scheduled';"
},
{
"fieldname": "exit_questionnaire_section",
"fieldtype": "Section Break",
"label": "Exit Questionnaire"
},
{
"fieldname": "ref_doctype",
"fieldtype": "Link",
"label": "Reference Document Type",
"options": "DocType"
},
{
"fieldname": "reference_document_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Reference Document Name",
"options": "ref_doctype"
},
{
"fieldname": "interview_summary_section",
"fieldtype": "Section Break",
"label": "Interview Details"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "interviewers",
"fieldtype": "Table MultiSelect",
"label": "Interviewers",
"mandatory_depends_on": "eval:doc.status==='Scheduled';",
"options": "Interviewer"
},
{
"fetch_from": "employee.date_of_joining",
"fieldname": "date_of_joining",
"fieldtype": "Date",
"label": "Date of Joining",
"read_only": 1
},
{
"fetch_from": "employee.reports_to",
"fieldname": "reports_to",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Reports To",
"options": "Employee",
"read_only": 1
},
{
"fieldname": "employee_details_section",
"fieldtype": "Section Break",
"label": "Employee Details"
},
{
"fetch_from": "employee.designation",
"fieldname": "designation",
"fieldtype": "Link",
"label": "Designation",
"options": "Designation",
"read_only": 1
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "HR-EXIT-INT-"
},
{
"default": "0",
"fieldname": "questionnaire_email_sent",
"fieldtype": "Check",
"in_standard_filter": 1,
"label": "Questionnaire Email Sent",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "email",
"fieldtype": "Data",
"label": "Email ID",
"options": "Email",
"read_only": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "Pending\nScheduled\nCompleted\nCancelled",
"reqd": 1
},
{
"fieldname": "employee_status_section",
"fieldtype": "Section Break"
},
{
"fieldname": "employee_status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Final Decision",
"mandatory_depends_on": "eval:doc.status==='Completed';",
"options": "\nEmployee Retained\nExit Confirmed"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Exit Interview",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "interview_summary",
"fieldtype": "Text Editor",
"label": "Interview Summary"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-12-07 23:39:22.645401",
"modified_by": "Administrator",
"module": "HR",
"name": "Exit Interview",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sender_field": "email",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "employee_name",
"track_changes": 1
}

131
erpnext/hr/doctype/exit_interview/exit_interview.py

@ -0,0 +1,131 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import get_link_to_form
from erpnext.hr.doctype.employee.employee import get_employee_email
class ExitInterview(Document):
def validate(self):
self.validate_relieving_date()
self.validate_duplicate_interview()
self.set_employee_email()
def validate_relieving_date(self):
if not frappe.db.get_value('Employee', self.employee, 'relieving_date'):
frappe.throw(_('Please set the relieving date for employee {0}').format(
get_link_to_form('Employee', self.employee)),
title=_('Relieving Date Missing'))
def validate_duplicate_interview(self):
doc = frappe.db.exists('Exit Interview', {
'employee': self.employee,
'name': ('!=', self.name),
'docstatus': ('!=', 2)
})
if doc:
frappe.throw(_('Exit Interview {0} already exists for Employee: {1}').format(
get_link_to_form('Exit Interview', doc), frappe.bold(self.employee)),
frappe.DuplicateEntryError)
def set_employee_email(self):
employee = frappe.get_doc('Employee', self.employee)
self.email = get_employee_email(employee)
def on_submit(self):
if self.status != 'Completed':
frappe.throw(_('Only Completed documents can be submitted'))
self.update_interview_date_in_employee()
def on_cancel(self):
self.update_interview_date_in_employee()
self.db_set('status', 'Cancelled')
def update_interview_date_in_employee(self):
if self.docstatus == 1:
frappe.db.set_value('Employee', self.employee, 'held_on', self.date)
elif self.docstatus == 2:
frappe.db.set_value('Employee', self.employee, 'held_on', None)
@frappe.whitelist()
def send_exit_questionnaire(interviews):
interviews = get_interviews(interviews)
validate_questionnaire_settings()
email_success = []
email_failure = []
for exit_interview in interviews:
interview = frappe.get_doc('Exit Interview', exit_interview.get('name'))
if interview.get('questionnaire_email_sent'):
continue
employee = frappe.get_doc('Employee', interview.employee)
email = get_employee_email(employee)
context = interview.as_dict()
context.update(employee.as_dict())
template_name = frappe.db.get_single_value('HR Settings', 'exit_questionnaire_notification_template')
template = frappe.get_doc('Email Template', template_name)
if email:
frappe.sendmail(
recipients=email,
subject=template.subject,
message=frappe.render_template(template.response, context),
reference_doctype=interview.doctype,
reference_name=interview.name
)
interview.db_set('questionnaire_email_sent', True)
interview.notify_update()
email_success.append(email)
else:
email_failure.append(get_link_to_form('Employee', employee.name))
show_email_summary(email_success, email_failure)
def get_interviews(interviews):
import json
if isinstance(interviews, str):
interviews = json.loads(interviews)
if not len(interviews):
frappe.throw(_('Atleast one interview has to be selected.'))
return interviews
def validate_questionnaire_settings():
settings = frappe.db.get_value('HR Settings', 'HR Settings',
['exit_questionnaire_web_form', 'exit_questionnaire_notification_template'], as_dict=True)
if not settings.exit_questionnaire_web_form or not settings.exit_questionnaire_notification_template:
frappe.throw(
_('Please set {0} and {1} in {2}.').format(
frappe.bold('Exit Questionnaire Web Form'),
frappe.bold('Notification Template'),
get_link_to_form('HR Settings', 'HR Settings')),
title=_('Settings Missing')
)
def show_email_summary(email_success, email_failure):
message = ''
if email_success:
message += _('{0}: {1}').format(
frappe.bold('Sent Successfully'), ', '.join(email_success))
if message and email_failure:
message += '<br><br>'
if email_failure:
message += _('{0} due to missing email information for employee(s): {1}').format(
frappe.bold('Sending Failed'), ', '.join(email_failure))
frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True)

27
erpnext/hr/doctype/exit_interview/exit_interview_list.js

@ -0,0 +1,27 @@
frappe.listview_settings['Exit Interview'] = {
has_indicator_for_draft: 1,
get_indicator: function(doc) {
let status_color = {
'Pending': 'orange',
'Scheduled': 'yellow',
'Completed': 'green',
'Cancelled': 'red',
};
return [__(doc.status), status_color[doc.status], 'status,=,'+doc.status];
},
onload: function(listview) {
if (frappe.boot.user.can_write.includes('Exit Interview')) {
listview.page.add_action_item(__('Send Exit Questionnaires'), function() {
const interviews = listview.get_checked_items();
frappe.call({
method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire',
freeze: true,
args: {
'interviews': interviews
}
});
});
}
}
};

16
erpnext/hr/doctype/exit_interview/exit_questionnaire_notification_template.html

@ -0,0 +1,16 @@
<h2>Exit Questionnaire</h2>
<br>
<p>
Dear {{ employee_name }},
<br><br>
Thank you for the contribution you have made during your time at {{ company }}. We value your opinion and welcome the feedback on your experience working with us.
Request you to take out a few minutes to fill up this Exit Questionnaire.
{% set web_form = frappe.db.get_value('HR Settings', 'HR Settings', 'exit_questionnaire_web_form') %}
{% set web_form_link = frappe.utils.get_url(uri=frappe.db.get_value('Web Form', web_form, 'route')) %}
<br><br>
<a class="btn btn-primary" href="{{ web_form_link }}" target="_blank">{{ _('Submit Now') }}</a>
</p>

118
erpnext/hr/doctype/exit_interview/test_exit_interview.py

@ -0,0 +1,118 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import os
import unittest
import frappe
from frappe import _
from frappe.core.doctype.user_permission.test_user_permission import create_user
from frappe.tests.test_webform import create_custom_doctype, create_webform
from frappe.utils import getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.exit_interview.exit_interview import send_exit_questionnaire
class TestExitInterview(unittest.TestCase):
def setUp(self):
frappe.db.sql('delete from `tabExit Interview`')
def test_duplicate_interview(self):
employee = make_employee('employeeexit1@example.com')
frappe.db.set_value('Employee', employee, 'relieving_date', getdate())
interview = create_exit_interview(employee)
doc = frappe.copy_doc(interview)
self.assertRaises(frappe.DuplicateEntryError, doc.save)
def test_relieving_date_validation(self):
employee = make_employee('employeeexit2@example.com')
# unset relieving date
frappe.db.set_value('Employee', employee, 'relieving_date', None)
interview = create_exit_interview(employee, save=False)
self.assertRaises(frappe.ValidationError, interview.save)
# set relieving date
frappe.db.set_value('Employee', employee, 'relieving_date', getdate())
interview = create_exit_interview(employee)
self.assertTrue(interview.name)
def test_interview_date_updated_in_employee_master(self):
employee = make_employee('employeeexit3@example.com')
frappe.db.set_value('Employee', employee, 'relieving_date', getdate())
interview = create_exit_interview(employee)
interview.status = 'Completed'
interview.employee_status = 'Exit Confirmed'
# exit interview date updated on submit
interview.submit()
self.assertEqual(frappe.db.get_value('Employee', employee, 'held_on'), interview.date)
# exit interview reset on cancel
interview.reload()
interview.cancel()
self.assertEqual(frappe.db.get_value('Employee', employee, 'held_on'), None)
def test_send_exit_questionnaire(self):
create_custom_doctype()
create_webform()
template = create_notification_template()
webform = frappe.db.get_all('Web Form', limit=1)
frappe.db.set_value('HR Settings', 'HR Settings', {
'exit_questionnaire_web_form': webform[0].name,
'exit_questionnaire_notification_template': template
})
employee = make_employee('employeeexit3@example.com')
frappe.db.set_value('Employee', employee, 'relieving_date', getdate())
interview = create_exit_interview(employee)
send_exit_questionnaire([interview])
email_queue = frappe.db.get_all('Email Queue', ['name', 'message'], limit=1)
self.assertTrue('Subject: Exit Questionnaire Notification' in email_queue[0].message)
def tearDown(self):
frappe.db.rollback()
def create_exit_interview(employee, save=True):
interviewer = create_user('test_exit_interviewer@example.com')
doc = frappe.get_doc({
'doctype': 'Exit Interview',
'employee': employee,
'company': '_Test Company',
'status': 'Pending',
'date': getdate(),
'interviewers': [{
'interviewer': interviewer.name
}],
'interview_summary': 'Test'
})
if save:
return doc.insert()
return doc
def create_notification_template():
template = frappe.db.exists('Email Template', _('Exit Questionnaire Notification'))
if not template:
base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
response = frappe.read_file(os.path.join(base_path, 'exit_interview/exit_questionnaire_notification_template.html'))
template = frappe.get_doc({
'doctype': 'Email Template',
'name': _('Exit Questionnaire Notification'),
'response': response,
'subject': _('Exit Questionnaire Notification'),
'owner': frappe.session.user,
}).insert(ignore_permissions=True)
template = template.name
return template

29
erpnext/hr/doctype/hr_settings/hr_settings.json

@ -36,7 +36,11 @@
"remind_before",
"column_break_4",
"send_interview_feedback_reminder",
"feedback_reminder_notification_template"
"feedback_reminder_notification_template",
"employee_exit_section",
"exit_questionnaire_web_form",
"column_break_34",
"exit_questionnaire_notification_template"
],
"fields": [
{
@ -226,13 +230,34 @@
"fieldname": "check_vacancies",
"fieldtype": "Check",
"label": "Check Vacancies On Job Offer Creation"
},
{
"fieldname": "employee_exit_section",
"fieldtype": "Section Break",
"label": "Employee Exit Settings"
},
{
"fieldname": "exit_questionnaire_web_form",
"fieldtype": "Link",
"label": "Exit Questionnaire Web Form",
"options": "Web Form"
},
{
"fieldname": "exit_questionnaire_notification_template",
"fieldtype": "Link",
"label": "Exit Questionnaire Notification Template",
"options": "Email Template"
},
{
"fieldname": "column_break_34",
"fieldtype": "Column Break"
}
],
"icon": "fa fa-cog",
"idx": 1,
"issingle": 1,
"links": [],
"modified": "2021-10-01 23:46:11.098236",
"modified": "2021-12-05 14:48:10.884253",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",

0
erpnext/hr/notification/exit_interview_scheduled/__init__.py

29
erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.json

@ -0,0 +1,29 @@
{
"attach_print": 0,
"channel": "Email",
"condition": "doc.date and doc.email and doc.docstatus != 2 and doc.status == 'Scheduled'",
"creation": "2021-12-05 22:11:47.263933",
"date_changed": "date",
"days_in_advance": 1,
"docstatus": 0,
"doctype": "Notification",
"document_type": "Exit Interview",
"enabled": 1,
"event": "Days Before",
"idx": 0,
"is_standard": 1,
"message": "<table class=\"panel-header\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n\t<tr height=\"10\"></tr>\n\t<tr>\n\t\t<td width=\"15\"></td>\n\t\t<td>\n\t\t\t<div class=\"text-medium text-muted\">\n\t\t\t\t<span>{{_(\"Exit Interview Scheduled:\")}} {{ doc.name }}</span>\n\t\t\t</div>\n\t\t</td>\n\t\t<td width=\"15\"></td>\n\t</tr>\n\t<tr height=\"10\"></tr>\n</table>\n\n<table class=\"panel-body\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n\t<tr height=\"10\"></tr>\n\t<tr>\n\t\t<td width=\"15\"></td>\n\t\t<td>\n\t\t\t<div>\n\t\t\t\t<ul class=\"list-unstyled\" style=\"line-height: 1.7\">\n\t\t\t\t\t<li>{{_(\"Employee\")}}: <b>{{ doc.employee }} - {{ doc.employee_name }}</b></li>\n\t\t\t\t\t<li>{{_(\"Date\")}}: <b>{{ doc.date }}</b></li>\n\t\t\t\t\t<li> {{_(\"Interviewers\")}}: </li>\n\t\t\t\t\t{% for entry in doc.interviewers %}\n\t\t\t\t\t\t<ul>\n\t\t\t\t\t\t\t<li>{{ entry.user }}</li>\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t{% endfor %}\n\t\t\t\t\t<li>{{ _(\"Interview Document\") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>\n\t\t\t\t</ul>\n\t\t\t</div>\n\t\t</td>\n\t\t<td width=\"15\"></td>\n\t</tr>\n\t<tr height=\"10\"></tr>\n</table>\n",
"modified": "2021-12-05 22:26:57.096159",
"modified_by": "Administrator",
"module": "HR",
"name": "Exit Interview Scheduled",
"owner": "Administrator",
"recipients": [
{
"receiver_by_document_field": "email"
}
],
"send_system_notification": 0,
"send_to_all_assignees": 1,
"subject": "Exit Interview Scheduled: {{ doc.name }}"
}

37
erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.md

@ -0,0 +1,37 @@
<table class="panel-header" border="0" cellpadding="0" cellspacing="0" width="100%">
<tr height="10"></tr>
<tr>
<td width="15"></td>
<td>
<div class="text-medium text-muted">
<h2>{{_("Exit Interview Scheduled:")}} {{ doc.name }}</h2>
</div>
</td>
<td width="15"></td>
</tr>
<tr height="10"></tr>
</table>
<table class="panel-body" border="0" cellpadding="0" cellspacing="0" width="100%">
<tr height="10"></tr>
<tr>
<td width="15"></td>
<td>
<div>
<ul class="list-unstyled" style="line-height: 1.7">
<li><b>{{_("Employee")}}: </b>{{ doc.employee }} - {{ doc.employee_name }}</li>
<li><b>{{_("Date")}}: </b>{{ frappe.utils.formatdate(doc.date) }}</li>
<li><b>{{_("Interviewers")}}:</b> </li>
{% for entry in doc.interviewers %}
<ul>
<li>{{ entry.user }}</li>
</ul>
{% endfor %}
<li><b>{{ _("Interview Document") }}:</b> {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>
</ul>
</div>
</td>
<td width="15"></td>
</tr>
<tr height="10"></tr>
</table>

6
erpnext/hr/notification/exit_interview_scheduled/exit_interview_scheduled.py

@ -0,0 +1,6 @@
# import frappe
def get_context(context):
# do your magic here
pass

0
erpnext/hr/report/employee_exits/__init__.py

77
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"
}
]
};

33
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"
}
]
}

231
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.query_builder import Order
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': 120
},
{
'label': _('Relieving Date'),
'fieldname': 'relieving_date',
'fieldtype': 'Date',
'width': 120
},
{
'label': _('Exit Interview'),
'fieldname': 'exit_interview',
'fieldtype': 'Link',
'options': 'Exit Interview',
'width': 150
},
{
'label': _('Interview Status'),
'fieldname': 'interview_status',
'fieldtype': 'Data',
'width': 130
},
{
'label': _('Final Decision'),
'fieldname': 'employee_status',
'fieldtype': 'Data',
'width': 150
},
{
'label': _('Full and Final Statement'),
'fieldname': 'full_and_final_statement',
'fieldtype': 'Link',
'options': 'Full and Final Statement',
'width': 180
},
{
'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
}
]
def get_data(filters):
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()
.orderby(employee.relieving_date, order=Order.asc)
.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 == '') | (fnf.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'
},
]

242
erpnext/hr/report/employee_exits/test_employee_exits.py

@ -0,0 +1,242 @@
import unittest
import frappe
from frappe.utils import add_days, getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.exit_interview.test_exit_interview import create_exit_interview
from erpnext.hr.doctype.full_and_final_statement.test_full_and_final_statement import (
create_full_and_final_statement,
)
from erpnext.hr.report.employee_exits.employee_exits import execute
class TestEmployeeExits(unittest.TestCase):
@classmethod
def setUpClass(cls):
create_company()
frappe.db.sql("delete from `tabEmployee` where company='Test Company'")
frappe.db.sql("delete from `tabFull and Final Statement` where company='Test Company'")
frappe.db.sql("delete from `tabExit Interview` where company='Test Company'")
cls.create_records()
@classmethod
def tearDownClass(cls):
frappe.db.rollback()
@classmethod
def create_records(cls):
cls.emp1 = make_employee(
'employeeexit1@example.com',
company='Test Company',
date_of_joining=getdate('01-10-2021'),
relieving_date=add_days(getdate(), 14),
designation='Accountant'
)
cls.emp2 = make_employee(
'employeeexit2@example.com',
company='Test Company',
date_of_joining=getdate('01-12-2021'),
relieving_date=add_days(getdate(), 15),
designation='Accountant'
)
cls.emp3 = make_employee(
'employeeexit3@example.com',
company='Test Company',
date_of_joining=getdate('02-12-2021'),
relieving_date=add_days(getdate(), 29),
designation='Engineer'
)
cls.emp4 = make_employee(
'employeeexit4@example.com',
company='Test Company',
date_of_joining=getdate('01-12-2021'),
relieving_date=add_days(getdate(), 30),
designation='Engineer'
)
# exit interview for 3 employees only
cls.interview1 = create_exit_interview(cls.emp1)
cls.interview2 = create_exit_interview(cls.emp2)
cls.interview3 = create_exit_interview(cls.emp3)
# create fnf for some records
cls.fnf1 = create_full_and_final_statement(cls.emp1)
cls.fnf2 = create_full_and_final_statement(cls.emp2)
# link questionnaire for a few records
# setting employee doctype as reference instead of creating a questionnaire
# since this is just for a test
frappe.db.set_value('Exit Interview', cls.interview1.name, {
'ref_doctype': 'Employee',
'reference_document_name': cls.emp1
})
frappe.db.set_value('Exit Interview', cls.interview2.name, {
'ref_doctype': 'Employee',
'reference_document_name': cls.emp2
})
frappe.db.set_value('Exit Interview', cls.interview3.name, {
'ref_doctype': 'Employee',
'reference_document_name': cls.emp3
})
def test_employee_exits_summary(self):
filters = {
'company': 'Test Company',
'from_date': getdate(),
'to_date': add_days(getdate(), 15),
'designation': 'Accountant'
}
report = execute(filters)
employee1 = frappe.get_doc('Employee', self.emp1)
employee2 = frappe.get_doc('Employee', self.emp2)
expected_data = [
{
'employee': employee1.name,
'employee_name': employee1.employee_name,
'date_of_joining': employee1.date_of_joining,
'relieving_date': employee1.relieving_date,
'department': employee1.department,
'designation': employee1.designation,
'reports_to': None,
'exit_interview': self.interview1.name,
'interview_status': self.interview1.status,
'employee_status': '',
'questionnaire': employee1.name,
'full_and_final_statement': self.fnf1.name
},
{
'employee': employee2.name,
'employee_name': employee2.employee_name,
'date_of_joining': employee2.date_of_joining,
'relieving_date': employee2.relieving_date,
'department': employee2.department,
'designation': employee2.designation,
'reports_to': None,
'exit_interview': self.interview2.name,
'interview_status': self.interview2.status,
'employee_status': '',
'questionnaire': employee2.name,
'full_and_final_statement': self.fnf2.name
}
]
self.assertEqual(expected_data, report[1]) # rows
def test_pending_exit_interviews_summary(self):
filters = {
'company': 'Test Company',
'from_date': getdate(),
'to_date': add_days(getdate(), 30),
'exit_interview_pending': 1
}
report = execute(filters)
employee4 = frappe.get_doc('Employee', self.emp4)
expected_data = [{
'employee': employee4.name,
'employee_name': employee4.employee_name,
'date_of_joining': employee4.date_of_joining,
'relieving_date': employee4.relieving_date,
'department': employee4.department,
'designation': employee4.designation,
'reports_to': None,
'exit_interview': None,
'interview_status': None,
'employee_status': None,
'questionnaire': None,
'full_and_final_statement': None
}]
self.assertEqual(expected_data, report[1]) # rows
def test_pending_exit_questionnaire_summary(self):
filters = {
'company': 'Test Company',
'from_date': getdate(),
'to_date': add_days(getdate(), 30),
'questionnaire_pending': 1
}
report = execute(filters)
employee4 = frappe.get_doc('Employee', self.emp4)
expected_data = [{
'employee': employee4.name,
'employee_name': employee4.employee_name,
'date_of_joining': employee4.date_of_joining,
'relieving_date': employee4.relieving_date,
'department': employee4.department,
'designation': employee4.designation,
'reports_to': None,
'exit_interview': None,
'interview_status': None,
'employee_status': None,
'questionnaire': None,
'full_and_final_statement': None
}]
self.assertEqual(expected_data, report[1]) # rows
def test_pending_fnf_summary(self):
filters = {
'company': 'Test Company',
'fnf_pending': 1
}
report = execute(filters)
employee3 = frappe.get_doc('Employee', self.emp3)
employee4 = frappe.get_doc('Employee', self.emp4)
expected_data = [
{
'employee': employee3.name,
'employee_name': employee3.employee_name,
'date_of_joining': employee3.date_of_joining,
'relieving_date': employee3.relieving_date,
'department': employee3.department,
'designation': employee3.designation,
'reports_to': None,
'exit_interview': self.interview3.name,
'interview_status': self.interview3.status,
'employee_status': '',
'questionnaire': employee3.name,
'full_and_final_statement': None
},
{
'employee': employee4.name,
'employee_name': employee4.employee_name,
'date_of_joining': employee4.date_of_joining,
'relieving_date': employee4.relieving_date,
'department': employee4.department,
'designation': employee4.designation,
'reports_to': None,
'exit_interview': None,
'interview_status': None,
'employee_status': None,
'questionnaire': None,
'full_and_final_statement': None
}
]
self.assertEqual(expected_data, report[1]) # rows
def create_company():
if not frappe.db.exists('Company', 'Test Company'):
frappe.get_doc({
'doctype': 'Company',
'company_name': 'Test Company',
'default_currency': 'INR',
'country': 'India'
}).insert()

871
erpnext/hr/workspace/hr/hr.json

@ -5,7 +5,7 @@
"label": "Outgoing Salary"
}
],
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Human Resource\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Outgoing Salary\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Employee\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Leave Application\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Job Applicant\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Monthly Attendance Sheet\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Employee\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Employee Lifecycle\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Shift Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Leaves\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Expense Claims\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Fleet Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Recruitment\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loans\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Training\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Performance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Key Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}]",
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Human Resource\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Outgoing Salary\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Employee\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leave Application\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Attendance\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Applicant\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Monthly Attendance Sheet\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports &amp; Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Lifecycle\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Exit\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Shift Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Leaves\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Attendance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Expense Claims\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loans\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Recruitment\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Performance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Fleet Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Training\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]",
"creation": "2020-03-02 15:48:58.322521",
"docstatus": 0,
"doctype": "Workspace",
@ -15,14 +15,6 @@
"idx": 0,
"label": "HR",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
@ -111,14 +103,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Lifecycle",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Job Applicant",
"hidden": 0,
@ -227,14 +211,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Shift Management",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
@ -268,14 +244,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Leaves",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
@ -386,14 +354,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Attendance",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Employee",
"hidden": 0,
@ -449,14 +409,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Expense Claims",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Employee",
"hidden": 0,
@ -489,14 +441,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Settings",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
@ -530,14 +474,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Fleet Management",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
@ -581,14 +517,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Recruitment",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
@ -808,14 +736,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Key Reports",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Attendance",
"hidden": 0,
@ -933,9 +853,796 @@
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Lifecycle",
"link_count": 7,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Job Applicant",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Onboarding",
"link_count": 0,
"link_to": "Employee Onboarding",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Skill Map",
"link_count": 0,
"link_to": "Employee Skill Map",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Promotion",
"link_count": 0,
"link_to": "Employee Promotion",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Transfer",
"link_count": 0,
"link_to": "Employee Transfer",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Grievance Type",
"link_count": 0,
"link_to": "Grievance Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Grievance",
"link_count": 0,
"link_to": "Employee Grievance",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Onboarding Template",
"link_count": 0,
"link_to": "Employee Onboarding Template",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Exit",
"link_count": 4,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Separation Template",
"link_count": 0,
"link_to": "Employee Separation Template",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Separation",
"link_count": 0,
"link_to": "Employee Separation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Full and Final Statement",
"link_count": 0,
"link_to": "Full and Final Statement",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Exit Interview",
"link_count": 0,
"link_to": "Exit Interview",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee",
"link_count": 8,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Employee",
"link_count": 0,
"link_to": "Employee",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Employment Type",
"link_count": 0,
"link_to": "Employment Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Branch",
"link_count": 0,
"link_to": "Branch",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Department",
"link_count": 0,
"link_to": "Department",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Designation",
"link_count": 0,
"link_to": "Designation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Grade",
"link_count": 0,
"link_to": "Employee Grade",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Group",
"link_count": 0,
"link_to": "Employee Group",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Health Insurance",
"link_count": 0,
"link_to": "Employee Health Insurance",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Key Reports",
"link_count": 7,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Attendance",
"hidden": 0,
"is_query_report": 1,
"label": "Monthly Attendance Sheet",
"link_count": 0,
"link_to": "Monthly Attendance Sheet",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Staffing Plan",
"hidden": 0,
"is_query_report": 1,
"label": "Recruitment Analytics",
"link_count": 0,
"link_to": "Recruitment Analytics",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 1,
"label": "Employee Analytics",
"link_count": 0,
"link_to": "Employee Analytics",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 1,
"label": "Employee Leave Balance",
"link_count": 0,
"link_to": "Employee Leave Balance",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 1,
"label": "Employee Leave Balance Summary",
"link_count": 0,
"link_to": "Employee Leave Balance Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee Advance",
"hidden": 0,
"is_query_report": 1,
"label": "Employee Advance Summary",
"link_count": 0,
"link_to": "Employee Advance Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Exits",
"link_count": 0,
"link_to": "Employee Exits",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Recruitment",
"link_count": 11,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Job Opening",
"link_count": 0,
"link_to": "Job Opening",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Referral",
"link_count": 0,
"link_to": "Employee Referral",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Job Applicant",
"link_count": 0,
"link_to": "Job Applicant",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Job Offer",
"link_count": 0,
"link_to": "Job Offer",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Staffing Plan",
"link_count": 0,
"link_to": "Staffing Plan",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Appointment Letter",
"link_count": 0,
"link_to": "Appointment Letter",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Appointment Letter Template",
"link_count": 0,
"link_to": "Appointment Letter Template",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Interview Type",
"link_count": 0,
"link_to": "Interview Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Interview Round",
"link_count": 0,
"link_to": "Interview Round",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Interview",
"link_count": 0,
"link_to": "Interview",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Interview Feedback",
"link_count": 0,
"link_to": "Interview Feedback",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Fleet Management",
"link_count": 4,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Driver",
"link_count": 0,
"link_to": "Driver",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Vehicle",
"link_count": 0,
"link_to": "Vehicle",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Vehicle Log",
"link_count": 0,
"link_to": "Vehicle Log",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Vehicle",
"hidden": 0,
"is_query_report": 1,
"label": "Vehicle Expenses",
"link_count": 0,
"link_to": "Vehicle Expenses",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Settings",
"link_count": 3,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "HR Settings",
"link_count": 0,
"link_to": "HR Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Daily Work Summary Group",
"link_count": 0,
"link_to": "Daily Work Summary Group",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Team Updates",
"link_count": 0,
"link_to": "team-updates",
"link_type": "Page",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Expense Claims",
"link_count": 3,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Expense Claim",
"link_count": 0,
"link_to": "Expense Claim",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Advance",
"link_count": 0,
"link_to": "Employee Advance",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Travel Request",
"link_count": 0,
"link_to": "Travel Request",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Attendance",
"link_count": 5,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Attendance Tool",
"link_count": 0,
"link_to": "Employee Attendance Tool",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Attendance",
"link_count": 0,
"link_to": "Attendance",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Attendance Request",
"link_count": 0,
"link_to": "Attendance Request",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Upload Attendance",
"link_count": 0,
"link_to": "Upload Attendance",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Checkin",
"link_count": 0,
"link_to": "Employee Checkin",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Leaves",
"link_count": 10,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Holiday List",
"link_count": 0,
"link_to": "Holiday List",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Type",
"link_count": 0,
"link_to": "Leave Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Period",
"link_count": 0,
"link_to": "Leave Period",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Leave Type",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Policy",
"link_count": 0,
"link_to": "Leave Policy",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Leave Policy",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Policy Assignment",
"link_count": 0,
"link_to": "Leave Policy Assignment",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Application",
"link_count": 0,
"link_to": "Leave Application",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Allocation",
"link_count": 0,
"link_to": "Leave Allocation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Encashment",
"link_count": 0,
"link_to": "Leave Encashment",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Block List",
"link_count": 0,
"link_to": "Leave Block List",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Compensatory Leave Request",
"link_count": 0,
"link_to": "Compensatory Leave Request",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Shift Management",
"link_count": 3,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Shift Type",
"link_count": 0,
"link_to": "Shift Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Shift Request",
"link_count": 0,
"link_to": "Shift Request",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Shift Assignment",
"link_count": 0,
"link_to": "Shift Assignment",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2021-08-31 12:18:59.842919",
"modified": "2021-12-05 22:05:13.004462",
"modified_by": "Administrator",
"module": "HR",
"name": "HR",

3
erpnext/manufacturing/doctype/work_order/test_work_order.py

@ -29,6 +29,9 @@ class TestWorkOrder(ERPNextTestCase):
self.warehouse = '_Test Warehouse 2 - _TC'
self.item = '_Test Item'
def tearDown(self):
frappe.db.rollback()
def check_planned_qty(self):
planned0 = frappe.db.get_value("Bin", {"item_code": "_Test FG Item",

1
erpnext/patches.txt

@ -316,3 +316,4 @@ erpnext.patches.v13_0.create_ksa_vat_custom_fields
erpnext.patches.v14_0.migrate_crm_settings
erpnext.patches.v13_0.rename_ksa_qr_field
erpnext.patches.v13_0.disable_ksa_print_format_for_others
erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template

27
erpnext/patches/v14_0/add_default_exit_questionnaire_notification_template.py

@ -0,0 +1,27 @@
import os
import frappe
from frappe import _
def execute():
frappe.reload_doc("email", "doctype", "email_template")
frappe.reload_doc("hr", "doctype", "hr_settings")
template = frappe.db.exists("Email Template", _("Exit Questionnaire Notification"))
if not template:
base_path = frappe.get_app_path("erpnext", "hr", "doctype")
response = frappe.read_file(os.path.join(base_path, "exit_interview/exit_questionnaire_notification_template.html"))
template = frappe.get_doc({
"doctype": "Email Template",
"name": _("Exit Questionnaire Notification"),
"response": response,
"subject": _("Exit Questionnaire Notification"),
"owner": frappe.session.user,
}).insert(ignore_permissions=True)
template = template.name
hr_settings = frappe.get_doc("HR Settings")
hr_settings.exit_questionnaire_notification_template = template
hr_settings.save()

2
erpnext/setup/setup_wizard/operations/defaults_setup.py

@ -68,6 +68,8 @@ def set_default_settings(args):
hr_settings.send_interview_feedback_reminder = 1
hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder")
hr_settings.exit_questionnaire_notification_template = _("Exit Questionnaire Notification")
hr_settings.save()
def set_no_copy_fields_in_variant_settings():

5
erpnext/setup/setup_wizard/operations/install_fixtures.py

@ -278,6 +278,11 @@ def install(country=None):
records += [{'doctype': 'Email Template', 'name': _('Interview Feedback Reminder'), 'response': response,
'subject': _('Interview Feedback Reminder'), 'owner': frappe.session.user}]
response = frappe.read_file(os.path.join(base_path, 'exit_interview/exit_questionnaire_notification_template.html'))
records += [{'doctype': 'Email Template', 'name': _('Exit Questionnaire Notification'), 'response': response,
'subject': _('Exit Questionnaire Notification'), 'owner': frappe.session.user}]
base_path = frappe.get_app_path("erpnext", "stock", "doctype")
response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html"))

Loading…
Cancel
Save