From 76541b6b52d6788ed109c368256d9c599af4a58b Mon Sep 17 00:00:00 2001 From: Viny Selopal Date: Thu, 18 Apr 2024 17:45:40 +0530 Subject: [PATCH] feat: allow adding formula to calculate final score in appraisal cycle (cherry picked from commit 1712990dea952734fd5cf13b37a8356466f4392c) # Conflicts: # hrms/hr/doctype/appraisal_cycle/appraisal_cycle.json --- hrms/hr/doctype/appraisal/appraisal.py | 32 ++++++++++++++-- hrms/hr/doctype/appraisal/test_appraisal.py | 20 +++++++++- .../appraisal_cycle/appraisal_cycle.js | 38 +++++++++++++++++++ .../appraisal_cycle/appraisal_cycle.json | 25 ++++++++++++ .../appraisal_cycle/test_appraisal_cycle.py | 8 ++++ hrms/public/js/hrms.bundle.js | 1 + 6 files changed, 119 insertions(+), 5 deletions(-) diff --git a/hrms/hr/doctype/appraisal/appraisal.py b/hrms/hr/doctype/appraisal/appraisal.py index 9ea9b1b9d4..4950e894ad 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,9 +180,34 @@ 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 - - self.final_score = flt(final_score, self.precision("final_score")) + if self.appraisal_cycle: + final_score = 0 + appraisal_cycle_doc = frappe.get_doc("Appraisal Cycle", self.appraisal_cycle) + employee_doc = frappe.get_doc("Employee", self.employee) + + formula = appraisal_cycle_doc.final_score_formula + based_on_formula = appraisal_cycle_doc.calculate_final_score_based_on_formula + + if not based_on_formula == 1: + final_score = ( + flt(self.total_score) + flt(self.avg_feedback_score) + flt(self.self_score) + ) / 3 + else: + sanitized_formula = sanitize_expression(formula) + + 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()) + + final_score = frappe.safe_eval(sanitized_formula, data) + + self.final_score = flt(final_score, self.precision("final_score")) @frappe.whitelist() def add_feedback(self, feedback, feedback_ratings): diff --git a/hrms/hr/doctype/appraisal/test_appraisal.py b/hrms/hr/doctype/appraisal/test_appraisal.py index 029b6c52bd..66e5eae137 100644 --- a/hrms/hr/doctype/appraisal/test_appraisal.py +++ b/hrms/hr/doctype/appraisal/test_appraisal.py @@ -67,9 +67,24 @@ def test_manual_kra_rating(self): self.assertEqual(appraisal.final_score, 1.2) def test_final_score(self): - cycle = create_appraisal_cycle(designation="Engineer", kra_evaluation_method="Manual Rating") + cycle = create_appraisal_cycle( + designation="Engineer", kra_evaluation_method="Manual Rating", based_on_formula=0 + ) + cycle.create_appraisals() + appraisal = self.setup_appraisal_cycle(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", based_on_formula=1 + ) cycle.create_appraisals() + appraisal = self.setup_appraisal_cycle(cycle) + + self.assertEqual(appraisal.final_score, 3.8) + def setup_appraisal_cycle(self, cycle): appraisal = frappe.db.exists("Appraisal", {"appraisal_cycle": cycle.name, "employee": self.employee1}) appraisal = frappe.get_doc("Appraisal", appraisal) @@ -97,7 +112,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..f8a9dc6c0e 100644 --- a/hrms/hr/doctype/appraisal_cycle/appraisal_cycle.js +++ b/hrms/hr/doctype/appraisal_cycle/appraisal_cycle.js @@ -3,6 +3,44 @@ frappe.ui.form.on("Appraisal Cycle", { refresh(frm) { + async function set_autocompletions_for_final_score_formula(frm) { + const autocompletions = [ + { + value: "goal_score", + score: 8, + meta: "Goal field", + }, + { + value: "average_feedback_score", + score: 8, + meta: "Appraisal field", + }, + { + value: "self_appraisal_score", + score: 8, + meta: "Appraisal field", + }, + ]; + + const doctypes = ["Employee", "Appraisal Cycle"]; + + await Promise.all( + doctypes.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); + } + + set_autocompletions_for_final_score_formula(frm); frm.set_query("department", () => { return { filters: { diff --git a/hrms/hr/doctype/appraisal_cycle/appraisal_cycle.json b/hrms/hr/doctype/appraisal_cycle/appraisal_cycle.json index 4a376ef75f..41aa3c6173 100644 --- a/hrms/hr/doctype/appraisal_cycle/appraisal_cycle.json +++ b/hrms/hr/doctype/appraisal_cycle/appraisal_cycle.json @@ -17,6 +17,9 @@ "section_break_4", "description", "settings_section", + "calculate_final_score_based_on_formula", + "final_score_formula", + "column_break_wpgg", "kra_evaluation_method", "applicable_for_tab", "filters_section", @@ -154,6 +157,24 @@ "label": "Status", "options": "Not Started\nIn Progress\nCompleted", "read_only": 1 + }, + { + "depends_on": "eval:doc.calculate_final_score_based_on_formula === 1;", + "fieldname": "final_score_formula", + "fieldtype": "Code", + "label": "Final Score Formula", + "max_height": "5rem", + "options": "PythonExpression" + }, + { + "default": "0", + "fieldname": "calculate_final_score_based_on_formula", + "fieldtype": "Check", + "label": "Calculate final score based on formula" + }, + { + "fieldname": "column_break_wpgg", + "fieldtype": "Column Break" } ], "index_web_pages_for_search": 1, @@ -171,7 +192,11 @@ "link_fieldname": "appraisal_cycle" } ], +<<<<<<< HEAD "modified": "2023-03-29 12:28:36.247120", +======= + "modified": "2024-04-16 13:15:55.493958", +>>>>>>> 1712990de (feat: allow adding formula to calculate final score in appraisal cycle) "modified_by": "Administrator", "module": "HR", "name": "Appraisal Cycle", diff --git a/hrms/hr/doctype/appraisal_cycle/test_appraisal_cycle.py b/hrms/hr/doctype/appraisal_cycle/test_appraisal_cycle.py index 6ac8268edc..7b82b2c4fa 100644 --- a/hrms/hr/doctype/appraisal_cycle/test_appraisal_cycle.py +++ b/hrms/hr/doctype/appraisal_cycle/test_appraisal_cycle.py @@ -59,6 +59,8 @@ def create_appraisal_cycle(**args): if frappe.db.exists("Appraisal Cycle", name): frappe.delete_doc("Appraisal Cycle", name, force=True) + based_on_formula = args.based_on_formula + appraisal_cycle = frappe.get_doc( { "doctype": "Appraisal Cycle", @@ -66,8 +68,14 @@ def create_appraisal_cycle(**args): "company": args.company or "_Test Appraisal", "start_date": args.start_date or "2022-01-01", "end_date": args.end_date or "2022-03-31", + "calculate_final_score_based_on_formula": based_on_formula, } ) + if based_on_formula: + appraisal_cycle.final_score_formula = ( + args.final_score_formula + or "goal_score * 0.2 + self_appraisal_score * 0.2 + average_feedback_score * 0.6" + ) if args.kra_evaluation_method: appraisal_cycle.kra_evaluation_method = args.kra_evaluation_method diff --git a/hrms/public/js/hrms.bundle.js b/hrms/public/js/hrms.bundle.js index f53beddd87..d366ad7504 100644 --- a/hrms/public/js/hrms.bundle.js +++ b/hrms/public/js/hrms.bundle.js @@ -5,3 +5,4 @@ import "./templates/rating.html"; import "./utils"; import "./utils/payroll_utils"; import "./utils/leave_utils"; +import "./utils/appraisal_utils";