feat: Tracking Multi-round interview (#25482) (#27724)
* feat: Tracking Multi-round interview
* fix: releted to scheduler event and formating
* fix: job applicant UI/UX and conflicts
* test: Interview Round
* fix(test): Employee referral, Employee Onboarding, Job Offer
* fix: sider
* feat: set default value in Hr settings
* feat: added validation for designation
* test: Interview
* test: Added validatiolns for skill
* test: Interview feedback
* fix: sider
* fix: remove unnecessary validations and form label cleanups
* chore: clean-up Interview Round and Interview Type doctype
* fix: remove redundant Rating Value, only keep Rating
* fix: update interview details on feedback submission
- make interview feedback submission dialog minimizable
* fix: show submit feedback button only if feedback doesn't exist
* refactor: Interview and Feedback statuses and workflow
* fix(HR Settings): clean up interview settings
* refactor: Interview
* refactor: Interview Feedback, remove unnecessary validations
* chore: update notification messages
* chore: remove unnecessary formatting changes in attendance list and leave application
* refactor: Job Applicant to Interview mapping
* chore: sorted imports
* chore: sorted imports
* fix: sider issues
* fix: linter issues
* fix: sider issues
* fix: tests
* fix: sorted imports
* fix: tests, sider
* fix: therapy plan test
* fix: sider issues
* feat: Include From Time and To Time fields in Interview for cleaner data
* feat: Interview Calendar
* fix: allow renaming masters
* fix: add more fields to list view and standard filter
* fix: validate overlapping interviews
* fix: update tests
* fix: linter issues
* refactor: replace reminder messages with Email Templates
* fix: sider issues
Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
(cherry picked from commit 57e66f958cd57d66a6fd3b19f6cd3593eab63666)
Co-authored-by: Anurag Mishra <32095923+Anurag810@users.noreply.github.com>
diff --git a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py
index 099146c..9dd97a6 100644
--- a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py
+++ b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py
@@ -21,6 +21,7 @@
def setUp(self):
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
+ frappe.db.sql('delete from `tabPatient Appointment`')
make_pos_profile()
def test_medical_record(self):
diff --git a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
index 4f96f6a..021ba9b 100644
--- a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
+++ b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
@@ -6,7 +6,7 @@
import unittest
import frappe
-from frappe.utils import flt, getdate, nowdate
+from frappe.utils import add_days, flt, getdate, nowdate
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import (
create_appointment,
@@ -33,10 +33,12 @@
self.assertEqual(plan.status, 'Not Started')
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
+ session.start_date = getdate()
frappe.get_doc(session).submit()
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'In Progress')
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
+ session.start_date = add_days(getdate(), 1)
frappe.get_doc(session).submit()
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
@@ -44,6 +46,7 @@
appointment = create_appointment(patient, practitioner, nowdate())
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name)
+ session.start_date = add_days(getdate(), 2)
session = frappe.get_doc(session)
session.submit()
self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')
diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
index 6d63f39..b31a952 100644
--- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
+++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
@@ -6,7 +6,7 @@
import frappe
from frappe.model.document import Document
-from frappe.utils import flt, today
+from frappe.utils import flt
class TherapyPlan(Document):
@@ -63,8 +63,6 @@
therapy_session.exercises = therapy_type.exercises
therapy_session.appointment = appointment
- if frappe.flags.in_test:
- therapy_session.start_date = today()
return therapy_session.as_dict()
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index a8f1617..b89b10b 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -344,6 +344,7 @@
"all": [
"erpnext.projects.doctype.project.project.project_status_update_reminder",
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder",
+ "erpnext.hr.doctype.interview.interview.send_interview_reminder",
"erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts"
],
"hourly": [
@@ -388,6 +389,7 @@
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
"erpnext.non_profit.doctype.membership.membership.set_expired_status"
+ "erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder"
],
"daily_long": [
"erpnext.setup.doctype.email_digest.email_digest.send",
diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js
index 9a3bac0..6b3c29a 100644
--- a/erpnext/hr/doctype/attendance/attendance_list.js
+++ b/erpnext/hr/doctype/attendance/attendance_list.js
@@ -9,83 +9,86 @@
return [__(doc.status), "orange", "status,=," + doc.status];
}
},
+
onload: function(list_view) {
let me = this;
- const months = moment.months()
- list_view.page.add_inner_button( __("Mark Attendance"), function() {
+ const months = moment.months();
+ list_view.page.add_inner_button(__("Mark Attendance"), function() {
let dialog = new frappe.ui.Dialog({
title: __("Mark Attendance"),
- fields: [
- {
- fieldname: 'employee',
- label: __('For Employee'),
- fieldtype: 'Link',
- options: 'Employee',
- get_query: () => {
- return {query: "erpnext.controllers.queries.employee_query"}
- },
- reqd: 1,
- onchange: function() {
- dialog.set_df_property("unmarked_days", "hidden", 1);
- dialog.set_df_property("status", "hidden", 1);
- dialog.set_df_property("month", "value", '');
+ fields: [{
+ fieldname: 'employee',
+ label: __('For Employee'),
+ fieldtype: 'Link',
+ options: 'Employee',
+ get_query: () => {
+ return {query: "erpnext.controllers.queries.employee_query"};
+ },
+ reqd: 1,
+ onchange: function() {
+ dialog.set_df_property("unmarked_days", "hidden", 1);
+ dialog.set_df_property("status", "hidden", 1);
+ dialog.set_df_property("month", "value", '');
+ dialog.set_df_property("unmarked_days", "options", []);
+ dialog.no_unmarked_days_left = false;
+ }
+ },
+ {
+ label: __("For Month"),
+ fieldtype: "Select",
+ fieldname: "month",
+ options: months,
+ reqd: 1,
+ onchange: function() {
+ if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
+ dialog.set_df_property("status", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", []);
dialog.no_unmarked_days_left = false;
+ me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options => {
+ if (options.length > 0) {
+ dialog.set_df_property("unmarked_days", "hidden", 0);
+ dialog.set_df_property("unmarked_days", "options", options);
+ } else {
+ dialog.no_unmarked_days_left = true;
+ }
+ });
}
- },
- {
- label: __("For Month"),
- fieldtype: "Select",
- fieldname: "month",
- options: months,
- reqd: 1,
- onchange: function() {
- if(dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
- dialog.set_df_property("status", "hidden", 0);
- dialog.set_df_property("unmarked_days", "options", []);
- dialog.no_unmarked_days_left = false;
- me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options =>{
- if (options.length > 0) {
- dialog.set_df_property("unmarked_days", "hidden", 0);
- dialog.set_df_property("unmarked_days", "options", options);
- } else {
- dialog.no_unmarked_days_left = true;
- }
- });
- }
- }
- },
- {
- label: __("Status"),
- fieldtype: "Select",
- fieldname: "status",
- options: ["Present", "Absent", "Half Day", "Work From Home"],
- hidden:1,
- reqd: 1,
+ }
+ },
+ {
+ label: __("Status"),
+ fieldtype: "Select",
+ fieldname: "status",
+ options: ["Present", "Absent", "Half Day", "Work From Home"],
+ hidden: 1,
+ reqd: 1,
- },
- {
- label: __("Unmarked Attendance for days"),
- fieldname: "unmarked_days",
- fieldtype: "MultiCheck",
- options: [],
- columns: 2,
- hidden: 1
- },
- ],
- primary_action(data) {
+ },
+ {
+ label: __("Unmarked Attendance for days"),
+ fieldname: "unmarked_days",
+ fieldtype: "MultiCheck",
+ options: [],
+ columns: 2,
+ hidden: 1
+ }],
+ primary_action(data) {
if (cur_dialog.no_unmarked_days_left) {
- frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",[dialog.fields_dict.month.value, dialog.fields_dict.employee.value]));
+ frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",
+ [dialog.fields_dict.month.value, dialog.fields_dict.employee.value]));
} else {
- frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status,data.month]), () => {
+ frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status, data.month]), () => {
frappe.call({
method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
args: {
data: data
},
- callback: function(r) {
+ callback: function (r) {
if (r.message === 1) {
- frappe.show_alert({message: __("Attendance Marked"), indicator: 'blue'});
+ frappe.show_alert({
+ message: __("Attendance Marked"),
+ indicator: 'blue'
+ });
cur_dialog.hide();
}
}
@@ -101,21 +104,26 @@
dialog.show();
});
},
- get_multi_select_options: function(employee, month){
+
+ get_multi_select_options: function(employee, month) {
return new Promise(resolve => {
frappe.call({
method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days',
async: false,
- args:{
+ args: {
employee: employee,
month: month,
}
}).then(r => {
var options = [];
- for(var d in r.message){
+ for (var d in r.message) {
var momentObj = moment(r.message[d], 'YYYY-MM-DD');
var date = momentObj.format('DD-MM-YYYY');
- options.push({ "label":date, "value": r.message[d] , "checked": 1});
+ options.push({
+ "label": date,
+ "value": r.message[d],
+ "checked": 1
+ });
}
resolve(options);
});
diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
index eae600d..1e3b9cb 100644
--- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
+++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
@@ -71,6 +71,7 @@
applicant = frappe.new_doc('Job Applicant')
applicant.applicant_name = 'Test Researcher'
applicant.email_id = 'test@researcher.com'
+ applicant.designation = 'Researcher'
applicant.status = 'Open'
applicant.cover_letter = 'I am a great Researcher.'
applicant.insert()
diff --git a/erpnext/hr/doctype/employee_referral/employee_referral.py b/erpnext/hr/doctype/employee_referral/employee_referral.py
index 5cb5bb5..db356bf 100644
--- a/erpnext/hr/doctype/employee_referral/employee_referral.py
+++ b/erpnext/hr/doctype/employee_referral/employee_referral.py
@@ -38,8 +38,10 @@
status = "Open"
job_applicant = frappe.new_doc("Job Applicant")
+ job_applicant.source = "Employee Referral"
job_applicant.employee_referral = emp_ref.name
job_applicant.status = status
+ job_applicant.designation = emp_ref.for_designation
job_applicant.applicant_name = emp_ref.full_name
job_applicant.email_id = emp_ref.email
job_applicant.phone_number = emp_ref.contact_no
diff --git a/erpnext/hr/doctype/employee_referral/test_employee_referral.py b/erpnext/hr/doctype/employee_referral/test_employee_referral.py
index d0ee2fc..1340f62 100644
--- a/erpnext/hr/doctype/employee_referral/test_employee_referral.py
+++ b/erpnext/hr/doctype/employee_referral/test_employee_referral.py
@@ -17,6 +17,11 @@
class TestEmployeeReferral(unittest.TestCase):
+
+ def setUp(self):
+ frappe.db.sql("DELETE FROM `tabJob Applicant`")
+ frappe.db.sql("DELETE FROM `tabEmployee Referral`")
+
def test_workflow_and_status_sync(self):
emp_ref = create_employee_referral()
@@ -50,6 +55,10 @@
add_sal = create_additional_salary(emp_ref)
self.assertTrue(add_sal.ref_docname, emp_ref.name)
+ def tearDown(self):
+ frappe.db.sql("DELETE FROM `tabJob Applicant`")
+ frappe.db.sql("DELETE FROM `tabEmployee Referral`")
+
def create_employee_referral():
emp_ref = frappe.new_doc("Employee Referral")
diff --git a/erpnext/hr/doctype/expected_skill_set/__init__.py b/erpnext/hr/doctype/expected_skill_set/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/expected_skill_set/__init__.py
diff --git a/erpnext/hr/doctype/expected_skill_set/expected_skill_set.json b/erpnext/hr/doctype/expected_skill_set/expected_skill_set.json
new file mode 100644
index 0000000..899f5bd
--- /dev/null
+++ b/erpnext/hr/doctype/expected_skill_set/expected_skill_set.json
@@ -0,0 +1,40 @@
+{
+ "actions": [],
+ "creation": "2021-04-12 13:05:06.741330",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "skill",
+ "description"
+ ],
+ "fields": [
+ {
+ "fieldname": "skill",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Skill",
+ "options": "Skill",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "skill.description",
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Description"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-04-12 14:26:33.062549",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Expected Skill Set",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/expected_skill_set/expected_skill_set.py b/erpnext/hr/doctype/expected_skill_set/expected_skill_set.py
new file mode 100644
index 0000000..27120c1
--- /dev/null
+++ b/erpnext/hr/doctype/expected_skill_set/expected_skill_set.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+# import frappe
+from frappe.model.document import Document
+
+
+class ExpectedSkillSet(Document):
+ pass
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json
index 8aa3c0c..4bc066f 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.json
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.json
@@ -30,7 +30,13 @@
"auto_leave_encashment",
"restrict_backdated_leave_application",
"hiring_settings",
- "check_vacancies"
+ "check_vacancies",
+ "send_interview_reminder",
+ "interview_reminder_template",
+ "remind_before",
+ "column_break_29",
+ "send_interview_feedback_reminder",
+ "feedback_reminder_notification_template"
],
"fields": [
{
@@ -143,6 +149,13 @@
"label": "Standard Working Hours"
},
{
+ "default": "00:15:00",
+ "depends_on": "send_interview_reminder",
+ "fieldname": "remind_before",
+ "fieldtype": "Time",
+ "label": "Remind Before"
+ },
+ {
"collapsible": 1,
"fieldname": "reminders_section",
"fieldtype": "Section Break",
@@ -181,13 +194,45 @@
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "send_interview_reminder",
+ "fieldtype": "Check",
+ "label": "Send Interview Reminder"
+ },
+ {
+ "default": "0",
+ "fieldname": "send_interview_feedback_reminder",
+ "fieldtype": "Check",
+ "label": "Send Interview Feedback Reminder"
+ },
+ {
+ "fieldname": "column_break_29",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "send_interview_feedback_reminder",
+ "fieldname": "feedback_reminder_notification_template",
+ "fieldtype": "Link",
+ "label": "Feedback Reminder Notification Template",
+ "mandatory_depends_on": "send_interview_feedback_reminder",
+ "options": "Email Template"
+ },
+ {
+ "depends_on": "send_interview_reminder",
+ "fieldname": "interview_reminder_template",
+ "fieldtype": "Link",
+ "label": "Interview Reminder Notification Template",
+ "mandatory_depends_on": "send_interview_reminder",
+ "options": "Email Template"
}
],
"icon": "fa fa-cog",
"idx": 1,
"issingle": 1,
"links": [],
- "modified": "2021-08-24 14:54:12.834162",
+ "modified": "2021-09-30 22:42:14.683983",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",
diff --git a/erpnext/hr/doctype/interview/__init__.py b/erpnext/hr/doctype/interview/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/interview/__init__.py
diff --git a/erpnext/hr/doctype/interview/interview.js b/erpnext/hr/doctype/interview/interview.js
new file mode 100644
index 0000000..6341e3a
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview.js
@@ -0,0 +1,237 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Interview', {
+ onload: function (frm) {
+ frm.events.set_job_applicant_query(frm);
+
+ frm.set_query('interviewer', 'interview_details', function () {
+ return {
+ query: 'erpnext.hr.doctype.interview.interview.get_interviewer_list'
+ };
+ });
+ },
+
+ refresh: function (frm) {
+ if (frm.doc.docstatus != 2 && !frm.doc.__islocal) {
+ if (frm.doc.status === 'Pending') {
+ frm.add_custom_button(__('Reschedule Interview'), function() {
+ frm.events.show_reschedule_dialog(frm);
+ frm.refresh();
+ });
+ }
+
+ let allowed_interviewers = [];
+ frm.doc.interview_details.forEach(values => {
+ allowed_interviewers.push(values.interviewer);
+ });
+
+ if ((allowed_interviewers.includes(frappe.session.user))) {
+ frappe.db.get_value('Interview Feedback', {'interviewer': frappe.session.user, 'interview': frm.doc.name, 'docstatus': 1}, 'name', (r) => {
+ if (Object.keys(r).length === 0) {
+ frm.add_custom_button(__('Submit Feedback'), function () {
+ frappe.call({
+ method: 'erpnext.hr.doctype.interview.interview.get_expected_skill_set',
+ args: {
+ interview_round: frm.doc.interview_round
+ },
+ callback: function (r) {
+ frm.events.show_feedback_dialog(frm, r.message);
+ frm.refresh();
+ }
+ });
+ }).addClass('btn-primary');
+ }
+ });
+ }
+ }
+ },
+
+ show_reschedule_dialog: function (frm) {
+ let d = new frappe.ui.Dialog({
+ title: 'Reschedule Interview',
+ fields: [
+ {
+ label: 'Schedule On',
+ fieldname: 'scheduled_on',
+ fieldtype: 'Date',
+ reqd: 1
+ },
+ {
+ label: 'From Time',
+ fieldname: 'from_time',
+ fieldtype: 'Time',
+ reqd: 1
+ },
+ {
+ label: 'To Time',
+ fieldname: 'to_time',
+ fieldtype: 'Time',
+ reqd: 1
+ }
+ ],
+ primary_action_label: 'Reschedule',
+ primary_action(values) {
+ frm.call({
+ method: 'reschedule_interview',
+ doc: frm.doc,
+ args: {
+ scheduled_on: values.scheduled_on,
+ from_time: values.from_time,
+ to_time: values.to_time
+ }
+ }).then(() => {
+ frm.refresh();
+ d.hide();
+ });
+ }
+ });
+ d.show();
+ },
+
+ show_feedback_dialog: function (frm, data) {
+ let fields = frm.events.get_fields_for_feedback();
+
+ let d = new frappe.ui.Dialog({
+ title: __('Submit Feedback'),
+ fields: [
+ {
+ fieldname: 'skill_set',
+ fieldtype: 'Table',
+ label: __('Skill Assessment'),
+ cannot_add_rows: false,
+ in_editable_grid: true,
+ reqd: 1,
+ fields: fields,
+ data: data
+ },
+ {
+ fieldname: 'result',
+ fieldtype: 'Select',
+ options: ['', 'Cleared', 'Rejected'],
+ label: __('Result')
+ },
+ {
+ fieldname: 'feedback',
+ fieldtype: 'Small Text',
+ label: __('Feedback')
+ }
+ ],
+ size: 'large',
+ minimizable: true,
+ primary_action: function(values) {
+ frappe.call({
+ method: 'erpnext.hr.doctype.interview.interview.create_interview_feedback',
+ args: {
+ data: values,
+ interview_name: frm.doc.name,
+ interviewer: frappe.session.user,
+ job_applicant: frm.doc.job_applicant
+ }
+ }).then(() => {
+ frm.refresh();
+ });
+ d.hide();
+ }
+ });
+ d.show();
+ },
+
+ get_fields_for_feedback: function () {
+ return [{
+ fieldtype: 'Link',
+ fieldname: 'skill',
+ options: 'Skill',
+ in_list_view: 1,
+ label: __('Skill')
+ }, {
+ fieldtype: 'Rating',
+ fieldname: 'rating',
+ label: __('Rating'),
+ in_list_view: 1,
+ reqd: 1,
+ }];
+ },
+
+ set_job_applicant_query: function (frm) {
+ frm.set_query('job_applicant', function () {
+ let job_applicant_filters = {
+ status: ['!=', 'Rejected']
+ };
+ if (frm.doc.designation) {
+ job_applicant_filters.designation = frm.doc.designation;
+ }
+ return {
+ filters: job_applicant_filters
+ };
+ });
+ },
+
+ interview_round: async function (frm) {
+ frm.events.reset_values(frm);
+ frm.set_value('job_applicant', '');
+
+ let round_data = (await frappe.db.get_value('Interview Round', frm.doc.interview_round, 'designation')).message;
+ frm.set_value('designation', round_data.designation);
+ frm.events.set_job_applicant_query(frm);
+
+ if (frm.doc.interview_round) {
+ frm.events.set_interview_details(frm);
+ } else {
+ frm.set_value('interview_details', []);
+ }
+ },
+
+ set_interview_details: function (frm) {
+ frappe.call({
+ method: 'erpnext.hr.doctype.interview.interview.get_interviewers',
+ args: {
+ interview_round: frm.doc.interview_round
+ },
+ callback: function (data) {
+ let interview_details = data.message;
+ frm.set_value('interview_details', []);
+ if (data.message.length) {
+ frm.set_value('interview_details', interview_details);
+ }
+ }
+ });
+ },
+
+ job_applicant: function (frm) {
+ if (!frm.doc.interview_round) {
+ frm.doc.job_applicant = '';
+ frm.refresh();
+ frappe.throw(__('Select Interview Round First'));
+ }
+
+ if (frm.doc.job_applicant) {
+ frm.events.set_designation_and_job_opening(frm);
+ } else {
+ frm.events.reset_values(frm);
+ }
+ },
+
+ set_designation_and_job_opening: async function (frm) {
+ let round_data = (await frappe.db.get_value('Interview Round', frm.doc.interview_round, 'designation')).message;
+ frm.set_value('designation', round_data.designation);
+ frm.events.set_job_applicant_query(frm);
+
+ let job_applicant_data = (await frappe.db.get_value(
+ 'Job Applicant', frm.doc.job_applicant, ['designation', 'job_title', 'resume_link'],
+ )).message;
+
+ if (!round_data.designation) {
+ frm.set_value('designation', job_applicant_data.designation);
+ }
+
+ frm.set_value('job_opening', job_applicant_data.job_title);
+ frm.set_value('resume_link', job_applicant_data.resume_link);
+ },
+
+ reset_values: function (frm) {
+ frm.set_value('designation', '');
+ frm.set_value('job_opening', '');
+ frm.set_value('resume_link', '');
+ }
+});
diff --git a/erpnext/hr/doctype/interview/interview.json b/erpnext/hr/doctype/interview/interview.json
new file mode 100644
index 0000000..0d393e7
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview.json
@@ -0,0 +1,254 @@
+{
+ "actions": [],
+ "autoname": "HR-INT-.YYYY.-.####",
+ "creation": "2021-04-12 15:03:11.524090",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "interview_details_section",
+ "interview_round",
+ "job_applicant",
+ "job_opening",
+ "designation",
+ "resume_link",
+ "column_break_4",
+ "status",
+ "scheduled_on",
+ "from_time",
+ "to_time",
+ "interview_feedback_section",
+ "interview_details",
+ "ratings_section",
+ "expected_average_rating",
+ "column_break_12",
+ "average_rating",
+ "section_break_13",
+ "interview_summary",
+ "reminded",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "job_applicant",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Job Applicant",
+ "options": "Job Applicant",
+ "reqd": 1
+ },
+ {
+ "fieldname": "job_opening",
+ "fieldtype": "Link",
+ "label": "Job Opening",
+ "options": "Job Opening",
+ "read_only": 1
+ },
+ {
+ "fieldname": "interview_round",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Interview Round",
+ "options": "Interview Round",
+ "reqd": 1
+ },
+ {
+ "default": "Pending",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Status",
+ "options": "Pending\nUnder Review\nCleared\nRejected",
+ "reqd": 1
+ },
+ {
+ "fieldname": "ratings_section",
+ "fieldtype": "Section Break",
+ "label": "Ratings"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "average_rating",
+ "fieldtype": "Rating",
+ "in_list_view": 1,
+ "label": "Obtained Average Rating",
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "interview_summary",
+ "fieldtype": "Text"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "resume_link",
+ "fieldtype": "Data",
+ "label": "Resume link"
+ },
+ {
+ "fieldname": "interview_details_section",
+ "fieldtype": "Section Break",
+ "label": "Details"
+ },
+ {
+ "fetch_from": "interview_round.expected_average_rating",
+ "fieldname": "expected_average_rating",
+ "fieldtype": "Rating",
+ "label": "Expected Average Rating",
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_break_13",
+ "fieldtype": "Section Break",
+ "label": "Interview Summary"
+ },
+ {
+ "fieldname": "column_break_12",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "interview_round.designation",
+ "fieldname": "designation",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Designation",
+ "options": "Designation",
+ "read_only": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Interview",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "scheduled_on",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Scheduled On",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "reminded",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Reminded"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "interview_details",
+ "fieldtype": "Table",
+ "options": "Interview Detail"
+ },
+ {
+ "fieldname": "interview_feedback_section",
+ "fieldtype": "Section Break",
+ "label": "Feedback"
+ },
+ {
+ "fieldname": "from_time",
+ "fieldtype": "Time",
+ "in_list_view": 1,
+ "label": "From Time",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "to_time",
+ "fieldtype": "Time",
+ "in_list_view": 1,
+ "label": "To Time",
+ "reqd": 1,
+ "set_only_once": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [
+ {
+ "link_doctype": "Interview Feedback",
+ "link_fieldname": "interview"
+ }
+ ],
+ "modified": "2021-09-30 13:30:05.421035",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Interview",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Interviewer",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "job_applicant",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interview/interview.py b/erpnext/hr/doctype/interview/interview.py
new file mode 100644
index 0000000..955acca
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview.py
@@ -0,0 +1,293 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+import datetime
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import cstr, get_datetime, get_link_to_form
+
+
+class DuplicateInterviewRoundError(frappe.ValidationError):
+ pass
+
+class Interview(Document):
+ def validate(self):
+ self.validate_duplicate_interview()
+ self.validate_designation()
+ self.validate_overlap()
+
+ def on_submit(self):
+ if self.status not in ['Cleared', 'Rejected']:
+ frappe.throw(_('Only Interviews with Cleared or Rejected status can be submitted.'), title=_('Not Allowed'))
+
+ def validate_duplicate_interview(self):
+ duplicate_interview = frappe.db.exists('Interview', {
+ 'job_applicant': self.job_applicant,
+ 'interview_round': self.interview_round,
+ 'docstatus': 1
+ }
+ )
+
+ if duplicate_interview:
+ frappe.throw(_('Job Applicants are not allowed to appear twice for the same Interview round. Interview {0} already scheduled for Job Applicant {1}').format(
+ frappe.bold(get_link_to_form('Interview', duplicate_interview)),
+ frappe.bold(self.job_applicant)
+ ))
+
+ def validate_designation(self):
+ applicant_designation = frappe.db.get_value('Job Applicant', self.job_applicant, 'designation')
+ if self.designation :
+ if self.designation != applicant_designation:
+ frappe.throw(_('Interview Round {0} is only for Designation {1}. Job Applicant has applied for the role {2}').format(
+ self.interview_round, frappe.bold(self.designation), applicant_designation),
+ exc=DuplicateInterviewRoundError)
+ else:
+ self.designation = applicant_designation
+
+ def validate_overlap(self):
+ interviewers = [entry.interviewer for entry in self.interview_details] or ['']
+
+ overlaps = frappe.db.sql("""
+ SELECT interview.name
+ FROM `tabInterview` as interview
+ INNER JOIN `tabInterview Detail` as detail
+ WHERE
+ interview.scheduled_on = %s and interview.name != %s and interview.docstatus != 2
+ and (interview.job_applicant = %s or detail.interviewer IN %s) and
+ ((from_time < %s and to_time > %s) or
+ (from_time > %s and to_time < %s) or
+ (from_time = %s))
+ """, (self.scheduled_on, self.name, self.job_applicant, interviewers,
+ self.from_time, self.to_time, self.from_time, self.to_time, self.from_time))
+
+ if overlaps:
+ overlapping_details = _('Interview overlaps with {0}').format(get_link_to_form('Interview', overlaps[0][0]))
+ frappe.throw(overlapping_details, title=_('Overlap'))
+
+
+ @frappe.whitelist()
+ def reschedule_interview(self, scheduled_on, from_time, to_time):
+ original_date = self.scheduled_on
+ from_time = self.from_time
+ to_time = self.to_time
+
+ self.db_set({
+ 'scheduled_on': scheduled_on,
+ 'from_time': from_time,
+ 'to_time': to_time
+ })
+ self.notify_update()
+
+ recipients = get_recipients(self.name)
+
+ try:
+ frappe.sendmail(
+ recipients= recipients,
+ subject=_('Interview: {0} Rescheduled').format(self.name),
+ message=_('Your Interview session is rescheduled from {0} {1} - {2} to {3} {4} - {5}').format(
+ original_date, from_time, to_time, self.scheduled_on, self.from_time, self.to_time),
+ reference_doctype=self.doctype,
+ reference_name=self.name
+ )
+ except Exception:
+ frappe.msgprint(_('Failed to send the Interview Reschedule notification. Please configure your email account.'))
+
+ frappe.msgprint(_('Interview Rescheduled successfully'), indicator='green')
+
+
+def get_recipients(name, for_feedback=0):
+ interview = frappe.get_doc('Interview', name)
+
+ if for_feedback:
+ recipients = [d.interviewer for d in interview.interview_details if not d.interview_feedback]
+ else:
+ recipients = [d.interviewer for d in interview.interview_details]
+ recipients.append(frappe.db.get_value('Job Applicant', interview.job_applicant, 'email_id'))
+
+ return recipients
+
+
+@frappe.whitelist()
+def get_interviewers(interview_round):
+ return frappe.get_all('Interviewer', filters={'parent': interview_round}, fields=['user as interviewer'])
+
+
+def send_interview_reminder():
+ reminder_settings = frappe.db.get_value('HR Settings', 'HR Settings',
+ ['send_interview_reminder', 'interview_reminder_template'], as_dict=True)
+
+ if not reminder_settings.send_interview_reminder:
+ return
+
+ remind_before = cstr(frappe.db.get_single_value('HR Settings', 'remind_before')) or '01:00:00'
+ remind_before = datetime.datetime.strptime(remind_before, '%H:%M:%S')
+ reminder_date_time = datetime.datetime.now() + datetime.timedelta(
+ hours=remind_before.hour, minutes=remind_before.minute, seconds=remind_before.second)
+
+ interviews = frappe.get_all('Interview', filters={
+ 'scheduled_on': ['between', (datetime.datetime.now(), reminder_date_time)],
+ 'status': 'Pending',
+ 'reminded': 0,
+ 'docstatus': ['!=', 2]
+ })
+
+ interview_template = frappe.get_doc('Email Template', reminder_settings.interview_reminder_template)
+
+ for d in interviews:
+ doc = frappe.get_doc('Interview', d.name)
+ context = doc.as_dict()
+ message = frappe.render_template(interview_template.response, context)
+ recipients = get_recipients(doc.name)
+
+ frappe.sendmail(
+ recipients= recipients,
+ subject=interview_template.subject,
+ message=message,
+ reference_doctype=doc.doctype,
+ reference_name=doc.name
+ )
+
+ doc.db_set('reminded', 1)
+
+
+def send_daily_feedback_reminder():
+ reminder_settings = frappe.db.get_value('HR Settings', 'HR Settings',
+ ['send_interview_feedback_reminder', 'feedback_reminder_notification_template'], as_dict=True)
+
+ if not reminder_settings.send_interview_feedback_reminder:
+ return
+
+ interview_feedback_template = frappe.get_doc('Email Template', reminder_settings.feedback_reminder_notification_template)
+ interviews = frappe.get_all('Interview', filters={'status': ['in', ['Under Review', 'Pending']], 'docstatus': ['!=', 2]})
+
+ for entry in interviews:
+ recipients = get_recipients(entry.name, for_feedback=1)
+
+ doc = frappe.get_doc('Interview', entry.name)
+ context = doc.as_dict()
+
+ message = frappe.render_template(interview_feedback_template.response, context)
+
+ if len(recipients):
+ frappe.sendmail(
+ recipients= recipients,
+ subject=interview_feedback_template.subject,
+ message=message,
+ reference_doctype='Interview',
+ reference_name=entry.name
+ )
+
+
+@frappe.whitelist()
+def get_expected_skill_set(interview_round):
+ return frappe.get_all('Expected Skill Set', filters ={'parent': interview_round}, fields=['skill'])
+
+
+@frappe.whitelist()
+def create_interview_feedback(data, interview_name, interviewer, job_applicant):
+ import json
+
+ from six import string_types
+
+ if isinstance(data, string_types):
+ data = frappe._dict(json.loads(data))
+
+ if frappe.session.user != interviewer:
+ frappe.throw(_('Only Interviewer Are allowed to submit Interview Feedback'))
+
+ interview_feedback = frappe.new_doc('Interview Feedback')
+ interview_feedback.interview = interview_name
+ interview_feedback.interviewer = interviewer
+ interview_feedback.job_applicant = job_applicant
+
+ for d in data.skill_set:
+ d = frappe._dict(d)
+ interview_feedback.append('skill_assessment', {'skill': d.skill, 'rating': d.rating})
+
+ interview_feedback.feedback = data.feedback
+ interview_feedback.result = data.result
+
+ interview_feedback.save()
+ interview_feedback.submit()
+
+ frappe.msgprint(_('Interview Feedback {0} submitted successfully').format(
+ get_link_to_form('Interview Feedback', interview_feedback.name)))
+
+
+@frappe.whitelist()
+@frappe.validate_and_sanitize_search_inputs
+def get_interviewer_list(doctype, txt, searchfield, start, page_len, filters):
+ filters = [
+ ['Has Role', 'parent', 'like', '%{}%'.format(txt)],
+ ['Has Role', 'role', '=', 'interviewer'],
+ ['Has Role', 'parenttype', '=', 'User']
+ ]
+
+ if filters and isinstance(filters, list):
+ filters.extend(filters)
+
+ return frappe.get_all('Has Role', limit_start=start, limit_page_length=page_len,
+ filters=filters, fields = ['parent'], as_list=1)
+
+
+@frappe.whitelist()
+def get_events(start, end, filters=None):
+ """Returns events for Gantt / Calendar view rendering.
+
+ :param start: Start date-time.
+ :param end: End date-time.
+ :param filters: Filters (JSON).
+ """
+ from frappe.desk.calendar import get_event_conditions
+
+ events = []
+
+ event_color = {
+ "Pending": "#fff4f0",
+ "Under Review": "#d3e8fc",
+ "Cleared": "#eaf5ed",
+ "Rejected": "#fce7e7"
+ }
+
+ conditions = get_event_conditions('Interview', filters)
+
+ interviews = frappe.db.sql("""
+ SELECT DISTINCT
+ `tabInterview`.name, `tabInterview`.job_applicant, `tabInterview`.interview_round,
+ `tabInterview`.scheduled_on, `tabInterview`.status, `tabInterview`.from_time as from_time,
+ `tabInterview`.to_time as to_time
+ from
+ `tabInterview`
+ where
+ (`tabInterview`.scheduled_on between %(start)s and %(end)s)
+ and docstatus != 2
+ {conditions}
+ """.format(conditions=conditions), {
+ "start": start,
+ "end": end
+ }, as_dict=True, update={"allDay": 0})
+
+ for d in interviews:
+ subject_data = []
+ for field in ["name", "job_applicant", "interview_round"]:
+ if not d.get(field):
+ continue
+ subject_data.append(d.get(field))
+
+ color = event_color.get(d.status)
+ interview_data = {
+ 'from': get_datetime('%s %s' % (d.scheduled_on, d.from_time or '00:00:00')),
+ 'to': get_datetime('%s %s' % (d.scheduled_on, d.to_time or '00:00:00')),
+ 'name': d.name,
+ 'subject': '\n'.join(subject_data),
+ 'color': color if color else "#89bcde"
+ }
+
+ events.append(interview_data)
+
+ return events
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interview/interview_calendar.js b/erpnext/hr/doctype/interview/interview_calendar.js
new file mode 100644
index 0000000..b46b72e
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview_calendar.js
@@ -0,0 +1,14 @@
+
+frappe.views.calendar['Interview'] = {
+ field_map: {
+ 'start': 'from',
+ 'end': 'to',
+ 'id': 'name',
+ 'title': 'subject',
+ 'allDay': 'allDay',
+ 'color': 'color'
+ },
+ order_by: 'scheduled_on',
+ gantt: true,
+ get_events_method: 'erpnext.hr.doctype.interview.interview.get_events'
+};
diff --git a/erpnext/hr/doctype/interview/interview_feedback_reminder_template.html b/erpnext/hr/doctype/interview/interview_feedback_reminder_template.html
new file mode 100644
index 0000000..8d39fb5
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview_feedback_reminder_template.html
@@ -0,0 +1,5 @@
+<h1>Interview Feedback Reminder</h1>
+
+<p>
+ Interview Feedback for Interview {{ name }} is not submitted yet. Please submit your feedback. Thank you, good day!
+</p>
diff --git a/erpnext/hr/doctype/interview/interview_list.js b/erpnext/hr/doctype/interview/interview_list.js
new file mode 100644
index 0000000..b1f072f
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview_list.js
@@ -0,0 +1,12 @@
+frappe.listview_settings['Interview'] = {
+ has_indicator_for_draft: 1,
+ get_indicator: function(doc) {
+ let status_color = {
+ 'Pending': 'orange',
+ 'Under Review': 'blue',
+ 'Cleared': 'green',
+ 'Rejected': 'red',
+ };
+ return [__(doc.status), status_color[doc.status], 'status,=,'+doc.status];
+ }
+};
diff --git a/erpnext/hr/doctype/interview/interview_reminder_notification_template.html b/erpnext/hr/doctype/interview/interview_reminder_notification_template.html
new file mode 100644
index 0000000..76de46e
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview_reminder_notification_template.html
@@ -0,0 +1,5 @@
+<h1>Interview Reminder</h1>
+
+<p>
+ Interview: {{name}} is scheduled on {{scheduled_on}} from {{from_time}} to {{to_time}}
+</p>
diff --git a/erpnext/hr/doctype/interview/test_interview.py b/erpnext/hr/doctype/interview/test_interview.py
new file mode 100644
index 0000000..4612e17
--- /dev/null
+++ b/erpnext/hr/doctype/interview/test_interview.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import datetime
+import os
+import unittest
+
+import frappe
+from frappe import _
+from frappe.core.doctype.user_permission.test_user_permission import create_user
+from frappe.utils import add_days, getdate, nowtime
+
+from erpnext.hr.doctype.designation.test_designation import create_designation
+from erpnext.hr.doctype.interview.interview import DuplicateInterviewRoundError
+from erpnext.hr.doctype.job_applicant.test_job_applicant import create_job_applicant
+
+
+class TestInterview(unittest.TestCase):
+ def test_validations_for_designation(self):
+ job_applicant = create_job_applicant()
+ interview = create_interview_and_dependencies(job_applicant.name, designation='_Test_Sales_manager', save=0)
+ self.assertRaises(DuplicateInterviewRoundError, interview.save)
+
+ def test_notification_on_rescheduling(self):
+ job_applicant = create_job_applicant()
+ interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -4))
+
+ previous_scheduled_date = interview.scheduled_on
+ frappe.db.sql("DELETE FROM `tabEmail Queue`")
+
+ interview.reschedule_interview(add_days(getdate(previous_scheduled_date), 2),
+ from_time=nowtime(), to_time=nowtime())
+ interview.reload()
+
+ self.assertEqual(interview.scheduled_on, add_days(getdate(previous_scheduled_date), 2))
+
+ notification = frappe.get_all("Email Queue", filters={"message": ("like", "%Your Interview session is rescheduled from%")})
+ self.assertIsNotNone(notification)
+
+ def test_notification_for_scheduling(self):
+ from erpnext.hr.doctype.interview.interview import send_interview_reminder
+
+ setup_reminder_settings()
+
+ job_applicant = create_job_applicant()
+ scheduled_on = datetime.datetime.now() + datetime.timedelta(minutes=10)
+
+ interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=scheduled_on)
+
+ frappe.db.sql("DELETE FROM `tabEmail Queue`")
+ send_interview_reminder()
+
+ interview.reload()
+
+ email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
+ self.assertTrue("Subject: Interview Reminder" in email_queue[0].message)
+
+ def test_notification_for_feedback_submission(self):
+ from erpnext.hr.doctype.interview.interview import send_daily_feedback_reminder
+
+ setup_reminder_settings()
+
+ job_applicant = create_job_applicant()
+ scheduled_on = add_days(getdate(), -4)
+ create_interview_and_dependencies(job_applicant.name, scheduled_on=scheduled_on)
+
+ frappe.db.sql("DELETE FROM `tabEmail Queue`")
+ send_daily_feedback_reminder()
+
+ email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
+ self.assertTrue("Subject: Interview Feedback Reminder" in email_queue[0].message)
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+
+def create_interview_and_dependencies(job_applicant, scheduled_on=None, from_time=None, to_time=None, designation=None, save=1):
+ if designation:
+ designation=create_designation(designation_name = "_Test_Sales_manager").name
+
+ interviewer_1 = create_user("test_interviewer1@example.com", "Interviewer")
+ interviewer_2 = create_user("test_interviewer2@example.com", "Interviewer")
+
+ interview_round = create_interview_round(
+ "Technical Round", ["Python", "JS"],
+ designation=designation, save=True
+ )
+
+ interview = frappe.new_doc("Interview")
+ interview.interview_round = interview_round.name
+ interview.job_applicant = job_applicant
+ interview.scheduled_on = scheduled_on or getdate()
+ interview.from_time = from_time or nowtime()
+ interview.to_time = to_time or nowtime()
+
+ interview.append("interview_details", {"interviewer": interviewer_1.name})
+ interview.append("interview_details", {"interviewer": interviewer_2.name})
+
+ if save:
+ interview.save()
+
+ return interview
+
+def create_interview_round(name, skill_set, interviewers=[], designation=None, save=True):
+ create_skill_set(skill_set)
+ interview_round = frappe.new_doc("Interview Round")
+ interview_round.round_name = name
+ interview_round.interview_type = create_interview_type()
+ interview_round.expected_average_rating = 4
+ if designation:
+ interview_round.designation = designation
+
+ for skill in skill_set:
+ interview_round.append("expected_skill_set", {"skill": skill})
+
+ for interviewer in interviewers:
+ interview_round.append("interviewer", {
+ "user": interviewer
+ })
+
+ if save:
+ interview_round.save()
+
+ return interview_round
+
+def create_skill_set(skill_set):
+ for skill in skill_set:
+ if not frappe.db.exists("Skill", skill):
+ doc = frappe.new_doc("Skill")
+ doc.skill_name = skill
+ doc.save()
+
+def create_interview_type(name="test_interview_type"):
+ if frappe.db.exists("Interview Type", name):
+ return frappe.get_doc("Interview Type", name).name
+ else:
+ doc = frappe.new_doc("Interview Type")
+ doc.name = name
+ doc.description = "_Test_Description"
+ doc.save()
+
+ return doc.name
+
+def setup_reminder_settings():
+ if not frappe.db.exists('Email Template', _('Interview Reminder')):
+ base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
+ response = frappe.read_file(os.path.join(base_path, 'interview/interview_reminder_notification_template.html'))
+
+ frappe.get_doc({
+ 'doctype': 'Email Template',
+ 'name': _('Interview Reminder'),
+ 'response': response,
+ 'subject': _('Interview Reminder'),
+ 'owner': frappe.session.user,
+ }).insert(ignore_permissions=True)
+
+ if not frappe.db.exists('Email Template', _('Interview Feedback Reminder')):
+ base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
+ response = frappe.read_file(os.path.join(base_path, 'interview/interview_feedback_reminder_template.html'))
+
+ frappe.get_doc({
+ 'doctype': 'Email Template',
+ 'name': _('Interview Feedback Reminder'),
+ 'response': response,
+ 'subject': _('Interview Feedback Reminder'),
+ 'owner': frappe.session.user,
+ }).insert(ignore_permissions=True)
+
+ hr_settings = frappe.get_doc('HR Settings')
+ hr_settings.interview_reminder_template = _('Interview Reminder')
+ hr_settings.feedback_reminder_notification_template = _('Interview Feedback Reminder')
+ hr_settings.save()
diff --git a/erpnext/hr/doctype/interview_detail/__init__.py b/erpnext/hr/doctype/interview_detail/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/interview_detail/__init__.py
diff --git a/erpnext/hr/doctype/interview_detail/interview_detail.js b/erpnext/hr/doctype/interview_detail/interview_detail.js
new file mode 100644
index 0000000..88518ca
--- /dev/null
+++ b/erpnext/hr/doctype/interview_detail/interview_detail.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Interview Detail', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/hr/doctype/interview_detail/interview_detail.json b/erpnext/hr/doctype/interview_detail/interview_detail.json
new file mode 100644
index 0000000..b5b49c0
--- /dev/null
+++ b/erpnext/hr/doctype/interview_detail/interview_detail.json
@@ -0,0 +1,74 @@
+{
+ "actions": [],
+ "creation": "2021-04-12 16:24:10.382863",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "interviewer",
+ "interview_feedback",
+ "average_rating",
+ "result",
+ "column_break_4",
+ "comments"
+ ],
+ "fields": [
+ {
+ "fieldname": "interviewer",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Interviewer",
+ "options": "User"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "interview_feedback",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Interview Feedback",
+ "options": "Interview Feedback",
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "average_rating",
+ "fieldtype": "Rating",
+ "in_list_view": 1,
+ "label": "Average Rating",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "allow_on_submit": 1,
+ "fetch_from": "interview_feedback.feedback",
+ "fieldname": "comments",
+ "fieldtype": "Text",
+ "label": "Comments",
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "result",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Result",
+ "options": "\nCleared\nRejected",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-09-29 13:13:25.865063",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Interview Detail",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interview_detail/interview_detail.py b/erpnext/hr/doctype/interview_detail/interview_detail.py
new file mode 100644
index 0000000..8be3d34
--- /dev/null
+++ b/erpnext/hr/doctype/interview_detail/interview_detail.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+# import frappe
+from frappe.model.document import Document
+
+
+class InterviewDetail(Document):
+ pass
diff --git a/erpnext/hr/doctype/interview_detail/test_interview_detail.py b/erpnext/hr/doctype/interview_detail/test_interview_detail.py
new file mode 100644
index 0000000..a29dfff
--- /dev/null
+++ b/erpnext/hr/doctype/interview_detail/test_interview_detail.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+
+class TestInterviewDetail(unittest.TestCase):
+ pass
diff --git a/erpnext/hr/doctype/interview_feedback/__init__.py b/erpnext/hr/doctype/interview_feedback/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/interview_feedback/__init__.py
diff --git a/erpnext/hr/doctype/interview_feedback/interview_feedback.js b/erpnext/hr/doctype/interview_feedback/interview_feedback.js
new file mode 100644
index 0000000..dec559f
--- /dev/null
+++ b/erpnext/hr/doctype/interview_feedback/interview_feedback.js
@@ -0,0 +1,54 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Interview Feedback', {
+ onload: function(frm) {
+ frm.ignore_doctypes_on_cancel_all = ['Interview'];
+
+ frm.set_query('interview', function() {
+ return {
+ filters: {
+ docstatus: ['!=', 2]
+ }
+ };
+ });
+ },
+
+ interview_round: function(frm) {
+ frappe.call({
+ method: 'erpnext.hr.doctype.interview.interview.get_expected_skill_set',
+ args: {
+ interview_round: frm.doc.interview_round
+ },
+ callback: function(r) {
+ frm.set_value('skill_assessment', r.message);
+ }
+ });
+ },
+
+ interview: function(frm) {
+ frappe.call({
+ method: 'erpnext.hr.doctype.interview_feedback.interview_feedback.get_applicable_interviewers',
+ args: {
+ interview: frm.doc.interview || ''
+ },
+ callback: function(r) {
+ frm.set_query('interviewer', function() {
+ return {
+ filters: {
+ name: ['in', r.message]
+ }
+ };
+ });
+ }
+ });
+
+ },
+
+ interviewer: function(frm) {
+ if (!frm.doc.interview) {
+ frappe.throw(__('Select Interview first'));
+ frm.set_value('interviewer', '');
+ }
+ }
+});
diff --git a/erpnext/hr/doctype/interview_feedback/interview_feedback.json b/erpnext/hr/doctype/interview_feedback/interview_feedback.json
new file mode 100644
index 0000000..6a2f7e8
--- /dev/null
+++ b/erpnext/hr/doctype/interview_feedback/interview_feedback.json
@@ -0,0 +1,171 @@
+{
+ "actions": [],
+ "autoname": "HR-INT-FEED-.####",
+ "creation": "2021-04-12 17:03:13.833285",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "details_section",
+ "interview",
+ "interview_round",
+ "job_applicant",
+ "column_break_3",
+ "interviewer",
+ "result",
+ "section_break_4",
+ "skill_assessment",
+ "average_rating",
+ "section_break_7",
+ "feedback",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "interview",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Interview",
+ "options": "Interview",
+ "reqd": 1
+ },
+ {
+ "allow_in_quick_entry": 1,
+ "fetch_from": "interview.interview_round",
+ "fieldname": "interview_round",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Interview Round",
+ "options": "Interview Round",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "interviewer",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Interviewer",
+ "options": "User",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break",
+ "label": "Skill Assessment"
+ },
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "skill_assessment",
+ "fieldtype": "Table",
+ "options": "Skill Assessment",
+ "reqd": 1
+ },
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "average_rating",
+ "fieldtype": "Rating",
+ "in_list_view": 1,
+ "label": "Average Rating",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_7",
+ "fieldtype": "Section Break",
+ "label": "Feedback"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Interview Feedback",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "feedback",
+ "fieldtype": "Text"
+ },
+ {
+ "fieldname": "result",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Result",
+ "options": "\nCleared\nRejected",
+ "reqd": 1
+ },
+ {
+ "fieldname": "details_section",
+ "fieldtype": "Section Break",
+ "label": "Details"
+ },
+ {
+ "fetch_from": "interview.job_applicant",
+ "fieldname": "job_applicant",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Job Applicant",
+ "options": "Job Applicant",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2021-09-30 13:30:49.955352",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Interview Feedback",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Interviewer",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "interviewer",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interview_feedback/interview_feedback.py b/erpnext/hr/doctype/interview_feedback/interview_feedback.py
new file mode 100644
index 0000000..1c5a494
--- /dev/null
+++ b/erpnext/hr/doctype/interview_feedback/interview_feedback.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import flt, get_link_to_form, getdate
+
+
+class InterviewFeedback(Document):
+ def validate(self):
+ self.validate_interviewer()
+ self.validate_interview_date()
+ self.validate_duplicate()
+ self.calculate_average_rating()
+
+ def on_submit(self):
+ self.update_interview_details()
+
+ def on_cancel(self):
+ self.update_interview_details()
+
+ def validate_interviewer(self):
+ applicable_interviewers = get_applicable_interviewers(self.interview)
+ if self.interviewer not in applicable_interviewers:
+ frappe.throw(_('{0} is not allowed to submit Interview Feedback for the Interview: {1}').format(
+ frappe.bold(self.interviewer), frappe.bold(self.interview)))
+
+ def validate_interview_date(self):
+ scheduled_date = frappe.db.get_value('Interview', self.interview, 'scheduled_on')
+
+ if getdate() < getdate(scheduled_date) and self.docstatus == 1:
+ frappe.throw(_('{0} submission before {1} is not allowed').format(
+ frappe.bold('Interview Feedback'),
+ frappe.bold('Interview Scheduled Date')
+ ))
+
+ def validate_duplicate(self):
+ duplicate_feedback = frappe.db.exists('Interview Feedback', {
+ 'interviewer': self.interviewer,
+ 'interview': self.interview,
+ 'docstatus': 1
+ })
+
+ if duplicate_feedback:
+ frappe.throw(_('Feedback already submitted for the Interview {0}. Please cancel the previous Interview Feedback {1} to continue.').format(
+ self.interview, get_link_to_form('Interview Feedback', duplicate_feedback)))
+
+ def calculate_average_rating(self):
+ total_rating = 0
+ for d in self.skill_assessment:
+ if d.rating:
+ total_rating += d.rating
+
+ self.average_rating = flt(total_rating / len(self.skill_assessment) if len(self.skill_assessment) else 0)
+
+ def update_interview_details(self):
+ doc = frappe.get_doc('Interview', self.interview)
+ total_rating = 0
+
+ if self.docstatus == 2:
+ for entry in doc.interview_details:
+ if entry.interview_feedback == self.name:
+ entry.average_rating = entry.interview_feedback = entry.comments = entry.result = None
+ break
+ else:
+ for entry in doc.interview_details:
+ if entry.interviewer == self.interviewer:
+ entry.average_rating = self.average_rating
+ entry.interview_feedback = self.name
+ entry.comments = self.feedback
+ entry.result = self.result
+
+ if entry.average_rating:
+ total_rating += entry.average_rating
+
+ doc.average_rating = flt(total_rating / len(doc.interview_details) if len(doc.interview_details) else 0)
+ doc.save()
+ doc.notify_update()
+
+
+@frappe.whitelist()
+def get_applicable_interviewers(interview):
+ data = frappe.get_all('Interview Detail', filters={'parent': interview}, fields=['interviewer'])
+ return [d.interviewer for d in data]
diff --git a/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
new file mode 100644
index 0000000..c4b7981
--- /dev/null
+++ b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import unittest
+
+import frappe
+from frappe.utils import add_days, flt, getdate
+
+from erpnext.hr.doctype.interview.test_interview import (
+ create_interview_and_dependencies,
+ create_skill_set,
+)
+from erpnext.hr.doctype.job_applicant.test_job_applicant import create_job_applicant
+
+
+class TestInterviewFeedback(unittest.TestCase):
+ def test_validation_for_skill_set(self):
+ frappe.set_user("Administrator")
+ job_applicant = create_job_applicant()
+ interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -1))
+ skill_ratings = get_skills_rating(interview.interview_round)
+
+ interviewer = interview.interview_details[0].interviewer
+ create_skill_set(['Leadership'])
+
+ interview_feedback = create_interview_feedback(interview.name, interviewer, skill_ratings)
+ interview_feedback.append("skill_assessment", {"skill": 'Leadership', 'rating': 4})
+ frappe.set_user(interviewer)
+
+ self.assertRaises(frappe.ValidationError, interview_feedback.save)
+
+ frappe.set_user("Administrator")
+
+ def test_average_ratings_on_feedback_submission_and_cancellation(self):
+ job_applicant = create_job_applicant()
+ interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -1))
+ skill_ratings = get_skills_rating(interview.interview_round)
+
+ # For First Interviewer Feedback
+ interviewer = interview.interview_details[0].interviewer
+ frappe.set_user(interviewer)
+
+ # calculating Average
+ feedback_1 = create_interview_feedback(interview.name, interviewer, skill_ratings)
+
+ total_rating = 0
+ for d in feedback_1.skill_assessment:
+ if d.rating:
+ total_rating += d.rating
+
+ avg_rating = flt(total_rating / len(feedback_1.skill_assessment) if len(feedback_1.skill_assessment) else 0)
+
+ self.assertEqual(flt(avg_rating, 3), feedback_1.average_rating)
+
+ avg_on_interview_detail = frappe.db.get_value('Interview Detail', {
+ 'parent': feedback_1.interview,
+ 'interviewer': feedback_1.interviewer,
+ 'interview_feedback': feedback_1.name
+ }, 'average_rating')
+
+ # 1. average should be reflected in Interview Detail.
+ self.assertEqual(avg_on_interview_detail, round(feedback_1.average_rating))
+
+ '''For Second Interviewer Feedback'''
+ interviewer = interview.interview_details[1].interviewer
+ frappe.set_user(interviewer)
+
+ feedback_2 = create_interview_feedback(interview.name, interviewer, skill_ratings)
+ interview.reload()
+
+ feedback_2.cancel()
+ interview.reload()
+
+ frappe.set_user("Administrator")
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+
+def create_interview_feedback(interview, interviewer, skills_ratings):
+ interview_feedback = frappe.new_doc("Interview Feedback")
+ interview_feedback.interview = interview
+ interview_feedback.interviewer = interviewer
+ interview_feedback.result = "Cleared"
+
+ for rating in skills_ratings:
+ interview_feedback.append("skill_assessment", rating)
+
+ interview_feedback.save()
+ interview_feedback.submit()
+
+ return interview_feedback
+
+
+def get_skills_rating(interview_round):
+ import random
+
+ skills = frappe.get_all("Expected Skill Set", filters={"parent": interview_round}, fields = ["skill"])
+ for d in skills:
+ d["rating"] = random.randint(1, 5)
+ return skills
diff --git a/erpnext/hr/doctype/interview_round/__init__.py b/erpnext/hr/doctype/interview_round/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/interview_round/__init__.py
diff --git a/erpnext/hr/doctype/interview_round/interview_round.js b/erpnext/hr/doctype/interview_round/interview_round.js
new file mode 100644
index 0000000..6a608b0
--- /dev/null
+++ b/erpnext/hr/doctype/interview_round/interview_round.js
@@ -0,0 +1,24 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Interview Round", {
+ refresh: function(frm) {
+ if (!frm.doc.__islocal) {
+ frm.add_custom_button(__("Create Interview"), function() {
+ frm.events.create_interview(frm);
+ });
+ }
+ },
+ create_interview: function(frm) {
+ frappe.call({
+ method: "erpnext.hr.doctype.interview_round.interview_round.create_interview",
+ args: {
+ doc: frm.doc
+ },
+ callback: function (r) {
+ var doclist = frappe.model.sync(r.message);
+ frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
+ }
+ });
+ }
+});
diff --git a/erpnext/hr/doctype/interview_round/interview_round.json b/erpnext/hr/doctype/interview_round/interview_round.json
new file mode 100644
index 0000000..9c95185
--- /dev/null
+++ b/erpnext/hr/doctype/interview_round/interview_round.json
@@ -0,0 +1,118 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:round_name",
+ "creation": "2021-04-12 12:57:19.902866",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "round_name",
+ "interview_type",
+ "interviewers",
+ "column_break_3",
+ "designation",
+ "expected_average_rating",
+ "expected_skills_section",
+ "expected_skill_set"
+ ],
+ "fields": [
+ {
+ "fieldname": "round_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Round Name",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "designation",
+ "fieldtype": "Link",
+ "label": "Designation",
+ "options": "Designation"
+ },
+ {
+ "fieldname": "expected_skills_section",
+ "fieldtype": "Section Break",
+ "label": "Expected Skillset"
+ },
+ {
+ "fieldname": "expected_skill_set",
+ "fieldtype": "Table",
+ "options": "Expected Skill Set",
+ "reqd": 1
+ },
+ {
+ "fieldname": "expected_average_rating",
+ "fieldtype": "Rating",
+ "label": "Expected Average Rating",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "interview_type",
+ "fieldtype": "Link",
+ "label": "Interview Type",
+ "options": "Interview Type",
+ "reqd": 1
+ },
+ {
+ "fieldname": "interviewers",
+ "fieldtype": "Table MultiSelect",
+ "label": "Interviewers",
+ "options": "Interviewer"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-09-30 13:01:25.666660",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Interview Round",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Interviewer",
+ "select": 1,
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interview_round/interview_round.py b/erpnext/hr/doctype/interview_round/interview_round.py
new file mode 100644
index 0000000..8230c78
--- /dev/null
+++ b/erpnext/hr/doctype/interview_round/interview_round.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+import json
+
+import frappe
+from frappe.model.document import Document
+
+
+class InterviewRound(Document):
+ pass
+
+@frappe.whitelist()
+def create_interview(doc):
+ if isinstance(doc, str):
+ doc = json.loads(doc)
+ doc = frappe.get_doc(doc)
+
+ interview = frappe.new_doc("Interview")
+ interview.interview_round = doc.name
+ interview.designation = doc.designation
+
+ if doc.interviewers:
+ interview.interview_details = []
+ for data in doc.interviewers:
+ interview.append("interview_details", {
+ "interviewer": data.user
+ })
+ return interview
+
+
+
diff --git a/erpnext/hr/doctype/interview_round/test_interview_round.py b/erpnext/hr/doctype/interview_round/test_interview_round.py
new file mode 100644
index 0000000..932d3de
--- /dev/null
+++ b/erpnext/hr/doctype/interview_round/test_interview_round.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import unittest
+
+# import frappe
+
+
+class TestInterviewRound(unittest.TestCase):
+ pass
+
diff --git a/erpnext/hr/doctype/interview_type/__init__.py b/erpnext/hr/doctype/interview_type/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/interview_type/__init__.py
diff --git a/erpnext/hr/doctype/interview_type/interview_type.js b/erpnext/hr/doctype/interview_type/interview_type.js
new file mode 100644
index 0000000..af77b52
--- /dev/null
+++ b/erpnext/hr/doctype/interview_type/interview_type.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Interview Type', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/hr/doctype/interview_type/interview_type.json b/erpnext/hr/doctype/interview_type/interview_type.json
new file mode 100644
index 0000000..14636a1
--- /dev/null
+++ b/erpnext/hr/doctype/interview_type/interview_type.json
@@ -0,0 +1,73 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "Prompt",
+ "creation": "2021-04-12 14:44:40.664034",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "description"
+ ],
+ "fields": [
+ {
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "in_list_view": 1,
+ "label": "Description"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [
+ {
+ "link_doctype": "Interview Round",
+ "link_fieldname": "interview_type"
+ }
+ ],
+ "modified": "2021-09-30 13:00:16.471518",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Interview Type",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interview_type/interview_type.py b/erpnext/hr/doctype/interview_type/interview_type.py
new file mode 100644
index 0000000..ee5be54
--- /dev/null
+++ b/erpnext/hr/doctype/interview_type/interview_type.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+# import frappe
+from frappe.model.document import Document
+
+
+class InterviewType(Document):
+ pass
diff --git a/erpnext/hr/doctype/interview_type/test_interview_type.py b/erpnext/hr/doctype/interview_type/test_interview_type.py
new file mode 100644
index 0000000..a5d3cf9
--- /dev/null
+++ b/erpnext/hr/doctype/interview_type/test_interview_type.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+
+class TestInterviewType(unittest.TestCase):
+ pass
diff --git a/erpnext/hr/doctype/interviewer/__init__.py b/erpnext/hr/doctype/interviewer/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/interviewer/__init__.py
diff --git a/erpnext/hr/doctype/interviewer/interviewer.json b/erpnext/hr/doctype/interviewer/interviewer.json
new file mode 100644
index 0000000..a37b8b0
--- /dev/null
+++ b/erpnext/hr/doctype/interviewer/interviewer.json
@@ -0,0 +1,31 @@
+{
+ "actions": [],
+ "creation": "2021-04-12 17:38:19.354734",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "user"
+ ],
+ "fields": [
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "User",
+ "options": "User"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-04-13 13:41:35.817568",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Interviewer",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interviewer/interviewer.py b/erpnext/hr/doctype/interviewer/interviewer.py
new file mode 100644
index 0000000..1c8dbbe
--- /dev/null
+++ b/erpnext/hr/doctype/interviewer/interviewer.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+# import frappe
+from frappe.model.document import Document
+
+
+class Interviewer(Document):
+ pass
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.js b/erpnext/hr/doctype/job_applicant/job_applicant.js
index 7658bc9..d7b1c6c 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant.js
+++ b/erpnext/hr/doctype/job_applicant/job_applicant.js
@@ -8,6 +8,24 @@
frappe.ui.form.on("Job Applicant", {
refresh: function(frm) {
+ frm.set_query("job_title", function() {
+ return {
+ filters: {
+ 'status': 'Open'
+ }
+ };
+ });
+ frm.events.create_custom_buttons(frm);
+ frm.events.make_dashboard(frm);
+ },
+
+ create_custom_buttons: function(frm) {
+ if (!frm.doc.__islocal && frm.doc.status !== "Rejected" && frm.doc.status !== "Accepted") {
+ frm.add_custom_button(__("Create Interview"), function() {
+ frm.events.create_dialog(frm);
+ });
+ }
+
if (!frm.doc.__islocal) {
if (frm.doc.__onload && frm.doc.__onload.job_offer) {
$('[data-doctype="Employee Onboarding"]').find("button").show();
@@ -28,14 +46,57 @@
});
}
}
+ },
- frm.set_query("job_title", function() {
- return {
- filters: {
- 'status': 'Open'
- }
- };
+ make_dashboard: function(frm) {
+ frappe.call({
+ method: "erpnext.hr.doctype.job_applicant.job_applicant.get_interview_details",
+ args: {
+ job_applicant: frm.doc.name
+ },
+ callback: function(r) {
+ $("div").remove(".form-dashboard-section.custom");
+ frm.dashboard.add_section(
+ frappe.render_template('job_applicant_dashboard', {
+ data: r.message
+ }),
+ __("Interview Summary")
+ );
+ }
});
+ },
+ create_dialog: function(frm) {
+ let d = new frappe.ui.Dialog({
+ title: 'Enter Interview Round',
+ fields: [
+ {
+ label: 'Interview Round',
+ fieldname: 'interview_round',
+ fieldtype: 'Link',
+ options: 'Interview Round'
+ },
+ ],
+ primary_action_label: 'Create Interview',
+ primary_action(values) {
+ frm.events.create_interview(frm, values);
+ d.hide();
+ }
+ });
+ d.show();
+ },
+
+ create_interview: function (frm, values) {
+ frappe.call({
+ method: "erpnext.hr.doctype.job_applicant.job_applicant.create_interview",
+ args: {
+ doc: frm.doc,
+ interview_round: values.interview_round
+ },
+ callback: function (r) {
+ var doclist = frappe.model.sync(r.message);
+ frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
+ }
+ });
}
});
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.json b/erpnext/hr/doctype/job_applicant/job_applicant.json
index bcea5f5..200f675 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant.json
+++ b/erpnext/hr/doctype/job_applicant/job_applicant.json
@@ -9,16 +9,20 @@
"email_append_to": 1,
"engine": "InnoDB",
"field_order": [
+ "details_section",
"applicant_name",
"email_id",
"phone_number",
"country",
- "status",
"column_break_3",
"job_title",
+ "designation",
+ "status",
+ "source_and_rating_section",
"source",
"source_name",
"employee_referral",
+ "column_break_13",
"applicant_rating",
"section_break_6",
"notes",
@@ -84,7 +88,8 @@
},
{
"fieldname": "section_break_6",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Resume"
},
{
"fieldname": "cover_letter",
@@ -160,13 +165,34 @@
"label": "Employee Referral",
"options": "Employee Referral",
"read_only": 1
+ },
+ {
+ "fieldname": "details_section",
+ "fieldtype": "Section Break",
+ "label": "Details"
+ },
+ {
+ "fieldname": "source_and_rating_section",
+ "fieldtype": "Section Break",
+ "label": "Source and Rating"
+ },
+ {
+ "fieldname": "column_break_13",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "job_opening.designation",
+ "fieldname": "designation",
+ "fieldtype": "Link",
+ "label": "Designation",
+ "options": "Designation"
}
],
"icon": "fa fa-user",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-03-24 15:51:11.117517",
+ "modified": "2021-09-29 23:06:10.904260",
"modified_by": "Administrator",
"module": "HR",
"name": "Job Applicant",
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.py b/erpnext/hr/doctype/job_applicant/job_applicant.py
index 6971e5b..151f492 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant.py
+++ b/erpnext/hr/doctype/job_applicant/job_applicant.py
@@ -8,7 +8,9 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import comma_and, validate_email_address
+from frappe.utils import validate_email_address
+
+from erpnext.hr.doctype.interview.interview import get_interviewers
class DuplicationError(frappe.ValidationError): pass
@@ -26,7 +28,6 @@
self.name = " - ".join(keys)
def validate(self):
- self.check_email_id_is_unique()
if self.email_id:
validate_email_address(self.email_id, True)
@@ -44,11 +45,44 @@
elif self.status in ["Accepted", "Rejected"]:
emp_ref.db_set("status", self.status)
+@frappe.whitelist()
+def create_interview(doc, interview_round):
+ import json
- def check_email_id_is_unique(self):
- if self.email_id:
- names = frappe.db.sql_list("""select name from `tabJob Applicant`
- where email_id=%s and name!=%s and job_title=%s""", (self.email_id, self.name, self.job_title))
+ from six import string_types
- if names:
- frappe.throw(_("Email Address must be unique, already exists for {0}").format(comma_and(names)), frappe.DuplicateEntryError)
+ if isinstance(doc, string_types):
+ doc = json.loads(doc)
+ doc = frappe.get_doc(doc)
+
+ round_designation = frappe.db.get_value("Interview Round", interview_round, "designation")
+
+ if round_designation and doc.designation and round_designation != doc.designation:
+ frappe.throw(_("Interview Round {0} is only applicable for the Designation {1}").format(interview_round, round_designation))
+
+ interview = frappe.new_doc("Interview")
+ interview.interview_round = interview_round
+ interview.job_applicant = doc.name
+ interview.designation = doc.designation
+ interview.resume_link = doc.resume_link
+ interview.job_opening = doc.job_title
+ interviewer_detail = get_interviewers(interview_round)
+
+ for d in interviewer_detail:
+ interview.append("interview_details", {
+ "interviewer": d.interviewer
+ })
+ return interview
+
+@frappe.whitelist()
+def get_interview_details(job_applicant):
+ interview_details = frappe.db.get_all("Interview",
+ filters={"job_applicant":job_applicant, "docstatus": ["!=", 2]},
+ fields=["name", "interview_round", "expected_average_rating", "average_rating", "status"]
+ )
+ interview_detail_map = {}
+
+ for detail in interview_details:
+ interview_detail_map[detail.name] = detail
+
+ return interview_detail_map
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html
new file mode 100644
index 0000000..c286787
--- /dev/null
+++ b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html
@@ -0,0 +1,44 @@
+
+{% if not jQuery.isEmptyObject(data) %}
+
+<table class="table table-bordered small">
+ <thead>
+ <tr>
+ <th style="width: 16%" class="text-left">{{ __("Interview") }}</th>
+ <th style="width: 16%" class="text-left">{{ __("Interview Round") }}</th>
+ <th style="width: 12%" class="text-left">{{ __("Status") }}</th>
+ <th style="width: 14%" class="text-left">{{ __("Expected Rating") }}</th>
+ <th style="width: 10%" class="text-left">{{ __("Rating") }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for(const [key, value] of Object.entries(data)) { %}
+ <tr>
+ <td class="text-left"> {%= key %} </td>
+ <td class="text-left"> {%= value["interview_round"] %} </td>
+ <td class="text-left"> {%= value["status"] %} </td>
+ <td class="text-left">
+ {% for (i = 0; i < value["expected_average_rating"]; i++) { %}
+ <span class="fa fa-star " style="color: #F6C35E;"></span>
+ {% } %}
+ {% for (i = 0; i < (5-value["expected_average_rating"]); i++) { %}
+ <span class="fa fa-star " style="color: #E7E9EB;"></span>
+ {% } %}
+ </td>
+ <td class="text-left">
+ {% if(value["average_rating"]){ %}
+ {% for (i = 0; i < value["average_rating"]; i++) { %}
+ <span class="fa fa-star " style="color: #F6C35E;"></span>
+ {% } %}
+ {% for (i = 0; i < (5-value["average_rating"]); i++) { %}
+ <span class="fa fa-star " style="color: #E7E9EB;"></span>
+ {% } %}
+ {% } %}
+ </td>
+ </tr>
+ {% } %}
+ </tbody>
+</table>
+{% else %}
+<p style="margin-top: 30px;"> No Interview has been scheduled.</p>
+{% endif %}
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py
index c005943..2f7795f 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py
+++ b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py
@@ -2,14 +2,17 @@
def get_data():
- return {
- 'fieldname': 'job_applicant',
- 'transactions': [
- {
- 'items': ['Employee', 'Employee Onboarding']
- },
- {
- 'items': ['Job Offer']
- },
- ],
- }
+ return {
+ 'fieldname': 'job_applicant',
+ 'transactions': [
+ {
+ 'items': ['Employee', 'Employee Onboarding']
+ },
+ {
+ 'items': ['Job Offer', 'Appointment Letter']
+ },
+ {
+ 'items': ['Interview']
+ }
+ ],
+ }
diff --git a/erpnext/hr/doctype/job_applicant/test_job_applicant.py b/erpnext/hr/doctype/job_applicant/test_job_applicant.py
index e583e25..8fc1290 100644
--- a/erpnext/hr/doctype/job_applicant/test_job_applicant.py
+++ b/erpnext/hr/doctype/job_applicant/test_job_applicant.py
@@ -7,7 +7,8 @@
import frappe
-# test_records = frappe.get_test_records('Job Applicant')
+from erpnext.hr.doctype.designation.test_designation import create_designation
+
class TestJobApplicant(unittest.TestCase):
pass
@@ -25,7 +26,8 @@
job_applicant = frappe.get_doc({
"doctype": "Job Applicant",
- "status": args.status or "Open"
+ "status": args.status or "Open",
+ "designation": create_designation().name
})
job_applicant.update(filters)
diff --git a/erpnext/hr/doctype/job_offer/test_job_offer.py b/erpnext/hr/doctype/job_offer/test_job_offer.py
index 3f3eca1..162b245 100644
--- a/erpnext/hr/doctype/job_offer/test_job_offer.py
+++ b/erpnext/hr/doctype/job_offer/test_job_offer.py
@@ -32,6 +32,7 @@
self.assertTrue(frappe.db.exists("Job Offer", job_offer.name))
def test_job_applicant_update(self):
+ frappe.db.set_value("HR Settings", None, "check_vacancies", 0)
create_staffing_plan()
job_applicant = create_job_applicant(email_id="test_job_applicants@example.com")
job_offer = create_job_offer(job_applicant=job_applicant.name)
@@ -43,7 +44,11 @@
job_offer.status = "Rejected"
job_offer.submit()
job_applicant.reload()
- self.assertEqual(job_applicant.status, "Rejected")
+ self.assertEquals(job_applicant.status, "Rejected")
+ frappe.db.set_value("HR Settings", None, "check_vacancies", 1)
+
+ def tearDown(self):
+ frappe.db.sql("DELETE FROM `tabJob Offer` WHERE 1")
def create_job_offer(**args):
args = frappe._dict(args)
diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js
index 9ccb915..2eaaeec 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.js
+++ b/erpnext/hr/doctype/leave_application/leave_application.js
@@ -1,8 +1,8 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-cur_frm.add_fetch('employee','employee_name','employee_name');
-cur_frm.add_fetch('employee','company','company');
+cur_frm.add_fetch('employee', 'employee_name', 'employee_name');
+cur_frm.add_fetch('employee', 'company', 'company');
frappe.ui.form.on("Leave Application", {
setup: function(frm) {
@@ -19,7 +19,6 @@
frm.set_query("employee", erpnext.queries.employee);
},
onload: function(frm) {
-
// Ignore cancellation of doctype on cancel all.
frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"];
@@ -42,9 +41,9 @@
},
validate: function(frm) {
- if (frm.doc.from_date == frm.doc.to_date && frm.doc.half_day == 1){
+ if (frm.doc.from_date == frm.doc.to_date && frm.doc.half_day == 1) {
frm.doc.half_day_date = frm.doc.from_date;
- }else if (frm.doc.half_day == 0){
+ } else if (frm.doc.half_day == 0) {
frm.doc.half_day_date = "";
}
frm.toggle_reqd("half_day_date", frm.doc.half_day == 1);
@@ -79,14 +78,14 @@
__("Allocated Leaves")
);
frm.dashboard.show();
- let allowed_leave_types = Object.keys(leave_details);
+ let allowed_leave_types = Object.keys(leave_details);
// lwps should be allowed, lwps don't have any allocation
allowed_leave_types = allowed_leave_types.concat(lwps);
- frm.set_query('leave_type', function(){
+ frm.set_query('leave_type', function() {
return {
- filters : [
+ filters: [
['leave_type_name', 'in', allowed_leave_types]
]
};
@@ -99,7 +98,7 @@
frm.trigger("calculate_total_days");
}
cur_frm.set_intro("");
- if(frm.doc.__islocal && !in_list(frappe.user_roles, "Employee")) {
+ if (frm.doc.__islocal && !in_list(frappe.user_roles, "Employee")) {
frm.set_intro(__("Fill the form and save it"));
}
@@ -118,7 +117,7 @@
},
leave_approver: function(frm) {
- if(frm.doc.leave_approver){
+ if (frm.doc.leave_approver) {
frm.set_value("leave_approver_name", frappe.user.full_name(frm.doc.leave_approver));
}
},
@@ -131,12 +130,10 @@
if (frm.doc.half_day) {
if (frm.doc.from_date == frm.doc.to_date) {
frm.set_value("half_day_date", frm.doc.from_date);
- }
- else {
+ } else {
frm.trigger("half_day_datepicker");
}
- }
- else {
+ } else {
frm.set_value("half_day_date", "");
}
frm.trigger("calculate_total_days");
@@ -167,7 +164,7 @@
},
get_leave_balance: function(frm) {
- if(frm.doc.docstatus==0 && frm.doc.employee && frm.doc.leave_type && frm.doc.from_date && frm.doc.to_date) {
+ if (frm.doc.docstatus === 0 && frm.doc.employee && frm.doc.leave_type && frm.doc.from_date && frm.doc.to_date) {
return frappe.call({
method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_balance_on",
args: {
@@ -180,8 +177,7 @@
callback: function(r) {
if (!r.exc && r.message) {
frm.set_value('leave_balance', r.message);
- }
- else {
+ } else {
frm.set_value('leave_balance', "0");
}
}
@@ -190,12 +186,12 @@
},
calculate_total_days: function(frm) {
- if(frm.doc.from_date && frm.doc.to_date && frm.doc.employee && frm.doc.leave_type) {
+ if (frm.doc.from_date && frm.doc.to_date && frm.doc.employee && frm.doc.leave_type) {
var from_date = Date.parse(frm.doc.from_date);
var to_date = Date.parse(frm.doc.to_date);
- if(to_date < from_date){
+ if (to_date < from_date) {
frappe.msgprint(__("To Date cannot be less than From Date"));
frm.set_value('to_date', '');
return;
@@ -222,7 +218,7 @@
},
set_leave_approver: function(frm) {
- if(frm.doc.employee) {
+ if (frm.doc.employee) {
// server call is done to include holidays in leave days calculations
return frappe.call({
method: 'erpnext.hr.doctype.leave_application.leave_application.get_leave_approver',
diff --git a/erpnext/hr/doctype/skill_assessment/__init__.py b/erpnext/hr/doctype/skill_assessment/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/skill_assessment/__init__.py
diff --git a/erpnext/hr/doctype/skill_assessment/skill_assessment.json b/erpnext/hr/doctype/skill_assessment/skill_assessment.json
new file mode 100644
index 0000000..8b935c4
--- /dev/null
+++ b/erpnext/hr/doctype/skill_assessment/skill_assessment.json
@@ -0,0 +1,41 @@
+{
+ "actions": [],
+ "creation": "2021-04-12 17:07:39.656289",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "skill",
+ "rating"
+ ],
+ "fields": [
+ {
+ "fieldname": "skill",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Skill",
+ "options": "Skill",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "rating",
+ "fieldtype": "Rating",
+ "in_list_view": 1,
+ "label": "Rating",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-04-12 17:18:14.032298",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Skill Assessment",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/skill_assessment/skill_assessment.py b/erpnext/hr/doctype/skill_assessment/skill_assessment.py
new file mode 100644
index 0000000..3b74c4e
--- /dev/null
+++ b/erpnext/hr/doctype/skill_assessment/skill_assessment.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+# import frappe
+from frappe.model.document import Document
+
+
+class SkillAssessment(Document):
+ pass
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 3adc3e9..9e38a9c 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -313,3 +313,4 @@
erpnext.patches.v13_0.modify_invalid_gain_loss_gl_entries
erpnext.patches.v13_0.fix_additional_cost_in_mfg_stock_entry
erpnext.patches.v13_0.set_status_in_maintenance_schedule_table
+erpnext.patches.v13_0.add_default_interview_notification_templates
diff --git a/erpnext/patches/v13_0/add_default_interview_notification_templates.py b/erpnext/patches/v13_0/add_default_interview_notification_templates.py
new file mode 100644
index 0000000..5e8a27f
--- /dev/null
+++ b/erpnext/patches/v13_0/add_default_interview_notification_templates.py
@@ -0,0 +1,37 @@
+from __future__ import unicode_literals
+
+import os
+
+import frappe
+from frappe import _
+
+
+def execute():
+ if not frappe.db.exists('Email Template', _('Interview Reminder')):
+ base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
+ response = frappe.read_file(os.path.join(base_path, 'interview/interview_reminder_notification_template.html'))
+
+ frappe.get_doc({
+ 'doctype': 'Email Template',
+ 'name': _('Interview Reminder'),
+ 'response': response,
+ 'subject': _('Interview Reminder'),
+ 'owner': frappe.session.user,
+ }).insert(ignore_permissions=True)
+
+ if not frappe.db.exists('Email Template', _('Interview Feedback Reminder')):
+ base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
+ response = frappe.read_file(os.path.join(base_path, 'interview/interview_feedback_reminder_template.html'))
+
+ frappe.get_doc({
+ 'doctype': 'Email Template',
+ 'name': _('Interview Feedback Reminder'),
+ 'response': response,
+ 'subject': _('Interview Feedback Reminder'),
+ 'owner': frappe.session.user,
+ }).insert(ignore_permissions=True)
+
+ hr_settings = frappe.get_doc('HR Settings')
+ hr_settings.interview_reminder_template = _('Interview Reminder')
+ hr_settings.feedback_reminder_notification_template = _('Interview Feedback Reminder')
+ hr_settings.save()
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index bff36a4..9ed6686 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -171,8 +171,6 @@
days_in_month = no_of_days[0]
no_of_holidays = no_of_days[1]
- self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 1)
-
ss.reload()
payment_days_based_comp_amount = 0
for component in ss.earnings:
diff --git a/erpnext/setup/setup_wizard/operations/defaults_setup.py b/erpnext/setup/setup_wizard/operations/defaults_setup.py
index 6dd0fb1..55d5ec8 100644
--- a/erpnext/setup/setup_wizard/operations/defaults_setup.py
+++ b/erpnext/setup/setup_wizard/operations/defaults_setup.py
@@ -62,6 +62,13 @@
hr_settings.emp_created_by = "Naming Series"
hr_settings.leave_approval_notification_template = _("Leave Approval Notification")
hr_settings.leave_status_notification_template = _("Leave Status Notification")
+
+ hr_settings.send_interview_reminder = 1
+ hr_settings.interview_reminder_template = _("Interview Reminder")
+ hr_settings.remind_before = "00:15:00"
+
+ hr_settings.send_interview_feedback_reminder = 1
+ hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder")
hr_settings.save()
def set_no_copy_fields_in_variant_settings():
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 907967c..c473395 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -264,16 +264,26 @@
base_path = frappe.get_app_path("erpnext", "hr", "doctype")
response = frappe.read_file(os.path.join(base_path, "leave_application/leave_application_email_template.html"))
- records += [{'doctype': 'Email Template', 'name': _("Leave Approval Notification"), 'response': response,\
+ records += [{'doctype': 'Email Template', 'name': _("Leave Approval Notification"), 'response': response,
'subject': _("Leave Approval Notification"), 'owner': frappe.session.user}]
- records += [{'doctype': 'Email Template', 'name': _("Leave Status Notification"), 'response': response,\
+ records += [{'doctype': 'Email Template', 'name': _("Leave Status Notification"), 'response': response,
'subject': _("Leave Status Notification"), 'owner': frappe.session.user}]
+ response = frappe.read_file(os.path.join(base_path, "interview/interview_reminder_notification_template.html"))
+
+ records += [{'doctype': 'Email Template', 'name': _('Interview Reminder'), 'response': response,
+ 'subject': _('Interview Reminder'), 'owner': frappe.session.user}]
+
+ response = frappe.read_file(os.path.join(base_path, "interview/interview_feedback_reminder_template.html"))
+
+ records += [{'doctype': 'Email Template', 'name': _('Interview Feedback Reminder'), 'response': response,
+ 'subject': _('Interview Feedback Reminder'), '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"))
- records += [{'doctype': 'Email Template', 'name': _("Dispatch Notification"), 'response': response,\
+ records += [{'doctype': 'Email Template', 'name': _("Dispatch Notification"), 'response': response,
'subject': _("Your order is out for delivery!"), 'owner': frappe.session.user}]
# Records for the Supplier Scorecard
@@ -317,6 +327,14 @@
hr_settings.emp_created_by = "Naming Series"
hr_settings.leave_approval_notification_template = _("Leave Approval Notification")
hr_settings.leave_status_notification_template = _("Leave Status Notification")
+
+ hr_settings.send_interview_reminder = 1
+ hr_settings.interview_reminder_template = _("Interview Reminder")
+ hr_settings.remind_before = "00:15:00"
+
+ hr_settings.send_interview_feedback_reminder = 1
+ hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder")
+
hr_settings.save()
def update_item_variant_settings():