* 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):
|
def setUp(self):
|
||||||
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
|
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.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
|
||||||
|
frappe.db.sql('delete from `tabPatient Appointment`')
|
||||||
make_pos_profile()
|
make_pos_profile()
|
||||||
|
|
||||||
def test_medical_record(self):
|
def test_medical_record(self):
|
||||||
|
@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
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 (
|
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import (
|
||||||
create_appointment,
|
create_appointment,
|
||||||
@ -33,10 +33,12 @@ class TestTherapyPlan(unittest.TestCase):
|
|||||||
self.assertEqual(plan.status, 'Not Started')
|
self.assertEqual(plan.status, 'Not Started')
|
||||||
|
|
||||||
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
|
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
|
||||||
|
session.start_date = getdate()
|
||||||
frappe.get_doc(session).submit()
|
frappe.get_doc(session).submit()
|
||||||
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'In Progress')
|
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 = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
|
||||||
|
session.start_date = add_days(getdate(), 1)
|
||||||
frappe.get_doc(session).submit()
|
frappe.get_doc(session).submit()
|
||||||
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
|
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())
|
appointment = create_appointment(patient, practitioner, nowdate())
|
||||||
|
|
||||||
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name)
|
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 = frappe.get_doc(session)
|
||||||
session.submit()
|
session.submit()
|
||||||
self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')
|
self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')
|
||||||
|
@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import flt, today
|
from frappe.utils import flt
|
||||||
|
|
||||||
|
|
||||||
class TherapyPlan(Document):
|
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.exercises = therapy_type.exercises
|
||||||
therapy_session.appointment = appointment
|
therapy_session.appointment = appointment
|
||||||
|
|
||||||
if frappe.flags.in_test:
|
|
||||||
therapy_session.start_date = today()
|
|
||||||
return therapy_session.as_dict()
|
return therapy_session.as_dict()
|
||||||
|
|
||||||
|
|
||||||
|
@ -344,6 +344,7 @@ scheduler_events = {
|
|||||||
"all": [
|
"all": [
|
||||||
"erpnext.projects.doctype.project.project.project_status_update_reminder",
|
"erpnext.projects.doctype.project.project.project_status_update_reminder",
|
||||||
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_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"
|
"erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts"
|
||||||
],
|
],
|
||||||
"hourly": [
|
"hourly": [
|
||||||
@ -388,6 +389,7 @@ scheduler_events = {
|
|||||||
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
|
"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.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
|
||||||
"erpnext.non_profit.doctype.membership.membership.set_expired_status"
|
"erpnext.non_profit.doctype.membership.membership.set_expired_status"
|
||||||
|
"erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder"
|
||||||
],
|
],
|
||||||
"daily_long": [
|
"daily_long": [
|
||||||
"erpnext.setup.doctype.email_digest.email_digest.send",
|
"erpnext.setup.doctype.email_digest.email_digest.send",
|
||||||
|
@ -9,20 +9,20 @@ frappe.listview_settings['Attendance'] = {
|
|||||||
return [__(doc.status), "orange", "status,=," + doc.status];
|
return [__(doc.status), "orange", "status,=," + doc.status];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onload: function(list_view) {
|
onload: function(list_view) {
|
||||||
let me = this;
|
let me = this;
|
||||||
const months = moment.months()
|
const months = moment.months();
|
||||||
list_view.page.add_inner_button(__("Mark Attendance"), function() {
|
list_view.page.add_inner_button(__("Mark Attendance"), function() {
|
||||||
let dialog = new frappe.ui.Dialog({
|
let dialog = new frappe.ui.Dialog({
|
||||||
title: __("Mark Attendance"),
|
title: __("Mark Attendance"),
|
||||||
fields: [
|
fields: [{
|
||||||
{
|
|
||||||
fieldname: 'employee',
|
fieldname: 'employee',
|
||||||
label: __('For Employee'),
|
label: __('For Employee'),
|
||||||
fieldtype: 'Link',
|
fieldtype: 'Link',
|
||||||
options: 'Employee',
|
options: 'Employee',
|
||||||
get_query: () => {
|
get_query: () => {
|
||||||
return {query: "erpnext.controllers.queries.employee_query"}
|
return {query: "erpnext.controllers.queries.employee_query"};
|
||||||
},
|
},
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
onchange: function() {
|
onchange: function() {
|
||||||
@ -71,11 +71,11 @@ frappe.listview_settings['Attendance'] = {
|
|||||||
options: [],
|
options: [],
|
||||||
columns: 2,
|
columns: 2,
|
||||||
hidden: 1
|
hidden: 1
|
||||||
},
|
}],
|
||||||
],
|
|
||||||
primary_action(data) {
|
primary_action(data) {
|
||||||
if (cur_dialog.no_unmarked_days_left) {
|
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 {
|
} 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({
|
frappe.call({
|
||||||
@ -85,7 +85,10 @@ frappe.listview_settings['Attendance'] = {
|
|||||||
},
|
},
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
if (r.message === 1) {
|
if (r.message === 1) {
|
||||||
frappe.show_alert({message: __("Attendance Marked"), indicator: 'blue'});
|
frappe.show_alert({
|
||||||
|
message: __("Attendance Marked"),
|
||||||
|
indicator: 'blue'
|
||||||
|
});
|
||||||
cur_dialog.hide();
|
cur_dialog.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -101,6 +104,7 @@ frappe.listview_settings['Attendance'] = {
|
|||||||
dialog.show();
|
dialog.show();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
get_multi_select_options: function(employee, month) {
|
get_multi_select_options: function(employee, month) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
@ -115,7 +119,11 @@ frappe.listview_settings['Attendance'] = {
|
|||||||
for (var d in r.message) {
|
for (var d in r.message) {
|
||||||
var momentObj = moment(r.message[d], 'YYYY-MM-DD');
|
var momentObj = moment(r.message[d], 'YYYY-MM-DD');
|
||||||
var date = momentObj.format('DD-MM-YYYY');
|
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);
|
resolve(options);
|
||||||
});
|
});
|
||||||
|
@ -71,6 +71,7 @@ def get_job_applicant():
|
|||||||
applicant = frappe.new_doc('Job Applicant')
|
applicant = frappe.new_doc('Job Applicant')
|
||||||
applicant.applicant_name = 'Test Researcher'
|
applicant.applicant_name = 'Test Researcher'
|
||||||
applicant.email_id = 'test@researcher.com'
|
applicant.email_id = 'test@researcher.com'
|
||||||
|
applicant.designation = 'Researcher'
|
||||||
applicant.status = 'Open'
|
applicant.status = 'Open'
|
||||||
applicant.cover_letter = 'I am a great Researcher.'
|
applicant.cover_letter = 'I am a great Researcher.'
|
||||||
applicant.insert()
|
applicant.insert()
|
||||||
|
@ -38,8 +38,10 @@ def create_job_applicant(source_name, target_doc=None):
|
|||||||
status = "Open"
|
status = "Open"
|
||||||
|
|
||||||
job_applicant = frappe.new_doc("Job Applicant")
|
job_applicant = frappe.new_doc("Job Applicant")
|
||||||
|
job_applicant.source = "Employee Referral"
|
||||||
job_applicant.employee_referral = emp_ref.name
|
job_applicant.employee_referral = emp_ref.name
|
||||||
job_applicant.status = status
|
job_applicant.status = status
|
||||||
|
job_applicant.designation = emp_ref.for_designation
|
||||||
job_applicant.applicant_name = emp_ref.full_name
|
job_applicant.applicant_name = emp_ref.full_name
|
||||||
job_applicant.email_id = emp_ref.email
|
job_applicant.email_id = emp_ref.email
|
||||||
job_applicant.phone_number = emp_ref.contact_no
|
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):
|
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):
|
def test_workflow_and_status_sync(self):
|
||||||
emp_ref = create_employee_referral()
|
emp_ref = create_employee_referral()
|
||||||
|
|
||||||
@ -50,6 +55,10 @@ class TestEmployeeReferral(unittest.TestCase):
|
|||||||
add_sal = create_additional_salary(emp_ref)
|
add_sal = create_additional_salary(emp_ref)
|
||||||
self.assertTrue(add_sal.ref_docname, emp_ref.name)
|
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():
|
def create_employee_referral():
|
||||||
emp_ref = frappe.new_doc("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",
|
"auto_leave_encashment",
|
||||||
"restrict_backdated_leave_application",
|
"restrict_backdated_leave_application",
|
||||||
"hiring_settings",
|
"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": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -142,6 +148,13 @@
|
|||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"label": "Standard Working Hours"
|
"label": "Standard Working Hours"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"default": "00:15:00",
|
||||||
|
"depends_on": "send_interview_reminder",
|
||||||
|
"fieldname": "remind_before",
|
||||||
|
"fieldtype": "Time",
|
||||||
|
"label": "Remind Before"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "reminders_section",
|
"fieldname": "reminders_section",
|
||||||
@ -181,13 +194,45 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_11",
|
"fieldname": "column_break_11",
|
||||||
"fieldtype": "Column Break"
|
"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",
|
"icon": "fa fa-cog",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-08-24 14:54:12.834162",
|
"modified": "2021-09-30 22:42:14.683983",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "HR Settings",
|
"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", {
|
frappe.ui.form.on("Job Applicant", {
|
||||||
refresh: function(frm) {
|
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.__islocal) {
|
||||||
if (frm.doc.__onload && frm.doc.__onload.job_offer) {
|
if (frm.doc.__onload && frm.doc.__onload.job_offer) {
|
||||||
$('[data-doctype="Employee Onboarding"]').find("button").show();
|
$('[data-doctype="Employee Onboarding"]').find("button").show();
|
||||||
@ -28,14 +46,57 @@ frappe.ui.form.on("Job Applicant", {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
frm.set_query("job_title", function() {
|
make_dashboard: function(frm) {
|
||||||
return {
|
frappe.call({
|
||||||
filters: {
|
method: "erpnext.hr.doctype.job_applicant.job_applicant.get_interview_details",
|
||||||
'status': 'Open'
|
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,
|
"email_append_to": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
|
"details_section",
|
||||||
"applicant_name",
|
"applicant_name",
|
||||||
"email_id",
|
"email_id",
|
||||||
"phone_number",
|
"phone_number",
|
||||||
"country",
|
"country",
|
||||||
"status",
|
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"job_title",
|
"job_title",
|
||||||
|
"designation",
|
||||||
|
"status",
|
||||||
|
"source_and_rating_section",
|
||||||
"source",
|
"source",
|
||||||
"source_name",
|
"source_name",
|
||||||
"employee_referral",
|
"employee_referral",
|
||||||
|
"column_break_13",
|
||||||
"applicant_rating",
|
"applicant_rating",
|
||||||
"section_break_6",
|
"section_break_6",
|
||||||
"notes",
|
"notes",
|
||||||
@ -84,7 +88,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_6",
|
"fieldname": "section_break_6",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Resume"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "cover_letter",
|
"fieldname": "cover_letter",
|
||||||
@ -160,13 +165,34 @@
|
|||||||
"label": "Employee Referral",
|
"label": "Employee Referral",
|
||||||
"options": "Employee Referral",
|
"options": "Employee Referral",
|
||||||
"read_only": 1
|
"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",
|
"icon": "fa fa-user",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-03-24 15:51:11.117517",
|
"modified": "2021-09-29 23:06:10.904260",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Job Applicant",
|
"name": "Job Applicant",
|
||||||
|
@ -8,7 +8,9 @@ from __future__ import unicode_literals
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
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
|
class DuplicationError(frappe.ValidationError): pass
|
||||||
@ -26,7 +28,6 @@ class JobApplicant(Document):
|
|||||||
self.name = " - ".join(keys)
|
self.name = " - ".join(keys)
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.check_email_id_is_unique()
|
|
||||||
if self.email_id:
|
if self.email_id:
|
||||||
validate_email_address(self.email_id, True)
|
validate_email_address(self.email_id, True)
|
||||||
|
|
||||||
@ -44,11 +45,44 @@ class JobApplicant(Document):
|
|||||||
elif self.status in ["Accepted", "Rejected"]:
|
elif self.status in ["Accepted", "Rejected"]:
|
||||||
emp_ref.db_set("status", self.status)
|
emp_ref.db_set("status", self.status)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def create_interview(doc, interview_round):
|
||||||
|
import json
|
||||||
|
|
||||||
def check_email_id_is_unique(self):
|
from six import string_types
|
||||||
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))
|
|
||||||
|
|
||||||
if names:
|
if isinstance(doc, string_types):
|
||||||
frappe.throw(_("Email Address must be unique, already exists for {0}").format(comma_and(names)), frappe.DuplicateEntryError)
|
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': ['Employee', 'Employee Onboarding']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'items': ['Job Offer']
|
'items': ['Job Offer', 'Appointment Letter']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'items': ['Interview']
|
||||||
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,8 @@ import unittest
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
# test_records = frappe.get_test_records('Job Applicant')
|
from erpnext.hr.doctype.designation.test_designation import create_designation
|
||||||
|
|
||||||
|
|
||||||
class TestJobApplicant(unittest.TestCase):
|
class TestJobApplicant(unittest.TestCase):
|
||||||
pass
|
pass
|
||||||
@ -25,7 +26,8 @@ def create_job_applicant(**args):
|
|||||||
|
|
||||||
job_applicant = frappe.get_doc({
|
job_applicant = frappe.get_doc({
|
||||||
"doctype": "Job Applicant",
|
"doctype": "Job Applicant",
|
||||||
"status": args.status or "Open"
|
"status": args.status or "Open",
|
||||||
|
"designation": create_designation().name
|
||||||
})
|
})
|
||||||
|
|
||||||
job_applicant.update(filters)
|
job_applicant.update(filters)
|
||||||
|
@ -32,6 +32,7 @@ class TestJobOffer(unittest.TestCase):
|
|||||||
self.assertTrue(frappe.db.exists("Job Offer", job_offer.name))
|
self.assertTrue(frappe.db.exists("Job Offer", job_offer.name))
|
||||||
|
|
||||||
def test_job_applicant_update(self):
|
def test_job_applicant_update(self):
|
||||||
|
frappe.db.set_value("HR Settings", None, "check_vacancies", 0)
|
||||||
create_staffing_plan()
|
create_staffing_plan()
|
||||||
job_applicant = create_job_applicant(email_id="test_job_applicants@example.com")
|
job_applicant = create_job_applicant(email_id="test_job_applicants@example.com")
|
||||||
job_offer = create_job_offer(job_applicant=job_applicant.name)
|
job_offer = create_job_offer(job_applicant=job_applicant.name)
|
||||||
@ -43,7 +44,11 @@ class TestJobOffer(unittest.TestCase):
|
|||||||
job_offer.status = "Rejected"
|
job_offer.status = "Rejected"
|
||||||
job_offer.submit()
|
job_offer.submit()
|
||||||
job_applicant.reload()
|
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):
|
def create_job_offer(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
@ -19,7 +19,6 @@ frappe.ui.form.on("Leave Application", {
|
|||||||
frm.set_query("employee", erpnext.queries.employee);
|
frm.set_query("employee", erpnext.queries.employee);
|
||||||
},
|
},
|
||||||
onload: function(frm) {
|
onload: function(frm) {
|
||||||
|
|
||||||
// Ignore cancellation of doctype on cancel all.
|
// Ignore cancellation of doctype on cancel all.
|
||||||
frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"];
|
frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"];
|
||||||
|
|
||||||
@ -131,12 +130,10 @@ frappe.ui.form.on("Leave Application", {
|
|||||||
if (frm.doc.half_day) {
|
if (frm.doc.half_day) {
|
||||||
if (frm.doc.from_date == frm.doc.to_date) {
|
if (frm.doc.from_date == frm.doc.to_date) {
|
||||||
frm.set_value("half_day_date", frm.doc.from_date);
|
frm.set_value("half_day_date", frm.doc.from_date);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
frm.trigger("half_day_datepicker");
|
frm.trigger("half_day_datepicker");
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
frm.set_value("half_day_date", "");
|
frm.set_value("half_day_date", "");
|
||||||
}
|
}
|
||||||
frm.trigger("calculate_total_days");
|
frm.trigger("calculate_total_days");
|
||||||
@ -167,7 +164,7 @@ frappe.ui.form.on("Leave Application", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
get_leave_balance: function(frm) {
|
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({
|
return frappe.call({
|
||||||
method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_balance_on",
|
method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_balance_on",
|
||||||
args: {
|
args: {
|
||||||
@ -180,8 +177,7 @@ frappe.ui.form.on("Leave Application", {
|
|||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if (!r.exc && r.message) {
|
if (!r.exc && r.message) {
|
||||||
frm.set_value('leave_balance', r.message);
|
frm.set_value('leave_balance', r.message);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
frm.set_value('leave_balance', "0");
|
frm.set_value('leave_balance', "0");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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.modify_invalid_gain_loss_gl_entries
|
||||||
erpnext.patches.v13_0.fix_additional_cost_in_mfg_stock_entry
|
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.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]
|
days_in_month = no_of_days[0]
|
||||||
no_of_holidays = no_of_days[1]
|
no_of_holidays = no_of_days[1]
|
||||||
|
|
||||||
self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 1)
|
|
||||||
|
|
||||||
ss.reload()
|
ss.reload()
|
||||||
payment_days_based_comp_amount = 0
|
payment_days_based_comp_amount = 0
|
||||||
for component in ss.earnings:
|
for component in ss.earnings:
|
||||||
|
@ -62,6 +62,13 @@ def set_default_settings(args):
|
|||||||
hr_settings.emp_created_by = "Naming Series"
|
hr_settings.emp_created_by = "Naming Series"
|
||||||
hr_settings.leave_approval_notification_template = _("Leave Approval Notification")
|
hr_settings.leave_approval_notification_template = _("Leave Approval Notification")
|
||||||
hr_settings.leave_status_notification_template = _("Leave Status 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()
|
hr_settings.save()
|
||||||
|
|
||||||
def set_no_copy_fields_in_variant_settings():
|
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")
|
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"))
|
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}]
|
'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}]
|
'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")
|
base_path = frappe.get_app_path("erpnext", "stock", "doctype")
|
||||||
response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html"))
|
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}]
|
'subject': _("Your order is out for delivery!"), 'owner': frappe.session.user}]
|
||||||
|
|
||||||
# Records for the Supplier Scorecard
|
# Records for the Supplier Scorecard
|
||||||
@ -317,6 +327,14 @@ def update_hr_defaults():
|
|||||||
hr_settings.emp_created_by = "Naming Series"
|
hr_settings.emp_created_by = "Naming Series"
|
||||||
hr_settings.leave_approval_notification_template = _("Leave Approval Notification")
|
hr_settings.leave_approval_notification_template = _("Leave Approval Notification")
|
||||||
hr_settings.leave_status_notification_template = _("Leave Status 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()
|
hr_settings.save()
|
||||||
|
|
||||||
def update_item_variant_settings():
|
def update_item_variant_settings():
|
||||||
|
Loading…
x
Reference in New Issue
Block a user