Merge branch 'version-13-hotfix' into e-comm-web-item-name-thumbnail-fix

This commit is contained in:
Marica
2021-10-03 13:30:50 +05:30
committed by GitHub
92 changed files with 3094 additions and 339 deletions

View File

@@ -6,7 +6,7 @@ context('Organizational Chart', () => {
it('navigates to org chart', () => {
cy.visit('/app');
cy.awesomebar('Organizational Chart');
cy.visit('/app/organizational-chart');
cy.url().should('include', '/organizational-chart');
cy.window().its('frappe.csrf_token').then(csrf_token => {

View File

@@ -7,7 +7,7 @@ context('Organizational Chart Mobile', () => {
it('navigates to org chart', () => {
cy.viewport(375, 667);
cy.visit('/app');
cy.awesomebar('Organizational Chart');
cy.visit('/app/organizational-chart');
cy.url().should('include', '/organizational-chart');
cy.window().its('frappe.csrf_token').then(csrf_token => {

View File

@@ -12,11 +12,6 @@ frappe.ui.form.on('Chart of Accounts Importer', {
frm.set_df_property('import_file_section', 'hidden', frm.doc.company ? 0 : 1);
frm.set_df_property('chart_preview', 'hidden',
$(frm.fields_dict['chart_tree'].wrapper).html()!="" ? 0 : 1);
// Show import button when file is successfully attached
if (frm.page && frm.page.show_import_button) {
create_import_button(frm);
}
},
download_template: function(frm) {
@@ -78,7 +73,12 @@ frappe.ui.form.on('Chart of Accounts Importer', {
frm.page.set_indicator("");
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper on removing file
} else {
generate_tree_preview(frm);
frappe.run_serially([
() => validate_coa(frm),
() => generate_tree_preview(frm),
() => create_import_button(frm),
() => frm.set_df_property('chart_preview', 'hidden', 0),
]);
}
},
@@ -104,24 +104,26 @@ frappe.ui.form.on('Chart of Accounts Importer', {
});
var create_import_button = function(frm) {
frm.page.set_primary_action(__("Import"), function () {
frappe.call({
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.import_coa",
args: {
file_name: frm.doc.import_file,
company: frm.doc.company
},
freeze: true,
freeze_message: __("Creating Accounts..."),
callback: function(r) {
if(!r.exc) {
clearInterval(frm.page["interval"]);
frm.page.set_indicator(__('Import Successful'), 'blue');
create_reset_button(frm);
if (frm.page.show_import_button) {
frm.page.set_primary_action(__("Import"), function () {
return frappe.call({
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.import_coa",
args: {
file_name: frm.doc.import_file,
company: frm.doc.company
},
freeze: true,
freeze_message: __("Creating Accounts..."),
callback: function(r) {
if (!r.exc) {
clearInterval(frm.page["interval"]);
frm.page.set_indicator(__('Import Successful'), 'blue');
create_reset_button(frm);
}
}
}
});
}).addClass('btn btn-primary');
});
}).addClass('btn btn-primary');
}
};
var create_reset_button = function(frm) {
@@ -132,13 +134,35 @@ var create_reset_button = function(frm) {
}).addClass('btn btn-primary');
};
var validate_coa = function(frm) {
if (frm.doc.import_file) {
let parent = __('All Accounts');
return frappe.call({
'method': 'erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa',
'args': {
file_name: frm.doc.import_file,
parent: parent,
doctype: 'Chart of Accounts Importer',
file_type: frm.doc.file_type,
for_validate: 1
},
callback: function(r) {
if (r.message['show_import_button']) {
frm.page['show_import_button'] = Boolean(r.message['show_import_button']);
}
}
});
}
};
var generate_tree_preview = function(frm) {
if (frm.doc.import_file) {
let parent = __('All Accounts');
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper to load new data
// generate tree structure based on the csv data
new frappe.ui.Tree({
return new frappe.ui.Tree({
parent: $(frm.fields_dict['chart_tree'].wrapper),
label: parent,
expandable: true,

View File

@@ -64,6 +64,7 @@ def import_coa(file_name, company):
else:
data = generate_data_from_excel(file_doc, extension)
frappe.local.flags.ignore_root_company_validation = True
forest = build_forest(data)
create_charts(company, custom_chart=forest)
@@ -128,7 +129,7 @@ def generate_data_from_excel(file_doc, extension, as_dict=False):
return data
@frappe.whitelist()
def get_coa(doctype, parent, is_root=False, file_name=None):
def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0):
''' called by tree view (to fetch node's children) '''
file_doc, extension = get_file(file_name)
@@ -140,14 +141,20 @@ def get_coa(doctype, parent, is_root=False, file_name=None):
data = generate_data_from_excel(file_doc, extension)
validate_columns(data)
validate_accounts(data)
forest = build_forest(data)
accounts = build_tree_from_json("", chart_data=forest) # returns alist of dict in a tree render-able form
validate_accounts(file_doc, extension)
# filter out to show data for the selected node only
accounts = [d for d in accounts if d['parent_account']==parent]
if not for_validate:
forest = build_forest(data)
accounts = build_tree_from_json("", chart_data=forest) # returns a list of dict in a tree render-able form
return accounts
# filter out to show data for the selected node only
accounts = [d for d in accounts if d['parent_account']==parent]
return accounts
else:
return {
'show_import_button': 1
}
def build_forest(data):
'''
@@ -304,10 +311,7 @@ def get_sample_template(writer):
@frappe.whitelist()
def validate_accounts(file_name):
file_doc, extension = get_file(file_name)
def validate_accounts(file_doc, extension):
if extension == 'csv':
accounts = generate_data_from_csv(file_doc, as_dict=True)
else:
@@ -326,8 +330,6 @@ def validate_accounts(file_name):
validate_root(accounts_dict)
validate_account_types(accounts_dict)
return [True, len(accounts)]
def validate_root(accounts):
@@ -340,9 +342,19 @@ def validate_root(accounts):
elif account.get("root_type") not in get_root_types() and account.get("account_name"):
error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name")))
validate_missing_roots(roots)
if error_messages:
frappe.throw("<br>".join(error_messages))
def validate_missing_roots(roots):
root_types_added = set(d.get('root_type') for d in roots)
missing = list(set(get_root_types()) - root_types_added)
if missing:
frappe.throw(_("Please add Root Account for - {0}").format(' , '.join(missing)))
def get_root_types():
return ('Asset', 'Liability', 'Expense', 'Income', 'Equity')
@@ -368,15 +380,6 @@ def get_mandatory_account_types():
{'account_type': 'Stock', 'root_type': 'Asset'}
]
def validate_account_types(accounts):
account_types_for_ledger = ["Cost of Goods Sold", "Depreciation", "Fixed Asset", "Payable", "Receivable", "Stock Adjustment"]
account_types = [accounts[d]["account_type"] for d in accounts if not cint(accounts[d]['is_group']) == 1]
missing = list(set(account_types_for_ledger) - set(account_types))
if missing:
frappe.throw(_("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)))
def unset_existing_data(company):
linked = frappe.db.sql('''select fieldname from tabDocField
where fieldtype="Link" and options="Account" and parent="Company"''', as_dict=True)

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

@@ -338,6 +338,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": [
@@ -383,6 +384,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,83 +9,86 @@ 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: [
{
fieldname: 'employee',
label: __('For Employee'),
fieldtype: 'Link',
options: 'Employee',
get_query: () => {
return {query: "erpnext.controllers.queries.employee_query"}
},
reqd: 1,
onchange: function() {
dialog.set_df_property("unmarked_days", "hidden", 1);
dialog.set_df_property("status", "hidden", 1);
dialog.set_df_property("month", "value", '');
fields: [{
fieldname: 'employee',
label: __('For Employee'),
fieldtype: 'Link',
options: 'Employee',
get_query: () => {
return {query: "erpnext.controllers.queries.employee_query"};
},
reqd: 1,
onchange: function() {
dialog.set_df_property("unmarked_days", "hidden", 1);
dialog.set_df_property("status", "hidden", 1);
dialog.set_df_property("month", "value", '');
dialog.set_df_property("unmarked_days", "options", []);
dialog.no_unmarked_days_left = false;
}
},
{
label: __("For Month"),
fieldtype: "Select",
fieldname: "month",
options: months,
reqd: 1,
onchange: function() {
if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
dialog.set_df_property("status", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", []);
dialog.no_unmarked_days_left = false;
me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options => {
if (options.length > 0) {
dialog.set_df_property("unmarked_days", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", options);
} else {
dialog.no_unmarked_days_left = true;
}
});
}
},
{
label: __("For Month"),
fieldtype: "Select",
fieldname: "month",
options: months,
reqd: 1,
onchange: function() {
if(dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
dialog.set_df_property("status", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", []);
dialog.no_unmarked_days_left = false;
me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options =>{
if (options.length > 0) {
dialog.set_df_property("unmarked_days", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", options);
} else {
dialog.no_unmarked_days_left = true;
}
});
}
}
},
{
label: __("Status"),
fieldtype: "Select",
fieldname: "status",
options: ["Present", "Absent", "Half Day", "Work From Home"],
hidden:1,
reqd: 1,
}
},
{
label: __("Status"),
fieldtype: "Select",
fieldname: "status",
options: ["Present", "Absent", "Half Day", "Work From Home"],
hidden: 1,
reqd: 1,
},
{
label: __("Unmarked Attendance for days"),
fieldname: "unmarked_days",
fieldtype: "MultiCheck",
options: [],
columns: 2,
hidden: 1
},
],
primary_action(data) {
},
{
label: __("Unmarked Attendance for days"),
fieldname: "unmarked_days",
fieldtype: "MultiCheck",
options: [],
columns: 2,
hidden: 1
}],
primary_action(data) {
if (cur_dialog.no_unmarked_days_left) {
frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",[dialog.fields_dict.month.value, dialog.fields_dict.employee.value]));
frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",
[dialog.fields_dict.month.value, dialog.fields_dict.employee.value]));
} else {
frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status,data.month]), () => {
frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status, data.month]), () => {
frappe.call({
method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
args: {
data: data
},
callback: function(r) {
callback: function (r) {
if (r.message === 1) {
frappe.show_alert({message: __("Attendance Marked"), indicator: 'blue'});
frappe.show_alert({
message: __("Attendance Marked"),
indicator: 'blue'
});
cur_dialog.hide();
}
}
@@ -101,21 +104,26 @@ 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

@@ -4,40 +4,46 @@
frappe.provide("erpnext.hr");
erpnext.hr.EmployeeController = frappe.ui.form.Controller.extend({
setup: function() {
this.frm.fields_dict.user_id.get_query = function(doc, cdt, cdn) {
this.frm.fields_dict.user_id.get_query = function() {
return {
query: "frappe.core.doctype.user.user.user_query",
filters: {ignore_user_type: 1}
}
}
this.frm.fields_dict.reports_to.get_query = function(doc, cdt, cdn) {
return { query: "erpnext.controllers.queries.employee_query"} }
filters: {
ignore_user_type: 1
}
};
};
this.frm.fields_dict.reports_to.get_query = function() {
return {
query: "erpnext.controllers.queries.employee_query"
};
};
},
refresh: function() {
var me = this;
erpnext.toggle_naming_series();
},
date_of_birth: function() {
return cur_frm.call({
method: "get_retirement_date",
args: {date_of_birth: this.frm.doc.date_of_birth}
args: {
date_of_birth: this.frm.doc.date_of_birth
}
});
},
salutation: function() {
if(this.frm.doc.salutation) {
if (this.frm.doc.salutation) {
this.frm.set_value("gender", {
"Mr": "Male",
"Ms": "Female"
}[this.frm.doc.salutation]);
} [this.frm.doc.salutation]);
}
},
});
frappe.ui.form.on('Employee',{
setup: function(frm) {
frappe.ui.form.on('Employee', {
setup: function (frm) {
frm.set_query("leave_policy", function() {
return {
"filters": {
@@ -46,7 +52,7 @@ frappe.ui.form.on('Employee',{
};
});
},
onload:function(frm) {
onload: function (frm) {
frm.set_query("department", function() {
return {
"filters": {
@@ -55,23 +61,28 @@ frappe.ui.form.on('Employee',{
};
});
},
prefered_contact_email:function(frm){
frm.events.update_contact(frm)
prefered_contact_email: function(frm) {
frm.events.update_contact(frm);
},
personal_email:function(frm){
frm.events.update_contact(frm)
personal_email: function(frm) {
frm.events.update_contact(frm);
},
company_email:function(frm){
frm.events.update_contact(frm)
company_email: function(frm) {
frm.events.update_contact(frm);
},
user_id:function(frm){
frm.events.update_contact(frm)
user_id: function(frm) {
frm.events.update_contact(frm);
},
update_contact:function(frm){
update_contact: function(frm) {
var prefered_email_fieldname = frappe.model.scrub(frm.doc.prefered_contact_email) || 'user_id';
frm.set_value("prefered_email",
frm.fields_dict[prefered_email_fieldname].value)
frm.fields_dict[prefered_email_fieldname].value);
},
status: function(frm) {
return frm.call({
method: "deactivate_sales_person",
@@ -81,19 +92,63 @@ frappe.ui.form.on('Employee',{
}
});
},
create_user: function(frm) {
if (!frm.doc.prefered_email)
{
frappe.throw(__("Please enter Preferred Contact Email"))
if (!frm.doc.prefered_email) {
frappe.throw(__("Please enter Preferred Contact Email"));
}
frappe.call({
method: "erpnext.hr.doctype.employee.employee.create_user",
args: { employee: frm.doc.name, email: frm.doc.prefered_email },
callback: function(r)
{
frm.set_value("user_id", r.message)
args: {
employee: frm.doc.name,
email: frm.doc.prefered_email
},
callback: function (r) {
frm.set_value("user_id", r.message);
}
});
}
});
cur_frm.cscript = new erpnext.hr.EmployeeController({frm: cur_frm});
cur_frm.cscript = new erpnext.hr.EmployeeController({
frm: cur_frm
});
frappe.tour['Employee'] = [
{
fieldname: "first_name",
title: "First Name",
description: __("Enter First and Last name of Employee, based on Which Full Name will be updated. IN transactions, it will be Full Name which will be fetched.")
},
{
fieldname: "company",
title: "Company",
description: __("Select a Company this Employee belongs to. Other HR features like Payroll. Expense Claims and Leaves for this Employee will be created for a given company only.")
},
{
fieldname: "date_of_birth",
title: "Date of Birth",
description: __("Select Date of Birth. This will validate Employees age and prevent hiring of under-age staff.")
},
{
fieldname: "date_of_joining",
title: "Date of Joining",
description: __("Select Date of joining. It will have impact on the first salary calculation, Leave allocation on pro-rata bases.")
},
{
fieldname: "holiday_list",
title: "Holiday List",
description: __("Select a default Holiday List for this Employee. The days listed in Holiday List will not be counted in Leave Application.")
},
{
fieldname: "reports_to",
title: "Reports To",
description: __("Here, you can select a senior of this Employee. Based on this, Organization Chart will be populated.")
},
{
fieldname: "leave_approver",
title: "Leave Approver",
description: __("Select Leave Approver for an employee. The user one who will look after his/her Leave application")
},
];

View File

@@ -72,6 +72,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

@@ -1,10 +1,10 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Holiday List', {
frappe.ui.form.on("Holiday List", {
refresh: function(frm) {
if (frm.doc.holidays) {
frm.set_value('total_holidays', frm.doc.holidays.length);
frm.set_value("total_holidays", frm.doc.holidays.length);
}
},
from_date: function(frm) {
@@ -14,3 +14,36 @@ frappe.ui.form.on('Holiday List', {
}
}
});
frappe.tour["Holiday List"] = [
{
fieldname: "holiday_list_name",
title: "Holiday List Name",
description: __("Enter a name for this Holiday List."),
},
{
fieldname: "from_date",
title: "From Date",
description: __("Based on your HR Policy, select your leave allocation period's start date"),
},
{
fieldname: "to_date",
title: "To Date",
description: __("Based on your HR Policy, select your leave allocation period's end date"),
},
{
fieldname: "weekly_off",
title: "Weekly Off",
description: __("Select your weekly off day"),
},
{
fieldname: "get_weekly_off_dates",
title: "Add Holidays",
description: __("Click on Add to Holidays. This will populate the holidays table with all the dates that fall on the selected weekly off. Repeat the process for populating the dates for all your weekly holidays"),
},
{
fieldname: "holidays",
title: "Holidays",
description: __("Here, your weekly offs are pre-populated based on the previous selections. You can add more rows to also add public and national holidays individually.")
},
];

View File

@@ -2,7 +2,22 @@
// For license information, please see license.txt
frappe.ui.form.on('HR Settings', {
restrict_backdated_leave_application: function(frm) {
frm.toggle_reqd("role_allowed_to_create_backdated_leave_application", frm.doc.restrict_backdated_leave_application);
}
});
frappe.tour['HR Settings'] = [
{
fieldname: 'emp_created_by',
title: 'Employee Naming By',
description: __('Employee can be named by Employee ID if you assign one, or via Naming Series. Select your preference here.'),
},
{
fieldname: 'standard_working_hours',
title: 'Standard Working Hours',
description: __('Enter the Standard Working Hours for a normal work day. These hours will be used in calculations of reports such as Employee Hours Utilization and Project Profitability analysis.'),
},
{
fieldname: 'leave_and_expense_claim_settings',
title: 'Leave and Expense Clain Settings',
description: __('Review various other settings related to Employee Leaves and Expense Claim')
}
];

View File

@@ -7,30 +7,36 @@
"engine": "InnoDB",
"field_order": [
"employee_settings",
"retirement_age",
"emp_created_by",
"column_break_4",
"standard_working_hours",
"expense_approver_mandatory_in_expense_claim",
"column_break_9",
"retirement_age",
"reminders_section",
"send_birthday_reminders",
"column_break_9",
"send_work_anniversary_reminders",
"column_break_11",
"send_work_anniversary_reminders",
"column_break_18",
"send_holiday_reminders",
"frequency",
"leave_settings",
"leave_and_expense_claim_settings",
"send_leave_notification",
"leave_approval_notification_template",
"leave_status_notification_template",
"role_allowed_to_create_backdated_leave_application",
"column_break_18",
"leave_approver_mandatory_in_leave_application",
"restrict_backdated_leave_application",
"role_allowed_to_create_backdated_leave_application",
"column_break_29",
"expense_approver_mandatory_in_expense_claim",
"show_leaves_of_all_department_members_in_calendar",
"auto_leave_encashment",
"restrict_backdated_leave_application",
"hiring_settings",
"check_vacancies"
"hiring_settings_section",
"check_vacancies",
"send_interview_reminder",
"interview_reminder_template",
"remind_before",
"column_break_4",
"send_interview_feedback_reminder",
"feedback_reminder_notification_template"
],
"fields": [
{
@@ -39,17 +45,16 @@
"label": "Employee Settings"
},
{
"description": "Enter retirement age in years",
"fieldname": "retirement_age",
"fieldtype": "Data",
"label": "Retirement Age"
"label": "Retirement Age (In Years)"
},
{
"default": "Naming Series",
"description": "Employee records are created using the selected field",
"description": "Employee records are created using the selected option",
"fieldname": "emp_created_by",
"fieldtype": "Select",
"label": "Employee Records to be created by",
"label": "Employee Naming By",
"options": "Naming Series\nEmployee Number\nFull Name"
},
{
@@ -62,28 +67,6 @@
"fieldtype": "Check",
"label": "Expense Approver Mandatory In Expense Claim"
},
{
"collapsible": 1,
"fieldname": "leave_settings",
"fieldtype": "Section Break",
"label": "Leave Settings"
},
{
"depends_on": "eval: doc.send_leave_notification == 1",
"fieldname": "leave_approval_notification_template",
"fieldtype": "Link",
"label": "Leave Approval Notification Template",
"mandatory_depends_on": "eval: doc.send_leave_notification == 1",
"options": "Email Template"
},
{
"depends_on": "eval: doc.send_leave_notification == 1",
"fieldname": "leave_status_notification_template",
"fieldtype": "Link",
"label": "Leave Status Notification Template",
"mandatory_depends_on": "eval: doc.send_leave_notification == 1",
"options": "Email Template"
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
@@ -100,35 +83,18 @@
"fieldtype": "Check",
"label": "Show Leaves Of All Department Members In Calendar"
},
{
"collapsible": 1,
"fieldname": "hiring_settings",
"fieldtype": "Section Break",
"label": "Hiring Settings"
},
{
"default": "0",
"fieldname": "check_vacancies",
"fieldtype": "Check",
"label": "Check Vacancies On Job Offer Creation"
},
{
"default": "0",
"fieldname": "auto_leave_encashment",
"fieldtype": "Check",
"label": "Auto Leave Encashment"
},
{
"default": "0",
"fieldname": "restrict_backdated_leave_application",
"fieldtype": "Check",
"label": "Restrict Backdated Leave Application"
},
{
"depends_on": "eval:doc.restrict_backdated_leave_application == 1",
"fieldname": "role_allowed_to_create_backdated_leave_application",
"fieldtype": "Link",
"label": "Role Allowed to Create Backdated Leave Application",
"mandatory_depends_on": "eval:doc.restrict_backdated_leave_application == 1",
"options": "Role"
},
{
@@ -137,11 +103,40 @@
"fieldtype": "Check",
"label": "Send Leave Notification"
},
{
"depends_on": "eval: doc.send_leave_notification == 1",
"fieldname": "leave_approval_notification_template",
"fieldtype": "Link",
"label": "Leave Approval Notification Template",
"mandatory_depends_on": "eval: doc.send_leave_notification == 1",
"options": "Email Template"
},
{
"depends_on": "eval: doc.send_leave_notification == 1",
"fieldname": "leave_status_notification_template",
"fieldtype": "Link",
"label": "Leave Status Notification Template",
"mandatory_depends_on": "eval: doc.send_leave_notification == 1",
"options": "Email Template"
},
{
"fieldname": "standard_working_hours",
"fieldtype": "Int",
"label": "Standard Working Hours"
},
{
"collapsible": 1,
"fieldname": "leave_and_expense_claim_settings",
"fieldtype": "Section Break",
"label": "Leave and Expense Claim Settings"
},
{
"default": "00:15:00",
"depends_on": "send_interview_reminder",
"fieldname": "remind_before",
"fieldtype": "Time",
"label": "Remind Before"
},
{
"collapsible": 1,
"fieldname": "reminders_section",
@@ -166,6 +161,7 @@
"fieldname": "frequency",
"fieldtype": "Select",
"label": "Set the frequency for holiday reminders",
"mandatory_depends_on": "send_holiday_reminders",
"options": "Weekly\nMonthly"
},
{
@@ -181,13 +177,62 @@
{
"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"
},
{
"default": "0",
"fieldname": "restrict_backdated_leave_application",
"fieldtype": "Check",
"label": "Restrict Backdated Leave Application"
},
{
"fieldname": "hiring_settings_section",
"fieldtype": "Section Break",
"label": "Hiring Settings"
},
{
"default": "0",
"fieldname": "check_vacancies",
"fieldtype": "Check",
"label": "Check Vacancies On Job Offer Creation"
}
],
"icon": "fa fa-cog",
"idx": 1,
"issingle": 1,
"links": [],
"modified": "2021-08-24 14:54:12.834162",
"modified": "2021-10-01 23:46:11.098236",
"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

@@ -2,14 +2,17 @@ from __future__ import unicode_literals
def get_data():
return {
'fieldname': 'job_applicant',
'transactions': [
{
'items': ['Employee', 'Employee Onboarding']
},
{
'items': ['Job Offer']
},
],
}
return {
'fieldname': 'job_applicant',
'transactions': [
{
'items': ['Employee', 'Employee Onboarding']
},
{
'items': ['Job Offer', 'Appointment Letter']
},
{
'items': ['Interview']
}
],
}

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,14 +1,14 @@
// 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', 'employee_name', 'employee_name');
frappe.ui.form.on("Leave Allocation", {
onload: function(frm) {
// Ignore cancellation of doctype on cancel all.
frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"];
if(!frm.doc.from_date) frm.set_value("from_date", frappe.datetime.get_today());
if (!frm.doc.from_date) frm.set_value("from_date", frappe.datetime.get_today());
frm.set_query("employee", function() {
return {
@@ -25,9 +25,9 @@ frappe.ui.form.on("Leave Allocation", {
},
refresh: function(frm) {
if(frm.doc.docstatus === 1 && frm.doc.expired) {
if (frm.doc.docstatus === 1 && frm.doc.expired) {
var valid_expiry = moment(frappe.datetime.get_today()).isBetween(frm.doc.from_date, frm.doc.to_date);
if(valid_expiry) {
if (valid_expiry) {
// expire current allocation
frm.add_custom_button(__('Expire Allocation'), function() {
frm.trigger("expire_allocation");
@@ -44,8 +44,8 @@ frappe.ui.form.on("Leave Allocation", {
'expiry_date': frappe.datetime.get_today()
},
freeze: true,
callback: function(r){
if(!r.exc){
callback: function(r) {
if (!r.exc) {
frappe.msgprint(__("Allocation Expired!"));
}
frm.refresh();
@@ -77,8 +77,8 @@ frappe.ui.form.on("Leave Allocation", {
},
leave_policy: function(frm) {
if(frm.doc.leave_policy && frm.doc.leave_type) {
frappe.db.get_value("Leave Policy Detail",{
if (frm.doc.leave_policy && frm.doc.leave_type) {
frappe.db.get_value("Leave Policy Detail", {
'parent': frm.doc.leave_policy,
'leave_type': frm.doc.leave_type
}, 'annual_allocation', (r) => {
@@ -91,13 +91,41 @@ frappe.ui.form.on("Leave Allocation", {
return frappe.call({
method: "set_total_leaves_allocated",
doc: frm.doc,
callback: function(r) {
callback: function() {
frm.refresh_fields();
}
})
});
} else if (cint(frm.doc.carry_forward) == 0) {
frm.set_value("unused_leaves", 0);
frm.set_value("total_leaves_allocated", flt(frm.doc.new_leaves_allocated));
}
}
});
frappe.tour["Leave Allocation"] = [
{
fieldname: "employee",
title: "Employee",
description: __("Select the Employee for which you want to allocate leaves.")
},
{
fieldname: "leave_type",
title: "Leave Type",
description: __("Select the Leave Type like Sick leave, Privilege Leave, Casual Leave, etc.")
},
{
fieldname: "from_date",
title: "From Date",
description: __("Select the date from which this Leave Allocation will be valid.")
},
{
fieldname: "to_date",
title: "To Date",
description: __("Select the date after which this Leave Allocation will expire.")
},
{
fieldname: "new_leaves_allocated",
title: "New Leaves Allocated",
description: __("Enter the number of leaves you want to allocate for the period.")
}
];

View File

@@ -219,7 +219,8 @@
"fieldname": "leave_policy_assignment",
"fieldtype": "Link",
"label": "Leave Policy Assignment",
"options": "Leave Policy Assignment"
"options": "Leave Policy Assignment",
"read_only": 1
},
{
"fetch_from": "employee.company",
@@ -236,7 +237,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-06-03 15:28:26.335104",
"modified": "2021-10-01 15:28:26.335104",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Allocation",

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);
@@ -79,14 +78,14 @@ frappe.ui.form.on("Leave Application", {
__("Allocated Leaves")
);
frm.dashboard.show();
let allowed_leave_types = Object.keys(leave_details);
let allowed_leave_types = Object.keys(leave_details);
// lwps should be allowed, lwps don't have any allocation
allowed_leave_types = allowed_leave_types.concat(lwps);
frm.set_query('leave_type', function(){
frm.set_query('leave_type', function() {
return {
filters : [
filters: [
['leave_type_name', 'in', allowed_leave_types]
]
};
@@ -99,7 +98,7 @@ 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");
@@ -163,11 +160,11 @@ frappe.ui.form.on("Leave Application", {
half_day_datepicker.update({
minDate: frappe.datetime.str_to_obj(frm.doc.from_date),
maxDate: frappe.datetime.str_to_obj(frm.doc.to_date)
})
});
},
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: {
@@ -177,11 +174,10 @@ frappe.ui.form.on("Leave Application", {
leave_type: frm.doc.leave_type,
consider_all_leaves_in_the_allocation_period: true
},
callback: function(r) {
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',
@@ -238,3 +234,36 @@ frappe.ui.form.on("Leave Application", {
}
}
});
frappe.tour["Leave Application"] = [
{
fieldname: "employee",
title: "Employee",
description: __("Select the Employee.")
},
{
fieldname: "leave_type",
title: "Leave Type",
description: __("Select type of leave the employee wants to apply for, like Sick Leave, Privilege Leave, Casual Leave, etc.")
},
{
fieldname: "from_date",
title: "From Date",
description: __("Select the start date for your Leave Application.")
},
{
fieldname: "to_date",
title: "To Date",
description: __("Select the end date for your Leave Application.")
},
{
fieldname: "half_day",
title: "Half Day",
description: __("To apply for a Half Day check 'Half Day' and select the Half Day Date")
},
{
fieldname: "leave_approver",
title: "Leave Approver",
description: __("Select your Leave Approver i.e. the person who approves or rejects your leaves.")
}
];

View File

@@ -76,6 +76,7 @@ class LeaveApplication(Document):
# notify leave applier about approval
if frappe.db.get_single_value("HR Settings", "send_leave_notification"):
self.notify_employee()
self.create_leave_ledger_entry()
self.reload()
@@ -108,7 +109,13 @@ class LeaveApplication(Document):
if frappe.db.get_single_value("HR Settings", "restrict_backdated_leave_application"):
if self.from_date and getdate(self.from_date) < getdate():
allowed_role = frappe.db.get_single_value("HR Settings", "role_allowed_to_create_backdated_leave_application")
if allowed_role not in frappe.get_roles():
user = frappe.get_doc("User", frappe.session.user)
user_roles = [d.role for d in user.roles]
if not allowed_role:
frappe.throw(_("Backdated Leave Application is restricted. Please set the {} in {}").format(
frappe.bold("Role Allowed to Create Backdated Leave Application"), get_link_to_form("HR Settings", "HR Settings")))
if (allowed_role and allowed_role not in user_roles):
frappe.throw(_("Only users with the {0} role can create backdated leave applications").format(allowed_role))
if self.from_date and self.to_date and (getdate(self.to_date) < getdate(self.from_date)):

View File

@@ -121,6 +121,7 @@ class TestLeaveApplication(unittest.TestCase):
application = self.get_application(_test_records[0])
application.insert()
application.reload()
application.status = "Approved"
self.assertRaises(LeaveDayBlockedError, application.submit)

View File

@@ -2,3 +2,37 @@ frappe.ui.form.on("Leave Type", {
refresh: function(frm) {
}
});
frappe.tour["Leave Type"] = [
{
fieldname: "max_leaves_allowed",
title: "Maximum Leave Allocation Allowed",
description: __("This field allows you to set the maximum number of leaves that can be allocated annually for this Leave Type while creating the Leave Policy")
},
{
fieldname: "max_continuous_days_allowed",
title: "Maximum Consecutive Leaves Allowed",
description: __("This field allows you to set the maximum number of consecutive leaves an Employee can apply for.")
},
{
fieldname: "is_optional_leave",
title: "Is Optional Leave",
description: __("Optional Leaves are holidays that Employees can choose to avail from a list of holidays published by the company.")
},
{
fieldname: "is_compensatory",
title: "Is Compensatory Leave",
description: __("Leaves you can avail against a holiday you worked on. You can claim Compensatory Off Leave using Compensatory Leave request. Click") + " <a href='https://docs.erpnext.com/docs/v13/user/manual/en/human-resources/compensatory-leave-request' target='_blank'>here</a> " + __('to know more')
},
{
fieldname: "allow_encashment",
title: "Allow Encashment",
description: __("From here, you can enable encashment for the balance leaves.")
},
{
fieldname: "is_earned_leave",
title: "Is Earned Leaves",
description: __("Earned Leaves are leaves earned by an Employee after working with the company for a certain amount of time. Enabling this will allocate leaves on pro-rata basis by automatically updating Leave Allocation for leaves of this type at intervals set by 'Earned Leave Frequency.")
}
];

View File

@@ -50,7 +50,7 @@
{
"fieldname": "max_leaves_allowed",
"fieldtype": "Int",
"label": "Max Leaves Allowed"
"label": "Maximum Leave Allocation Allowed"
},
{
"fieldname": "applicable_after",
@@ -61,7 +61,7 @@
"fieldname": "max_continuous_days_allowed",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Maximum Continuous Days Applicable",
"label": "Maximum Consecutive Leaves Allowed",
"oldfieldname": "max_days_allowed",
"oldfieldtype": "Data"
},
@@ -87,6 +87,7 @@
},
{
"default": "0",
"description": "These leaves are holidays permitted by the company however, availing it is optional for an Employee.",
"fieldname": "is_optional_leave",
"fieldtype": "Check",
"label": "Is Optional Leave"
@@ -205,6 +206,7 @@
},
{
"depends_on": "eval:doc.is_ppl == 1",
"description": "For a day of leave taken, if you still pay (say) 50% of the daily salary, then enter 0.50 in this field.",
"fieldname": "fraction_of_daily_salary_per_leave",
"fieldtype": "Float",
"label": "Fraction of Daily Salary per Leave",
@@ -214,7 +216,7 @@
"icon": "fa fa-flag",
"idx": 1,
"links": [],
"modified": "2021-08-12 16:10:36.464690",
"modified": "2021-10-02 11:59:40.503359",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Type",

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

@@ -13,17 +13,14 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/human-resources",
"idx": 0,
"is_complete": 0,
"modified": "2020-07-08 14:05:47.018799",
"modified": "2021-05-19 05:32:01.794628",
"modified_by": "Administrator",
"module": "HR",
"name": "Human Resource",
"owner": "Administrator",
"steps": [
{
"step": "Create Department"
},
{
"step": "Create Designation"
"step": "HR Settings"
},
{
"step": "Create Holiday list"
@@ -31,6 +28,9 @@
{
"step": "Create Employee"
},
{
"step": "Data import"
},
{
"step": "Create Leave Type"
},
@@ -39,9 +39,6 @@
},
{
"step": "Create Leave Application"
},
{
"step": "HR Settings"
}
],
"subtitle": "Employee, Leaves, and more.",

View File

@@ -1,18 +1,20 @@
{
"action": "Create Entry",
"action": "Show Form Tour",
"action_label": "Show Tour",
"creation": "2020-05-14 11:43:25.561152",
"description": "<h3>Employee</h3>\n\nAn individual who works and is recognized for his rights and duties in your company is your Employee. You can manage the Employee master. It captures the demographic, personal and professional details, joining and leave details, etc.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 1,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-05-14 12:26:28.629074",
"modified": "2021-05-19 04:50:02.240321",
"modified_by": "Administrator",
"name": "Create Employee",
"owner": "Administrator",
"reference_document": "Employee",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Create Employee",
"validate_action": 0

View File

@@ -1,18 +1,20 @@
{
"action": "Create Entry",
"action": "Show Form Tour",
"action_label": "Show Tour",
"creation": "2020-05-28 11:47:34.700174",
"description": "<h3>Holiday List.</h3>\n\nHoliday List is a list which contains the dates of holidays. Most organizations have a standard Holiday List for their employees. However, some of them may have different holiday lists based on different Locations or Departments. In ERPNext, you can configure multiple Holiday Lists.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 1,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-05-14 12:25:38.068582",
"modified": "2021-05-19 04:19:52.305199",
"modified_by": "Administrator",
"name": "Create Holiday list",
"owner": "Administrator",
"reference_document": "Holiday List",
"show_form_tour": 0,
"show_full_form": 1,
"title": "Create Holiday List",
"validate_action": 0

View File

@@ -1,18 +1,20 @@
{
"action": "Create Entry",
"action": "Show Form Tour",
"action_label": "Show Tour",
"creation": "2020-05-14 11:48:56.123718",
"description": "<h3>Leave Allocation</h3>\n\nLeave Allocation enables you to allocate a specific number of leaves of a particular type to an Employee so that, an employee will be able to create a Leave Application only if Leaves are allocated. ",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 1,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-05-14 11:48:56.123718",
"modified": "2021-05-19 04:22:34.220238",
"modified_by": "Administrator",
"name": "Create Leave Allocation",
"owner": "Administrator",
"reference_document": "Leave Allocation",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Create Leave Allocation",
"validate_action": 0

View File

@@ -1,18 +1,20 @@
{
"action": "Create Entry",
"action": "Show Form Tour",
"action_label": "Show Tour",
"creation": "2020-05-14 11:49:45.400764",
"description": "<h3>Leave Application</h3>\n\nLeave Application is a formal document created by an Employee to apply for Leaves for a particular time period based on there leave allocation and leave type according to there need.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 1,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-05-14 11:49:45.400764",
"modified": "2021-05-19 04:39:09.893474",
"modified_by": "Administrator",
"name": "Create Leave Application",
"owner": "Administrator",
"reference_document": "Leave Application",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Create Leave Application",
"validate_action": 0

View File

@@ -1,18 +1,20 @@
{
"action": "Create Entry",
"action": "Show Form Tour",
"action_label": "Show Tour",
"creation": "2020-05-27 11:17:31.119312",
"description": "<h3>Leave Type</h3>\n\nLeave type is defined based on many factors and features like encashment, earned leaves, partially paid, without pay and, a lot more. To check other options and to define your leave type click on Show Tour.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 1,
"is_single": 0,
"is_skipped": 0,
"modified": "2020-05-20 11:17:31.119312",
"modified": "2021-05-19 04:32:48.135406",
"modified_by": "Administrator",
"name": "Create Leave Type",
"owner": "Administrator",
"reference_document": "Leave Type",
"show_form_tour": 0,
"show_full_form": 1,
"title": "Create Leave Type",
"validate_action": 0

View File

@@ -0,0 +1,21 @@
{
"action": "Watch Video",
"action_label": "",
"creation": "2021-05-19 05:29:16.809610",
"description": "<h3>Data Import</h3>\n\nData import is the tool to migrate your existing data like Employee, Customer, Supplier, and a lot more to our ERPNext system.\nGo through the video for a detailed explanation of this tool.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-05-19 05:29:16.809610",
"modified_by": "Administrator",
"name": "Data import",
"owner": "Administrator",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Data Import",
"validate_action": 1,
"video_url": "https://www.youtube.com/watch?v=DQyqeurPI64"
}

View File

@@ -1,18 +1,20 @@
{
"action": "Update Settings",
"action": "Show Form Tour",
"action_label": "Explore",
"creation": "2020-05-28 13:13:52.427711",
"description": "<h3>HR Settings</h3>\n\nHr Settings consists of major settings related to Employee Lifecycle, Leave Management, etc. Click on Explore, to explore Hr Settings.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_mandatory": 0,
"is_single": 1,
"is_skipped": 0,
"modified": "2020-05-20 11:16:42.430974",
"modified": "2021-05-18 07:02:05.747548",
"modified_by": "Administrator",
"name": "HR Settings",
"owner": "Administrator",
"reference_document": "HR Settings",
"show_form_tour": 0,
"show_full_form": 0,
"title": "HR Settings",
"validate_action": 0

View File

@@ -215,7 +215,32 @@ frappe.ui.form.on("BOM", {
label: __('Qty To Manufacture'),
fieldname: 'qty',
reqd: 1,
default: 1
default: 1,
onchange: () => {
const { quantity, items: rm } = frm.doc;
const variant_items_map = rm.reduce((acc, item) => {
acc[item.item_code] = item.qty;
return acc;
}, {});
const mf_qty = cur_dialog.fields_list.filter(
(f) => f.df.fieldname === "qty"
)[0]?.value;
const items = cur_dialog.fields.filter(
(f) => f.fieldname === "items"
)[0]?.data;
if (!items) {
return;
}
items.forEach((item) => {
item.qty =
(variant_items_map[item.item_code] * mf_qty) /
quantity;
});
cur_dialog.refresh();
}
});
}

View File

@@ -320,3 +320,4 @@ erpnext.patches.v13_0.modify_invalid_gain_loss_gl_entries
erpnext.patches.v13_0.fix_additional_cost_in_mfg_stock_entry
erpnext.patches.v13_0.shopping_cart_to_ecommerce
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

@@ -63,7 +63,7 @@ erpnext.HierarchyChart = class {
});
node.parent.append(node_card);
node.$link = $(`#${node.id}`);
node.$link = $(`[id="${node.id}"]`);
}
show() {
@@ -223,7 +223,7 @@ erpnext.HierarchyChart = class {
let node = undefined;
$.each(r.message, (_i, data) => {
if ($(`#${data.id}`).length)
if ($(`[id="${data.id}"]`).length)
return;
node = new me.Node({
@@ -263,7 +263,7 @@ erpnext.HierarchyChart = class {
this.refresh_connectors(node.parent_id);
// rebuild incoming connections
let grandparent = $(`#${node.parent_id}`).attr('data-parent');
let grandparent = $(`[id="${node.parent_id}"]`).attr('data-parent');
this.refresh_connectors(grandparent);
}
@@ -282,7 +282,7 @@ erpnext.HierarchyChart = class {
show_active_path(node) {
// mark node parent on active path
$(`#${node.parent_id}`).addClass('active-path');
$(`[id="${node.parent_id}"]`).addClass('active-path');
}
load_children(node, deep=false) {
@@ -317,7 +317,7 @@ erpnext.HierarchyChart = class {
render_child_nodes(node, child_nodes) {
const last_level = this.$hierarchy.find('.level:last').index();
const current_level = $(`#${node.id}`).parent().parent().parent().index();
const current_level = $(`[id="${node.id}"]`).parent().parent().parent().index();
if (last_level === current_level) {
this.$hierarchy.append(`
@@ -382,7 +382,7 @@ erpnext.HierarchyChart = class {
node.$children = $('<ul class="node-children"></ul>');
const last_level = this.$hierarchy.find('.level:last').index();
const node_level = $(`#${node.id}`).parent().parent().parent().index();
const node_level = $(`[id="${node.id}"]`).parent().parent().parent().index();
if (last_level === node_level) {
this.$hierarchy.append(`
@@ -489,7 +489,7 @@ erpnext.HierarchyChart = class {
set_path_attributes(path, parent_id, child_id) {
path.setAttribute("data-parent", parent_id);
path.setAttribute("data-child", child_id);
const parent = $(`#${parent_id}`);
const parent = $(`[id="${parent_id}"]`);
if (parent.hasClass('active')) {
path.setAttribute("class", "active-connector");
@@ -513,7 +513,7 @@ erpnext.HierarchyChart = class {
}
collapse_previous_level_nodes(node) {
let node_parent = $(`#${node.parent_id}`);
let node_parent = $(`[id="${node.parent_id}"]`);
let previous_level_nodes = node_parent.parent().parent().children('li');
let node_card = undefined;
@@ -545,7 +545,7 @@ erpnext.HierarchyChart = class {
setup_node_click_action(node) {
let me = this;
let node_element = $(`#${node.id}`);
let node_element = $(`[id="${node.id}"]`);
node_element.click(function() {
const is_sibling = me.selected_node.parent_id === node.parent_id;
@@ -563,7 +563,7 @@ erpnext.HierarchyChart = class {
}
setup_edit_node_action(node) {
let node_element = $(`#${node.id}`);
let node_element = $(`[id="${node.id}"]`);
let me = this;
node_element.find('.btn-edit-node').click(function() {
@@ -572,7 +572,7 @@ erpnext.HierarchyChart = class {
}
remove_levels_after_node(node) {
let level = $(`#${node.id}`).parent().parent().parent().index();
let level = $(`[id="${node.id}"]`).parent().parent().parent().index();
level = $('.hierarchy > li:eq('+ level + ')');
level.nextAll('li').remove();
@@ -595,7 +595,7 @@ erpnext.HierarchyChart = class {
const parent = $(path).data('parent');
const child = $(path).data('child');
if ($(`#${parent}`).length && $(`#${child}`).length)
if ($(`[id="${parent}"]`).length && $(`[id="${child}"]`).length)
return;
$(path).remove();

View File

@@ -54,7 +54,7 @@ erpnext.HierarchyChartMobile = class {
});
node.parent.append(node_card);
node.$link = $(`#${node.id}`);
node.$link = $(`[id="${node.id}"]`);
node.$link.addClass('mobile-node');
}
@@ -184,7 +184,7 @@ erpnext.HierarchyChartMobile = class {
this.refresh_connectors(node.parent_id, node.id);
// rebuild incoming connections of parent
let grandparent = $(`#${node.parent_id}`).attr('data-parent');
let grandparent = $(`[id="${node.parent_id}"]`).attr('data-parent');
this.refresh_connectors(grandparent, node.parent_id);
}
@@ -221,7 +221,7 @@ erpnext.HierarchyChartMobile = class {
show_active_path(node) {
// mark node parent on active path
$(`#${node.parent_id}`).addClass('active-path');
$(`[id="${node.parent_id}"]`).addClass('active-path');
}
load_children(node) {
@@ -256,7 +256,7 @@ erpnext.HierarchyChartMobile = class {
if (child_nodes) {
$.each(child_nodes, (_i, data) => {
this.add_node(node, data);
$(`#${data.id}`).addClass('active-child');
$(`[id="${data.id}"]`).addClass('active-child');
setTimeout(() => {
this.add_connector(node.id, data.id);
@@ -293,9 +293,9 @@ erpnext.HierarchyChartMobile = class {
let connector = undefined;
if ($(`#${parent_id}`).hasClass('active')) {
if ($(`[id="${parent_id}"]`).hasClass('active')) {
connector = this.get_connector_for_active_node(parent_node, child_node);
} else if ($(`#${parent_id}`).hasClass('active-path')) {
} else if ($(`[id="${parent_id}"]`).hasClass('active-path')) {
connector = this.get_connector_for_collapsed_node(parent_node, child_node);
}
@@ -351,7 +351,7 @@ erpnext.HierarchyChartMobile = class {
set_path_attributes(path, parent_id, child_id) {
path.setAttribute("data-parent", parent_id);
path.setAttribute("data-child", child_id);
const parent = $(`#${parent_id}`);
const parent = $(`[id="${parent_id}"]`);
if (parent.hasClass('active')) {
path.setAttribute("class", "active-connector");
@@ -374,7 +374,7 @@ erpnext.HierarchyChartMobile = class {
setup_node_click_action(node) {
let me = this;
let node_element = $(`#${node.id}`);
let node_element = $(`[id="${node.id}"]`);
node_element.click(function() {
let el = undefined;
@@ -398,7 +398,7 @@ erpnext.HierarchyChartMobile = class {
}
setup_edit_node_action(node) {
let node_element = $(`#${node.id}`);
let node_element = $(`[id="${node.id}"]`);
let me = this;
node_element.find('.btn-edit-node').click(function() {
@@ -512,7 +512,7 @@ erpnext.HierarchyChartMobile = class {
}
remove_levels_after_node(node) {
let level = $(`#${node.id}`).parent().parent().index();
let level = $(`[id="${node.id}"]`).parent().parent().index();
level = $('.hierarchy-mobile > li:eq('+ level + ')');
level.nextAll('li').remove();
@@ -533,7 +533,7 @@ erpnext.HierarchyChartMobile = class {
const parent = $(path).data('parent');
const child = $(path).data('child');
if ($(`#${parent}`).length && $(`#${child}`).length)
if ($(`[id="${parent}"]`).length && $(`[id="${child}"]`).length)
return;
$(path).remove();

View File

@@ -64,6 +64,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():

View File

@@ -7,7 +7,7 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, get_link_to_form, now, today
from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime, today
from frappe.utils.user import get_users_with_role
from rq.timeouts import JobTimeoutException
@@ -126,6 +126,9 @@ def notify_error_to_stock_managers(doc, traceback):
frappe.sendmail(recipients=recipients, subject=subject, message=message)
def repost_entries():
if not in_configured_timeslot():
return
riv_entries = get_repost_item_valuation_entries()
for row in riv_entries:
@@ -144,3 +147,26 @@ def get_repost_item_valuation_entries():
WHERE status in ('Queued', 'In Progress') and creation <= %s and docstatus = 1
ORDER BY timestamp(posting_date, posting_time) asc, creation asc
""", now(), as_dict=1)
def in_configured_timeslot(repost_settings=None, current_time=None):
"""Check if current time is in configured timeslot for reposting."""
if repost_settings is None:
repost_settings = frappe.get_cached_doc("Stock Reposting Settings")
if not repost_settings.limit_reposting_timeslot:
return True
if get_weekday() == repost_settings.limits_dont_apply_on:
return True
start_time = repost_settings.start_time
end_time = repost_settings.end_time
now_time = current_time or nowtime()
if start_time < end_time:
return end_time >= now_time >= start_time
else:
return now_time >= start_time or now_time <= end_time

View File

@@ -1,11 +1,72 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
import frappe
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import (
in_configured_timeslot,
)
class TestRepostItemValuation(unittest.TestCase):
pass
def test_repost_time_slot(self):
repost_settings = frappe.get_doc("Stock Reposting Settings")
positive_cases = [
{"limit_reposting_timeslot": 0},
{
"limit_reposting_timeslot": 1,
"start_time": "18:00:00",
"end_time": "09:00:00",
"current_time": "20:00:00",
},
{
"limit_reposting_timeslot": 1,
"start_time": "09:00:00",
"end_time": "18:00:00",
"current_time": "12:00:00",
},
{
"limit_reposting_timeslot": 1,
"start_time": "23:00:00",
"end_time": "09:00:00",
"current_time": "2:00:00",
},
]
for case in positive_cases:
repost_settings.update(case)
self.assertTrue(
in_configured_timeslot(repost_settings, case.get("current_time")),
msg=f"Exepcted true from : {case}",
)
negative_cases = [
{
"limit_reposting_timeslot": 1,
"start_time": "18:00:00",
"end_time": "09:00:00",
"current_time": "09:01:00",
},
{
"limit_reposting_timeslot": 1,
"start_time": "09:00:00",
"end_time": "18:00:00",
"current_time": "19:00:00",
},
{
"limit_reposting_timeslot": 1,
"start_time": "23:00:00",
"end_time": "09:00:00",
"current_time": "22:00:00",
},
]
for case in negative_cases:
repost_settings.update(case)
self.assertFalse(
in_configured_timeslot(repost_settings, case.get("current_time")),
msg=f"Exepcted false from : {case}",
)

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('Stock Reposting Settings', {
// refresh: function(frm) {
// }
});

View File

@@ -0,0 +1,72 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-10-01 10:56:30.814787",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"scheduling_section",
"limit_reposting_timeslot",
"start_time",
"end_time",
"limits_dont_apply_on"
],
"fields": [
{
"fieldname": "scheduling_section",
"fieldtype": "Section Break",
"label": "Scheduling"
},
{
"depends_on": "limit_reposting_timeslot",
"fieldname": "start_time",
"fieldtype": "Time",
"label": "Start Time",
"mandatory_depends_on": "limit_reposting_timeslot"
},
{
"depends_on": "limit_reposting_timeslot",
"fieldname": "end_time",
"fieldtype": "Time",
"label": "End Time",
"mandatory_depends_on": "limit_reposting_timeslot"
},
{
"depends_on": "limit_reposting_timeslot",
"fieldname": "limits_dont_apply_on",
"fieldtype": "Select",
"label": "Limits don't apply on",
"options": "\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday"
},
{
"default": "0",
"fieldname": "limit_reposting_timeslot",
"fieldtype": "Check",
"label": "Limit timeslot for Stock Reposting"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-10-01 11:27:28.981594",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reposting Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,28 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from frappe.model.document import Document
from frappe.utils import add_to_date, get_datetime, get_time_str, time_diff_in_hours
class StockRepostingSettings(Document):
def validate(self):
self.set_minimum_reposting_time_slot()
def set_minimum_reposting_time_slot(self):
"""Ensure that timeslot for reposting is at least 12 hours."""
if not self.limit_reposting_timeslot:
return
start_time = get_datetime(self.start_time)
end_time = get_datetime(self.end_time)
if start_time > end_time:
end_time = add_to_date(end_time, days=1, as_datetime=True)
diff = time_diff_in_hours(end_time, start_time)
if diff < 10:
self.end_time = get_time_str(add_to_date(self.start_time, hours=10, as_datetime=True))

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestStockRepostingSettings(unittest.TestCase):
pass

View File

@@ -7,6 +7,8 @@ def create_employee_records():
create_company()
create_missing_designation()
frappe.db.sql("DELETE FROM tabEmployee WHERE company='Test Org Chart'")
emp1 = create_employee('Test Employee 1', 'CEO')
emp2 = create_employee('Test Employee 2', 'CTO')
emp3 = create_employee('Test Employee 3', 'Head of Marketing and Sales', emp1)