diff --git a/hrms/hr/doctype/appraisal/appraisal.py b/hrms/hr/doctype/appraisal/appraisal.py index 9ea9b1b9d4..fa72e4f35d 100644 --- a/hrms/hr/doctype/appraisal/appraisal.py +++ b/hrms/hr/doctype/appraisal/appraisal.py @@ -9,6 +9,7 @@ from hrms.hr.doctype.appraisal_cycle.appraisal_cycle import validate_active_appraisal_cycle from hrms.hr.utils import validate_active_employee +from hrms.payroll.utils import sanitize_expression class Appraisal(Document): @@ -179,7 +180,27 @@ def calculate_avg_feedback_score(self, update=False): self.db_update() def calculate_final_score(self): - final_score = (flt(self.total_score) + flt(self.avg_feedback_score) + flt(self.self_score)) / 3 + final_score = 0 + appraisal_cycle_doc = frappe.get_cached_doc("Appraisal Cycle", self.appraisal_cycle) + + formula = appraisal_cycle_doc.final_score_formula + based_on_formula = appraisal_cycle_doc.calculate_final_score_based_on_formula + + if based_on_formula: + employee_doc = frappe.get_cached_doc("Employee", self.employee) + data = { + "goal_score": flt(self.total_score), + "average_feedback_score": flt(self.avg_feedback_score), + "self_appraisal_score": flt(self.self_score), + } + data.update(appraisal_cycle_doc.as_dict()) + data.update(employee_doc.as_dict()) + data.update(self.as_dict()) + + sanitized_formula = sanitize_expression(formula) + final_score = frappe.safe_eval(sanitized_formula, data) + else: + final_score = (flt(self.total_score) + flt(self.avg_feedback_score) + flt(self.self_score)) / 3 self.final_score = flt(final_score, self.precision("final_score")) diff --git a/hrms/hr/doctype/appraisal/test_appraisal.py b/hrms/hr/doctype/appraisal/test_appraisal.py index 029b6c52bd..bff9a04db1 100644 --- a/hrms/hr/doctype/appraisal/test_appraisal.py +++ b/hrms/hr/doctype/appraisal/test_appraisal.py @@ -69,7 +69,26 @@ def test_manual_kra_rating(self): def test_final_score(self): cycle = create_appraisal_cycle(designation="Engineer", kra_evaluation_method="Manual Rating") cycle.create_appraisals() + appraisal = self.setup_appraisal(cycle) + self.assertEqual(appraisal.final_score, 3.767) + + def test_final_score_using_formula(self): + cycle = create_appraisal_cycle(designation="Engineer", kra_evaluation_method="Manual Rating") + cycle.update( + { + "calculate_final_score_based_on_formula": 1, + "final_score_formula": "(goal_score + self_appraisal_score + average_feedback_score)/3 if self_appraisal_score else (goal_score + self_appraisal_score)/2", + } + ) + cycle.save() + cycle.create_appraisals() + + appraisal = self.setup_appraisal(cycle) + + self.assertEqual(appraisal.final_score, 3.767) + + def setup_appraisal(self, cycle): appraisal = frappe.db.exists("Appraisal", {"appraisal_cycle": cycle.name, "employee": self.employee1}) appraisal = frappe.get_doc("Appraisal", appraisal) @@ -97,7 +116,8 @@ def test_final_score(self): feedback.submit() appraisal.reload() - self.assertEqual(appraisal.final_score, 3.767) + + return appraisal def test_goal_score(self): """ diff --git a/hrms/hr/doctype/appraisal_cycle/appraisal_cycle.js b/hrms/hr/doctype/appraisal_cycle/appraisal_cycle.js index b342030618..dccae27988 100644 --- a/hrms/hr/doctype/appraisal_cycle/appraisal_cycle.js +++ b/hrms/hr/doctype/appraisal_cycle/appraisal_cycle.js @@ -13,6 +13,7 @@ frappe.ui.form.on("Appraisal Cycle", { frm.trigger("show_custom_buttons"); frm.trigger("show_appraisal_summary"); + frm.trigger("set_autocompletions_for_final_score_formula"); }, show_custom_buttons(frm) { @@ -26,28 +27,42 @@ frappe.ui.form.on("Appraisal Cycle", { frappe.set_route("Tree", "Goal"); }); - let className = ""; let appraisals_created = frm.doc.__onload?.appraisals_created; if (frm.doc.status !== "Completed") { - className = appraisals_created ? "btn-default" : "btn-primary"; - - frm.add_custom_button(__("Create Appraisals"), () => { - frm.trigger("create_appraisals"); - }).addClass(className); + if (appraisals_created) { + frm.add_custom_button(__("Create Appraisals"), () => { + frm.trigger("create_appraisals"); + }); + } else { + frm.page.set_primary_action(__("Create Appraisals"), () => { + frm.trigger("create_appraisals"); + }); + } } - className = appraisals_created ? "btn-primary" : "btn-default"; - if (frm.doc.status === "Not Started") { - frm.add_custom_button(__("Start"), () => { - frm.set_value("status", "In Progress"); - frm.save(); - }).addClass(className); + if (appraisals_created) { + frm.page.set_primary_action(__("Start"), () => { + frm.set_value("status", "In Progress"); + frm.save(); + }); + } else { + frm.add_custom_button(__("Start"), () => { + frm.set_value("status", "In Progress"); + frm.save(); + }); + } } else if (frm.doc.status === "In Progress") { - frm.add_custom_button(__("Mark as Completed"), () => { - frm.trigger("complete_cycle"); - }).addClass(className); + if (appraisals_created) { + frm.page.set_primary_action(__("Mark as Completed"), () => { + frm.trigger("complete_cycle"); + }); + } else { + frm.add_custom_button(__("Mark as Completed"), () => { + frm.trigger("complete_cycle"); + }); + } } else if (frm.doc.status === "Completed") { frm.add_custom_button(__("Mark as In Progress"), () => { frm.set_value("status", "In Progress"); @@ -56,6 +71,42 @@ frappe.ui.form.on("Appraisal Cycle", { } }, + set_autocompletions_for_final_score_formula: async (frm) => { + const autocompletions = [ + { + value: "goal_score", + score: 10, + meta: __("Total Goal Score"), + }, + { + value: "average_feedback_score", + score: 10, + meta: __("Average Feedback Score"), + }, + { + value: "self_appraisal_score", + score: 10, + meta: __("Self Appraisal Score"), + }, + ]; + + await Promise.all( + ["Employee", "Appraisal Cycle", "Appraisal"].map((doctype) => + frappe.model.with_doctype(doctype, () => { + autocompletions.push( + ...frappe.get_meta(doctype).fields.map((f) => ({ + value: f.fieldname, + score: 8, + meta: __("{0} Field", [doctype]), + })), + ); + }), + ), + ); + + frm.set_df_property("final_score_formula", "autocompletions", autocompletions); + }, + get_employees(frm) { frappe.call({ method: "set_employees", diff --git a/hrms/hr/doctype/appraisal_cycle/appraisal_cycle.json b/hrms/hr/doctype/appraisal_cycle/appraisal_cycle.json index 4a376ef75f..89a1afd742 100644 --- a/hrms/hr/doctype/appraisal_cycle/appraisal_cycle.json +++ b/hrms/hr/doctype/appraisal_cycle/appraisal_cycle.json @@ -17,7 +17,11 @@ "section_break_4", "description", "settings_section", + "column_break_vhzx", "kra_evaluation_method", + "section_break_zykh", + "calculate_final_score_based_on_formula", + "final_score_formula", "applicable_for_tab", "filters_section", "branch", @@ -154,6 +158,31 @@ "label": "Status", "options": "Not Started\nIn Progress\nCompleted", "read_only": 1 + }, + { + "depends_on": "calculate_final_score_based_on_formula", + "fieldname": "final_score_formula", + "fieldtype": "Code", + "label": "Final Score Formula", + "mandatory_depends_on": "calculate_final_score_based_on_formula", + "max_height": "5rem", + "options": "PythonExpression" + }, + { + "default": "0", + "description": "By default, the Final Score is calculated as the average of Goal Score, Feedback Score, and Self Appraisal Score. Enable this to set a different formula", + "fieldname": "calculate_final_score_based_on_formula", + "fieldtype": "Check", + "label": "Calculate Final Score based on Formula" + }, + { + "fieldname": "column_break_vhzx", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_zykh", + "fieldtype": "Section Break", + "hide_border": 1 } ], "index_web_pages_for_search": 1, @@ -171,7 +200,7 @@ "link_fieldname": "appraisal_cycle" } ], - "modified": "2023-03-29 12:28:36.247120", + "modified": "2024-05-29 18:15:06.443594", "modified_by": "Administrator", "module": "HR", "name": "Appraisal Cycle",