* 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>
This commit is contained in:
parent
a04f9c904e
commit
b9942ad639
@ -21,6 +21,7 @@ class TestPatientMedicalRecord(unittest.TestCase):
|
||||
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):
|
||||
|
@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
||||
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 @@ class TestTherapyPlan(unittest.TestCase):
|
||||
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 @@ class TestTherapyPlan(unittest.TestCase):
|
||||
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')
|
||||
|
@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
||||
|
||||
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 @@ def make_therapy_session(therapy_plan, patient, therapy_type, company, appointme
|
||||
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()
|
||||
|
||||
|
||||
|
@ -344,6 +344,7 @@ scheduler_events = {
|
||||
"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 @@ scheduler_events = {
|
||||
"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",
|
||||
|
@ -9,20 +9,20 @@ frappe.listview_settings['Attendance'] = {
|
||||
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: [
|
||||
{
|
||||
fields: [{
|
||||
fieldname: 'employee',
|
||||
label: __('For Employee'),
|
||||
fieldtype: 'Link',
|
||||
options: 'Employee',
|
||||
get_query: () => {
|
||||
return {query: "erpnext.controllers.queries.employee_query"}
|
||||
return {query: "erpnext.controllers.queries.employee_query"};
|
||||
},
|
||||
reqd: 1,
|
||||
onchange: function() {
|
||||
@ -40,11 +40,11 @@ frappe.listview_settings['Attendance'] = {
|
||||
options: months,
|
||||
reqd: 1,
|
||||
onchange: function() {
|
||||
if(dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
|
||||
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 =>{
|
||||
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);
|
||||
@ -60,7 +60,7 @@ frappe.listview_settings['Attendance'] = {
|
||||
fieldtype: "Select",
|
||||
fieldname: "status",
|
||||
options: ["Present", "Absent", "Half Day", "Work From Home"],
|
||||
hidden:1,
|
||||
hidden: 1,
|
||||
reqd: 1,
|
||||
|
||||
},
|
||||
@ -71,21 +71,24 @@ frappe.listview_settings['Attendance'] = {
|
||||
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 @@ frappe.listview_settings['Attendance'] = {
|
||||
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);
|
||||
});
|
||||
|
@ -71,6 +71,7 @@ def get_job_applicant():
|
||||
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()
|
||||
|
@ -38,8 +38,10 @@ def create_job_applicant(source_name, target_doc=None):
|
||||
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
|
||||
|
@ -17,6 +17,11 @@ from erpnext.hr.doctype.employee_referral.employee_referral import (
|
||||
|
||||
|
||||
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 @@ class TestEmployeeReferral(unittest.TestCase):
|
||||
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")
|
||||
|
0
erpnext/hr/doctype/expected_skill_set/__init__.py
Normal file
0
erpnext/hr/doctype/expected_skill_set/__init__.py
Normal file
@ -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
|
||||
}
|
12
erpnext/hr/doctype/expected_skill_set/expected_skill_set.py
Normal file
12
erpnext/hr/doctype/expected_skill_set/expected_skill_set.py
Normal file
@ -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
|
@ -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": [
|
||||
{
|
||||
@ -142,6 +148,13 @@
|
||||
"fieldtype": "Int",
|
||||
"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",
|
||||
@ -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",
|
||||
|
0
erpnext/hr/doctype/interview/__init__.py
Normal file
0
erpnext/hr/doctype/interview/__init__.py
Normal file
237
erpnext/hr/doctype/interview/interview.js
Normal file
237
erpnext/hr/doctype/interview/interview.js
Normal file
@ -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', '');
|
||||
}
|
||||
});
|
254
erpnext/hr/doctype/interview/interview.json
Normal file
254
erpnext/hr/doctype/interview/interview.json
Normal file
@ -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
|
||||
}
|
293
erpnext/hr/doctype/interview/interview.py
Normal file
293
erpnext/hr/doctype/interview/interview.py
Normal file
@ -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
|
14
erpnext/hr/doctype/interview/interview_calendar.js
Normal file
14
erpnext/hr/doctype/interview/interview_calendar.js
Normal file
@ -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'
|
||||
};
|
@ -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>
|
12
erpnext/hr/doctype/interview/interview_list.js
Normal file
12
erpnext/hr/doctype/interview/interview_list.js
Normal file
@ -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];
|
||||
}
|
||||
};
|
@ -0,0 +1,5 @@
|
||||
<h1>Interview Reminder</h1>
|
||||
|
||||
<p>
|
||||
Interview: {{name}} is scheduled on {{scheduled_on}} from {{from_time}} to {{to_time}}
|
||||
</p>
|
174
erpnext/hr/doctype/interview/test_interview.py
Normal file
174
erpnext/hr/doctype/interview/test_interview.py
Normal file
@ -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()
|
0
erpnext/hr/doctype/interview_detail/__init__.py
Normal file
0
erpnext/hr/doctype/interview_detail/__init__.py
Normal file
8
erpnext/hr/doctype/interview_detail/interview_detail.js
Normal file
8
erpnext/hr/doctype/interview_detail/interview_detail.js
Normal file
@ -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) {
|
||||
|
||||
// }
|
||||
});
|
74
erpnext/hr/doctype/interview_detail/interview_detail.json
Normal file
74
erpnext/hr/doctype/interview_detail/interview_detail.json
Normal file
@ -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
|
||||
}
|
12
erpnext/hr/doctype/interview_detail/interview_detail.py
Normal file
12
erpnext/hr/doctype/interview_detail/interview_detail.py
Normal file
@ -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
|
11
erpnext/hr/doctype/interview_detail/test_interview_detail.py
Normal file
11
erpnext/hr/doctype/interview_detail/test_interview_detail.py
Normal file
@ -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
|
0
erpnext/hr/doctype/interview_feedback/__init__.py
Normal file
0
erpnext/hr/doctype/interview_feedback/__init__.py
Normal file
54
erpnext/hr/doctype/interview_feedback/interview_feedback.js
Normal file
54
erpnext/hr/doctype/interview_feedback/interview_feedback.js
Normal file
@ -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', '');
|
||||
}
|
||||
}
|
||||
});
|
171
erpnext/hr/doctype/interview_feedback/interview_feedback.json
Normal file
171
erpnext/hr/doctype/interview_feedback/interview_feedback.json
Normal file
@ -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
|
||||
}
|
88
erpnext/hr/doctype/interview_feedback/interview_feedback.py
Normal file
88
erpnext/hr/doctype/interview_feedback/interview_feedback.py
Normal file
@ -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]
|
103
erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
Normal file
103
erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
Normal file
@ -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
|
0
erpnext/hr/doctype/interview_round/__init__.py
Normal file
0
erpnext/hr/doctype/interview_round/__init__.py
Normal file
24
erpnext/hr/doctype/interview_round/interview_round.js
Normal file
24
erpnext/hr/doctype/interview_round/interview_round.js
Normal file
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
118
erpnext/hr/doctype/interview_round/interview_round.json
Normal file
118
erpnext/hr/doctype/interview_round/interview_round.json
Normal file
@ -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
|
||||
}
|
35
erpnext/hr/doctype/interview_round/interview_round.py
Normal file
35
erpnext/hr/doctype/interview_round/interview_round.py
Normal file
@ -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
|
||||
|
||||
|
||||
|
13
erpnext/hr/doctype/interview_round/test_interview_round.py
Normal file
13
erpnext/hr/doctype/interview_round/test_interview_round.py
Normal file
@ -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
|
||||
|
0
erpnext/hr/doctype/interview_type/__init__.py
Normal file
0
erpnext/hr/doctype/interview_type/__init__.py
Normal file
8
erpnext/hr/doctype/interview_type/interview_type.js
Normal file
8
erpnext/hr/doctype/interview_type/interview_type.js
Normal file
@ -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) {
|
||||
|
||||
// }
|
||||
});
|
73
erpnext/hr/doctype/interview_type/interview_type.json
Normal file
73
erpnext/hr/doctype/interview_type/interview_type.json
Normal file
@ -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
|
||||
}
|
12
erpnext/hr/doctype/interview_type/interview_type.py
Normal file
12
erpnext/hr/doctype/interview_type/interview_type.py
Normal file
@ -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
|
11
erpnext/hr/doctype/interview_type/test_interview_type.py
Normal file
11
erpnext/hr/doctype/interview_type/test_interview_type.py
Normal file
@ -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
|
0
erpnext/hr/doctype/interviewer/__init__.py
Normal file
0
erpnext/hr/doctype/interviewer/__init__.py
Normal file
31
erpnext/hr/doctype/interviewer/interviewer.json
Normal file
31
erpnext/hr/doctype/interviewer/interviewer.json
Normal file
@ -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
|
||||
}
|
12
erpnext/hr/doctype/interviewer/interviewer.py
Normal file
12
erpnext/hr/doctype/interviewer/interviewer.py
Normal file
@ -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
|
@ -8,6 +8,24 @@ cur_frm.email_field = "email_id";
|
||||
|
||||
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 @@ frappe.ui.form.on("Job Applicant", {
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -8,7 +8,9 @@ from __future__ import unicode_literals
|
||||
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 @@ class JobApplicant(Document):
|
||||
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 @@ class JobApplicant(Document):
|
||||
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
|
||||
|
@ -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 %}
|
@ -9,7 +9,10 @@ def get_data():
|
||||
'items': ['Employee', 'Employee Onboarding']
|
||||
},
|
||||
{
|
||||
'items': ['Job Offer']
|
||||
'items': ['Job Offer', 'Appointment Letter']
|
||||
},
|
||||
{
|
||||
'items': ['Interview']
|
||||
}
|
||||
],
|
||||
}
|
||||
|
@ -7,7 +7,8 @@ import unittest
|
||||
|
||||
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 @@ def create_job_applicant(**args):
|
||||
|
||||
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)
|
||||
|
@ -32,6 +32,7 @@ class TestJobOffer(unittest.TestCase):
|
||||
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 @@ class TestJobOffer(unittest.TestCase):
|
||||
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)
|
||||
|
@ -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 @@ frappe.ui.form.on("Leave Application", {
|
||||
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 @@ frappe.ui.form.on("Leave Application", {
|
||||
},
|
||||
|
||||
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);
|
||||
@ -84,9 +83,9 @@ frappe.ui.form.on("Leave Application", {
|
||||
// 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 @@ frappe.ui.form.on("Leave Application", {
|
||||
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 @@ frappe.ui.form.on("Leave Application", {
|
||||
},
|
||||
|
||||
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 @@ frappe.ui.form.on("Leave Application", {
|
||||
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 @@ frappe.ui.form.on("Leave Application", {
|
||||
},
|
||||
|
||||
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 @@ frappe.ui.form.on("Leave Application", {
|
||||
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 @@ frappe.ui.form.on("Leave Application", {
|
||||
},
|
||||
|
||||
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 @@ frappe.ui.form.on("Leave Application", {
|
||||
},
|
||||
|
||||
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',
|
||||
|
0
erpnext/hr/doctype/skill_assessment/__init__.py
Normal file
0
erpnext/hr/doctype/skill_assessment/__init__.py
Normal file
41
erpnext/hr/doctype/skill_assessment/skill_assessment.json
Normal file
41
erpnext/hr/doctype/skill_assessment/skill_assessment.json
Normal file
@ -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
|
||||
}
|
12
erpnext/hr/doctype/skill_assessment/skill_assessment.py
Normal file
12
erpnext/hr/doctype/skill_assessment/skill_assessment.py
Normal file
@ -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
|
@ -313,3 +313,4 @@ erpnext.patches.v13_0.create_custom_field_for_finance_book
|
||||
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
|
||||
|
@ -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()
|
@ -171,8 +171,6 @@ class TestSalarySlip(unittest.TestCase):
|
||||
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:
|
||||
|
@ -62,6 +62,13 @@ def set_default_settings(args):
|
||||
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():
|
||||
|
@ -264,16 +264,26 @@ def install(country=None):
|
||||
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 @@ def update_hr_defaults():
|
||||
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():
|
||||
|
Loading…
x
Reference in New Issue
Block a user