feat: Tracking Multi-round interview (#25482) (#27724)

* feat: Tracking Multi-round interview

* fix: releted to scheduler event and formating

* fix: job applicant UI/UX and conflicts

* test: Interview Round

* fix(test): Employee referral, Employee Onboarding, Job Offer

* fix: sider

* feat: set default value in Hr settings

* feat: added validation for designation

* test: Interview

* test: Added validatiolns for skill

* test: Interview feedback

* fix: sider

* fix: remove unnecessary validations and form label cleanups

* chore: clean-up Interview Round and Interview Type doctype

* fix: remove redundant Rating Value, only keep Rating

* fix: update interview details on feedback submission

- make interview feedback submission dialog minimizable

* fix: show submit feedback button only if feedback doesn't exist

* refactor: Interview and Feedback statuses and workflow

* fix(HR Settings): clean up interview settings

* refactor: Interview

* refactor: Interview Feedback, remove unnecessary validations

* chore: update notification messages

* chore: remove unnecessary formatting changes in attendance list and leave application

* refactor: Job Applicant to Interview mapping

* chore: sorted imports

* chore: sorted imports

* fix: sider issues

* fix: linter issues

* fix: sider issues

* fix: tests

* fix: sorted imports

* fix: tests, sider

* fix: therapy plan test

* fix: sider issues

* feat: Include From Time and To Time fields in Interview for cleaner data

* feat: Interview Calendar

* fix: allow renaming masters

* fix: add more fields to list view and standard filter

* fix: validate overlapping interviews

* fix: update tests

* fix: linter issues

* refactor: replace reminder messages with Email Templates

* fix: sider issues

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
(cherry picked from commit 57e66f958cd57d66a6fd3b19f6cd3593eab63666)

Co-authored-by: Anurag Mishra <32095923+Anurag810@users.noreply.github.com>
This commit is contained in:
mergify[bot] 2021-10-01 13:15:40 +05:30 committed by GitHub
parent a04f9c904e
commit b9942ad639
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 2385 additions and 127 deletions

View File

@ -21,6 +21,7 @@ class TestPatientMedicalRecord(unittest.TestCase):
def setUp(self):
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
frappe.db.sql('delete from `tabPatient Appointment`')
make_pos_profile()
def test_medical_record(self):

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import flt, getdate, nowdate
from frappe.utils import add_days, flt, getdate, nowdate
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import (
create_appointment,
@ -33,10 +33,12 @@ class TestTherapyPlan(unittest.TestCase):
self.assertEqual(plan.status, 'Not Started')
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
session.start_date = getdate()
frappe.get_doc(session).submit()
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'In Progress')
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
session.start_date = add_days(getdate(), 1)
frappe.get_doc(session).submit()
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
@ -44,6 +46,7 @@ class TestTherapyPlan(unittest.TestCase):
appointment = create_appointment(patient, practitioner, nowdate())
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name)
session.start_date = add_days(getdate(), 2)
session = frappe.get_doc(session)
session.submit()
self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils import flt, today
from frappe.utils import flt
class TherapyPlan(Document):
@ -63,8 +63,6 @@ def make_therapy_session(therapy_plan, patient, therapy_type, company, appointme
therapy_session.exercises = therapy_type.exercises
therapy_session.appointment = appointment
if frappe.flags.in_test:
therapy_session.start_date = today()
return therapy_session.as_dict()

View File

@ -344,6 +344,7 @@ scheduler_events = {
"all": [
"erpnext.projects.doctype.project.project.project_status_update_reminder",
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder",
"erpnext.hr.doctype.interview.interview.send_interview_reminder",
"erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts"
],
"hourly": [
@ -388,6 +389,7 @@ scheduler_events = {
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
"erpnext.non_profit.doctype.membership.membership.set_expired_status"
"erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder"
],
"daily_long": [
"erpnext.setup.doctype.email_digest.email_digest.send",

View File

@ -9,20 +9,20 @@ frappe.listview_settings['Attendance'] = {
return [__(doc.status), "orange", "status,=," + doc.status];
}
},
onload: function(list_view) {
let me = this;
const months = moment.months()
list_view.page.add_inner_button( __("Mark Attendance"), function() {
const months = moment.months();
list_view.page.add_inner_button(__("Mark Attendance"), function() {
let dialog = new frappe.ui.Dialog({
title: __("Mark Attendance"),
fields: [
{
fields: [{
fieldname: 'employee',
label: __('For Employee'),
fieldtype: 'Link',
options: 'Employee',
get_query: () => {
return {query: "erpnext.controllers.queries.employee_query"}
return {query: "erpnext.controllers.queries.employee_query"};
},
reqd: 1,
onchange: function() {
@ -40,11 +40,11 @@ frappe.listview_settings['Attendance'] = {
options: months,
reqd: 1,
onchange: function() {
if(dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
dialog.set_df_property("status", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", []);
dialog.no_unmarked_days_left = false;
me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options =>{
me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options => {
if (options.length > 0) {
dialog.set_df_property("unmarked_days", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", options);
@ -60,7 +60,7 @@ frappe.listview_settings['Attendance'] = {
fieldtype: "Select",
fieldname: "status",
options: ["Present", "Absent", "Half Day", "Work From Home"],
hidden:1,
hidden: 1,
reqd: 1,
},
@ -71,21 +71,24 @@ frappe.listview_settings['Attendance'] = {
options: [],
columns: 2,
hidden: 1
},
],
}],
primary_action(data) {
if (cur_dialog.no_unmarked_days_left) {
frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",[dialog.fields_dict.month.value, dialog.fields_dict.employee.value]));
frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",
[dialog.fields_dict.month.value, dialog.fields_dict.employee.value]));
} else {
frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status,data.month]), () => {
frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status, data.month]), () => {
frappe.call({
method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
args: {
data: data
},
callback: function(r) {
callback: function (r) {
if (r.message === 1) {
frappe.show_alert({message: __("Attendance Marked"), indicator: 'blue'});
frappe.show_alert({
message: __("Attendance Marked"),
indicator: 'blue'
});
cur_dialog.hide();
}
}
@ -101,21 +104,26 @@ frappe.listview_settings['Attendance'] = {
dialog.show();
});
},
get_multi_select_options: function(employee, month){
get_multi_select_options: function(employee, month) {
return new Promise(resolve => {
frappe.call({
method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days',
async: false,
args:{
args: {
employee: employee,
month: month,
}
}).then(r => {
var options = [];
for(var d in r.message){
for (var d in r.message) {
var momentObj = moment(r.message[d], 'YYYY-MM-DD');
var date = momentObj.format('DD-MM-YYYY');
options.push({ "label":date, "value": r.message[d] , "checked": 1});
options.push({
"label": date,
"value": r.message[d],
"checked": 1
});
}
resolve(options);
});

View File

@ -71,6 +71,7 @@ def get_job_applicant():
applicant = frappe.new_doc('Job Applicant')
applicant.applicant_name = 'Test Researcher'
applicant.email_id = 'test@researcher.com'
applicant.designation = 'Researcher'
applicant.status = 'Open'
applicant.cover_letter = 'I am a great Researcher.'
applicant.insert()

View File

@ -38,8 +38,10 @@ def create_job_applicant(source_name, target_doc=None):
status = "Open"
job_applicant = frappe.new_doc("Job Applicant")
job_applicant.source = "Employee Referral"
job_applicant.employee_referral = emp_ref.name
job_applicant.status = status
job_applicant.designation = emp_ref.for_designation
job_applicant.applicant_name = emp_ref.full_name
job_applicant.email_id = emp_ref.email
job_applicant.phone_number = emp_ref.contact_no

View File

@ -17,6 +17,11 @@ from erpnext.hr.doctype.employee_referral.employee_referral import (
class TestEmployeeReferral(unittest.TestCase):
def setUp(self):
frappe.db.sql("DELETE FROM `tabJob Applicant`")
frappe.db.sql("DELETE FROM `tabEmployee Referral`")
def test_workflow_and_status_sync(self):
emp_ref = create_employee_referral()
@ -50,6 +55,10 @@ class TestEmployeeReferral(unittest.TestCase):
add_sal = create_additional_salary(emp_ref)
self.assertTrue(add_sal.ref_docname, emp_ref.name)
def tearDown(self):
frappe.db.sql("DELETE FROM `tabJob Applicant`")
frappe.db.sql("DELETE FROM `tabEmployee Referral`")
def create_employee_referral():
emp_ref = frappe.new_doc("Employee Referral")

View 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
}

View 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

View File

@ -30,7 +30,13 @@
"auto_leave_encashment",
"restrict_backdated_leave_application",
"hiring_settings",
"check_vacancies"
"check_vacancies",
"send_interview_reminder",
"interview_reminder_template",
"remind_before",
"column_break_29",
"send_interview_feedback_reminder",
"feedback_reminder_notification_template"
],
"fields": [
{
@ -142,6 +148,13 @@
"fieldtype": "Int",
"label": "Standard Working Hours"
},
{
"default": "00:15:00",
"depends_on": "send_interview_reminder",
"fieldname": "remind_before",
"fieldtype": "Time",
"label": "Remind Before"
},
{
"collapsible": 1,
"fieldname": "reminders_section",
@ -181,13 +194,45 @@
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "send_interview_reminder",
"fieldtype": "Check",
"label": "Send Interview Reminder"
},
{
"default": "0",
"fieldname": "send_interview_feedback_reminder",
"fieldtype": "Check",
"label": "Send Interview Feedback Reminder"
},
{
"fieldname": "column_break_29",
"fieldtype": "Column Break"
},
{
"depends_on": "send_interview_feedback_reminder",
"fieldname": "feedback_reminder_notification_template",
"fieldtype": "Link",
"label": "Feedback Reminder Notification Template",
"mandatory_depends_on": "send_interview_feedback_reminder",
"options": "Email Template"
},
{
"depends_on": "send_interview_reminder",
"fieldname": "interview_reminder_template",
"fieldtype": "Link",
"label": "Interview Reminder Notification Template",
"mandatory_depends_on": "send_interview_reminder",
"options": "Email Template"
}
],
"icon": "fa fa-cog",
"idx": 1,
"issingle": 1,
"links": [],
"modified": "2021-08-24 14:54:12.834162",
"modified": "2021-09-30 22:42:14.683983",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",

View File

View 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', '');
}
});

View 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
}

View 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

View 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'
};

View File

@ -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>

View 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];
}
};

View File

@ -0,0 +1,5 @@
<h1>Interview Reminder</h1>
<p>
Interview: {{name}} is scheduled on {{scheduled_on}} from {{from_time}} to {{to_time}}
</p>

View 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()

View 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) {
// }
});

View 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
}

View 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

View 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

View 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', '');
}
}
});

View 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
}

View 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]

View 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

View 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);
}
});
}
});

View 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
}

View 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

View 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

View 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) {
// }
});

View 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
}

View 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

View 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

View 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
}

View 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

View File

@ -8,6 +8,24 @@ cur_frm.email_field = "email_id";
frappe.ui.form.on("Job Applicant", {
refresh: function(frm) {
frm.set_query("job_title", function() {
return {
filters: {
'status': 'Open'
}
};
});
frm.events.create_custom_buttons(frm);
frm.events.make_dashboard(frm);
},
create_custom_buttons: function(frm) {
if (!frm.doc.__islocal && frm.doc.status !== "Rejected" && frm.doc.status !== "Accepted") {
frm.add_custom_button(__("Create Interview"), function() {
frm.events.create_dialog(frm);
});
}
if (!frm.doc.__islocal) {
if (frm.doc.__onload && frm.doc.__onload.job_offer) {
$('[data-doctype="Employee Onboarding"]').find("button").show();
@ -28,14 +46,57 @@ frappe.ui.form.on("Job Applicant", {
});
}
}
},
frm.set_query("job_title", function() {
return {
filters: {
'status': 'Open'
make_dashboard: function(frm) {
frappe.call({
method: "erpnext.hr.doctype.job_applicant.job_applicant.get_interview_details",
args: {
job_applicant: frm.doc.name
},
callback: function(r) {
$("div").remove(".form-dashboard-section.custom");
frm.dashboard.add_section(
frappe.render_template('job_applicant_dashboard', {
data: r.message
}),
__("Interview Summary")
);
}
};
});
},
create_dialog: function(frm) {
let d = new frappe.ui.Dialog({
title: 'Enter Interview Round',
fields: [
{
label: 'Interview Round',
fieldname: 'interview_round',
fieldtype: 'Link',
options: 'Interview Round'
},
],
primary_action_label: 'Create Interview',
primary_action(values) {
frm.events.create_interview(frm, values);
d.hide();
}
});
d.show();
},
create_interview: function (frm, values) {
frappe.call({
method: "erpnext.hr.doctype.job_applicant.job_applicant.create_interview",
args: {
doc: frm.doc,
interview_round: values.interview_round
},
callback: function (r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
}
});
}
});

View File

@ -9,16 +9,20 @@
"email_append_to": 1,
"engine": "InnoDB",
"field_order": [
"details_section",
"applicant_name",
"email_id",
"phone_number",
"country",
"status",
"column_break_3",
"job_title",
"designation",
"status",
"source_and_rating_section",
"source",
"source_name",
"employee_referral",
"column_break_13",
"applicant_rating",
"section_break_6",
"notes",
@ -84,7 +88,8 @@
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"label": "Resume"
},
{
"fieldname": "cover_letter",
@ -160,13 +165,34 @@
"label": "Employee Referral",
"options": "Employee Referral",
"read_only": 1
},
{
"fieldname": "details_section",
"fieldtype": "Section Break",
"label": "Details"
},
{
"fieldname": "source_and_rating_section",
"fieldtype": "Section Break",
"label": "Source and Rating"
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
},
{
"fetch_from": "job_opening.designation",
"fieldname": "designation",
"fieldtype": "Link",
"label": "Designation",
"options": "Designation"
}
],
"icon": "fa fa-user",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-03-24 15:51:11.117517",
"modified": "2021-09-29 23:06:10.904260",
"modified_by": "Administrator",
"module": "HR",
"name": "Job Applicant",

View File

@ -8,7 +8,9 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import comma_and, validate_email_address
from frappe.utils import validate_email_address
from erpnext.hr.doctype.interview.interview import get_interviewers
class DuplicationError(frappe.ValidationError): pass
@ -26,7 +28,6 @@ class JobApplicant(Document):
self.name = " - ".join(keys)
def validate(self):
self.check_email_id_is_unique()
if self.email_id:
validate_email_address(self.email_id, True)
@ -44,11 +45,44 @@ class JobApplicant(Document):
elif self.status in ["Accepted", "Rejected"]:
emp_ref.db_set("status", self.status)
@frappe.whitelist()
def create_interview(doc, interview_round):
import json
def check_email_id_is_unique(self):
if self.email_id:
names = frappe.db.sql_list("""select name from `tabJob Applicant`
where email_id=%s and name!=%s and job_title=%s""", (self.email_id, self.name, self.job_title))
from six import string_types
if names:
frappe.throw(_("Email Address must be unique, already exists for {0}").format(comma_and(names)), frappe.DuplicateEntryError)
if isinstance(doc, string_types):
doc = json.loads(doc)
doc = frappe.get_doc(doc)
round_designation = frappe.db.get_value("Interview Round", interview_round, "designation")
if round_designation and doc.designation and round_designation != doc.designation:
frappe.throw(_("Interview Round {0} is only applicable for the Designation {1}").format(interview_round, round_designation))
interview = frappe.new_doc("Interview")
interview.interview_round = interview_round
interview.job_applicant = doc.name
interview.designation = doc.designation
interview.resume_link = doc.resume_link
interview.job_opening = doc.job_title
interviewer_detail = get_interviewers(interview_round)
for d in interviewer_detail:
interview.append("interview_details", {
"interviewer": d.interviewer
})
return interview
@frappe.whitelist()
def get_interview_details(job_applicant):
interview_details = frappe.db.get_all("Interview",
filters={"job_applicant":job_applicant, "docstatus": ["!=", 2]},
fields=["name", "interview_round", "expected_average_rating", "average_rating", "status"]
)
interview_detail_map = {}
for detail in interview_details:
interview_detail_map[detail.name] = detail
return interview_detail_map

View File

@ -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 %}

View File

@ -9,7 +9,10 @@ def get_data():
'items': ['Employee', 'Employee Onboarding']
},
{
'items': ['Job Offer']
'items': ['Job Offer', 'Appointment Letter']
},
{
'items': ['Interview']
}
],
}

View File

@ -7,7 +7,8 @@ import unittest
import frappe
# test_records = frappe.get_test_records('Job Applicant')
from erpnext.hr.doctype.designation.test_designation import create_designation
class TestJobApplicant(unittest.TestCase):
pass
@ -25,7 +26,8 @@ def create_job_applicant(**args):
job_applicant = frappe.get_doc({
"doctype": "Job Applicant",
"status": args.status or "Open"
"status": args.status or "Open",
"designation": create_designation().name
})
job_applicant.update(filters)

View File

@ -32,6 +32,7 @@ class TestJobOffer(unittest.TestCase):
self.assertTrue(frappe.db.exists("Job Offer", job_offer.name))
def test_job_applicant_update(self):
frappe.db.set_value("HR Settings", None, "check_vacancies", 0)
create_staffing_plan()
job_applicant = create_job_applicant(email_id="test_job_applicants@example.com")
job_offer = create_job_offer(job_applicant=job_applicant.name)
@ -43,7 +44,11 @@ class TestJobOffer(unittest.TestCase):
job_offer.status = "Rejected"
job_offer.submit()
job_applicant.reload()
self.assertEqual(job_applicant.status, "Rejected")
self.assertEquals(job_applicant.status, "Rejected")
frappe.db.set_value("HR Settings", None, "check_vacancies", 1)
def tearDown(self):
frappe.db.sql("DELETE FROM `tabJob Offer` WHERE 1")
def create_job_offer(**args):
args = frappe._dict(args)

View File

@ -1,8 +1,8 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
cur_frm.add_fetch('employee','employee_name','employee_name');
cur_frm.add_fetch('employee','company','company');
cur_frm.add_fetch('employee', 'employee_name', 'employee_name');
cur_frm.add_fetch('employee', 'company', 'company');
frappe.ui.form.on("Leave Application", {
setup: function(frm) {
@ -19,7 +19,6 @@ frappe.ui.form.on("Leave Application", {
frm.set_query("employee", erpnext.queries.employee);
},
onload: function(frm) {
// Ignore cancellation of doctype on cancel all.
frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"];
@ -42,9 +41,9 @@ frappe.ui.form.on("Leave Application", {
},
validate: function(frm) {
if (frm.doc.from_date == frm.doc.to_date && frm.doc.half_day == 1){
if (frm.doc.from_date == frm.doc.to_date && frm.doc.half_day == 1) {
frm.doc.half_day_date = frm.doc.from_date;
}else if (frm.doc.half_day == 0){
} else if (frm.doc.half_day == 0) {
frm.doc.half_day_date = "";
}
frm.toggle_reqd("half_day_date", frm.doc.half_day == 1);
@ -84,9 +83,9 @@ frappe.ui.form.on("Leave Application", {
// lwps should be allowed, lwps don't have any allocation
allowed_leave_types = allowed_leave_types.concat(lwps);
frm.set_query('leave_type', function(){
frm.set_query('leave_type', function() {
return {
filters : [
filters: [
['leave_type_name', 'in', allowed_leave_types]
]
};
@ -99,7 +98,7 @@ frappe.ui.form.on("Leave Application", {
frm.trigger("calculate_total_days");
}
cur_frm.set_intro("");
if(frm.doc.__islocal && !in_list(frappe.user_roles, "Employee")) {
if (frm.doc.__islocal && !in_list(frappe.user_roles, "Employee")) {
frm.set_intro(__("Fill the form and save it"));
}
@ -118,7 +117,7 @@ frappe.ui.form.on("Leave Application", {
},
leave_approver: function(frm) {
if(frm.doc.leave_approver){
if (frm.doc.leave_approver) {
frm.set_value("leave_approver_name", frappe.user.full_name(frm.doc.leave_approver));
}
},
@ -131,12 +130,10 @@ frappe.ui.form.on("Leave Application", {
if (frm.doc.half_day) {
if (frm.doc.from_date == frm.doc.to_date) {
frm.set_value("half_day_date", frm.doc.from_date);
}
else {
} else {
frm.trigger("half_day_datepicker");
}
}
else {
} else {
frm.set_value("half_day_date", "");
}
frm.trigger("calculate_total_days");
@ -167,7 +164,7 @@ frappe.ui.form.on("Leave Application", {
},
get_leave_balance: function(frm) {
if(frm.doc.docstatus==0 && frm.doc.employee && frm.doc.leave_type && frm.doc.from_date && frm.doc.to_date) {
if (frm.doc.docstatus === 0 && frm.doc.employee && frm.doc.leave_type && frm.doc.from_date && frm.doc.to_date) {
return frappe.call({
method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_balance_on",
args: {
@ -180,8 +177,7 @@ frappe.ui.form.on("Leave Application", {
callback: function(r) {
if (!r.exc && r.message) {
frm.set_value('leave_balance', r.message);
}
else {
} else {
frm.set_value('leave_balance', "0");
}
}
@ -190,12 +186,12 @@ frappe.ui.form.on("Leave Application", {
},
calculate_total_days: function(frm) {
if(frm.doc.from_date && frm.doc.to_date && frm.doc.employee && frm.doc.leave_type) {
if (frm.doc.from_date && frm.doc.to_date && frm.doc.employee && frm.doc.leave_type) {
var from_date = Date.parse(frm.doc.from_date);
var to_date = Date.parse(frm.doc.to_date);
if(to_date < from_date){
if (to_date < from_date) {
frappe.msgprint(__("To Date cannot be less than From Date"));
frm.set_value('to_date', '');
return;
@ -222,7 +218,7 @@ frappe.ui.form.on("Leave Application", {
},
set_leave_approver: function(frm) {
if(frm.doc.employee) {
if (frm.doc.employee) {
// server call is done to include holidays in leave days calculations
return frappe.call({
method: 'erpnext.hr.doctype.leave_application.leave_application.get_leave_approver',

View 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
}

View 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

View File

@ -313,3 +313,4 @@ erpnext.patches.v13_0.create_custom_field_for_finance_book
erpnext.patches.v13_0.modify_invalid_gain_loss_gl_entries
erpnext.patches.v13_0.fix_additional_cost_in_mfg_stock_entry
erpnext.patches.v13_0.set_status_in_maintenance_schedule_table
erpnext.patches.v13_0.add_default_interview_notification_templates

View File

@ -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()

View File

@ -171,8 +171,6 @@ class TestSalarySlip(unittest.TestCase):
days_in_month = no_of_days[0]
no_of_holidays = no_of_days[1]
self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 1)
ss.reload()
payment_days_based_comp_amount = 0
for component in ss.earnings:

View File

@ -62,6 +62,13 @@ def set_default_settings(args):
hr_settings.emp_created_by = "Naming Series"
hr_settings.leave_approval_notification_template = _("Leave Approval Notification")
hr_settings.leave_status_notification_template = _("Leave Status Notification")
hr_settings.send_interview_reminder = 1
hr_settings.interview_reminder_template = _("Interview Reminder")
hr_settings.remind_before = "00:15:00"
hr_settings.send_interview_feedback_reminder = 1
hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder")
hr_settings.save()
def set_no_copy_fields_in_variant_settings():

View File

@ -264,16 +264,26 @@ def install(country=None):
base_path = frappe.get_app_path("erpnext", "hr", "doctype")
response = frappe.read_file(os.path.join(base_path, "leave_application/leave_application_email_template.html"))
records += [{'doctype': 'Email Template', 'name': _("Leave Approval Notification"), 'response': response,\
records += [{'doctype': 'Email Template', 'name': _("Leave Approval Notification"), 'response': response,
'subject': _("Leave Approval Notification"), 'owner': frappe.session.user}]
records += [{'doctype': 'Email Template', 'name': _("Leave Status Notification"), 'response': response,\
records += [{'doctype': 'Email Template', 'name': _("Leave Status Notification"), 'response': response,
'subject': _("Leave Status Notification"), 'owner': frappe.session.user}]
response = frappe.read_file(os.path.join(base_path, "interview/interview_reminder_notification_template.html"))
records += [{'doctype': 'Email Template', 'name': _('Interview Reminder'), 'response': response,
'subject': _('Interview Reminder'), 'owner': frappe.session.user}]
response = frappe.read_file(os.path.join(base_path, "interview/interview_feedback_reminder_template.html"))
records += [{'doctype': 'Email Template', 'name': _('Interview Feedback Reminder'), 'response': response,
'subject': _('Interview Feedback Reminder'), 'owner': frappe.session.user}]
base_path = frappe.get_app_path("erpnext", "stock", "doctype")
response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html"))
records += [{'doctype': 'Email Template', 'name': _("Dispatch Notification"), 'response': response,\
records += [{'doctype': 'Email Template', 'name': _("Dispatch Notification"), 'response': response,
'subject': _("Your order is out for delivery!"), 'owner': frappe.session.user}]
# Records for the Supplier Scorecard
@ -317,6 +327,14 @@ def update_hr_defaults():
hr_settings.emp_created_by = "Naming Series"
hr_settings.leave_approval_notification_template = _("Leave Approval Notification")
hr_settings.leave_status_notification_template = _("Leave Status Notification")
hr_settings.send_interview_reminder = 1
hr_settings.interview_reminder_template = _("Interview Reminder")
hr_settings.remind_before = "00:15:00"
hr_settings.send_interview_feedback_reminder = 1
hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder")
hr_settings.save()
def update_item_variant_settings():