Browse Source
* 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 testsdevelop
Rucha Mahabal
3 years ago
committed by
GitHub
24 changed files with 2090 additions and 85 deletions
@ -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'); |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
}); |
@ -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 |
|||
} |
@ -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) |
@ -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 |
|||
} |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
}; |
@ -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> |
@ -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 |
@ -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 }}" |
|||
} |
@ -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> |
@ -0,0 +1,6 @@ |
|||
# import frappe |
|||
|
|||
|
|||
def get_context(context): |
|||
# do your magic here |
|||
pass |
@ -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" |
|||
} |
|||
] |
|||
}; |
@ -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" |
|||
} |
|||
] |
|||
} |
@ -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' |
|||
}, |
|||
] |
|||
|
@ -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() |
@ -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() |
Loading…
Reference in new issue