Merge pull request #27665 from nextchamp-saqib/trim-custom-field-length
fix: trim sales invoice custom field lengths
diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js
index 39b00d3..79e08b3 100644
--- a/cypress/integration/test_organizational_chart_desktop.js
+++ b/cypress/integration/test_organizational_chart_desktop.js
@@ -6,7 +6,7 @@
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 => {
diff --git a/cypress/integration/test_organizational_chart_mobile.js b/cypress/integration/test_organizational_chart_mobile.js
index 6e75151..161fae0 100644
--- a/cypress/integration/test_organizational_chart_mobile.js
+++ b/cypress/integration/test_organizational_chart_mobile.js
@@ -7,7 +7,7 @@
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 => {
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index bcd0771..71957e6 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -374,12 +374,15 @@
try:
make_gl_entries(gl_entries, cancel=(doc.docstatus == 2), merge_entries=True)
frappe.db.commit()
- except Exception:
- frappe.db.rollback()
- traceback = frappe.get_traceback()
- frappe.log_error(message=traceback)
+ except Exception as e:
+ if frappe.flags.in_test:
+ raise e
+ else:
+ frappe.db.rollback()
+ traceback = frappe.get_traceback()
+ frappe.log_error(message=traceback)
- frappe.flags.deferred_accounting_error = True
+ frappe.flags.deferred_accounting_error = True
def send_mail(deferred_process):
title = _("Error while processing deferred accounting for {0}").format(deferred_process)
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index bdd30f3..8a2e945 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1800,6 +1800,47 @@
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
+ def test_deferred_revenue_post_account_freeze_upto_by_admin(self):
+ frappe.set_user("Administrator")
+
+ frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
+ frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
+
+ deferred_account = create_account(account_name="Deferred Revenue",
+ parent_account="Current Liabilities - _TC", company="_Test Company")
+
+ item = create_item("_Test Item for Deferred Accounting")
+ item.enable_deferred_revenue = 1
+ item.deferred_revenue_account = deferred_account
+ item.no_of_months = 12
+ item.save()
+
+ si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_save=True)
+ si.items[0].enable_deferred_revenue = 1
+ si.items[0].service_start_date = "2019-01-10"
+ si.items[0].service_end_date = "2019-03-15"
+ si.items[0].deferred_revenue_account = deferred_account
+ si.save()
+ si.submit()
+
+ frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
+ frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', 'System Manager')
+
+ pda1 = frappe.get_doc(dict(
+ doctype='Process Deferred Accounting',
+ posting_date=nowdate(),
+ start_date="2019-01-01",
+ end_date="2019-03-31",
+ type="Income",
+ company="_Test Company"
+ ))
+
+ pda1.insert()
+ self.assertRaises(frappe.ValidationError, pda1.submit)
+
+ frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
+ frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
+
def test_fixed_deferred_revenue(self):
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 4bf2b82..0cee6f5 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -284,13 +284,16 @@
"""
Nobody can do GL Entries where posting date is before freezing date
except authorized person
+
+ Administrator has all the roles so this check will be bypassed if any role is allowed to post
+ Hence stop admin to bypass if accounts are freezed
"""
if not adv_adj:
acc_frozen_upto = frappe.db.get_value('Accounts Settings', None, 'acc_frozen_upto')
if acc_frozen_upto:
frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,'frozen_accounts_modifier')
if getdate(posting_date) <= getdate(acc_frozen_upto) \
- and not frozen_accounts_modifier in frappe.get_roles():
+ and not frozen_accounts_modifier in frappe.get_roles() or frappe.session.user == 'Administrator':
frappe.throw(_("You are not authorized to add or update entries before {0}").format(formatdate(acc_frozen_upto)))
def set_as_cancel(voucher_type, voucher_no):
diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js
index 701da43..ca3be03 100644
--- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js
+++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.js
@@ -30,7 +30,14 @@
"default": frappe.datetime.get_today()
},
{
- "fieldname": "purchase_order",
+ "fieldname":"project",
+ "label": __("Project"),
+ "fieldtype": "Link",
+ "width": "80",
+ "options": "Project"
+ },
+ {
+ "fieldname": "name",
"label": __("Purchase Order"),
"fieldtype": "Link",
"width": "80",
diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py
index 5d59456..1b25dd4 100644
--- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py
+++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py
@@ -41,14 +41,12 @@
if filters.get("from_date") and filters.get("to_date"):
conditions += " and po.transaction_date between %(from_date)s and %(to_date)s"
- if filters.get("company"):
- conditions += " and po.company = %(company)s"
+ for field in ['company', 'name', 'status']:
+ if filters.get(field):
+ conditions += f" and po.{field} = %({field})s"
- if filters.get("purchase_order"):
- conditions += " and po.name = %(purchase_order)s"
-
- if filters.get("status"):
- conditions += " and po.status in %(status)s"
+ if filters.get('project'):
+ conditions += " and poi.project = %(project)s"
return conditions
@@ -57,6 +55,7 @@
SELECT
po.transaction_date as date,
poi.schedule_date as required_date,
+ poi.project,
po.name as purchase_order,
po.status, po.supplier, poi.item_code,
poi.qty, poi.received_qty,
@@ -175,6 +174,12 @@
"fieldtype": "Link",
"options": "Supplier",
"width": 130
+ },{
+ "label": _("Project"),
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "options": "Project",
+ "width": 130
}]
if not filters.get("group_by_po"):
diff --git a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py
index 099146c..9dd97a6 100644
--- a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py
+++ b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py
@@ -21,6 +21,7 @@
def setUp(self):
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
+ frappe.db.sql('delete from `tabPatient Appointment`')
make_pos_profile()
def test_medical_record(self):
diff --git a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
index 4f96f6a..021ba9b 100644
--- a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
+++ b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
@@ -6,7 +6,7 @@
import unittest
import frappe
-from frappe.utils import flt, getdate, nowdate
+from frappe.utils import add_days, flt, getdate, nowdate
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import (
create_appointment,
@@ -33,10 +33,12 @@
self.assertEqual(plan.status, 'Not Started')
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
+ session.start_date = getdate()
frappe.get_doc(session).submit()
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'In Progress')
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
+ session.start_date = add_days(getdate(), 1)
frappe.get_doc(session).submit()
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
@@ -44,6 +46,7 @@
appointment = create_appointment(patient, practitioner, nowdate())
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name)
+ session.start_date = add_days(getdate(), 2)
session = frappe.get_doc(session)
session.submit()
self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')
diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
index 6d63f39..b31a952 100644
--- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
+++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
@@ -6,7 +6,7 @@
import frappe
from frappe.model.document import Document
-from frappe.utils import flt, today
+from frappe.utils import flt
class TherapyPlan(Document):
@@ -63,8 +63,6 @@
therapy_session.exercises = therapy_type.exercises
therapy_session.appointment = appointment
- if frappe.flags.in_test:
- therapy_session.start_date = today()
return therapy_session.as_dict()
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index a8f1617..b89b10b 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -344,6 +344,7 @@
"all": [
"erpnext.projects.doctype.project.project.project_status_update_reminder",
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder",
+ "erpnext.hr.doctype.interview.interview.send_interview_reminder",
"erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts"
],
"hourly": [
@@ -388,6 +389,7 @@
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
"erpnext.non_profit.doctype.membership.membership.set_expired_status"
+ "erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder"
],
"daily_long": [
"erpnext.setup.doctype.email_digest.email_digest.send",
diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js
index 9a3bac0..6b3c29a 100644
--- a/erpnext/hr/doctype/attendance/attendance_list.js
+++ b/erpnext/hr/doctype/attendance/attendance_list.js
@@ -9,83 +9,86 @@
return [__(doc.status), "orange", "status,=," + doc.status];
}
},
+
onload: function(list_view) {
let me = this;
- const months = moment.months()
- list_view.page.add_inner_button( __("Mark Attendance"), function() {
+ const months = moment.months();
+ list_view.page.add_inner_button(__("Mark Attendance"), function() {
let dialog = new frappe.ui.Dialog({
title: __("Mark Attendance"),
- fields: [
- {
- fieldname: 'employee',
- label: __('For Employee'),
- fieldtype: 'Link',
- options: 'Employee',
- get_query: () => {
- return {query: "erpnext.controllers.queries.employee_query"}
- },
- reqd: 1,
- onchange: function() {
- dialog.set_df_property("unmarked_days", "hidden", 1);
- dialog.set_df_property("status", "hidden", 1);
- dialog.set_df_property("month", "value", '');
+ fields: [{
+ fieldname: 'employee',
+ label: __('For Employee'),
+ fieldtype: 'Link',
+ options: 'Employee',
+ get_query: () => {
+ return {query: "erpnext.controllers.queries.employee_query"};
+ },
+ reqd: 1,
+ onchange: function() {
+ dialog.set_df_property("unmarked_days", "hidden", 1);
+ dialog.set_df_property("status", "hidden", 1);
+ dialog.set_df_property("month", "value", '');
+ dialog.set_df_property("unmarked_days", "options", []);
+ dialog.no_unmarked_days_left = false;
+ }
+ },
+ {
+ label: __("For Month"),
+ fieldtype: "Select",
+ fieldname: "month",
+ options: months,
+ reqd: 1,
+ onchange: function() {
+ if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
+ dialog.set_df_property("status", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", []);
dialog.no_unmarked_days_left = false;
+ me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options => {
+ if (options.length > 0) {
+ dialog.set_df_property("unmarked_days", "hidden", 0);
+ dialog.set_df_property("unmarked_days", "options", options);
+ } else {
+ dialog.no_unmarked_days_left = true;
+ }
+ });
}
- },
- {
- label: __("For Month"),
- fieldtype: "Select",
- fieldname: "month",
- options: months,
- reqd: 1,
- onchange: function() {
- if(dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
- dialog.set_df_property("status", "hidden", 0);
- dialog.set_df_property("unmarked_days", "options", []);
- dialog.no_unmarked_days_left = false;
- me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options =>{
- if (options.length > 0) {
- dialog.set_df_property("unmarked_days", "hidden", 0);
- dialog.set_df_property("unmarked_days", "options", options);
- } else {
- dialog.no_unmarked_days_left = true;
- }
- });
- }
- }
- },
- {
- label: __("Status"),
- fieldtype: "Select",
- fieldname: "status",
- options: ["Present", "Absent", "Half Day", "Work From Home"],
- hidden:1,
- reqd: 1,
+ }
+ },
+ {
+ label: __("Status"),
+ fieldtype: "Select",
+ fieldname: "status",
+ options: ["Present", "Absent", "Half Day", "Work From Home"],
+ hidden: 1,
+ reqd: 1,
- },
- {
- label: __("Unmarked Attendance for days"),
- fieldname: "unmarked_days",
- fieldtype: "MultiCheck",
- options: [],
- columns: 2,
- hidden: 1
- },
- ],
- primary_action(data) {
+ },
+ {
+ label: __("Unmarked Attendance for days"),
+ fieldname: "unmarked_days",
+ fieldtype: "MultiCheck",
+ options: [],
+ columns: 2,
+ hidden: 1
+ }],
+ primary_action(data) {
if (cur_dialog.no_unmarked_days_left) {
- frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",[dialog.fields_dict.month.value, dialog.fields_dict.employee.value]));
+ frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",
+ [dialog.fields_dict.month.value, dialog.fields_dict.employee.value]));
} else {
- frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status,data.month]), () => {
+ frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status, data.month]), () => {
frappe.call({
method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
args: {
data: data
},
- callback: function(r) {
+ callback: function (r) {
if (r.message === 1) {
- frappe.show_alert({message: __("Attendance Marked"), indicator: 'blue'});
+ frappe.show_alert({
+ message: __("Attendance Marked"),
+ indicator: 'blue'
+ });
cur_dialog.hide();
}
}
@@ -101,21 +104,26 @@
dialog.show();
});
},
- get_multi_select_options: function(employee, month){
+
+ get_multi_select_options: function(employee, month) {
return new Promise(resolve => {
frappe.call({
method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days',
async: false,
- args:{
+ args: {
employee: employee,
month: month,
}
}).then(r => {
var options = [];
- for(var d in r.message){
+ for (var d in r.message) {
var momentObj = moment(r.message[d], 'YYYY-MM-DD');
var date = momentObj.format('DD-MM-YYYY');
- options.push({ "label":date, "value": r.message[d] , "checked": 1});
+ options.push({
+ "label": date,
+ "value": r.message[d],
+ "checked": 1
+ });
}
resolve(options);
});
diff --git a/erpnext/hr/doctype/employee/employee.js b/erpnext/hr/doctype/employee/employee.js
index 5639cc9..13b33e2 100755
--- a/erpnext/hr/doctype/employee/employee.js
+++ b/erpnext/hr/doctype/employee/employee.js
@@ -15,19 +15,20 @@
}
refresh() {
- var me = this;
erpnext.toggle_naming_series();
}
date_of_birth() {
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() {
- if(this.frm.doc.salutation) {
+ if (this.frm.doc.salutation) {
this.frm.set_value("gender", {
"Mr": "Male",
"Ms": "Female"
@@ -36,8 +37,9 @@
}
};
-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 +48,7 @@
};
});
},
- onload:function(frm) {
+ onload: function (frm) {
frm.set_query("department", function() {
return {
"filters": {
@@ -55,23 +57,28 @@
};
});
},
- 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 +88,63 @@
}
});
},
+
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")
+ },
+];
diff --git a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py
index 7c751a4..1a1bcb2 100644
--- a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py
+++ b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py
@@ -55,8 +55,7 @@
else:
leave_type = None
- if not company:
- company = frappe.db.get_value("Employee", employee['employee'], "Company")
+ company = frappe.db.get_value("Employee", employee['employee'], "Company", cache=True)
attendance=frappe.get_doc(dict(
doctype='Attendance',
@@ -68,4 +67,4 @@
company=company
))
attendance.insert()
- attendance.submit()
+ attendance.submit()
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
index eae600d..1e3b9cb 100644
--- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
+++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
@@ -71,6 +71,7 @@
applicant = frappe.new_doc('Job Applicant')
applicant.applicant_name = 'Test Researcher'
applicant.email_id = 'test@researcher.com'
+ applicant.designation = 'Researcher'
applicant.status = 'Open'
applicant.cover_letter = 'I am a great Researcher.'
applicant.insert()
diff --git a/erpnext/hr/doctype/employee_referral/employee_referral.py b/erpnext/hr/doctype/employee_referral/employee_referral.py
index 5cb5bb5..db356bf 100644
--- a/erpnext/hr/doctype/employee_referral/employee_referral.py
+++ b/erpnext/hr/doctype/employee_referral/employee_referral.py
@@ -38,8 +38,10 @@
status = "Open"
job_applicant = frappe.new_doc("Job Applicant")
+ job_applicant.source = "Employee Referral"
job_applicant.employee_referral = emp_ref.name
job_applicant.status = status
+ job_applicant.designation = emp_ref.for_designation
job_applicant.applicant_name = emp_ref.full_name
job_applicant.email_id = emp_ref.email
job_applicant.phone_number = emp_ref.contact_no
diff --git a/erpnext/hr/doctype/employee_referral/test_employee_referral.py b/erpnext/hr/doctype/employee_referral/test_employee_referral.py
index d0ee2fc..1340f62 100644
--- a/erpnext/hr/doctype/employee_referral/test_employee_referral.py
+++ b/erpnext/hr/doctype/employee_referral/test_employee_referral.py
@@ -17,6 +17,11 @@
class TestEmployeeReferral(unittest.TestCase):
+
+ def setUp(self):
+ frappe.db.sql("DELETE FROM `tabJob Applicant`")
+ frappe.db.sql("DELETE FROM `tabEmployee Referral`")
+
def test_workflow_and_status_sync(self):
emp_ref = create_employee_referral()
@@ -50,6 +55,10 @@
add_sal = create_additional_salary(emp_ref)
self.assertTrue(add_sal.ref_docname, emp_ref.name)
+ def tearDown(self):
+ frappe.db.sql("DELETE FROM `tabJob Applicant`")
+ frappe.db.sql("DELETE FROM `tabEmployee Referral`")
+
def create_employee_referral():
emp_ref = frappe.new_doc("Employee Referral")
diff --git a/erpnext/hr/doctype/expected_skill_set/__init__.py b/erpnext/hr/doctype/expected_skill_set/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/expected_skill_set/__init__.py
diff --git a/erpnext/hr/doctype/expected_skill_set/expected_skill_set.json b/erpnext/hr/doctype/expected_skill_set/expected_skill_set.json
new file mode 100644
index 0000000..899f5bd
--- /dev/null
+++ b/erpnext/hr/doctype/expected_skill_set/expected_skill_set.json
@@ -0,0 +1,40 @@
+{
+ "actions": [],
+ "creation": "2021-04-12 13:05:06.741330",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "skill",
+ "description"
+ ],
+ "fields": [
+ {
+ "fieldname": "skill",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Skill",
+ "options": "Skill",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "skill.description",
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Description"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-04-12 14:26:33.062549",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Expected Skill Set",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/expected_skill_set/expected_skill_set.py b/erpnext/hr/doctype/expected_skill_set/expected_skill_set.py
new file mode 100644
index 0000000..27120c1
--- /dev/null
+++ b/erpnext/hr/doctype/expected_skill_set/expected_skill_set.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+# import frappe
+from frappe.model.document import Document
+
+
+class ExpectedSkillSet(Document):
+ pass
diff --git a/erpnext/hr/doctype/holiday_list/holiday_list.js b/erpnext/hr/doctype/holiday_list/holiday_list.js
index 462bd8b..ea033c7 100644
--- a/erpnext/hr/doctype/holiday_list/holiday_list.js
+++ b/erpnext/hr/doctype/holiday_list/holiday_list.js
@@ -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.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.")
+ },
+];
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.js b/erpnext/hr/doctype/hr_settings/hr_settings.js
index ec99472..6e26a1f 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.js
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.js
@@ -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')
+ }
+];
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json
index 8aa3c0c..5148435 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.json
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.json
@@ -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"
},
{
@@ -63,28 +68,6 @@
"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"
},
@@ -101,34 +84,17 @@
"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"
},
{
@@ -138,12 +104,41 @@
"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",
"fieldtype": "Section Break",
"label": "Reminders"
@@ -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",
diff --git a/erpnext/hr/doctype/interview/__init__.py b/erpnext/hr/doctype/interview/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/interview/__init__.py
diff --git a/erpnext/hr/doctype/interview/interview.js b/erpnext/hr/doctype/interview/interview.js
new file mode 100644
index 0000000..6341e3a
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview.js
@@ -0,0 +1,237 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Interview', {
+ onload: function (frm) {
+ frm.events.set_job_applicant_query(frm);
+
+ frm.set_query('interviewer', 'interview_details', function () {
+ return {
+ query: 'erpnext.hr.doctype.interview.interview.get_interviewer_list'
+ };
+ });
+ },
+
+ refresh: function (frm) {
+ if (frm.doc.docstatus != 2 && !frm.doc.__islocal) {
+ if (frm.doc.status === 'Pending') {
+ frm.add_custom_button(__('Reschedule Interview'), function() {
+ frm.events.show_reschedule_dialog(frm);
+ frm.refresh();
+ });
+ }
+
+ let allowed_interviewers = [];
+ frm.doc.interview_details.forEach(values => {
+ allowed_interviewers.push(values.interviewer);
+ });
+
+ if ((allowed_interviewers.includes(frappe.session.user))) {
+ frappe.db.get_value('Interview Feedback', {'interviewer': frappe.session.user, 'interview': frm.doc.name, 'docstatus': 1}, 'name', (r) => {
+ if (Object.keys(r).length === 0) {
+ frm.add_custom_button(__('Submit Feedback'), function () {
+ frappe.call({
+ method: 'erpnext.hr.doctype.interview.interview.get_expected_skill_set',
+ args: {
+ interview_round: frm.doc.interview_round
+ },
+ callback: function (r) {
+ frm.events.show_feedback_dialog(frm, r.message);
+ frm.refresh();
+ }
+ });
+ }).addClass('btn-primary');
+ }
+ });
+ }
+ }
+ },
+
+ show_reschedule_dialog: function (frm) {
+ let d = new frappe.ui.Dialog({
+ title: 'Reschedule Interview',
+ fields: [
+ {
+ label: 'Schedule On',
+ fieldname: 'scheduled_on',
+ fieldtype: 'Date',
+ reqd: 1
+ },
+ {
+ label: 'From Time',
+ fieldname: 'from_time',
+ fieldtype: 'Time',
+ reqd: 1
+ },
+ {
+ label: 'To Time',
+ fieldname: 'to_time',
+ fieldtype: 'Time',
+ reqd: 1
+ }
+ ],
+ primary_action_label: 'Reschedule',
+ primary_action(values) {
+ frm.call({
+ method: 'reschedule_interview',
+ doc: frm.doc,
+ args: {
+ scheduled_on: values.scheduled_on,
+ from_time: values.from_time,
+ to_time: values.to_time
+ }
+ }).then(() => {
+ frm.refresh();
+ d.hide();
+ });
+ }
+ });
+ d.show();
+ },
+
+ show_feedback_dialog: function (frm, data) {
+ let fields = frm.events.get_fields_for_feedback();
+
+ let d = new frappe.ui.Dialog({
+ title: __('Submit Feedback'),
+ fields: [
+ {
+ fieldname: 'skill_set',
+ fieldtype: 'Table',
+ label: __('Skill Assessment'),
+ cannot_add_rows: false,
+ in_editable_grid: true,
+ reqd: 1,
+ fields: fields,
+ data: data
+ },
+ {
+ fieldname: 'result',
+ fieldtype: 'Select',
+ options: ['', 'Cleared', 'Rejected'],
+ label: __('Result')
+ },
+ {
+ fieldname: 'feedback',
+ fieldtype: 'Small Text',
+ label: __('Feedback')
+ }
+ ],
+ size: 'large',
+ minimizable: true,
+ primary_action: function(values) {
+ frappe.call({
+ method: 'erpnext.hr.doctype.interview.interview.create_interview_feedback',
+ args: {
+ data: values,
+ interview_name: frm.doc.name,
+ interviewer: frappe.session.user,
+ job_applicant: frm.doc.job_applicant
+ }
+ }).then(() => {
+ frm.refresh();
+ });
+ d.hide();
+ }
+ });
+ d.show();
+ },
+
+ get_fields_for_feedback: function () {
+ return [{
+ fieldtype: 'Link',
+ fieldname: 'skill',
+ options: 'Skill',
+ in_list_view: 1,
+ label: __('Skill')
+ }, {
+ fieldtype: 'Rating',
+ fieldname: 'rating',
+ label: __('Rating'),
+ in_list_view: 1,
+ reqd: 1,
+ }];
+ },
+
+ set_job_applicant_query: function (frm) {
+ frm.set_query('job_applicant', function () {
+ let job_applicant_filters = {
+ status: ['!=', 'Rejected']
+ };
+ if (frm.doc.designation) {
+ job_applicant_filters.designation = frm.doc.designation;
+ }
+ return {
+ filters: job_applicant_filters
+ };
+ });
+ },
+
+ interview_round: async function (frm) {
+ frm.events.reset_values(frm);
+ frm.set_value('job_applicant', '');
+
+ let round_data = (await frappe.db.get_value('Interview Round', frm.doc.interview_round, 'designation')).message;
+ frm.set_value('designation', round_data.designation);
+ frm.events.set_job_applicant_query(frm);
+
+ if (frm.doc.interview_round) {
+ frm.events.set_interview_details(frm);
+ } else {
+ frm.set_value('interview_details', []);
+ }
+ },
+
+ set_interview_details: function (frm) {
+ frappe.call({
+ method: 'erpnext.hr.doctype.interview.interview.get_interviewers',
+ args: {
+ interview_round: frm.doc.interview_round
+ },
+ callback: function (data) {
+ let interview_details = data.message;
+ frm.set_value('interview_details', []);
+ if (data.message.length) {
+ frm.set_value('interview_details', interview_details);
+ }
+ }
+ });
+ },
+
+ job_applicant: function (frm) {
+ if (!frm.doc.interview_round) {
+ frm.doc.job_applicant = '';
+ frm.refresh();
+ frappe.throw(__('Select Interview Round First'));
+ }
+
+ if (frm.doc.job_applicant) {
+ frm.events.set_designation_and_job_opening(frm);
+ } else {
+ frm.events.reset_values(frm);
+ }
+ },
+
+ set_designation_and_job_opening: async function (frm) {
+ let round_data = (await frappe.db.get_value('Interview Round', frm.doc.interview_round, 'designation')).message;
+ frm.set_value('designation', round_data.designation);
+ frm.events.set_job_applicant_query(frm);
+
+ let job_applicant_data = (await frappe.db.get_value(
+ 'Job Applicant', frm.doc.job_applicant, ['designation', 'job_title', 'resume_link'],
+ )).message;
+
+ if (!round_data.designation) {
+ frm.set_value('designation', job_applicant_data.designation);
+ }
+
+ frm.set_value('job_opening', job_applicant_data.job_title);
+ frm.set_value('resume_link', job_applicant_data.resume_link);
+ },
+
+ reset_values: function (frm) {
+ frm.set_value('designation', '');
+ frm.set_value('job_opening', '');
+ frm.set_value('resume_link', '');
+ }
+});
diff --git a/erpnext/hr/doctype/interview/interview.json b/erpnext/hr/doctype/interview/interview.json
new file mode 100644
index 0000000..0d393e7
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview.json
@@ -0,0 +1,254 @@
+{
+ "actions": [],
+ "autoname": "HR-INT-.YYYY.-.####",
+ "creation": "2021-04-12 15:03:11.524090",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "interview_details_section",
+ "interview_round",
+ "job_applicant",
+ "job_opening",
+ "designation",
+ "resume_link",
+ "column_break_4",
+ "status",
+ "scheduled_on",
+ "from_time",
+ "to_time",
+ "interview_feedback_section",
+ "interview_details",
+ "ratings_section",
+ "expected_average_rating",
+ "column_break_12",
+ "average_rating",
+ "section_break_13",
+ "interview_summary",
+ "reminded",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "job_applicant",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Job Applicant",
+ "options": "Job Applicant",
+ "reqd": 1
+ },
+ {
+ "fieldname": "job_opening",
+ "fieldtype": "Link",
+ "label": "Job Opening",
+ "options": "Job Opening",
+ "read_only": 1
+ },
+ {
+ "fieldname": "interview_round",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Interview Round",
+ "options": "Interview Round",
+ "reqd": 1
+ },
+ {
+ "default": "Pending",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Status",
+ "options": "Pending\nUnder Review\nCleared\nRejected",
+ "reqd": 1
+ },
+ {
+ "fieldname": "ratings_section",
+ "fieldtype": "Section Break",
+ "label": "Ratings"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "average_rating",
+ "fieldtype": "Rating",
+ "in_list_view": 1,
+ "label": "Obtained Average Rating",
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "interview_summary",
+ "fieldtype": "Text"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "resume_link",
+ "fieldtype": "Data",
+ "label": "Resume link"
+ },
+ {
+ "fieldname": "interview_details_section",
+ "fieldtype": "Section Break",
+ "label": "Details"
+ },
+ {
+ "fetch_from": "interview_round.expected_average_rating",
+ "fieldname": "expected_average_rating",
+ "fieldtype": "Rating",
+ "label": "Expected Average Rating",
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_break_13",
+ "fieldtype": "Section Break",
+ "label": "Interview Summary"
+ },
+ {
+ "fieldname": "column_break_12",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "interview_round.designation",
+ "fieldname": "designation",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Designation",
+ "options": "Designation",
+ "read_only": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Interview",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "scheduled_on",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Scheduled On",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "reminded",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Reminded"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "interview_details",
+ "fieldtype": "Table",
+ "options": "Interview Detail"
+ },
+ {
+ "fieldname": "interview_feedback_section",
+ "fieldtype": "Section Break",
+ "label": "Feedback"
+ },
+ {
+ "fieldname": "from_time",
+ "fieldtype": "Time",
+ "in_list_view": 1,
+ "label": "From Time",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "to_time",
+ "fieldtype": "Time",
+ "in_list_view": 1,
+ "label": "To Time",
+ "reqd": 1,
+ "set_only_once": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [
+ {
+ "link_doctype": "Interview Feedback",
+ "link_fieldname": "interview"
+ }
+ ],
+ "modified": "2021-09-30 13:30:05.421035",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Interview",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Interviewer",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "job_applicant",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interview/interview.py b/erpnext/hr/doctype/interview/interview.py
new file mode 100644
index 0000000..955acca
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview.py
@@ -0,0 +1,293 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+import datetime
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import cstr, get_datetime, get_link_to_form
+
+
+class DuplicateInterviewRoundError(frappe.ValidationError):
+ pass
+
+class Interview(Document):
+ def validate(self):
+ self.validate_duplicate_interview()
+ self.validate_designation()
+ self.validate_overlap()
+
+ def on_submit(self):
+ if self.status not in ['Cleared', 'Rejected']:
+ frappe.throw(_('Only Interviews with Cleared or Rejected status can be submitted.'), title=_('Not Allowed'))
+
+ def validate_duplicate_interview(self):
+ duplicate_interview = frappe.db.exists('Interview', {
+ 'job_applicant': self.job_applicant,
+ 'interview_round': self.interview_round,
+ 'docstatus': 1
+ }
+ )
+
+ if duplicate_interview:
+ frappe.throw(_('Job Applicants are not allowed to appear twice for the same Interview round. Interview {0} already scheduled for Job Applicant {1}').format(
+ frappe.bold(get_link_to_form('Interview', duplicate_interview)),
+ frappe.bold(self.job_applicant)
+ ))
+
+ def validate_designation(self):
+ applicant_designation = frappe.db.get_value('Job Applicant', self.job_applicant, 'designation')
+ if self.designation :
+ if self.designation != applicant_designation:
+ frappe.throw(_('Interview Round {0} is only for Designation {1}. Job Applicant has applied for the role {2}').format(
+ self.interview_round, frappe.bold(self.designation), applicant_designation),
+ exc=DuplicateInterviewRoundError)
+ else:
+ self.designation = applicant_designation
+
+ def validate_overlap(self):
+ interviewers = [entry.interviewer for entry in self.interview_details] or ['']
+
+ overlaps = frappe.db.sql("""
+ SELECT interview.name
+ FROM `tabInterview` as interview
+ INNER JOIN `tabInterview Detail` as detail
+ WHERE
+ interview.scheduled_on = %s and interview.name != %s and interview.docstatus != 2
+ and (interview.job_applicant = %s or detail.interviewer IN %s) and
+ ((from_time < %s and to_time > %s) or
+ (from_time > %s and to_time < %s) or
+ (from_time = %s))
+ """, (self.scheduled_on, self.name, self.job_applicant, interviewers,
+ self.from_time, self.to_time, self.from_time, self.to_time, self.from_time))
+
+ if overlaps:
+ overlapping_details = _('Interview overlaps with {0}').format(get_link_to_form('Interview', overlaps[0][0]))
+ frappe.throw(overlapping_details, title=_('Overlap'))
+
+
+ @frappe.whitelist()
+ def reschedule_interview(self, scheduled_on, from_time, to_time):
+ original_date = self.scheduled_on
+ from_time = self.from_time
+ to_time = self.to_time
+
+ self.db_set({
+ 'scheduled_on': scheduled_on,
+ 'from_time': from_time,
+ 'to_time': to_time
+ })
+ self.notify_update()
+
+ recipients = get_recipients(self.name)
+
+ try:
+ frappe.sendmail(
+ recipients= recipients,
+ subject=_('Interview: {0} Rescheduled').format(self.name),
+ message=_('Your Interview session is rescheduled from {0} {1} - {2} to {3} {4} - {5}').format(
+ original_date, from_time, to_time, self.scheduled_on, self.from_time, self.to_time),
+ reference_doctype=self.doctype,
+ reference_name=self.name
+ )
+ except Exception:
+ frappe.msgprint(_('Failed to send the Interview Reschedule notification. Please configure your email account.'))
+
+ frappe.msgprint(_('Interview Rescheduled successfully'), indicator='green')
+
+
+def get_recipients(name, for_feedback=0):
+ interview = frappe.get_doc('Interview', name)
+
+ if for_feedback:
+ recipients = [d.interviewer for d in interview.interview_details if not d.interview_feedback]
+ else:
+ recipients = [d.interviewer for d in interview.interview_details]
+ recipients.append(frappe.db.get_value('Job Applicant', interview.job_applicant, 'email_id'))
+
+ return recipients
+
+
+@frappe.whitelist()
+def get_interviewers(interview_round):
+ return frappe.get_all('Interviewer', filters={'parent': interview_round}, fields=['user as interviewer'])
+
+
+def send_interview_reminder():
+ reminder_settings = frappe.db.get_value('HR Settings', 'HR Settings',
+ ['send_interview_reminder', 'interview_reminder_template'], as_dict=True)
+
+ if not reminder_settings.send_interview_reminder:
+ return
+
+ remind_before = cstr(frappe.db.get_single_value('HR Settings', 'remind_before')) or '01:00:00'
+ remind_before = datetime.datetime.strptime(remind_before, '%H:%M:%S')
+ reminder_date_time = datetime.datetime.now() + datetime.timedelta(
+ hours=remind_before.hour, minutes=remind_before.minute, seconds=remind_before.second)
+
+ interviews = frappe.get_all('Interview', filters={
+ 'scheduled_on': ['between', (datetime.datetime.now(), reminder_date_time)],
+ 'status': 'Pending',
+ 'reminded': 0,
+ 'docstatus': ['!=', 2]
+ })
+
+ interview_template = frappe.get_doc('Email Template', reminder_settings.interview_reminder_template)
+
+ for d in interviews:
+ doc = frappe.get_doc('Interview', d.name)
+ context = doc.as_dict()
+ message = frappe.render_template(interview_template.response, context)
+ recipients = get_recipients(doc.name)
+
+ frappe.sendmail(
+ recipients= recipients,
+ subject=interview_template.subject,
+ message=message,
+ reference_doctype=doc.doctype,
+ reference_name=doc.name
+ )
+
+ doc.db_set('reminded', 1)
+
+
+def send_daily_feedback_reminder():
+ reminder_settings = frappe.db.get_value('HR Settings', 'HR Settings',
+ ['send_interview_feedback_reminder', 'feedback_reminder_notification_template'], as_dict=True)
+
+ if not reminder_settings.send_interview_feedback_reminder:
+ return
+
+ interview_feedback_template = frappe.get_doc('Email Template', reminder_settings.feedback_reminder_notification_template)
+ interviews = frappe.get_all('Interview', filters={'status': ['in', ['Under Review', 'Pending']], 'docstatus': ['!=', 2]})
+
+ for entry in interviews:
+ recipients = get_recipients(entry.name, for_feedback=1)
+
+ doc = frappe.get_doc('Interview', entry.name)
+ context = doc.as_dict()
+
+ message = frappe.render_template(interview_feedback_template.response, context)
+
+ if len(recipients):
+ frappe.sendmail(
+ recipients= recipients,
+ subject=interview_feedback_template.subject,
+ message=message,
+ reference_doctype='Interview',
+ reference_name=entry.name
+ )
+
+
+@frappe.whitelist()
+def get_expected_skill_set(interview_round):
+ return frappe.get_all('Expected Skill Set', filters ={'parent': interview_round}, fields=['skill'])
+
+
+@frappe.whitelist()
+def create_interview_feedback(data, interview_name, interviewer, job_applicant):
+ import json
+
+ from six import string_types
+
+ if isinstance(data, string_types):
+ data = frappe._dict(json.loads(data))
+
+ if frappe.session.user != interviewer:
+ frappe.throw(_('Only Interviewer Are allowed to submit Interview Feedback'))
+
+ interview_feedback = frappe.new_doc('Interview Feedback')
+ interview_feedback.interview = interview_name
+ interview_feedback.interviewer = interviewer
+ interview_feedback.job_applicant = job_applicant
+
+ for d in data.skill_set:
+ d = frappe._dict(d)
+ interview_feedback.append('skill_assessment', {'skill': d.skill, 'rating': d.rating})
+
+ interview_feedback.feedback = data.feedback
+ interview_feedback.result = data.result
+
+ interview_feedback.save()
+ interview_feedback.submit()
+
+ frappe.msgprint(_('Interview Feedback {0} submitted successfully').format(
+ get_link_to_form('Interview Feedback', interview_feedback.name)))
+
+
+@frappe.whitelist()
+@frappe.validate_and_sanitize_search_inputs
+def get_interviewer_list(doctype, txt, searchfield, start, page_len, filters):
+ filters = [
+ ['Has Role', 'parent', 'like', '%{}%'.format(txt)],
+ ['Has Role', 'role', '=', 'interviewer'],
+ ['Has Role', 'parenttype', '=', 'User']
+ ]
+
+ if filters and isinstance(filters, list):
+ filters.extend(filters)
+
+ return frappe.get_all('Has Role', limit_start=start, limit_page_length=page_len,
+ filters=filters, fields = ['parent'], as_list=1)
+
+
+@frappe.whitelist()
+def get_events(start, end, filters=None):
+ """Returns events for Gantt / Calendar view rendering.
+
+ :param start: Start date-time.
+ :param end: End date-time.
+ :param filters: Filters (JSON).
+ """
+ from frappe.desk.calendar import get_event_conditions
+
+ events = []
+
+ event_color = {
+ "Pending": "#fff4f0",
+ "Under Review": "#d3e8fc",
+ "Cleared": "#eaf5ed",
+ "Rejected": "#fce7e7"
+ }
+
+ conditions = get_event_conditions('Interview', filters)
+
+ interviews = frappe.db.sql("""
+ SELECT DISTINCT
+ `tabInterview`.name, `tabInterview`.job_applicant, `tabInterview`.interview_round,
+ `tabInterview`.scheduled_on, `tabInterview`.status, `tabInterview`.from_time as from_time,
+ `tabInterview`.to_time as to_time
+ from
+ `tabInterview`
+ where
+ (`tabInterview`.scheduled_on between %(start)s and %(end)s)
+ and docstatus != 2
+ {conditions}
+ """.format(conditions=conditions), {
+ "start": start,
+ "end": end
+ }, as_dict=True, update={"allDay": 0})
+
+ for d in interviews:
+ subject_data = []
+ for field in ["name", "job_applicant", "interview_round"]:
+ if not d.get(field):
+ continue
+ subject_data.append(d.get(field))
+
+ color = event_color.get(d.status)
+ interview_data = {
+ 'from': get_datetime('%s %s' % (d.scheduled_on, d.from_time or '00:00:00')),
+ 'to': get_datetime('%s %s' % (d.scheduled_on, d.to_time or '00:00:00')),
+ 'name': d.name,
+ 'subject': '\n'.join(subject_data),
+ 'color': color if color else "#89bcde"
+ }
+
+ events.append(interview_data)
+
+ return events
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interview/interview_calendar.js b/erpnext/hr/doctype/interview/interview_calendar.js
new file mode 100644
index 0000000..b46b72e
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview_calendar.js
@@ -0,0 +1,14 @@
+
+frappe.views.calendar['Interview'] = {
+ field_map: {
+ 'start': 'from',
+ 'end': 'to',
+ 'id': 'name',
+ 'title': 'subject',
+ 'allDay': 'allDay',
+ 'color': 'color'
+ },
+ order_by: 'scheduled_on',
+ gantt: true,
+ get_events_method: 'erpnext.hr.doctype.interview.interview.get_events'
+};
diff --git a/erpnext/hr/doctype/interview/interview_feedback_reminder_template.html b/erpnext/hr/doctype/interview/interview_feedback_reminder_template.html
new file mode 100644
index 0000000..8d39fb5
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview_feedback_reminder_template.html
@@ -0,0 +1,5 @@
+<h1>Interview Feedback Reminder</h1>
+
+<p>
+ Interview Feedback for Interview {{ name }} is not submitted yet. Please submit your feedback. Thank you, good day!
+</p>
diff --git a/erpnext/hr/doctype/interview/interview_list.js b/erpnext/hr/doctype/interview/interview_list.js
new file mode 100644
index 0000000..b1f072f
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview_list.js
@@ -0,0 +1,12 @@
+frappe.listview_settings['Interview'] = {
+ has_indicator_for_draft: 1,
+ get_indicator: function(doc) {
+ let status_color = {
+ 'Pending': 'orange',
+ 'Under Review': 'blue',
+ 'Cleared': 'green',
+ 'Rejected': 'red',
+ };
+ return [__(doc.status), status_color[doc.status], 'status,=,'+doc.status];
+ }
+};
diff --git a/erpnext/hr/doctype/interview/interview_reminder_notification_template.html b/erpnext/hr/doctype/interview/interview_reminder_notification_template.html
new file mode 100644
index 0000000..76de46e
--- /dev/null
+++ b/erpnext/hr/doctype/interview/interview_reminder_notification_template.html
@@ -0,0 +1,5 @@
+<h1>Interview Reminder</h1>
+
+<p>
+ Interview: {{name}} is scheduled on {{scheduled_on}} from {{from_time}} to {{to_time}}
+</p>
diff --git a/erpnext/hr/doctype/interview/test_interview.py b/erpnext/hr/doctype/interview/test_interview.py
new file mode 100644
index 0000000..4612e17
--- /dev/null
+++ b/erpnext/hr/doctype/interview/test_interview.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import datetime
+import os
+import unittest
+
+import frappe
+from frappe import _
+from frappe.core.doctype.user_permission.test_user_permission import create_user
+from frappe.utils import add_days, getdate, nowtime
+
+from erpnext.hr.doctype.designation.test_designation import create_designation
+from erpnext.hr.doctype.interview.interview import DuplicateInterviewRoundError
+from erpnext.hr.doctype.job_applicant.test_job_applicant import create_job_applicant
+
+
+class TestInterview(unittest.TestCase):
+ def test_validations_for_designation(self):
+ job_applicant = create_job_applicant()
+ interview = create_interview_and_dependencies(job_applicant.name, designation='_Test_Sales_manager', save=0)
+ self.assertRaises(DuplicateInterviewRoundError, interview.save)
+
+ def test_notification_on_rescheduling(self):
+ job_applicant = create_job_applicant()
+ interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -4))
+
+ previous_scheduled_date = interview.scheduled_on
+ frappe.db.sql("DELETE FROM `tabEmail Queue`")
+
+ interview.reschedule_interview(add_days(getdate(previous_scheduled_date), 2),
+ from_time=nowtime(), to_time=nowtime())
+ interview.reload()
+
+ self.assertEqual(interview.scheduled_on, add_days(getdate(previous_scheduled_date), 2))
+
+ notification = frappe.get_all("Email Queue", filters={"message": ("like", "%Your Interview session is rescheduled from%")})
+ self.assertIsNotNone(notification)
+
+ def test_notification_for_scheduling(self):
+ from erpnext.hr.doctype.interview.interview import send_interview_reminder
+
+ setup_reminder_settings()
+
+ job_applicant = create_job_applicant()
+ scheduled_on = datetime.datetime.now() + datetime.timedelta(minutes=10)
+
+ interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=scheduled_on)
+
+ frappe.db.sql("DELETE FROM `tabEmail Queue`")
+ send_interview_reminder()
+
+ interview.reload()
+
+ email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
+ self.assertTrue("Subject: Interview Reminder" in email_queue[0].message)
+
+ def test_notification_for_feedback_submission(self):
+ from erpnext.hr.doctype.interview.interview import send_daily_feedback_reminder
+
+ setup_reminder_settings()
+
+ job_applicant = create_job_applicant()
+ scheduled_on = add_days(getdate(), -4)
+ create_interview_and_dependencies(job_applicant.name, scheduled_on=scheduled_on)
+
+ frappe.db.sql("DELETE FROM `tabEmail Queue`")
+ send_daily_feedback_reminder()
+
+ email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
+ self.assertTrue("Subject: Interview Feedback Reminder" in email_queue[0].message)
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+
+def create_interview_and_dependencies(job_applicant, scheduled_on=None, from_time=None, to_time=None, designation=None, save=1):
+ if designation:
+ designation=create_designation(designation_name = "_Test_Sales_manager").name
+
+ interviewer_1 = create_user("test_interviewer1@example.com", "Interviewer")
+ interviewer_2 = create_user("test_interviewer2@example.com", "Interviewer")
+
+ interview_round = create_interview_round(
+ "Technical Round", ["Python", "JS"],
+ designation=designation, save=True
+ )
+
+ interview = frappe.new_doc("Interview")
+ interview.interview_round = interview_round.name
+ interview.job_applicant = job_applicant
+ interview.scheduled_on = scheduled_on or getdate()
+ interview.from_time = from_time or nowtime()
+ interview.to_time = to_time or nowtime()
+
+ interview.append("interview_details", {"interviewer": interviewer_1.name})
+ interview.append("interview_details", {"interviewer": interviewer_2.name})
+
+ if save:
+ interview.save()
+
+ return interview
+
+def create_interview_round(name, skill_set, interviewers=[], designation=None, save=True):
+ create_skill_set(skill_set)
+ interview_round = frappe.new_doc("Interview Round")
+ interview_round.round_name = name
+ interview_round.interview_type = create_interview_type()
+ interview_round.expected_average_rating = 4
+ if designation:
+ interview_round.designation = designation
+
+ for skill in skill_set:
+ interview_round.append("expected_skill_set", {"skill": skill})
+
+ for interviewer in interviewers:
+ interview_round.append("interviewer", {
+ "user": interviewer
+ })
+
+ if save:
+ interview_round.save()
+
+ return interview_round
+
+def create_skill_set(skill_set):
+ for skill in skill_set:
+ if not frappe.db.exists("Skill", skill):
+ doc = frappe.new_doc("Skill")
+ doc.skill_name = skill
+ doc.save()
+
+def create_interview_type(name="test_interview_type"):
+ if frappe.db.exists("Interview Type", name):
+ return frappe.get_doc("Interview Type", name).name
+ else:
+ doc = frappe.new_doc("Interview Type")
+ doc.name = name
+ doc.description = "_Test_Description"
+ doc.save()
+
+ return doc.name
+
+def setup_reminder_settings():
+ if not frappe.db.exists('Email Template', _('Interview Reminder')):
+ base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
+ response = frappe.read_file(os.path.join(base_path, 'interview/interview_reminder_notification_template.html'))
+
+ frappe.get_doc({
+ 'doctype': 'Email Template',
+ 'name': _('Interview Reminder'),
+ 'response': response,
+ 'subject': _('Interview Reminder'),
+ 'owner': frappe.session.user,
+ }).insert(ignore_permissions=True)
+
+ if not frappe.db.exists('Email Template', _('Interview Feedback Reminder')):
+ base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
+ response = frappe.read_file(os.path.join(base_path, 'interview/interview_feedback_reminder_template.html'))
+
+ frappe.get_doc({
+ 'doctype': 'Email Template',
+ 'name': _('Interview Feedback Reminder'),
+ 'response': response,
+ 'subject': _('Interview Feedback Reminder'),
+ 'owner': frappe.session.user,
+ }).insert(ignore_permissions=True)
+
+ hr_settings = frappe.get_doc('HR Settings')
+ hr_settings.interview_reminder_template = _('Interview Reminder')
+ hr_settings.feedback_reminder_notification_template = _('Interview Feedback Reminder')
+ hr_settings.save()
diff --git a/erpnext/hr/doctype/interview_detail/__init__.py b/erpnext/hr/doctype/interview_detail/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/interview_detail/__init__.py
diff --git a/erpnext/hr/doctype/interview_detail/interview_detail.js b/erpnext/hr/doctype/interview_detail/interview_detail.js
new file mode 100644
index 0000000..88518ca
--- /dev/null
+++ b/erpnext/hr/doctype/interview_detail/interview_detail.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Interview Detail', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/hr/doctype/interview_detail/interview_detail.json b/erpnext/hr/doctype/interview_detail/interview_detail.json
new file mode 100644
index 0000000..b5b49c0
--- /dev/null
+++ b/erpnext/hr/doctype/interview_detail/interview_detail.json
@@ -0,0 +1,74 @@
+{
+ "actions": [],
+ "creation": "2021-04-12 16:24:10.382863",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "interviewer",
+ "interview_feedback",
+ "average_rating",
+ "result",
+ "column_break_4",
+ "comments"
+ ],
+ "fields": [
+ {
+ "fieldname": "interviewer",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Interviewer",
+ "options": "User"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "interview_feedback",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Interview Feedback",
+ "options": "Interview Feedback",
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "average_rating",
+ "fieldtype": "Rating",
+ "in_list_view": 1,
+ "label": "Average Rating",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "allow_on_submit": 1,
+ "fetch_from": "interview_feedback.feedback",
+ "fieldname": "comments",
+ "fieldtype": "Text",
+ "label": "Comments",
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "result",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Result",
+ "options": "\nCleared\nRejected",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-09-29 13:13:25.865063",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Interview Detail",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interview_detail/interview_detail.py b/erpnext/hr/doctype/interview_detail/interview_detail.py
new file mode 100644
index 0000000..8be3d34
--- /dev/null
+++ b/erpnext/hr/doctype/interview_detail/interview_detail.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+# import frappe
+from frappe.model.document import Document
+
+
+class InterviewDetail(Document):
+ pass
diff --git a/erpnext/hr/doctype/interview_detail/test_interview_detail.py b/erpnext/hr/doctype/interview_detail/test_interview_detail.py
new file mode 100644
index 0000000..a29dfff
--- /dev/null
+++ b/erpnext/hr/doctype/interview_detail/test_interview_detail.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+
+class TestInterviewDetail(unittest.TestCase):
+ pass
diff --git a/erpnext/hr/doctype/interview_feedback/__init__.py b/erpnext/hr/doctype/interview_feedback/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/interview_feedback/__init__.py
diff --git a/erpnext/hr/doctype/interview_feedback/interview_feedback.js b/erpnext/hr/doctype/interview_feedback/interview_feedback.js
new file mode 100644
index 0000000..dec559f
--- /dev/null
+++ b/erpnext/hr/doctype/interview_feedback/interview_feedback.js
@@ -0,0 +1,54 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Interview Feedback', {
+ onload: function(frm) {
+ frm.ignore_doctypes_on_cancel_all = ['Interview'];
+
+ frm.set_query('interview', function() {
+ return {
+ filters: {
+ docstatus: ['!=', 2]
+ }
+ };
+ });
+ },
+
+ interview_round: function(frm) {
+ frappe.call({
+ method: 'erpnext.hr.doctype.interview.interview.get_expected_skill_set',
+ args: {
+ interview_round: frm.doc.interview_round
+ },
+ callback: function(r) {
+ frm.set_value('skill_assessment', r.message);
+ }
+ });
+ },
+
+ interview: function(frm) {
+ frappe.call({
+ method: 'erpnext.hr.doctype.interview_feedback.interview_feedback.get_applicable_interviewers',
+ args: {
+ interview: frm.doc.interview || ''
+ },
+ callback: function(r) {
+ frm.set_query('interviewer', function() {
+ return {
+ filters: {
+ name: ['in', r.message]
+ }
+ };
+ });
+ }
+ });
+
+ },
+
+ interviewer: function(frm) {
+ if (!frm.doc.interview) {
+ frappe.throw(__('Select Interview first'));
+ frm.set_value('interviewer', '');
+ }
+ }
+});
diff --git a/erpnext/hr/doctype/interview_feedback/interview_feedback.json b/erpnext/hr/doctype/interview_feedback/interview_feedback.json
new file mode 100644
index 0000000..6a2f7e8
--- /dev/null
+++ b/erpnext/hr/doctype/interview_feedback/interview_feedback.json
@@ -0,0 +1,171 @@
+{
+ "actions": [],
+ "autoname": "HR-INT-FEED-.####",
+ "creation": "2021-04-12 17:03:13.833285",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "details_section",
+ "interview",
+ "interview_round",
+ "job_applicant",
+ "column_break_3",
+ "interviewer",
+ "result",
+ "section_break_4",
+ "skill_assessment",
+ "average_rating",
+ "section_break_7",
+ "feedback",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "interview",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Interview",
+ "options": "Interview",
+ "reqd": 1
+ },
+ {
+ "allow_in_quick_entry": 1,
+ "fetch_from": "interview.interview_round",
+ "fieldname": "interview_round",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Interview Round",
+ "options": "Interview Round",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "interviewer",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Interviewer",
+ "options": "User",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break",
+ "label": "Skill Assessment"
+ },
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "skill_assessment",
+ "fieldtype": "Table",
+ "options": "Skill Assessment",
+ "reqd": 1
+ },
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "average_rating",
+ "fieldtype": "Rating",
+ "in_list_view": 1,
+ "label": "Average Rating",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_7",
+ "fieldtype": "Section Break",
+ "label": "Feedback"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Interview Feedback",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "feedback",
+ "fieldtype": "Text"
+ },
+ {
+ "fieldname": "result",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Result",
+ "options": "\nCleared\nRejected",
+ "reqd": 1
+ },
+ {
+ "fieldname": "details_section",
+ "fieldtype": "Section Break",
+ "label": "Details"
+ },
+ {
+ "fetch_from": "interview.job_applicant",
+ "fieldname": "job_applicant",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Job Applicant",
+ "options": "Job Applicant",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2021-09-30 13:30:49.955352",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Interview Feedback",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Interviewer",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "interviewer",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interview_feedback/interview_feedback.py b/erpnext/hr/doctype/interview_feedback/interview_feedback.py
new file mode 100644
index 0000000..1c5a494
--- /dev/null
+++ b/erpnext/hr/doctype/interview_feedback/interview_feedback.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import flt, get_link_to_form, getdate
+
+
+class InterviewFeedback(Document):
+ def validate(self):
+ self.validate_interviewer()
+ self.validate_interview_date()
+ self.validate_duplicate()
+ self.calculate_average_rating()
+
+ def on_submit(self):
+ self.update_interview_details()
+
+ def on_cancel(self):
+ self.update_interview_details()
+
+ def validate_interviewer(self):
+ applicable_interviewers = get_applicable_interviewers(self.interview)
+ if self.interviewer not in applicable_interviewers:
+ frappe.throw(_('{0} is not allowed to submit Interview Feedback for the Interview: {1}').format(
+ frappe.bold(self.interviewer), frappe.bold(self.interview)))
+
+ def validate_interview_date(self):
+ scheduled_date = frappe.db.get_value('Interview', self.interview, 'scheduled_on')
+
+ if getdate() < getdate(scheduled_date) and self.docstatus == 1:
+ frappe.throw(_('{0} submission before {1} is not allowed').format(
+ frappe.bold('Interview Feedback'),
+ frappe.bold('Interview Scheduled Date')
+ ))
+
+ def validate_duplicate(self):
+ duplicate_feedback = frappe.db.exists('Interview Feedback', {
+ 'interviewer': self.interviewer,
+ 'interview': self.interview,
+ 'docstatus': 1
+ })
+
+ if duplicate_feedback:
+ frappe.throw(_('Feedback already submitted for the Interview {0}. Please cancel the previous Interview Feedback {1} to continue.').format(
+ self.interview, get_link_to_form('Interview Feedback', duplicate_feedback)))
+
+ def calculate_average_rating(self):
+ total_rating = 0
+ for d in self.skill_assessment:
+ if d.rating:
+ total_rating += d.rating
+
+ self.average_rating = flt(total_rating / len(self.skill_assessment) if len(self.skill_assessment) else 0)
+
+ def update_interview_details(self):
+ doc = frappe.get_doc('Interview', self.interview)
+ total_rating = 0
+
+ if self.docstatus == 2:
+ for entry in doc.interview_details:
+ if entry.interview_feedback == self.name:
+ entry.average_rating = entry.interview_feedback = entry.comments = entry.result = None
+ break
+ else:
+ for entry in doc.interview_details:
+ if entry.interviewer == self.interviewer:
+ entry.average_rating = self.average_rating
+ entry.interview_feedback = self.name
+ entry.comments = self.feedback
+ entry.result = self.result
+
+ if entry.average_rating:
+ total_rating += entry.average_rating
+
+ doc.average_rating = flt(total_rating / len(doc.interview_details) if len(doc.interview_details) else 0)
+ doc.save()
+ doc.notify_update()
+
+
+@frappe.whitelist()
+def get_applicable_interviewers(interview):
+ data = frappe.get_all('Interview Detail', filters={'parent': interview}, fields=['interviewer'])
+ return [d.interviewer for d in data]
diff --git a/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
new file mode 100644
index 0000000..c4b7981
--- /dev/null
+++ b/erpnext/hr/doctype/interview_feedback/test_interview_feedback.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import unittest
+
+import frappe
+from frappe.utils import add_days, flt, getdate
+
+from erpnext.hr.doctype.interview.test_interview import (
+ create_interview_and_dependencies,
+ create_skill_set,
+)
+from erpnext.hr.doctype.job_applicant.test_job_applicant import create_job_applicant
+
+
+class TestInterviewFeedback(unittest.TestCase):
+ def test_validation_for_skill_set(self):
+ frappe.set_user("Administrator")
+ job_applicant = create_job_applicant()
+ interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -1))
+ skill_ratings = get_skills_rating(interview.interview_round)
+
+ interviewer = interview.interview_details[0].interviewer
+ create_skill_set(['Leadership'])
+
+ interview_feedback = create_interview_feedback(interview.name, interviewer, skill_ratings)
+ interview_feedback.append("skill_assessment", {"skill": 'Leadership', 'rating': 4})
+ frappe.set_user(interviewer)
+
+ self.assertRaises(frappe.ValidationError, interview_feedback.save)
+
+ frappe.set_user("Administrator")
+
+ def test_average_ratings_on_feedback_submission_and_cancellation(self):
+ job_applicant = create_job_applicant()
+ interview = create_interview_and_dependencies(job_applicant.name, scheduled_on=add_days(getdate(), -1))
+ skill_ratings = get_skills_rating(interview.interview_round)
+
+ # For First Interviewer Feedback
+ interviewer = interview.interview_details[0].interviewer
+ frappe.set_user(interviewer)
+
+ # calculating Average
+ feedback_1 = create_interview_feedback(interview.name, interviewer, skill_ratings)
+
+ total_rating = 0
+ for d in feedback_1.skill_assessment:
+ if d.rating:
+ total_rating += d.rating
+
+ avg_rating = flt(total_rating / len(feedback_1.skill_assessment) if len(feedback_1.skill_assessment) else 0)
+
+ self.assertEqual(flt(avg_rating, 3), feedback_1.average_rating)
+
+ avg_on_interview_detail = frappe.db.get_value('Interview Detail', {
+ 'parent': feedback_1.interview,
+ 'interviewer': feedback_1.interviewer,
+ 'interview_feedback': feedback_1.name
+ }, 'average_rating')
+
+ # 1. average should be reflected in Interview Detail.
+ self.assertEqual(avg_on_interview_detail, round(feedback_1.average_rating))
+
+ '''For Second Interviewer Feedback'''
+ interviewer = interview.interview_details[1].interviewer
+ frappe.set_user(interviewer)
+
+ feedback_2 = create_interview_feedback(interview.name, interviewer, skill_ratings)
+ interview.reload()
+
+ feedback_2.cancel()
+ interview.reload()
+
+ frappe.set_user("Administrator")
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+
+def create_interview_feedback(interview, interviewer, skills_ratings):
+ interview_feedback = frappe.new_doc("Interview Feedback")
+ interview_feedback.interview = interview
+ interview_feedback.interviewer = interviewer
+ interview_feedback.result = "Cleared"
+
+ for rating in skills_ratings:
+ interview_feedback.append("skill_assessment", rating)
+
+ interview_feedback.save()
+ interview_feedback.submit()
+
+ return interview_feedback
+
+
+def get_skills_rating(interview_round):
+ import random
+
+ skills = frappe.get_all("Expected Skill Set", filters={"parent": interview_round}, fields = ["skill"])
+ for d in skills:
+ d["rating"] = random.randint(1, 5)
+ return skills
diff --git a/erpnext/hr/doctype/interview_round/__init__.py b/erpnext/hr/doctype/interview_round/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/interview_round/__init__.py
diff --git a/erpnext/hr/doctype/interview_round/interview_round.js b/erpnext/hr/doctype/interview_round/interview_round.js
new file mode 100644
index 0000000..6a608b0
--- /dev/null
+++ b/erpnext/hr/doctype/interview_round/interview_round.js
@@ -0,0 +1,24 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Interview Round", {
+ refresh: function(frm) {
+ if (!frm.doc.__islocal) {
+ frm.add_custom_button(__("Create Interview"), function() {
+ frm.events.create_interview(frm);
+ });
+ }
+ },
+ create_interview: function(frm) {
+ frappe.call({
+ method: "erpnext.hr.doctype.interview_round.interview_round.create_interview",
+ args: {
+ doc: frm.doc
+ },
+ callback: function (r) {
+ var doclist = frappe.model.sync(r.message);
+ frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
+ }
+ });
+ }
+});
diff --git a/erpnext/hr/doctype/interview_round/interview_round.json b/erpnext/hr/doctype/interview_round/interview_round.json
new file mode 100644
index 0000000..9c95185
--- /dev/null
+++ b/erpnext/hr/doctype/interview_round/interview_round.json
@@ -0,0 +1,118 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:round_name",
+ "creation": "2021-04-12 12:57:19.902866",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "round_name",
+ "interview_type",
+ "interviewers",
+ "column_break_3",
+ "designation",
+ "expected_average_rating",
+ "expected_skills_section",
+ "expected_skill_set"
+ ],
+ "fields": [
+ {
+ "fieldname": "round_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Round Name",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "designation",
+ "fieldtype": "Link",
+ "label": "Designation",
+ "options": "Designation"
+ },
+ {
+ "fieldname": "expected_skills_section",
+ "fieldtype": "Section Break",
+ "label": "Expected Skillset"
+ },
+ {
+ "fieldname": "expected_skill_set",
+ "fieldtype": "Table",
+ "options": "Expected Skill Set",
+ "reqd": 1
+ },
+ {
+ "fieldname": "expected_average_rating",
+ "fieldtype": "Rating",
+ "label": "Expected Average Rating",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "interview_type",
+ "fieldtype": "Link",
+ "label": "Interview Type",
+ "options": "Interview Type",
+ "reqd": 1
+ },
+ {
+ "fieldname": "interviewers",
+ "fieldtype": "Table MultiSelect",
+ "label": "Interviewers",
+ "options": "Interviewer"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-09-30 13:01:25.666660",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Interview Round",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Interviewer",
+ "select": 1,
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interview_round/interview_round.py b/erpnext/hr/doctype/interview_round/interview_round.py
new file mode 100644
index 0000000..8230c78
--- /dev/null
+++ b/erpnext/hr/doctype/interview_round/interview_round.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+import json
+
+import frappe
+from frappe.model.document import Document
+
+
+class InterviewRound(Document):
+ pass
+
+@frappe.whitelist()
+def create_interview(doc):
+ if isinstance(doc, str):
+ doc = json.loads(doc)
+ doc = frappe.get_doc(doc)
+
+ interview = frappe.new_doc("Interview")
+ interview.interview_round = doc.name
+ interview.designation = doc.designation
+
+ if doc.interviewers:
+ interview.interview_details = []
+ for data in doc.interviewers:
+ interview.append("interview_details", {
+ "interviewer": data.user
+ })
+ return interview
+
+
+
diff --git a/erpnext/hr/doctype/interview_round/test_interview_round.py b/erpnext/hr/doctype/interview_round/test_interview_round.py
new file mode 100644
index 0000000..932d3de
--- /dev/null
+++ b/erpnext/hr/doctype/interview_round/test_interview_round.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import unittest
+
+# import frappe
+
+
+class TestInterviewRound(unittest.TestCase):
+ pass
+
diff --git a/erpnext/hr/doctype/interview_type/__init__.py b/erpnext/hr/doctype/interview_type/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/interview_type/__init__.py
diff --git a/erpnext/hr/doctype/interview_type/interview_type.js b/erpnext/hr/doctype/interview_type/interview_type.js
new file mode 100644
index 0000000..af77b52
--- /dev/null
+++ b/erpnext/hr/doctype/interview_type/interview_type.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Interview Type', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/hr/doctype/interview_type/interview_type.json b/erpnext/hr/doctype/interview_type/interview_type.json
new file mode 100644
index 0000000..14636a1
--- /dev/null
+++ b/erpnext/hr/doctype/interview_type/interview_type.json
@@ -0,0 +1,73 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "Prompt",
+ "creation": "2021-04-12 14:44:40.664034",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "description"
+ ],
+ "fields": [
+ {
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "in_list_view": 1,
+ "label": "Description"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [
+ {
+ "link_doctype": "Interview Round",
+ "link_fieldname": "interview_type"
+ }
+ ],
+ "modified": "2021-09-30 13:00:16.471518",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Interview Type",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interview_type/interview_type.py b/erpnext/hr/doctype/interview_type/interview_type.py
new file mode 100644
index 0000000..ee5be54
--- /dev/null
+++ b/erpnext/hr/doctype/interview_type/interview_type.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+# import frappe
+from frappe.model.document import Document
+
+
+class InterviewType(Document):
+ pass
diff --git a/erpnext/hr/doctype/interview_type/test_interview_type.py b/erpnext/hr/doctype/interview_type/test_interview_type.py
new file mode 100644
index 0000000..a5d3cf9
--- /dev/null
+++ b/erpnext/hr/doctype/interview_type/test_interview_type.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+
+class TestInterviewType(unittest.TestCase):
+ pass
diff --git a/erpnext/hr/doctype/interviewer/__init__.py b/erpnext/hr/doctype/interviewer/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/interviewer/__init__.py
diff --git a/erpnext/hr/doctype/interviewer/interviewer.json b/erpnext/hr/doctype/interviewer/interviewer.json
new file mode 100644
index 0000000..a37b8b0
--- /dev/null
+++ b/erpnext/hr/doctype/interviewer/interviewer.json
@@ -0,0 +1,31 @@
+{
+ "actions": [],
+ "creation": "2021-04-12 17:38:19.354734",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "user"
+ ],
+ "fields": [
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "User",
+ "options": "User"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-04-13 13:41:35.817568",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Interviewer",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/interviewer/interviewer.py b/erpnext/hr/doctype/interviewer/interviewer.py
new file mode 100644
index 0000000..1c8dbbe
--- /dev/null
+++ b/erpnext/hr/doctype/interviewer/interviewer.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+# import frappe
+from frappe.model.document import Document
+
+
+class Interviewer(Document):
+ pass
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.js b/erpnext/hr/doctype/job_applicant/job_applicant.js
index 7658bc9..d7b1c6c 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant.js
+++ b/erpnext/hr/doctype/job_applicant/job_applicant.js
@@ -8,6 +8,24 @@
frappe.ui.form.on("Job Applicant", {
refresh: function(frm) {
+ frm.set_query("job_title", function() {
+ return {
+ filters: {
+ 'status': 'Open'
+ }
+ };
+ });
+ frm.events.create_custom_buttons(frm);
+ frm.events.make_dashboard(frm);
+ },
+
+ create_custom_buttons: function(frm) {
+ if (!frm.doc.__islocal && frm.doc.status !== "Rejected" && frm.doc.status !== "Accepted") {
+ frm.add_custom_button(__("Create Interview"), function() {
+ frm.events.create_dialog(frm);
+ });
+ }
+
if (!frm.doc.__islocal) {
if (frm.doc.__onload && frm.doc.__onload.job_offer) {
$('[data-doctype="Employee Onboarding"]').find("button").show();
@@ -28,14 +46,57 @@
});
}
}
+ },
- frm.set_query("job_title", function() {
- return {
- filters: {
- 'status': 'Open'
- }
- };
+ make_dashboard: function(frm) {
+ frappe.call({
+ method: "erpnext.hr.doctype.job_applicant.job_applicant.get_interview_details",
+ args: {
+ job_applicant: frm.doc.name
+ },
+ callback: function(r) {
+ $("div").remove(".form-dashboard-section.custom");
+ frm.dashboard.add_section(
+ frappe.render_template('job_applicant_dashboard', {
+ data: r.message
+ }),
+ __("Interview Summary")
+ );
+ }
});
+ },
+ create_dialog: function(frm) {
+ let d = new frappe.ui.Dialog({
+ title: 'Enter Interview Round',
+ fields: [
+ {
+ label: 'Interview Round',
+ fieldname: 'interview_round',
+ fieldtype: 'Link',
+ options: 'Interview Round'
+ },
+ ],
+ primary_action_label: 'Create Interview',
+ primary_action(values) {
+ frm.events.create_interview(frm, values);
+ d.hide();
+ }
+ });
+ d.show();
+ },
+
+ create_interview: function (frm, values) {
+ frappe.call({
+ method: "erpnext.hr.doctype.job_applicant.job_applicant.create_interview",
+ args: {
+ doc: frm.doc,
+ interview_round: values.interview_round
+ },
+ callback: function (r) {
+ var doclist = frappe.model.sync(r.message);
+ frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
+ }
+ });
}
});
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.json b/erpnext/hr/doctype/job_applicant/job_applicant.json
index bcea5f5..200f675 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant.json
+++ b/erpnext/hr/doctype/job_applicant/job_applicant.json
@@ -9,16 +9,20 @@
"email_append_to": 1,
"engine": "InnoDB",
"field_order": [
+ "details_section",
"applicant_name",
"email_id",
"phone_number",
"country",
- "status",
"column_break_3",
"job_title",
+ "designation",
+ "status",
+ "source_and_rating_section",
"source",
"source_name",
"employee_referral",
+ "column_break_13",
"applicant_rating",
"section_break_6",
"notes",
@@ -84,7 +88,8 @@
},
{
"fieldname": "section_break_6",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Resume"
},
{
"fieldname": "cover_letter",
@@ -160,13 +165,34 @@
"label": "Employee Referral",
"options": "Employee Referral",
"read_only": 1
+ },
+ {
+ "fieldname": "details_section",
+ "fieldtype": "Section Break",
+ "label": "Details"
+ },
+ {
+ "fieldname": "source_and_rating_section",
+ "fieldtype": "Section Break",
+ "label": "Source and Rating"
+ },
+ {
+ "fieldname": "column_break_13",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "job_opening.designation",
+ "fieldname": "designation",
+ "fieldtype": "Link",
+ "label": "Designation",
+ "options": "Designation"
}
],
"icon": "fa fa-user",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-03-24 15:51:11.117517",
+ "modified": "2021-09-29 23:06:10.904260",
"modified_by": "Administrator",
"module": "HR",
"name": "Job Applicant",
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.py b/erpnext/hr/doctype/job_applicant/job_applicant.py
index 6971e5b..151f492 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant.py
+++ b/erpnext/hr/doctype/job_applicant/job_applicant.py
@@ -8,7 +8,9 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import comma_and, validate_email_address
+from frappe.utils import validate_email_address
+
+from erpnext.hr.doctype.interview.interview import get_interviewers
class DuplicationError(frappe.ValidationError): pass
@@ -26,7 +28,6 @@
self.name = " - ".join(keys)
def validate(self):
- self.check_email_id_is_unique()
if self.email_id:
validate_email_address(self.email_id, True)
@@ -44,11 +45,44 @@
elif self.status in ["Accepted", "Rejected"]:
emp_ref.db_set("status", self.status)
+@frappe.whitelist()
+def create_interview(doc, interview_round):
+ import json
- def check_email_id_is_unique(self):
- if self.email_id:
- names = frappe.db.sql_list("""select name from `tabJob Applicant`
- where email_id=%s and name!=%s and job_title=%s""", (self.email_id, self.name, self.job_title))
+ from six import string_types
- if names:
- frappe.throw(_("Email Address must be unique, already exists for {0}").format(comma_and(names)), frappe.DuplicateEntryError)
+ if isinstance(doc, string_types):
+ doc = json.loads(doc)
+ doc = frappe.get_doc(doc)
+
+ round_designation = frappe.db.get_value("Interview Round", interview_round, "designation")
+
+ if round_designation and doc.designation and round_designation != doc.designation:
+ frappe.throw(_("Interview Round {0} is only applicable for the Designation {1}").format(interview_round, round_designation))
+
+ interview = frappe.new_doc("Interview")
+ interview.interview_round = interview_round
+ interview.job_applicant = doc.name
+ interview.designation = doc.designation
+ interview.resume_link = doc.resume_link
+ interview.job_opening = doc.job_title
+ interviewer_detail = get_interviewers(interview_round)
+
+ for d in interviewer_detail:
+ interview.append("interview_details", {
+ "interviewer": d.interviewer
+ })
+ return interview
+
+@frappe.whitelist()
+def get_interview_details(job_applicant):
+ interview_details = frappe.db.get_all("Interview",
+ filters={"job_applicant":job_applicant, "docstatus": ["!=", 2]},
+ fields=["name", "interview_round", "expected_average_rating", "average_rating", "status"]
+ )
+ interview_detail_map = {}
+
+ for detail in interview_details:
+ interview_detail_map[detail.name] = detail
+
+ return interview_detail_map
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html
new file mode 100644
index 0000000..c286787
--- /dev/null
+++ b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.html
@@ -0,0 +1,44 @@
+
+{% if not jQuery.isEmptyObject(data) %}
+
+<table class="table table-bordered small">
+ <thead>
+ <tr>
+ <th style="width: 16%" class="text-left">{{ __("Interview") }}</th>
+ <th style="width: 16%" class="text-left">{{ __("Interview Round") }}</th>
+ <th style="width: 12%" class="text-left">{{ __("Status") }}</th>
+ <th style="width: 14%" class="text-left">{{ __("Expected Rating") }}</th>
+ <th style="width: 10%" class="text-left">{{ __("Rating") }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for(const [key, value] of Object.entries(data)) { %}
+ <tr>
+ <td class="text-left"> {%= key %} </td>
+ <td class="text-left"> {%= value["interview_round"] %} </td>
+ <td class="text-left"> {%= value["status"] %} </td>
+ <td class="text-left">
+ {% for (i = 0; i < value["expected_average_rating"]; i++) { %}
+ <span class="fa fa-star " style="color: #F6C35E;"></span>
+ {% } %}
+ {% for (i = 0; i < (5-value["expected_average_rating"]); i++) { %}
+ <span class="fa fa-star " style="color: #E7E9EB;"></span>
+ {% } %}
+ </td>
+ <td class="text-left">
+ {% if(value["average_rating"]){ %}
+ {% for (i = 0; i < value["average_rating"]; i++) { %}
+ <span class="fa fa-star " style="color: #F6C35E;"></span>
+ {% } %}
+ {% for (i = 0; i < (5-value["average_rating"]); i++) { %}
+ <span class="fa fa-star " style="color: #E7E9EB;"></span>
+ {% } %}
+ {% } %}
+ </td>
+ </tr>
+ {% } %}
+ </tbody>
+</table>
+{% else %}
+<p style="margin-top: 30px;"> No Interview has been scheduled.</p>
+{% endif %}
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py
index c005943..2f7795f 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py
+++ b/erpnext/hr/doctype/job_applicant/job_applicant_dashboard.py
@@ -2,14 +2,17 @@
def get_data():
- return {
- 'fieldname': 'job_applicant',
- 'transactions': [
- {
- 'items': ['Employee', 'Employee Onboarding']
- },
- {
- 'items': ['Job Offer']
- },
- ],
- }
+ return {
+ 'fieldname': 'job_applicant',
+ 'transactions': [
+ {
+ 'items': ['Employee', 'Employee Onboarding']
+ },
+ {
+ 'items': ['Job Offer', 'Appointment Letter']
+ },
+ {
+ 'items': ['Interview']
+ }
+ ],
+ }
diff --git a/erpnext/hr/doctype/job_applicant/test_job_applicant.py b/erpnext/hr/doctype/job_applicant/test_job_applicant.py
index e583e25..8fc1290 100644
--- a/erpnext/hr/doctype/job_applicant/test_job_applicant.py
+++ b/erpnext/hr/doctype/job_applicant/test_job_applicant.py
@@ -7,7 +7,8 @@
import frappe
-# test_records = frappe.get_test_records('Job Applicant')
+from erpnext.hr.doctype.designation.test_designation import create_designation
+
class TestJobApplicant(unittest.TestCase):
pass
@@ -25,7 +26,8 @@
job_applicant = frappe.get_doc({
"doctype": "Job Applicant",
- "status": args.status or "Open"
+ "status": args.status or "Open",
+ "designation": create_designation().name
})
job_applicant.update(filters)
diff --git a/erpnext/hr/doctype/job_offer/test_job_offer.py b/erpnext/hr/doctype/job_offer/test_job_offer.py
index 3f3eca1..162b245 100644
--- a/erpnext/hr/doctype/job_offer/test_job_offer.py
+++ b/erpnext/hr/doctype/job_offer/test_job_offer.py
@@ -32,6 +32,7 @@
self.assertTrue(frappe.db.exists("Job Offer", job_offer.name))
def test_job_applicant_update(self):
+ frappe.db.set_value("HR Settings", None, "check_vacancies", 0)
create_staffing_plan()
job_applicant = create_job_applicant(email_id="test_job_applicants@example.com")
job_offer = create_job_offer(job_applicant=job_applicant.name)
@@ -43,7 +44,11 @@
job_offer.status = "Rejected"
job_offer.submit()
job_applicant.reload()
- self.assertEqual(job_applicant.status, "Rejected")
+ self.assertEquals(job_applicant.status, "Rejected")
+ frappe.db.set_value("HR Settings", None, "check_vacancies", 1)
+
+ def tearDown(self):
+ frappe.db.sql("DELETE FROM `tabJob Offer` WHERE 1")
def create_job_offer(**args):
args = frappe._dict(args)
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.js b/erpnext/hr/doctype/leave_allocation/leave_allocation.js
index d947641..9742387 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.js
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.js
@@ -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 @@
},
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 @@
'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 @@
},
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 @@
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.")
+ }
+];
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
index 3a6539e..52ee463 100644
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
@@ -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",
diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js
index 9ccb915..9e8cb55 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.js
+++ b/erpnext/hr/doctype/leave_application/leave_application.js
@@ -1,8 +1,8 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-cur_frm.add_fetch('employee','employee_name','employee_name');
-cur_frm.add_fetch('employee','company','company');
+cur_frm.add_fetch('employee', 'employee_name', 'employee_name');
+cur_frm.add_fetch('employee', 'company', 'company');
frappe.ui.form.on("Leave Application", {
setup: function(frm) {
@@ -19,7 +19,6 @@
frm.set_query("employee", erpnext.queries.employee);
},
onload: function(frm) {
-
// Ignore cancellation of doctype on cancel all.
frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"];
@@ -42,9 +41,9 @@
},
validate: function(frm) {
- if (frm.doc.from_date == frm.doc.to_date && frm.doc.half_day == 1){
+ if (frm.doc.from_date == frm.doc.to_date && frm.doc.half_day == 1) {
frm.doc.half_day_date = frm.doc.from_date;
- }else if (frm.doc.half_day == 0){
+ } else if (frm.doc.half_day == 0) {
frm.doc.half_day_date = "";
}
frm.toggle_reqd("half_day_date", frm.doc.half_day == 1);
@@ -79,14 +78,14 @@
__("Allocated Leaves")
);
frm.dashboard.show();
- let allowed_leave_types = Object.keys(leave_details);
+ let allowed_leave_types = Object.keys(leave_details);
// lwps should be allowed, lwps don't have any allocation
allowed_leave_types = allowed_leave_types.concat(lwps);
- frm.set_query('leave_type', function(){
+ frm.set_query('leave_type', function() {
return {
- filters : [
+ filters: [
['leave_type_name', 'in', allowed_leave_types]
]
};
@@ -99,7 +98,7 @@
frm.trigger("calculate_total_days");
}
cur_frm.set_intro("");
- if(frm.doc.__islocal && !in_list(frappe.user_roles, "Employee")) {
+ if (frm.doc.__islocal && !in_list(frappe.user_roles, "Employee")) {
frm.set_intro(__("Fill the form and save it"));
}
@@ -118,7 +117,7 @@
},
leave_approver: function(frm) {
- if(frm.doc.leave_approver){
+ if (frm.doc.leave_approver) {
frm.set_value("leave_approver_name", frappe.user.full_name(frm.doc.leave_approver));
}
},
@@ -131,12 +130,10 @@
if (frm.doc.half_day) {
if (frm.doc.from_date == frm.doc.to_date) {
frm.set_value("half_day_date", frm.doc.from_date);
- }
- else {
+ } else {
frm.trigger("half_day_datepicker");
}
- }
- else {
+ } else {
frm.set_value("half_day_date", "");
}
frm.trigger("calculate_total_days");
@@ -163,11 +160,11 @@
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 @@
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 @@
},
calculate_total_days: function(frm) {
- if(frm.doc.from_date && frm.doc.to_date && frm.doc.employee && frm.doc.leave_type) {
+ if (frm.doc.from_date && frm.doc.to_date && frm.doc.employee && frm.doc.leave_type) {
var from_date = Date.parse(frm.doc.from_date);
var to_date = Date.parse(frm.doc.to_date);
- if(to_date < from_date){
+ if (to_date < from_date) {
frappe.msgprint(__("To Date cannot be less than From Date"));
frm.set_value('to_date', '');
return;
@@ -222,7 +218,7 @@
},
set_leave_approver: function(frm) {
- if(frm.doc.employee) {
+ if (frm.doc.employee) {
// server call is done to include holidays in leave days calculations
return frappe.call({
method: 'erpnext.hr.doctype.leave_application.leave_application.get_leave_approver',
@@ -238,3 +234,36 @@
}
}
});
+
+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.")
+ }
+];
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 9e6fc6d..349ed7a 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -76,6 +76,7 @@
# 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 @@
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)):
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index b9c785a..629b20e 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -121,6 +121,7 @@
application = self.get_application(_test_records[0])
application.insert()
+ application.reload()
application.status = "Approved"
self.assertRaises(LeaveDayBlockedError, application.submit)
diff --git a/erpnext/hr/doctype/leave_type/leave_type.js b/erpnext/hr/doctype/leave_type/leave_type.js
index 8622309..b930ded 100644
--- a/erpnext/hr/doctype/leave_type/leave_type.js
+++ b/erpnext/hr/doctype/leave_type/leave_type.js
@@ -2,3 +2,37 @@
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.")
+ }
+];
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_type/leave_type.json b/erpnext/hr/doctype/leave_type/leave_type.json
index 8f2ae6e..06ca4cd 100644
--- a/erpnext/hr/doctype/leave_type/leave_type.json
+++ b/erpnext/hr/doctype/leave_type/leave_type.json
@@ -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",
diff --git a/erpnext/hr/doctype/skill_assessment/__init__.py b/erpnext/hr/doctype/skill_assessment/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/skill_assessment/__init__.py
diff --git a/erpnext/hr/doctype/skill_assessment/skill_assessment.json b/erpnext/hr/doctype/skill_assessment/skill_assessment.json
new file mode 100644
index 0000000..8b935c4
--- /dev/null
+++ b/erpnext/hr/doctype/skill_assessment/skill_assessment.json
@@ -0,0 +1,41 @@
+{
+ "actions": [],
+ "creation": "2021-04-12 17:07:39.656289",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "skill",
+ "rating"
+ ],
+ "fields": [
+ {
+ "fieldname": "skill",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Skill",
+ "options": "Skill",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "rating",
+ "fieldtype": "Rating",
+ "in_list_view": 1,
+ "label": "Rating",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-04-12 17:18:14.032298",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Skill Assessment",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/skill_assessment/skill_assessment.py b/erpnext/hr/doctype/skill_assessment/skill_assessment.py
new file mode 100644
index 0000000..3b74c4e
--- /dev/null
+++ b/erpnext/hr/doctype/skill_assessment/skill_assessment.py
@@ -0,0 +1,12 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+# import frappe
+from frappe.model.document import Document
+
+
+class SkillAssessment(Document):
+ pass
diff --git a/erpnext/hr/module_onboarding/human_resource/human_resource.json b/erpnext/hr/module_onboarding/human_resource/human_resource.json
index 518c002..cd11bd1 100644
--- a/erpnext/hr/module_onboarding/human_resource/human_resource.json
+++ b/erpnext/hr/module_onboarding/human_resource/human_resource.json
@@ -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"
@@ -32,6 +29,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.",
diff --git a/erpnext/hr/onboarding_step/create_employee/create_employee.json b/erpnext/hr/onboarding_step/create_employee/create_employee.json
index 3aa33c6..4782818 100644
--- a/erpnext/hr/onboarding_step/create_employee/create_employee.json
+++ b/erpnext/hr/onboarding_step/create_employee/create_employee.json
@@ -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
diff --git a/erpnext/hr/onboarding_step/create_holiday_list/create_holiday_list.json b/erpnext/hr/onboarding_step/create_holiday_list/create_holiday_list.json
index 32472b4..a08e85f 100644
--- a/erpnext/hr/onboarding_step/create_holiday_list/create_holiday_list.json
+++ b/erpnext/hr/onboarding_step/create_holiday_list/create_holiday_list.json
@@ -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
diff --git a/erpnext/hr/onboarding_step/create_leave_allocation/create_leave_allocation.json b/erpnext/hr/onboarding_step/create_leave_allocation/create_leave_allocation.json
index fa9941e..0b0ce3f 100644
--- a/erpnext/hr/onboarding_step/create_leave_allocation/create_leave_allocation.json
+++ b/erpnext/hr/onboarding_step/create_leave_allocation/create_leave_allocation.json
@@ -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
diff --git a/erpnext/hr/onboarding_step/create_leave_application/create_leave_application.json b/erpnext/hr/onboarding_step/create_leave_application/create_leave_application.json
index 1ed074e..af63aa5 100644
--- a/erpnext/hr/onboarding_step/create_leave_application/create_leave_application.json
+++ b/erpnext/hr/onboarding_step/create_leave_application/create_leave_application.json
@@ -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
diff --git a/erpnext/hr/onboarding_step/create_leave_type/create_leave_type.json b/erpnext/hr/onboarding_step/create_leave_type/create_leave_type.json
index 8cbfc5c..397f5cd 100644
--- a/erpnext/hr/onboarding_step/create_leave_type/create_leave_type.json
+++ b/erpnext/hr/onboarding_step/create_leave_type/create_leave_type.json
@@ -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
diff --git a/erpnext/hr/onboarding_step/data_import/data_import.json b/erpnext/hr/onboarding_step/data_import/data_import.json
new file mode 100644
index 0000000..ac343c6
--- /dev/null
+++ b/erpnext/hr/onboarding_step/data_import/data_import.json
@@ -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"
+}
\ No newline at end of file
diff --git a/erpnext/hr/onboarding_step/hr_settings/hr_settings.json b/erpnext/hr/onboarding_step/hr_settings/hr_settings.json
index 0a1d0ba..355664f 100644
--- a/erpnext/hr/onboarding_step/hr_settings/hr_settings.json
+++ b/erpnext/hr/onboarding_step/hr_settings/hr_settings.json
@@ -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
diff --git a/erpnext/maintenance/doctype/maintenance_schedule_detail/maintenance_schedule_detail.json b/erpnext/maintenance/doctype/maintenance_schedule_detail/maintenance_schedule_detail.json
index 8ccef6a..afe273f 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule_detail/maintenance_schedule_detail.json
+++ b/erpnext/maintenance/doctype/maintenance_schedule_detail/maintenance_schedule_detail.json
@@ -89,13 +89,14 @@
"width": "160px"
},
{
+ "allow_on_submit": 1,
"columns": 2,
+ "default": "Pending",
"fieldname": "completion_status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Completion Status",
- "options": "Pending\nPartially Completed\nFully Completed",
- "read_only": 1
+ "options": "Pending\nPartially Completed\nFully Completed"
},
{
"fieldname": "column_break_3",
@@ -125,10 +126,11 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-05-27 16:07:25.905015",
+ "modified": "2021-09-16 21:25:22.506485",
"modified_by": "Administrator",
"module": "Maintenance",
"name": "Maintenance Schedule Detail",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 8a92413..5f5c20a 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -215,7 +215,32 @@
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();
+ }
});
}
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 5091808..5dfd6be 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -312,3 +312,6 @@
erpnext.patches.v13_0.trim_sales_invoice_custom_field_length
erpnext.patches.v13_0.create_custom_field_for_finance_book
erpnext.patches.v13_0.modify_invalid_gain_loss_gl_entries
+erpnext.patches.v13_0.fix_additional_cost_in_mfg_stock_entry
+erpnext.patches.v13_0.set_status_in_maintenance_schedule_table
+erpnext.patches.v13_0.add_default_interview_notification_templates
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/add_default_interview_notification_templates.py b/erpnext/patches/v13_0/add_default_interview_notification_templates.py
new file mode 100644
index 0000000..5e8a27f
--- /dev/null
+++ b/erpnext/patches/v13_0/add_default_interview_notification_templates.py
@@ -0,0 +1,37 @@
+from __future__ import unicode_literals
+
+import os
+
+import frappe
+from frappe import _
+
+
+def execute():
+ if not frappe.db.exists('Email Template', _('Interview Reminder')):
+ base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
+ response = frappe.read_file(os.path.join(base_path, 'interview/interview_reminder_notification_template.html'))
+
+ frappe.get_doc({
+ 'doctype': 'Email Template',
+ 'name': _('Interview Reminder'),
+ 'response': response,
+ 'subject': _('Interview Reminder'),
+ 'owner': frappe.session.user,
+ }).insert(ignore_permissions=True)
+
+ if not frappe.db.exists('Email Template', _('Interview Feedback Reminder')):
+ base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
+ response = frappe.read_file(os.path.join(base_path, 'interview/interview_feedback_reminder_template.html'))
+
+ frappe.get_doc({
+ 'doctype': 'Email Template',
+ 'name': _('Interview Feedback Reminder'),
+ 'response': response,
+ 'subject': _('Interview Feedback Reminder'),
+ 'owner': frappe.session.user,
+ }).insert(ignore_permissions=True)
+
+ hr_settings = frappe.get_doc('HR Settings')
+ hr_settings.interview_reminder_template = _('Interview Reminder')
+ hr_settings.feedback_reminder_notification_template = _('Interview Feedback Reminder')
+ hr_settings.save()
diff --git a/erpnext/patches/v13_0/fix_additional_cost_in_mfg_stock_entry.py b/erpnext/patches/v13_0/fix_additional_cost_in_mfg_stock_entry.py
new file mode 100644
index 0000000..aeb8d8e
--- /dev/null
+++ b/erpnext/patches/v13_0/fix_additional_cost_in_mfg_stock_entry.py
@@ -0,0 +1,76 @@
+from typing import List, NewType
+
+import frappe
+
+StockEntryCode = NewType("StockEntryCode", str)
+
+
+def execute():
+ stock_entry_codes = find_broken_stock_entries()
+
+ for stock_entry_code in stock_entry_codes:
+ patched_stock_entry = patch_additional_cost(stock_entry_code)
+ create_repost_item_valuation(patched_stock_entry)
+
+
+def find_broken_stock_entries() -> List[StockEntryCode]:
+ period_closing_date = frappe.db.get_value(
+ "Period Closing Voucher", {"docstatus": 1}, "posting_date", order_by="posting_date desc"
+ )
+
+ stock_entries_to_patch = frappe.db.sql(
+ """
+ select se.name, sum(sed.additional_cost) as item_additional_cost, se.total_additional_costs
+ from `tabStock Entry` se
+ join `tabStock Entry Detail` sed
+ on sed.parent = se.name
+ where
+ se.docstatus = 1 and
+ se.posting_date > %s
+ group by
+ sed.parent
+ having
+ item_additional_cost != se.total_additional_costs
+ """,
+ period_closing_date,
+ as_dict=True,
+ )
+
+ return [d.name for d in stock_entries_to_patch]
+
+
+def patch_additional_cost(code: StockEntryCode):
+ stock_entry = frappe.get_doc("Stock Entry", code)
+ stock_entry.distribute_additional_costs()
+ stock_entry.update_valuation_rate()
+ stock_entry.set_total_incoming_outgoing_value()
+ stock_entry.set_total_amount()
+ stock_entry.db_update()
+ for item in stock_entry.items:
+ item.db_update()
+ return stock_entry
+
+
+def create_repost_item_valuation(stock_entry):
+ from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
+
+ # turn on recalculate flag so reposting corrects the incoming/outgoing rates.
+ frappe.db.set_value(
+ "Stock Ledger Entry",
+ {"voucher_no": stock_entry.name, "actual_qty": (">", 0)},
+ "recalculate_rate",
+ 1,
+ update_modified=False,
+ )
+
+ create_repost_item_valuation_entry(
+ args=frappe._dict(
+ {
+ "posting_date": stock_entry.posting_date,
+ "posting_time": stock_entry.posting_time,
+ "voucher_type": stock_entry.doctype,
+ "voucher_no": stock_entry.name,
+ "company": stock_entry.company,
+ }
+ )
+ )
diff --git a/erpnext/patches/v13_0/set_status_in_maintenance_schedule_table.py b/erpnext/patches/v13_0/set_status_in_maintenance_schedule_table.py
new file mode 100644
index 0000000..9887ad9
--- /dev/null
+++ b/erpnext/patches/v13_0/set_status_in_maintenance_schedule_table.py
@@ -0,0 +1,10 @@
+import frappe
+
+
+def execute():
+ frappe.reload_doc("maintenance", "doctype", "Maintenance Schedule Detail")
+ frappe.db.sql("""
+ UPDATE `tabMaintenance Schedule Detail`
+ SET completion_status = 'Pending'
+ WHERE docstatus < 2
+ """)
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index bff36a4..9ed6686 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -171,8 +171,6 @@
days_in_month = no_of_days[0]
no_of_holidays = no_of_days[1]
- self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 1)
-
ss.reload()
payment_days_based_comp_amount = 0
for component in ss.earnings:
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 403c498..63fd8a1 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -345,26 +345,14 @@
}
scan_barcode() {
- let scan_barcode_field = this.frm.fields_dict["scan_barcode"];
-
- let show_description = function(idx, exist = null) {
- if (exist) {
- frappe.show_alert({
- message: __('Row #{0}: Qty increased by 1', [idx]),
- indicator: 'green'
- });
- } else {
- frappe.show_alert({
- message: __('Row #{0}: Item added', [idx]),
- indicator: 'green'
- });
- }
- }
+ let me = this;
if(this.frm.doc.scan_barcode) {
frappe.call({
method: "erpnext.selling.page.point_of_sale.point_of_sale.search_for_serial_or_batch_or_barcode_number",
- args: { search_value: this.frm.doc.scan_barcode }
+ args: {
+ search_value: this.frm.doc.scan_barcode
+ }
}).then(r => {
const data = r && r.message;
if (!data || Object.keys(data).length === 0) {
@@ -375,49 +363,96 @@
return;
}
- let cur_grid = this.frm.fields_dict.items.grid;
-
- let row_to_modify = null;
- const existing_item_row = this.frm.doc.items.find(d => d.item_code === data.item_code);
- const blank_item_row = this.frm.doc.items.find(d => !d.item_code);
-
- if (existing_item_row) {
- row_to_modify = existing_item_row;
- } else if (blank_item_row) {
- row_to_modify = blank_item_row;
- }
-
- if (!row_to_modify) {
- // add new row
- row_to_modify = frappe.model.add_child(this.frm.doc, cur_grid.doctype, 'items');
- }
-
- show_description(row_to_modify.idx, row_to_modify.item_code);
-
- this.frm.from_barcode = this.frm.from_barcode ? this.frm.from_barcode + 1 : 1;
- frappe.model.set_value(row_to_modify.doctype, row_to_modify.name, {
- item_code: data.item_code,
- qty: (row_to_modify.qty || 0) + 1
- });
-
- ['serial_no', 'batch_no', 'barcode'].forEach(field => {
- if (data[field] && frappe.meta.has_field(row_to_modify.doctype, field)) {
-
- let value = (row_to_modify[field] && field === "serial_no")
- ? row_to_modify[field] + '\n' + data[field] : data[field];
-
- frappe.model.set_value(row_to_modify.doctype,
- row_to_modify.name, field, value);
- }
- });
-
- scan_barcode_field.set_value('');
- refresh_field("items");
+ me.modify_table_after_scan(data);
});
}
return false;
}
+ modify_table_after_scan(data) {
+ let scan_barcode_field = this.frm.fields_dict["scan_barcode"];
+ let cur_grid = this.frm.fields_dict.items.grid;
+ let row_to_modify = null;
+
+ // Check if batch is scanned and table has batch no field
+ let batch_no_scan = Boolean(data.batch_no) && frappe.meta.has_field(cur_grid.doctype, "batch_no");
+
+ if (batch_no_scan) {
+ row_to_modify = this.get_batch_row_to_modify(data.batch_no);
+ } else {
+ // serial or barcode scan
+ row_to_modify = this.get_row_to_modify_on_scan(row_to_modify, data);
+ }
+
+ if (!row_to_modify) {
+ // add new row if new item/batch is scanned
+ row_to_modify = frappe.model.add_child(this.frm.doc, cur_grid.doctype, 'items');
+ }
+
+ this.show_scan_message(row_to_modify.idx, row_to_modify.item_code);
+ this.set_scanned_values(row_to_modify, data, scan_barcode_field);
+ }
+
+ set_scanned_values(row_to_modify, data, scan_barcode_field) {
+ // increase qty and set scanned value and item in row
+ this.frm.from_barcode = this.frm.from_barcode ? this.frm.from_barcode + 1 : 1;
+ frappe.model.set_value(row_to_modify.doctype, row_to_modify.name, {
+ item_code: data.item_code,
+ qty: (row_to_modify.qty || 0) + 1
+ });
+
+ ['serial_no', 'batch_no', 'barcode'].forEach(field => {
+ if (data[field] && frappe.meta.has_field(row_to_modify.doctype, field)) {
+ let is_serial_no = row_to_modify[field] && field === "serial_no";
+ let value = data[field];
+
+ if (is_serial_no) {
+ value = row_to_modify[field] + '\n' + data[field];
+ }
+
+ frappe.model.set_value(row_to_modify.doctype, row_to_modify.name, field, value);
+ }
+ });
+
+ scan_barcode_field.set_value('');
+ refresh_field("items");
+ }
+
+ get_row_to_modify_on_scan(row_to_modify, data) {
+ // get an existing item row to increment or blank row to modify
+ const existing_item_row = this.frm.doc.items.find(d => d.item_code === data.item_code);
+ const blank_item_row = this.frm.doc.items.find(d => !d.item_code);
+
+ if (existing_item_row) {
+ row_to_modify = existing_item_row;
+ } else if (blank_item_row) {
+ row_to_modify = blank_item_row;
+ }
+
+ return row_to_modify;
+ }
+
+ get_batch_row_to_modify(batch_no) {
+ // get row if batch already exists in table
+ const existing_batch_row = this.frm.doc.items.find(d => d.batch_no === batch_no);
+ return existing_batch_row || null;
+ }
+
+ show_scan_message (idx, exist = null) {
+ // show new row or qty increase toast
+ if (exist) {
+ frappe.show_alert({
+ message: __('Row #{0}: Qty increased by 1', [idx]),
+ indicator: 'green'
+ });
+ } else {
+ frappe.show_alert({
+ message: __('Row #{0}: Item added', [idx]),
+ indicator: 'green'
+ });
+ }
+ }
+
apply_default_taxes() {
var me = this;
var taxes_and_charges_field = frappe.meta.get_docfield(me.frm.doc.doctype, "taxes_and_charges",
diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js
index 6286732..7b35819 100644
--- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js
+++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js
@@ -63,7 +63,7 @@
});
node.parent.append(node_card);
- node.$link = $(`#${node.id}`);
+ node.$link = $(`[id="${node.id}"]`);
}
show() {
@@ -223,7 +223,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
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 @@
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 @@
}
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 @@
}
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 @@
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();
diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js
index b1a8879..0a8ba78 100644
--- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js
+++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js
@@ -54,7 +54,7 @@
});
node.parent.append(node_card);
- node.$link = $(`#${node.id}`);
+ node.$link = $(`[id="${node.id}"]`);
node.$link.addClass('mobile-node');
}
@@ -184,7 +184,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
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 @@
}
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 @@
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();
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
index 490a7c4..fef1e76 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -31,6 +31,14 @@
.carousel-control-prev,
.carousel-control-next {
opacity: 1;
+ width: 8%;
+
+ @media (max-width: 1200px) {
+ width: 10%;
+ }
+ @media (max-width: 768px) {
+ width: 15%;
+ }
}
.carousel-body {
@@ -43,6 +51,8 @@
.carousel-content {
max-width: 400px;
+ margin-left: 5rem;
+ margin-right: 5rem;
}
.card {
diff --git a/erpnext/setup/setup_wizard/operations/defaults_setup.py b/erpnext/setup/setup_wizard/operations/defaults_setup.py
index 6dd0fb1..55d5ec8 100644
--- a/erpnext/setup/setup_wizard/operations/defaults_setup.py
+++ b/erpnext/setup/setup_wizard/operations/defaults_setup.py
@@ -62,6 +62,13 @@
hr_settings.emp_created_by = "Naming Series"
hr_settings.leave_approval_notification_template = _("Leave Approval Notification")
hr_settings.leave_status_notification_template = _("Leave Status Notification")
+
+ hr_settings.send_interview_reminder = 1
+ hr_settings.interview_reminder_template = _("Interview Reminder")
+ hr_settings.remind_before = "00:15:00"
+
+ hr_settings.send_interview_feedback_reminder = 1
+ hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder")
hr_settings.save()
def set_no_copy_fields_in_variant_settings():
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 907967c..c473395 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -264,16 +264,26 @@
base_path = frappe.get_app_path("erpnext", "hr", "doctype")
response = frappe.read_file(os.path.join(base_path, "leave_application/leave_application_email_template.html"))
- records += [{'doctype': 'Email Template', 'name': _("Leave Approval Notification"), 'response': response,\
+ records += [{'doctype': 'Email Template', 'name': _("Leave Approval Notification"), 'response': response,
'subject': _("Leave Approval Notification"), 'owner': frappe.session.user}]
- records += [{'doctype': 'Email Template', 'name': _("Leave Status Notification"), 'response': response,\
+ records += [{'doctype': 'Email Template', 'name': _("Leave Status Notification"), 'response': response,
'subject': _("Leave Status Notification"), 'owner': frappe.session.user}]
+ response = frappe.read_file(os.path.join(base_path, "interview/interview_reminder_notification_template.html"))
+
+ records += [{'doctype': 'Email Template', 'name': _('Interview Reminder'), 'response': response,
+ 'subject': _('Interview Reminder'), 'owner': frappe.session.user}]
+
+ response = frappe.read_file(os.path.join(base_path, "interview/interview_feedback_reminder_template.html"))
+
+ records += [{'doctype': 'Email Template', 'name': _('Interview Feedback Reminder'), 'response': response,
+ 'subject': _('Interview Feedback Reminder'), 'owner': frappe.session.user}]
+
base_path = frappe.get_app_path("erpnext", "stock", "doctype")
response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html"))
- records += [{'doctype': 'Email Template', 'name': _("Dispatch Notification"), 'response': response,\
+ records += [{'doctype': 'Email Template', 'name': _("Dispatch Notification"), 'response': response,
'subject': _("Your order is out for delivery!"), 'owner': frappe.session.user}]
# Records for the Supplier Scorecard
@@ -317,6 +327,14 @@
hr_settings.emp_created_by = "Naming Series"
hr_settings.leave_approval_notification_template = _("Leave Approval Notification")
hr_settings.leave_status_notification_template = _("Leave Status Notification")
+
+ hr_settings.send_interview_reminder = 1
+ hr_settings.interview_reminder_template = _("Interview Reminder")
+ hr_settings.remind_before = "00:15:00"
+
+ hr_settings.send_interview_feedback_reminder = 1
+ hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder")
+
hr_settings.save()
def update_item_variant_settings():
diff --git a/erpnext/shopping_cart/web_template/hero_slider/hero_slider.html b/erpnext/shopping_cart/web_template/hero_slider/hero_slider.html
index 1e3d0d0..e560f4a 100644
--- a/erpnext/shopping_cart/web_template/hero_slider/hero_slider.html
+++ b/erpnext/shopping_cart/web_template/hero_slider/hero_slider.html
@@ -1,7 +1,7 @@
{%- macro slide(image, title, subtitle, action, label, index, align="Left", theme="Dark") -%}
{%- set align_class = resolve_class({
'text-right': align == 'Right',
- 'text-centre': align == 'Center',
+ 'text-centre': align == 'Centre',
'text-left': align == 'Left',
}) -%}
@@ -15,7 +15,7 @@
<div class="carousel-body container d-flex {{ align_class }}">
<div class="carousel-content align-self-center">
{%- if title -%}<h1 class="{{ heading_class }}">{{ title }}</h1>{%- endif -%}
- {%- if subtitle -%}<p class="text-muted mt-2">{{ subtitle }}</p>{%- endif -%}
+ {%- if subtitle -%}<p class="{{ heading_class }} mt-2">{{ subtitle }}</p>{%- endif -%}
{%- if action -%}
<a href="{{ action }}" class="btn btn-primary mt-3">
{{ label }}
@@ -27,12 +27,14 @@
</div>
{%- endmacro -%}
-<div id="{{ slider_name }}" class="section-carousel carousel slide" data-ride="carousel">
+{%- set hero_slider_id = 'id-' + frappe.utils.generate_hash('HeroSlider', 12) -%}
+
+<div id="{{ hero_slider_id }}" class="section-carousel carousel slide" data-ride="carousel">
{%- if show_indicators -%}
<ol class="carousel-indicators">
{%- for index in ['1', '2', '3', '4', '5'] -%}
{%- if values['slide_' + index + '_image'] -%}
- <li data-target="#{{ slider_name }}" data-slide-to="{{ frappe.utils.cint(index) - 1 }}" class="{{ 'active' if index=='1' else ''}}"></li>
+ <li data-target="#{{ hero_slider_id }}" data-slide-to="{{ frappe.utils.cint(index) - 1 }}" class="{{ 'active' if index=='1' else ''}}"></li>
{%- endif -%}
{%- endfor -%}
</ol>
@@ -54,7 +56,7 @@
{%- endfor -%}
</div>
{%- if show_controls -%}
- <a class="carousel-control-prev" href="#{{ slider_name }}" role="button" data-slide="prev">
+ <a class="carousel-control-prev" href="#{{ hero_slider_id }}" role="button" data-slide="prev">
<div class="carousel-control">
<svg class="mr-1" width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.625 3.75L6.375 9L11.625 14.25" stroke="#4C5A67" stroke-linecap="round" stroke-linejoin="round"/>
@@ -62,7 +64,7 @@
</div>
<span class="sr-only">Previous</span>
</a>
- <a class="carousel-control-next" href="#{{ slider_name }}" role="button" data-slide="next">
+ <a class="carousel-control-next" href="#{{ hero_slider_id }}" role="button" data-slide="next">
<div class="carousel-control">
<svg class="ml-1" width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.375 14.25L11.625 9L6.375 3.75" stroke="#4C5A67" stroke-linecap="round" stroke-linejoin="round"/>
@@ -73,13 +75,12 @@
{%- endif -%}
</div>
-<script type="text/javascript">
- $('.carousel').carousel({
- interval: false,
- pause: "hover",
- wrap: true
- })
+<script>
+ frappe.ready(function () {
+ $('.carousel').carousel({
+ interval: false,
+ pause: "hover",
+ wrap: true
+ })
+ });
</script>
-
-<style>
-</style>
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index 62b3a6a..d86e52f 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -7,7 +7,7 @@
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 @@
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 @@
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
diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
index c70a9ec..c086f93 100644
--- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
@@ -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}",
+ )
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 7cb9665..157904b 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -548,44 +548,7 @@
calculate_basic_amount: function(frm, item) {
item.basic_amount = flt(flt(item.transfer_qty) * flt(item.basic_rate),
precision("basic_amount", item));
-
- frm.events.calculate_amount(frm);
- },
-
- calculate_amount: function(frm) {
frm.events.calculate_total_additional_costs(frm);
- let total_basic_amount = 0;
- if (in_list(["Repack", "Manufacture"], frm.doc.purpose)) {
- total_basic_amount = frappe.utils.sum(
- (frm.doc.items || []).map(function(i) {
- return i.is_finished_item ? flt(i.basic_amount) : 0;
- })
- );
- } else {
- total_basic_amount = frappe.utils.sum(
- (frm.doc.items || []).map(function(i) {
- return i.t_warehouse ? flt(i.basic_amount) : 0;
- })
- );
- }
- for (let i in frm.doc.items) {
- let item = frm.doc.items[i];
-
- if (((in_list(["Repack", "Manufacture"], frm.doc.purpose) && item.is_finished_item) || item.t_warehouse) && total_basic_amount) {
- item.additional_cost = (flt(item.basic_amount) / total_basic_amount) * frm.doc.total_additional_costs;
- } else {
- item.additional_cost = 0;
- }
-
- item.amount = flt(item.basic_amount + flt(item.additional_cost), precision("amount", item));
-
- if (flt(item.transfer_qty)) {
- item.valuation_rate = flt(flt(item.basic_rate) + (flt(item.additional_cost) / flt(item.transfer_qty)),
- precision("valuation_rate", item));
- }
- }
-
- refresh_field('items');
},
calculate_total_additional_costs: function(frm) {
@@ -781,11 +744,6 @@
amount: function(frm, cdt, cdn) {
frm.events.set_base_amount(frm, cdt, cdn);
- // Adding this check because same table in used in LCV
- // This causes an error if you try to post an LCV immediately after a Stock Entry
- if (frm.doc.doctype == 'Stock Entry') {
- frm.events.calculate_amount(frm);
- }
},
expense_account: function(frm, cdt, cdn) {
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 1c9b961..bd7d22b 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -555,22 +555,27 @@
def distribute_additional_costs(self):
# If no incoming items, set additional costs blank
- if not any([d.item_code for d in self.items if d.t_warehouse]):
+ if not any(d.item_code for d in self.items if d.t_warehouse):
self.additional_costs = []
- self.total_additional_costs = sum([flt(t.base_amount) for t in self.get("additional_costs")])
+ self.total_additional_costs = sum(flt(t.base_amount) for t in self.get("additional_costs"))
if self.purpose in ("Repack", "Manufacture"):
- incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.is_finished_item])
+ incoming_items_cost = sum(flt(t.basic_amount) for t in self.get("items") if t.is_finished_item)
else:
- incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse])
+ incoming_items_cost = sum(flt(t.basic_amount) for t in self.get("items") if t.t_warehouse)
- if incoming_items_cost:
- for d in self.get("items"):
- if (self.purpose in ("Repack", "Manufacture") and d.is_finished_item) or d.t_warehouse:
- d.additional_cost = (flt(d.basic_amount) / incoming_items_cost) * self.total_additional_costs
- else:
- d.additional_cost = 0
+ if not incoming_items_cost:
+ return
+
+ for d in self.get("items"):
+ if self.purpose in ("Repack", "Manufacture") and not d.is_finished_item:
+ d.additional_cost = 0
+ continue
+ elif not d.t_warehouse:
+ d.additional_cost = 0
+ continue
+ d.additional_cost = (flt(d.basic_amount) / incoming_items_cost) * self.total_additional_costs
def update_valuation_rate(self):
for d in self.get("items"):
@@ -805,7 +810,11 @@
def get_gl_entries(self, warehouse_account):
gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account)
- total_basic_amount = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse])
+ if self.purpose in ("Repack", "Manufacture"):
+ total_basic_amount = sum(flt(t.basic_amount) for t in self.get("items") if t.is_finished_item)
+ else:
+ total_basic_amount = sum(flt(t.basic_amount) for t in self.get("items") if t.t_warehouse)
+
divide_based_on = total_basic_amount
if self.get("additional_costs") and not total_basic_amount:
@@ -816,20 +825,24 @@
for t in self.get("additional_costs"):
for d in self.get("items"):
- if d.t_warehouse:
- item_account_wise_additional_cost.setdefault((d.item_code, d.name), {})
- item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, {
- "amount": 0.0,
- "base_amount": 0.0
- })
+ if self.purpose in ("Repack", "Manufacture") and not d.is_finished_item:
+ continue
+ elif not d.t_warehouse:
+ continue
- multiply_based_on = d.basic_amount if total_basic_amount else d.qty
+ item_account_wise_additional_cost.setdefault((d.item_code, d.name), {})
+ item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, {
+ "amount": 0.0,
+ "base_amount": 0.0
+ })
- item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["amount"] += \
- flt(t.amount * multiply_based_on) / divide_based_on
+ multiply_based_on = d.basic_amount if total_basic_amount else d.qty
- item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["base_amount"] += \
- flt(t.base_amount * multiply_based_on) / divide_based_on
+ item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["amount"] += \
+ flt(t.amount * multiply_based_on) / divide_based_on
+
+ item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["base_amount"] += \
+ flt(t.base_amount * multiply_based_on) / divide_based_on
if item_account_wise_additional_cost:
for d in self.get("items"):
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 46619eb..c9d0af5 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -837,6 +837,39 @@
frappe.db.set_default("allow_negative_stock", 0)
+ def test_additional_cost_distribution_manufacture(self):
+ se = frappe.get_doc(
+ doctype="Stock Entry",
+ purpose="Manufacture",
+ additional_costs=[frappe._dict(base_amount=100)],
+ items=[
+ frappe._dict(item_code="RM", basic_amount=10),
+ frappe._dict(item_code="FG", basic_amount=20, t_warehouse="X", is_finished_item=1),
+ frappe._dict(item_code="scrap", basic_amount=30, t_warehouse="X")
+ ],
+ )
+
+ se.distribute_additional_costs()
+
+ distributed_costs = [d.additional_cost for d in se.items]
+ self.assertEqual([0.0, 100.0, 0.0], distributed_costs)
+
+ def test_additional_cost_distribution_non_manufacture(self):
+ se = frappe.get_doc(
+ doctype="Stock Entry",
+ purpose="Material Receipt",
+ additional_costs=[frappe._dict(base_amount=100)],
+ items=[
+ frappe._dict(item_code="RECEIVED_1", basic_amount=20, t_warehouse="X"),
+ frappe._dict(item_code="RECEIVED_2", basic_amount=30, t_warehouse="X")
+ ],
+ )
+
+ se.distribute_additional_costs()
+
+ distributed_costs = [d.additional_cost for d in se.items]
+ self.assertEqual([40.0, 60.0], distributed_costs)
+
def make_serialized_item(**args):
args = frappe._dict(args)
se = frappe.copy_doc(test_records[0])
diff --git a/erpnext/stock/doctype/stock_reposting_settings/__init__.py b/erpnext/stock/doctype/stock_reposting_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/stock_reposting_settings/__init__.py
diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js
new file mode 100644
index 0000000..42d0723
--- /dev/null
+++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Stock Reposting Settings', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
new file mode 100644
index 0000000..2474059
--- /dev/null
+++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py
new file mode 100644
index 0000000..bab521d
--- /dev/null
+++ b/erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.py
@@ -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))
diff --git a/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py b/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py
new file mode 100644
index 0000000..fad74d3
--- /dev/null
+++ b/erpnext/stock/doctype/stock_reposting_settings/test_stock_reposting_settings.py
@@ -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
diff --git a/erpnext/tests/ui_test_helpers.py b/erpnext/tests/ui_test_helpers.py
index 76c7608..9c8c371 100644
--- a/erpnext/tests/ui_test_helpers.py
+++ b/erpnext/tests/ui_test_helpers.py
@@ -7,6 +7,8 @@
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)