From 14e725be4fb54997e18052cb9820f53d0691e3de Mon Sep 17 00:00:00 2001 From: seshikanth Date: Tue, 9 Jun 2026 13:31:02 +0530 Subject: [PATCH] #fix: Employee Performance Management Module --- .../Wizard/__init__.py | 3 + .../Wizard/cancel_wizard_hr.py | 63 ++ .../Wizard/cancel_wizard_hr.xml | 25 + .../Wizard/postpone_hr_appraisal.py | 70 ++ .../Wizard/postpone_hr_appraisal.xml | 23 + .../hrms_employee_appraisal/__init__.py | 2 + .../hrms_employee_appraisal/__manifest__.py | 37 + .../models/__init__.py | 4 + .../models/apprasial_conf.py | 162 +++++ .../models/employee_appraisal.py | 644 ++++++++++++++++++ .../models/hr_notice_appraisal.py | 265 +++++++ .../hrms_employee_appraisal/models/kpi_kra.py | 414 +++++++++++ .../security/ir.model.access.csv | 29 + .../static/description/icon.png | Bin 0 -> 6044 bytes .../static/src/css/wizard.css | 12 + .../static/src/img/download.png | Bin 0 -> 6044 bytes .../views/employee_appraisal.xml | 247 +++++++ .../views/employee_evalutor.xml | 90 +++ .../views/employee_template_appraisal.xml | 166 +++++ .../views/hr_notice_appraisal.xml | 102 +++ .../views/stage_config.xml | 40 ++ 21 files changed, 2398 insertions(+) create mode 100644 addons_extensions/hrms_employee_appraisal/Wizard/__init__.py create mode 100644 addons_extensions/hrms_employee_appraisal/Wizard/cancel_wizard_hr.py create mode 100644 addons_extensions/hrms_employee_appraisal/Wizard/cancel_wizard_hr.xml create mode 100644 addons_extensions/hrms_employee_appraisal/Wizard/postpone_hr_appraisal.py create mode 100644 addons_extensions/hrms_employee_appraisal/Wizard/postpone_hr_appraisal.xml create mode 100644 addons_extensions/hrms_employee_appraisal/__init__.py create mode 100644 addons_extensions/hrms_employee_appraisal/__manifest__.py create mode 100644 addons_extensions/hrms_employee_appraisal/models/__init__.py create mode 100644 addons_extensions/hrms_employee_appraisal/models/apprasial_conf.py create mode 100644 addons_extensions/hrms_employee_appraisal/models/employee_appraisal.py create mode 100644 addons_extensions/hrms_employee_appraisal/models/hr_notice_appraisal.py create mode 100644 addons_extensions/hrms_employee_appraisal/models/kpi_kra.py create mode 100644 addons_extensions/hrms_employee_appraisal/security/ir.model.access.csv create mode 100644 addons_extensions/hrms_employee_appraisal/static/description/icon.png create mode 100644 addons_extensions/hrms_employee_appraisal/static/src/css/wizard.css create mode 100644 addons_extensions/hrms_employee_appraisal/static/src/img/download.png create mode 100644 addons_extensions/hrms_employee_appraisal/views/employee_appraisal.xml create mode 100644 addons_extensions/hrms_employee_appraisal/views/employee_evalutor.xml create mode 100644 addons_extensions/hrms_employee_appraisal/views/employee_template_appraisal.xml create mode 100644 addons_extensions/hrms_employee_appraisal/views/hr_notice_appraisal.xml create mode 100644 addons_extensions/hrms_employee_appraisal/views/stage_config.xml diff --git a/addons_extensions/hrms_employee_appraisal/Wizard/__init__.py b/addons_extensions/hrms_employee_appraisal/Wizard/__init__.py new file mode 100644 index 000000000..bbc03c83f --- /dev/null +++ b/addons_extensions/hrms_employee_appraisal/Wizard/__init__.py @@ -0,0 +1,3 @@ +from . import cancel_wizard_hr +from . import postpone_hr_appraisal + diff --git a/addons_extensions/hrms_employee_appraisal/Wizard/cancel_wizard_hr.py b/addons_extensions/hrms_employee_appraisal/Wizard/cancel_wizard_hr.py new file mode 100644 index 000000000..7ed849b21 --- /dev/null +++ b/addons_extensions/hrms_employee_appraisal/Wizard/cancel_wizard_hr.py @@ -0,0 +1,63 @@ +from odoo import models, fields + + +class AppraisalCancelWizard(models.TransientModel): + _name = 'appraisal.cancel.wizard' + _description = 'Cancel Appraisal Wizard' + + reason = fields.Text( + string="Reason", + required=True + ) + + def action_confirm_cancel(self): + + notice = self.env['hr.notice.appraisal'].browse( + self.env.context.get('active_id') + ) + notice.write({ + 'state': 'cancelled', + 'cancel_reason': self.reason, + 'cancelled_date': fields.Datetime.now(), + 'cancelled_by_id': self.env.user.id, + }) + + notice.message_post( + body=f""" + Appraisal Cancelled
+ Reason: {self.reason} + """ + ) + employee_emails = notice.employee_ids.mapped('work_email') + manager_emails = notice.manager_ids.mapped('work_email') + + all_emails = employee_emails + manager_emails + + email_to = ",".join(filter(None, all_emails)) + + body_html = f""" +
+

Hello Team,

+

+ The appraisal has been cancelled. +

+
+ + + + + +
Reason{self.reason or ''}
+
+

Regards,

+

HR Team

+
+ """ + self.env['mail.mail'].create({ + 'subject': 'Appraisal Cancelled', + 'body_html': body_html, + 'email_to': email_to, + }).send() + return { + 'type': 'ir.actions.act_window_close' + } \ No newline at end of file diff --git a/addons_extensions/hrms_employee_appraisal/Wizard/cancel_wizard_hr.xml b/addons_extensions/hrms_employee_appraisal/Wizard/cancel_wizard_hr.xml new file mode 100644 index 000000000..a5adbeef0 --- /dev/null +++ b/addons_extensions/hrms_employee_appraisal/Wizard/cancel_wizard_hr.xml @@ -0,0 +1,25 @@ + + + + appraisal.cancel.wizard.form + appraisal.cancel.wizard + +
+ + + + +
+
+
+
+
+
+
\ No newline at end of file diff --git a/addons_extensions/hrms_employee_appraisal/Wizard/postpone_hr_appraisal.py b/addons_extensions/hrms_employee_appraisal/Wizard/postpone_hr_appraisal.py new file mode 100644 index 000000000..1737174cc --- /dev/null +++ b/addons_extensions/hrms_employee_appraisal/Wizard/postpone_hr_appraisal.py @@ -0,0 +1,70 @@ +from odoo import api, fields, models, tools + +class AppraisalPostponeWizard(models.TransientModel): + _name = 'appraisal.postpone.wizard' + _description = 'Appraisal Postpone Wizard' + + reason = fields.Text(string="Reason",required=True) + new_start_date = fields.Datetime(string="New Start Date") + new_end_date = fields.Datetime(string="New End Date") + + def action_confirm_postpone(self): + notice = self.env['hr.notice.appraisal'].browse( + self.env.context.get('active_id') + ) + notice.write({ + 'state': 'postponed', + 'postpone_reason': self.reason, + 'postponed_date': fields.Datetime.now(), + 'postponed_by_id': self.env.user.id, + 'new_start_date': self.new_start_date, + 'new_end_date': self.new_end_date, + }) + notice.message_post( + body=f""" + Appraisal Postponed
+ Reason: {self.reason} + """ + ) + employee_emails = notice.employee_ids.mapped('work_email') + manager_emails = notice.manager_ids.mapped('work_email') + + all_emails = employee_emails + manager_emails + + email_to = ",".join(filter(None, all_emails)) + + body_html = f""" +
+

Hello Team,

+

+ The appraisal has been postponed. +

+
+ + + + + + + + + + + + + +
Reason{self.reason or ''}
New Start Date{self.new_start_date or ''}
New End Date{self.new_end_date or ''}
+
+

Regards,

+

HR Team

+
+ """ + self.env['mail.mail'].create({ + 'subject': 'Appraisal Postponed', + 'body_html': body_html, + 'email_to': email_to, + }).send() + + return { + 'type': 'ir.actions.act_window_close' + } \ No newline at end of file diff --git a/addons_extensions/hrms_employee_appraisal/Wizard/postpone_hr_appraisal.xml b/addons_extensions/hrms_employee_appraisal/Wizard/postpone_hr_appraisal.xml new file mode 100644 index 000000000..dd3178096 --- /dev/null +++ b/addons_extensions/hrms_employee_appraisal/Wizard/postpone_hr_appraisal.xml @@ -0,0 +1,23 @@ + + + + appraisal.postpone.wizard.form + appraisal.postpone.wizard + +
+ + + + + +
+
+
+
+
+
+
\ No newline at end of file diff --git a/addons_extensions/hrms_employee_appraisal/__init__.py b/addons_extensions/hrms_employee_appraisal/__init__.py new file mode 100644 index 000000000..f6ee30af8 --- /dev/null +++ b/addons_extensions/hrms_employee_appraisal/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import Wizard \ No newline at end of file diff --git a/addons_extensions/hrms_employee_appraisal/__manifest__.py b/addons_extensions/hrms_employee_appraisal/__manifest__.py new file mode 100644 index 000000000..730887e30 --- /dev/null +++ b/addons_extensions/hrms_employee_appraisal/__manifest__.py @@ -0,0 +1,37 @@ +{ + 'name': 'PERFORMANCE MANAGEMENT', + 'version': '18.0.1.0.0', + 'category': 'PERFORMANCE MANAGEMENT', + 'summary': 'Manages the Employee Appraisal process of the employees', + 'description': """ + This module helps to create and approve/reject employee appraisal + """, + 'author': 'Seshi Kanth D', + 'company': 'FTPROTECH', + 'website': 'https://www.ftprotech.in', + + 'depends': ['base', 'hr','hr_employee_extended'], + + 'data': [ + 'security/ir.model.access.csv', + 'views/employee_appraisal.xml', + 'views/employee_evalutor.xml', + 'views/employee_template_appraisal.xml', + 'views/hr_notice_appraisal.xml', + 'views/stage_config.xml', + 'Wizard/cancel_wizard_hr.xml', + 'Wizard/postpone_hr_appraisal.xml', + ], + +'assets': { + 'web.assets_backend': [ + 'hrms_employee_appraisal/static/src/css/wizard.css' + ], + }, + + 'license': 'LGPL-3', + + 'installable': True, + 'auto_install': False, + 'application': True, +} \ No newline at end of file diff --git a/addons_extensions/hrms_employee_appraisal/models/__init__.py b/addons_extensions/hrms_employee_appraisal/models/__init__.py new file mode 100644 index 000000000..464c14494 --- /dev/null +++ b/addons_extensions/hrms_employee_appraisal/models/__init__.py @@ -0,0 +1,4 @@ +from . import employee_appraisal +from . import apprasial_conf +from . import kpi_kra +from . import hr_notice_appraisal diff --git a/addons_extensions/hrms_employee_appraisal/models/apprasial_conf.py b/addons_extensions/hrms_employee_appraisal/models/apprasial_conf.py new file mode 100644 index 000000000..97a005c30 --- /dev/null +++ b/addons_extensions/hrms_employee_appraisal/models/apprasial_conf.py @@ -0,0 +1,162 @@ +from odoo import api, fields, models + +class AppraisalYear(models.Model): + _name = 'employee.appraisal.year' + _description = 'Employee Appraisal Year' + _order = "year_seq" + _rec_name = 'appraisal_name' + + year = fields.Integer(string="Performance Period") + appraisal_name_id = fields.Char(string="Appraisal Type") + appraisal_name = fields.Char(string="Appraisal Name") + start_month = fields.Datetime('Starting Month') + end_month = fields.Datetime('End Month') + active = fields.Boolean(default=True) + year_seq = fields.Integer(string="Sequence") + appraisal_type_id = fields.Many2one( + 'employee.appraisal.type', + string="Appraisal Type" + ) + +class EmployeeAppraisalType(models.Model): + _name = 'employee.appraisal.type' + _description = 'Employee Appraisal Type' + + name = fields.Char(required=True) + seq = fields.Integer(string="Sequence") + + + +class AppraisalTemplate(models.Model): + _name = 'employee.appraisal.template' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _description = 'Employee Appraisal Template' + _order = "seq desc" + _rec_name = 'seq' + + name = fields.Char(string="Name") + employee_evaluator_name_id = fields.Many2one('employee.appraisal.evaluator', string="Employee Appraisal Evaluator") + employee_eva_id = fields.Many2one('hr.employee',string="Employee Appraisal Evaluator") + hr_employee_id = fields.Many2one('hr.employee',string="Employee HR Employee") + employee_department_id = fields.Many2one('hr.department',string="Department") + company_id = fields.Many2one('res.company', string="Company",default=lambda self: self.env.company) + email_notify_user_ids = fields.Many2many("res.users", string='Notify Users if New Evaluation Submited and Confirmed') + hr_email_notify = fields.Boolean("Send Email if HR APPROVE or REJECT Evaluation") + template_rating_bool = fields.Boolean() + template_point_bool = fields.Boolean() + stage_config_ids = fields.Many2many('employee.stage.config',string='Stages') + employee_state = fields.Selection([ + ('new', 'New'), + ('sent', 'sent'), + ], string='Status', copy=False, tracking=1, default='new') + kra_ids = fields.One2many('employee.appraisal.kra','template_appraisal_id',string="Key Result Areas",copy=True,tracking=True) + kra_weightage = fields.Integer(compute="_compute_total_kra_weightage",string="Total KRA Weightage") + notice_id = fields.Many2one('hr.notice.appraisal',string="Notice") + start_date = fields.Date(string="Start Date") + end_date = fields.Date(string="End Date") + is_readonly = fields.Boolean(compute="_compute_is_readonly") + mail_sent_employee_ids = fields.Many2many('hr.employee',string="Mail Sent Employees") + seq = fields.Char(string="Sequence",readonly=True,copy=False) + employee_ids = fields.Many2many('hr.employee','appraisal_employee_rel','appraisal_id','employee_id',string="Employees") + manager_ids = fields.Many2many('hr.employee','appraisal_manager_rel','appraisal_id','manager_id',string="Managers") + appraisal_period_id = fields.Many2one('employee.appraisal.year') + appraisal_period_type_id = fields.Many2one('employee.appraisal.type') + + @api.depends('kra_ids.kra_weightage') + def _compute_total_kra_weightage(self): + for rec in self: + rec.kra_weightage = sum( + rec.kra_ids.mapped('kra_weightage')) + + def action_sent_employee(self): + appraisal_config_obj = self.env['employee.appraisal.template.config'] + for rec in self: + first_stage = rec.stage_config_ids.sorted( + key=lambda s: s.seq + )[:1] + for employee in rec.employee_ids: + already_exists = appraisal_config_obj.search([ + ('template_id', '=', rec.id), + ('employee_appraisal_id', '=', employee.id) + ], limit=1) + if already_exists: + continue + appraisal = appraisal_config_obj.create({ + 'template_id': rec.id, + 'seq': rec.seq, + 'employee_appraisal_id': employee.id, + 'employee_ids': [(6, 0, rec.employee_ids.ids)], + 'manager_ids': [(6, 0, rec.manager_ids.ids)], + 'notice_id': rec.notice_id.id, + 'start_date': rec.start_date, + 'end_date': rec.end_date, + 'appraisal_period_id': rec.appraisal_period_id.id, + 'hr_apprai_id': rec.hr_employee_id.id, + 'managerapp_id': rec.employee_eva_id.id, + 'template_empl_rating_bool': rec.template_rating_bool, + 'template_empl_point_bool': rec.template_point_bool, + 'available_stage_ids': [ + (6, 0, rec.stage_config_ids.ids) + ], + 'stage_id': first_stage.id if first_stage else False, + # 'state': 'self_evaluation', + }) + appraisal._onchange_template_id() + employee_emails = rec.employee_ids.mapped('work_email') + manager_emails = rec.manager_ids.mapped('work_email') + all_emails = employee_emails + manager_emails + email_to = ",".join(filter(None, all_emails)) + body_html = f""" +
+

Hello Team,

+

+ Employee appraisal process has been initiated. +

+
+ + + + + + + + + + + + + + + + + +
Reference{rec.seq or ''}
Appraisal Template{rec.name or ''}
Start Date{rec.start_date or ''}
End Date{rec.end_date or ''}
+
+

+ Please complete the appraisal within timeline. +

+
+

+ Regards, +

+

+ Team Manager +

+
+ """ + ctx = { + 'default_model': 'employee.appraisal.template', + 'default_res_ids': [rec.id], + 'default_composition_mode': 'comment', + 'default_email_to': email_to, + 'default_subject': f'Employee Appraisal - {rec.seq}', + 'default_body': body_html, + 'mark_appraisal_sent_appraisal': True, + } + return { + 'type': 'ir.actions.act_window', + 'res_model': 'mail.compose.message', + 'view_mode': 'form', + 'target': 'new', + 'context': ctx, + } diff --git a/addons_extensions/hrms_employee_appraisal/models/employee_appraisal.py b/addons_extensions/hrms_employee_appraisal/models/employee_appraisal.py new file mode 100644 index 000000000..6ce8312bb --- /dev/null +++ b/addons_extensions/hrms_employee_appraisal/models/employee_appraisal.py @@ -0,0 +1,644 @@ +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError + +RATING_VALUATION = { + '1': 'unsatisfactory', + '2': 'needs_improvements', + '3': 'meet_expectation', + '4': 'exceed_expectation', + '5': 'outstanding', +} + + +class EmployeeAppraisal(models.Model): + _name = 'employee.appraisal.template.config' + _description = 'Employee Appraisal' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _order = "tracking_date desc" + _rec_name = 'template_id' + + name = fields.Char(string="Reference", copy=False) + employee_evaluator_name_id = fields.Many2one('employee.appraisal.evaluator', string="Employee Appraisal Evaluator") + hr_apprai_id = fields.Many2one('hr.employee') + managerapp_id = fields.Many2one('hr.employee') + performance_evaluator = fields.Selection([('manager', 'Manager'), ('colleague', 'Colleague'), ('own', 'Own')]) + template_id = fields.Many2one('employee.appraisal.template', string="Template") + stage_id = fields.Many2one('employee.stage.config',string='Stage') + available_stage_ids = fields.Many2many('employee.stage.config',compute='_compute_available_stages') + tracking_date = fields.Datetime(default=fields.Datetime.now, readonly=True, index=True) + company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.company) + state = fields.Selection([ + ('new', 'New'), + # ('draft', 'Draft'), + ('self_evaluation', 'Self Evaluation'), + ('colleague_manager', 'colleague Feedback & Manager Evaluation'), + # ('in_progress', 'Manager Evaluation'), + ('hr_evaluation', 'HR Evaluation'), + ('finance_team', 'Finance Evaluation'), + ('management_team', 'Management Evaluation'), + ('done', 'Completed'), + ('reject', 'Rejected') + ], string='Status', copy=False, tracking=1, default='new') + appraisal_period_id = fields.Many2one('employee.appraisal.year') + employee_appraisal_id = fields.Many2one('hr.employee') + image_1920 = fields.Image(related='employee_appraisal_id.image_1920') + employee_code = fields.Char(string='Employee Code', related='employee_appraisal_id.employee_id', store=True) + department_appraisal_id = fields.Many2one("hr.department", string="Department",related="employee_appraisal_id.department_id", store=True) + job_appraisal_id = fields.Many2one("hr.job", string="Job Position", related="employee_appraisal_id.job_id",store=True) + kra_line_ids = fields.One2many('employee.appraisal.kra.line', 'config_id', string='Kra') + manager_remarks = fields.Char(string="Manager Remarks") + hr_remarks = fields.Char(string="HR Remarks") + colleague_feed_ids = fields.One2many('colleague.feedback', 'employee_appraisal_feed_id', 'Colleague Feed Back') + created_by_id = fields.Many2one('hr.employee', string="Created By", default=lambda self: self.env.user.employee_id,readonly=True) + created_user_id = fields.Many2one('res.users', default=lambda self: self.env.user, readonly=True) + creator_email = fields.Char(related='created_by_id.work_email', string="Creator Email", readonly=True) + notice_id = fields.Many2one('hr.notice.appraisal', string="Notice") + start_date = fields.Date(string="Start Date") + end_date = fields.Date(string="End Date") + college_end_date = fields.Datetime(string="Colleges End Date") + college_end_date_time = fields.Datetime(string="Colleague End Date") + is_readonly = fields.Boolean(compute="_compute_is_readonly") + mail_sent_employee_ids = fields.Many2many('hr.employee', string="Mail Sent Employees") + seq = fields.Char(string="Sequence", readonly=True, copy=False) + employee_ids = fields.Many2many('hr.employee', 'appraisal_config_employee_rel', 'config_id', 'employee_id',string="Employees") + manager_ids = fields.Many2many('hr.employee', 'appraisal_config_manager_rel', 'config_id', 'manager_id',string="Managers") + total_employee_score = fields.Float(string="Employee Total Points", compute="_compute_total_scores", store=True) + total_manager_score = fields.Float(string="Manager Total Points", compute="_compute_total_scores", store=True) + total_hr_score = fields.Float(string="HR Total Points", compute="_compute_total_scores", store=True) + manager_email = fields.Char(compute="_compute_manager_email", string="Manager Email") + Note_appraisal = fields.Char(string="User Note", default="Please click KRA's Name, To open the KPI's",Readonly=True) + overall_score = fields.Float(compute="_compute_overall_scores", store=True) + overall_rating = fields.Selection([ + ('0', '0'), + ('1', '1'), + ('2', '2'), + ('3', '3'), + ('4', '4'), + ('5', '5'), + ], string="Overall Rating") + overall_rating_value = fields.Selection([ + ('unsatisfactory', 'Unsatisfactory'), + ('needs_improvements', 'Needs Improvements'), + ('meet_expectation', 'Meets Expectation'), + ('exceed_expectation', 'Exceeds Expectation'), + ('outstanding', 'Outstanding'), + ], string="Overall Rating Value") + overall_rating_star = fields.Selection([ + ('0', '0'), + ('1', '1'), + ('2', '2'), + ('3', '3'), + ('4', '4'), + ('5', '5'), + ], string="Overall Stars") + employee_overall_score = fields.Float(compute='_compute_overall_scores',store=True) + manager_overall_score = fields.Float(compute='_compute_overall_scores',store=True) + hr_overall_score = fields.Float(compute='_compute_overall_scores',store=True) + employee_overall_star = fields.Selection([ + ('0', '0'), + ('1', '1'), + ('2', '2'), + ('3', '3'), + ('4', '4'), + ('5', '5'), + ], compute='_compute_overall_scores', store=True) + manager_overall_star = fields.Selection([ + ('0', '0'), + ('1', '1'), + ('2', '2'), + ('3', '3'), + ('4', '4'), + ('5', '5'), + ], compute='_compute_overall_scores', store=True) + hr_overall_star = fields.Selection([ + ('0', '0'), + ('1', '1'), + ('2', '2'), + ('3', '3'), + ('4', '4'), + ('5', '5'), + ], compute='_compute_overall_scores', store=True) + finance_user_id = fields.Many2one('res.users', string="Finance Approved By", readonly=True) + current_salary = fields.Float(string="Current Salary") + appraisal_percentage = fields.Float(string="Appraisal %") + appraisal_amount = fields.Float(string="Appraisal Amount") + new_salary = fields.Float(string="Revised Salary", compute="_compute_new_salary", store=True) + finance_remarks = fields.Text(string="Finance Remarks") + finance_date = fields.Datetime(string="Finance Approval Date", readonly=True) + template_empl_rating_bool = fields.Boolean('Star Rating') + template_empl_point_bool = fields.Boolean('Point Rating') + + @api.depends('template_id') + def _compute_available_stages(self): + for rec in self: + rec.available_stage_ids = rec.template_id.stage_config_ids + + @api.depends( + 'kra_line_ids.kpi_line_ids.rating_star', + 'kra_line_ids.kpi_line_ids.manager_rating_star', + 'kra_line_ids.kpi_line_ids.hr_rating_star', + 'kra_line_ids.kpi_line_ids.employee_score', + 'kra_line_ids.kpi_line_ids.manager_score', + 'kra_line_ids.kpi_line_ids.hr_score', + ) + def _compute_overall_scores(self): + for rec in self: + + employee_score = 0.0 + manager_score = 0.0 + hr_score = 0.0 + + # ================================== + # STAR RATING MODE + # ================================== + if rec.template_empl_rating_bool: + + employee_ratings = [ + int(kpi.rating_star) + for kra in rec.kra_line_ids + for kpi in kra.kpi_line_ids + if kpi.rating_star + ] + + manager_ratings = [ + int(kpi.manager_rating_star) + for kra in rec.kra_line_ids + for kpi in kra.kpi_line_ids + if kpi.manager_rating_star + ] + + hr_ratings = [ + int(kpi.hr_rating_star) + for kra in rec.kra_line_ids + for kpi in kra.kpi_line_ids + if kpi.hr_rating_star + ] + + employee_score = ( + sum(employee_ratings) / len(employee_ratings) + if employee_ratings else 0 + ) + + manager_score = ( + sum(manager_ratings) / len(manager_ratings) + if manager_ratings else 0 + ) + + hr_score = ( + sum(hr_ratings) / len(hr_ratings) + if hr_ratings else 0 + ) + + rec.employee_overall_score = round(employee_score, 2) + rec.manager_overall_score = round(manager_score, 2) + rec.hr_overall_score = round(hr_score, 2) + + rec.employee_overall_star = str(round(employee_score)) if employee_score else '0' + rec.manager_overall_star = str(round(manager_score)) if manager_score else '0' + rec.hr_overall_star = str(round(hr_score)) if hr_score else '0' + + # Final Overall + overall_avg = ( + employee_score + + manager_score + + hr_score + ) / 3 if (employee_score or manager_score or hr_score) else 0 + + rec.overall_score = round(overall_avg, 2) + + overall_star = str(round(overall_avg)) if overall_avg else '0' + + rec.overall_rating = overall_star + rec.overall_rating_star = overall_star + rec.overall_rating_value = RATING_VALUATION.get( + overall_star, + False + ) + + # ================================== + # POINT RATING MODE + # ================================== + elif rec.template_empl_point_bool: + + employee_scores = [ + kpi.employee_score + for kra in rec.kra_line_ids + for kpi in kra.kpi_line_ids + if kpi.employee_score is not False + ] + + manager_scores = [ + kpi.manager_score + for kra in rec.kra_line_ids + for kpi in kra.kpi_line_ids + if kpi.manager_score is not False + ] + + hr_scores = [ + kpi.hr_score + for kra in rec.kra_line_ids + for kpi in kra.kpi_line_ids + if kpi.hr_score is not False + ] + + employee_score = ( + sum(employee_scores) / len(employee_scores) + if employee_scores else 0 + ) + + manager_score = ( + sum(manager_scores) / len(manager_scores) + if manager_scores else 0 + ) + + hr_score = ( + sum(hr_scores) / len(hr_scores) + if hr_scores else 0 + ) + + rec.employee_overall_score = round(employee_score, 2) + rec.manager_overall_score = round(manager_score, 2) + rec.hr_overall_score = round(hr_score, 2) + + rec.employee_overall_star = '0' + rec.manager_overall_star = '0' + rec.hr_overall_star = '0' + + # Final Overall + overall_avg = ( + employee_score + + manager_score + + hr_score + ) / 3 if (employee_score or manager_score or hr_score) else 0 + + rec.overall_score = round(overall_avg, 2) + + if overall_avg <= 1: + rating = '1' + elif overall_avg <= 2: + rating = '2' + elif overall_avg <= 3: + rating = '3' + elif overall_avg <= 4: + rating = '4' + else: + rating = '5' + + rec.overall_rating = rating + rec.overall_rating_star = rating + rec.overall_rating_value = RATING_VALUATION.get( + rating, + False + ) + + # ================================== + # NO CONFIGURATION + # ================================== + else: + rec.employee_overall_score = 0 + rec.manager_overall_score = 0 + rec.hr_overall_score = 0 + + rec.employee_overall_star = '0' + rec.manager_overall_star = '0' + rec.hr_overall_star = '0' + + rec.overall_score = 0 + rec.overall_rating = '0' + rec.overall_rating_star = '0' + rec.overall_rating_value = False + + @api.onchange('overall_rating_value') + def _onchange_overall_rating_value(self): + for rec in self: + if rec.overall_rating_value == 'outstanding': + rec.appraisal_percentage = 20 + elif rec.overall_rating_value == 'exceed_expectation': + rec.appraisal_percentage = 15 + elif rec.overall_rating_value == 'meet_expectation': + rec.appraisal_percentage = 10 + elif rec.overall_rating_value == 'needs_improvements': + rec.appraisal_percentage = 5 + else: + rec.appraisal_percentage = 0 + + @api.depends('manager_ids') + def _compute_manager_email(self): + for rec in self: + emails = rec.manager_ids.mapped('work_email') + rec.manager_email = ",".join(filter(None, emails)) + + @api.depends('end_date') + def _compute_is_readonly(self): + today = fields.Date.today() + for rec in self: + if rec.end_date and today > rec.end_date: + rec.is_readonly = True + else: + rec.is_readonly = False + + @api.depends( + 'kra_line_ids.kpi_line_ids.rating_star', + 'kra_line_ids.kpi_line_ids.manager_rating_star', + 'kra_line_ids.kpi_line_ids.hr_rating_star', + 'kra_line_ids.kpi_line_ids.employee_score', + 'kra_line_ids.kpi_line_ids.manager_score', + 'kra_line_ids.kpi_line_ids.hr_score', + ) + def _compute_total_scores(self): + for rec in self: + employee_total = 0.0 + manager_total = 0.0 + hr_total = 0.0 + + for kra in rec.kra_line_ids: + for kpi in kra.kpi_line_ids: + + # Star Rating Mode + if rec.template_empl_rating_bool: + employee_total += float(kpi.rating_star or 0) + manager_total += float(kpi.manager_rating_star or 0) + hr_total += float(kpi.hr_rating_star or 0) + + # Point Rating Mode + elif rec.template_empl_point_bool: + employee_total += float(kpi.employee_score or 0) + manager_total += float(kpi.manager_score or 0) + hr_total += float(kpi.hr_score or 0) + + rec.total_employee_score = employee_total + rec.total_manager_score = manager_total + rec.total_hr_score = hr_total + + @api.depends('current_salary', 'appraisal_amount') + def _compute_new_salary(self): + for rec in self: + rec.new_salary = ( + rec.current_salary + + rec.appraisal_amount + ) + + @api.onchange('appraisal_percentage') + def _onchange_appraisal_percentage(self): + for rec in self: + if rec.current_salary: + rec.appraisal_amount = ( + rec.current_salary * + rec.appraisal_percentage + ) / 100 + + def _move_to_next_stage(self): + self.ensure_one() + next_stage = self.env['employee.stage.config'].search( + [('seq', '>', self.stage_id.seq)], + order='seq asc', + limit=1 + ) + if next_stage: + self.stage_id = next_stage.id + + def action_finance_approve(self): + for rec in self: + rec.write({ + 'state': 'management_team', + 'finance_user_id': self.env.user.id, + 'finance_date': fields.Datetime.now() + }) + + rec.message_post( + body=_( + "Finance appraisal approved." + ) + ) + + def _send_manager_notification_mail(self): + self.ensure_one() + email_to = self.manager_email + if not email_to: + return + body_html = f""" +
+

Hello Manager,

+

+ Employee appraisal is ready for Manager Evaluation. +

+
+ + + + + + + + + + + + + +
Employee{self.employee_appraisal_id.name or ''}
Department{self.department_appraisal_id.name or ''}
Performance Period{self.appraisal_period_id.appraisal_type_id.id or ''}
+
+

+ Please complete manager evaluation. +

+
+

+ Regards,
+ HR Team +

+
+ """ + mail_values = { + 'subject': 'Manager Evaluation Notification', + 'body_html': body_html, + 'email_to': email_to, + } + self.env['mail.mail'].create(mail_values).send() + + def check_colleague_feedback_deadline(self): + now = fields.Datetime.now() + records = self.search([ + ('college_end_date_time', '!=', False), + ('college_end_date_time', '<=', now), + ('state', '=', 'colleague_feedback') + ]) + for rec in records: + rec.write({ + 'state': 'in_progress' + }) + rec.message_post( + body=_( + "Colleague feedback deadline completed automatically. " + "Stage moved to Manager Evaluation." + ) + ) + rec._send_manager_notification_mail() + return True + + def action_sent_employee_appraisal(self): + self.ensure_one() + employee_email = self.employee_appraisal_id.work_email or '' + creator_email = self.creator_email or '' + emails = ",".join( + filter(None, [employee_email, creator_email]) + ) + body_html = f""" +
+

Hello,Team

+

+ Your appraisal evaluation has been initiated. +

+
+ + + + + + + + + + + + + + + + +
+ Employee + + {self.employee_appraisal_id.name or ''} +
+ Template + + {self.template_id.name or ''} +
+ Performance Period + + {self.appraisal_period_id.appraisal_type_id.id or ''} +
+
+

+ Please complete the self evaluation before deadline. +

+
+

+ Regards, +

+

+ HR Team +

+
+ """ + ctx = { + 'default_model': 'employee.appraisal.template.config', + 'default_res_ids': [self.id], + 'default_composition_mode': 'comment', + 'default_email_to': emails, + 'default_subject': 'Employee Appraisal Notification', + 'default_body': body_html, + 'mark_appraisal_sent': True, + } + + return { + 'type': 'ir.actions.act_window', + 'res_model': 'mail.compose.message', + 'view_mode': 'form', + 'target': 'new', + 'context': ctx, + } + + def action_confirm(self): + for rec in self: + rec.state = 'self_evaluation' + + def action_confirm_manager(self): + for rec in self: + rec.state = 'hr_evaluation' + + def action_confirm_hr(self): + for rec in self: + rec.state = 'finance_team' + + def action_send_colleague_feedback(self): + for rec in self: + employees = self.env['hr.employee'].search([ + ('department_id', '=', rec.department_appraisal_id.id), + ('id', '!=', rec.employee_appraisal_id.id) + ]) + vals = [] + for emp in employees: + already_exists = self.env['colleague.feedback'].search([ + ('employee_appraisal_feed_id', '=', rec.id), + ('colleague_feed_id', '=', emp.id) + ], limit=1) + if not already_exists: + vals.append((0, 0, { + 'colleague_feed_id': emp.id, + })) + rec.colleague_feed_ids = vals + rec.state = 'colleague_manager' + + @api.onchange('template_id') + def _onchange_template_id(self): + self.kra_line_ids = [(5, 0, 0)] + if not self.template_id: + return + kra_vals = [] + for kra in self.template_id.kra_ids: + kpi_vals = [] + for kpi in kra.kpi_line_ids: + kpi_vals.append((0, 0, { + 'question': kpi.name, + 'description': kpi.kpi_description, + 'kpi_line_weightage': kpi.kpi_weightage, + 'manager_kpi_points': kpi.kpi_points, + 'manager_kpi_stars': kpi.kpi_ratings, + })) + kra_vals.append((0, 0, { + 'kra_id': kra.id, + 'name': kra.name, + 'description_line_kra': kra.description, + 'kra_line_weightage': kra.kra_weightage, + 'max_kra_points': kra.max_points, + 'max_kra_stars': kra.max_star_rating, + 'kpi_line_ids': kpi_vals, + })) + + self.kra_line_ids = kra_vals + + +class ColleagueFeedBack(models.Model): + _name = 'colleague.feedback' + _description = 'Colleague Feedback' + + colleague_feed_id = fields.Many2one('hr.employee', 'Employee', default=lambda self: self.env.user.employee_id.id) + feedback = fields.Char(string="Feedback") + employee_appraisal_feed_id = fields.Many2one('employee.appraisal.template.config') + state = fields.Selection([ + ('draft', 'Draft'), + ('submitted', 'Submitted') + ], default='draft') + + submitted_date = fields.Datetime() + + def action_submit_feedback(self): + + for rec in self: + rec.write({ + 'state': 'submitted', + 'submitted_date': fields.Datetime.now() + }) + + @api.constrains('colleague_feed_id', 'employee_appraisal_feed_id') + def _check_colleague_feed_id(self): + for rec in self: + duplicate = self.search([ + ('id', '!=', rec.id), + ('colleague_feed_id', '=', rec.colleague_feed_id.id), + ('employee_appraisal_feed_id', '=', rec.employee_appraisal_feed_id.id) + ], limit=1) + + if duplicate: + raise ValidationError(_('Colleague Feed Back already exists')) diff --git a/addons_extensions/hrms_employee_appraisal/models/hr_notice_appraisal.py b/addons_extensions/hrms_employee_appraisal/models/hr_notice_appraisal.py new file mode 100644 index 000000000..4432a9b6c --- /dev/null +++ b/addons_extensions/hrms_employee_appraisal/models/hr_notice_appraisal.py @@ -0,0 +1,265 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError +from random import randint + + +class HrNoticeAppraisal(models.Model): + _name = 'hr.notice.appraisal' + _description = 'Performance Cycle Notification' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _rec_name = 'subject' + _order = 'id desc' + + @api.returns('self') + def _default_employee_get(self): + return self.env.user.employee_id + + hr_employee_id = fields.Many2one('hr.employee', string='Employee', default=_default_employee_get) + subject = fields.Char(string="Subject", required=True, tracking=True) + body = fields.Html(string="Notice Body", required=True) + start_date = fields.Datetime(string="Start Date", required=True) + end_date = fields.Datetime(string="End Date", required=True) + employee_ids = fields.Many2many('hr.employee', 'notice_employee_rel', 'notice_id', 'employee_id',string="Employees") + manager_ids = fields.Many2many('hr.employee', 'notice_manager_rel', 'notice_id', 'manager_id', string="Managers") + state = fields.Selection([ + ('draft', 'Draft'), + ('sent', 'Sent'), + ('postponed', 'Postponed'), + ('cancelled', 'Cancelled'), + ], default='draft', tracking=True) + # appraisal_notice_id = fields.Many2one('employee.appraisal.year', string="Appraisal Period", required=True) + appraisal_type_id = fields.Many2one('employee.appraisal.type',string="Appraisal Type") + appraisal_notice_id = fields.Many2one('employee.appraisal.year',string="Appraisal Name",domain="[('appraisal_type_id', '=', appraisal_type_id)]") + template_appraisal_id = fields.Many2one('employee.appraisal.template', string="Template") + seq = fields.Char( string="Reference",readonly=True,copy=False,default="New") + employee_rating = fields.Boolean(string="Employee Rating") + employee_points = fields.Boolean(string="Employee Points") + cancel_reason = fields.Text(string="Cancel Reason",tracking=True) + cancelled_date = fields.Datetime(string="Cancelled On",tracking=True) + cancelled_by_id = fields.Many2one('res.users',string="Cancelled By",tracking=True) + postpone_reason = fields.Text(string="Postpone Reason",tracking=True) + postponed_date = fields.Datetime(string="Postponed On",tracking=True) + postponed_by_id = fields.Many2one('res.users',string="Postponed By",tracking=True) + new_start_date = fields.Datetime(string="New Start Date") + new_end_date = fields.Datetime(string="New End Date") + stage_config = fields.Many2many('employee.stage.config',string='Stages') + hr_department_ids = fields.Many2many('hr.department', string="Departments") + + @api.model + def create(self, vals): + if vals.get('seq', 'New') == 'New': + company = self.env.company + company_code = company.short_code or 'CMP' + today = fields.Datetime.now() + month = str(today.month).zfill(2) + year = str(today.year)[-2:] + prefix = f"{company_code}/{month}/{year}" + last_record = self.search([ + ('seq', '=like', f'{prefix}%') + ], order='id desc', limit=1) + number = 1 + if last_record and last_record.seq: + try: + number = int( + last_record.seq.split('/')[-1] + ) + 1 + except Exception: + number = 1 + vals['seq'] = ( + f"{prefix}/{str(number).zfill(3)}" + ) + return super().create(vals) + + @api.constrains('start_date', 'end_date') + def _check_dates(self): + for rec in self: + if rec.end_date < rec.start_date: + raise ValidationError(_("End Date must be greater than Start Date.")) + + @api.onchange('employee_ids') + def _onchange_employee_ids(self): + managers = self.employee_ids.mapped('parent_id') + departments = self.employee_ids.mapped('department_id') + self.manager_ids = [(6, 0, managers.ids)] + self.hr_department_ids = [(6,0, departments.ids)] + + def action_send_notice(self): + self.ensure_one() + employee_emails = self.employee_ids.mapped('work_email') + manager_emails = self.manager_ids.mapped('work_email') + all_emails = employee_emails + manager_emails + email_to = ",".join(filter(None, all_emails)) + template_obj = self.env['employee.appraisal.template'] + grouped_employees = {} + for employee in self.employee_ids: + manager = employee.parent_id + department = employee.department_id + key = ( + manager.id if manager else False, + department.id if department else False + ) + if key not in grouped_employees: + grouped_employees[key] = self.env['hr.employee'] + grouped_employees[key] |= employee + for (manager_id, department_id), employees in grouped_employees.items(): + already_exists = template_obj.search([ + ('notice_id', '=', self.id), + # ('employee_eva_id', '=', manager_id), + ('employee_department_id', '=', department_id), + ], limit=1) + if already_exists: + continue + template_obj.create({ + 'name': self.subject, + 'seq': self.seq, + 'employee_eva_id': manager_id, + 'employee_department_id': department_id, + 'employee_ids': [(6, 0, employees.ids)], + 'manager_ids': [(6, 0, [manager_id])] if manager_id else [(5, 0, 0)], + 'notice_id': self.id, + 'start_date': self.start_date, + 'end_date': self.end_date, + 'appraisal_period_id': self.appraisal_notice_id.id, + 'appraisal_period_type_id': self.appraisal_type_id.id, + 'template_rating_bool': self.employee_rating, + 'template_point_bool': self.employee_points, + 'hr_employee_id': self.hr_employee_id.id, + 'stage_config_ids': [(6, 0, self.stage_config.ids)], + }) + # print('1234',template_obj.create({})) + + body_html = f""" +
+

Hello,

+

{self.body or ''}

+
+ + + + + + + + + + + + + + +
+ Appraisal Period + + {self.appraisal_notice_id.appraisal_name or ''} +
+ Start Date + + {self.start_date or ''} +
+ End Date + + {self.end_date or ''} +
+
+

+ Please complete the appraisal within the given timeline. +

+
+

+ Regards, +

+

+ HR Team +

+
+ """ + ctx = { + 'default_model': 'hr.notice.appraisal', + 'default_res_ids': [self.id], + 'default_composition_mode': 'comment', + 'default_email_to': email_to, + 'default_subject': self.subject, + 'default_body': body_html, + 'mark_notice_sent': True, + } + return { + 'type': 'ir.actions.act_window', + 'res_model': 'mail.compose.message', + 'view_mode': 'form', + 'target': 'new', + 'context': ctx, + } + + def action_open_postpone_wizard(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': 'Postpone Appraisal', + 'res_model': 'appraisal.postpone.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'active_id': self.id + } + } + + def action_open_cancel_wizard(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': 'Cancel Appraisal', + 'res_model': 'appraisal.cancel.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'active_id': self.id + } + } + +class StageConfig(models.Model): + _name = 'employee.stage.config' + _description = 'employee.stage.config' + _order = 'seq' + + def _get_default_color_stage(self): + return randint(1, 11) + + name = fields.Char(required=True) + seq = fields.Integer(required=True) + active = fields.Boolean(default=True) + color = fields.Integer('Color', default=_get_default_color_stage) + + +class MailComposeMessage(models.TransientModel): + _inherit = 'mail.compose.message' + + def action_send_mail(self): + + res = super().action_send_mail() + + model = self.env.context.get('default_model') + res_ids = self.env.context.get('default_res_ids') + if self.env.context.get('mark_notice_sent'): + + if model == 'hr.notice.appraisal' and res_ids: + records = self.env[model].browse(res_ids) + + records.write({ + 'state': 'sent' + }) + if self.env.context.get('mark_appraisal_sent'): + if model == 'employee.appraisal.template.config' and res_ids: + records = self.env[model].browse(res_ids) + for record in records: + record._move_to_next_stage() + + if self.env.context.get('mark_appraisal_sent_appraisal'): + + if model == 'employee.appraisal.template' and res_ids: + records = self.env[model].browse(res_ids) + + records.write({ + 'employee_state': 'sent' + }) + + return res diff --git a/addons_extensions/hrms_employee_appraisal/models/kpi_kra.py b/addons_extensions/hrms_employee_appraisal/models/kpi_kra.py new file mode 100644 index 000000000..20edde453 --- /dev/null +++ b/addons_extensions/hrms_employee_appraisal/models/kpi_kra.py @@ -0,0 +1,414 @@ +from odoo import models, fields, api +from odoo.cli.scaffold import template + +RATING_VALUATION = { + '1': 'unsatisfactory', + '2': 'needs_improvements', + '3': 'meet_expectation', + '4': 'exceed_expectation', + '5': 'outstanding', +} + + +class EmployeeAppraisalKRA(models.Model): + _name = 'employee.appraisal.kra' + _description = 'Key Result Area' + _order = 'sequence,id' + + sequence = fields.Integer(string="Sequence", default=10) + template_appraisal_id = fields.Many2one('employee.appraisal.template', string="Template", ondelete='cascade') + name = fields.Text(string="KRA Name", required=True) + description = fields.Text(string="KRA Description", required=True) + kra_weightage = fields.Float(string="KRA Weightage", compute='_compute_kpi_weightage', store=True) + max_star_rating = fields.Selection([ + ('0', '0'), + ('1', '1'), + ('2', '2'), + ('3', '3'), + ('4', '4'), + ('5', '5'), + ], string='Maximum Stars', default='5') + max_points = fields.Float(string='Maximum Points', default=10) + kpi_line_ids = fields.One2many('employee.appraisal.kpi', 'kra_id', string="KPI Questions", copy=True) + kpi_count = fields.Integer(compute="_compute_kpi_count") + kra_template_rating_bool = fields.Boolean(related='template_appraisal_id.template_rating_bool', readonly=True) + kra_template_point_bool = fields.Boolean(related='template_appraisal_id.template_point_bool', readonly=True) + + def _compute_kpi_count(self): + for rec in self: + rec.kpi_count = len(rec.kpi_line_ids) + + @api.onchange('kpi_line_ids') + def _onchange_kpi_line_ids(self): + for rec in self: + if ( + rec.template_appraisal_id + and rec.template_appraisal_id.template_point_bool + ): + count = len(rec.kpi_line_ids) + + if count: + point_per_kpi = rec.max_points / count + + for kpi in rec.kpi_line_ids: + kpi.kpi_points = point_per_kpi + + @api.onchange('template_appraisal_id') + def onchange_template_appraisal_id(self): + for rec in self: + template = rec.template_appraisal_id + if template.template_rating_bool: + rec.max_star_rating = '5' + if template.template_point_bool: + rec.max_points = 10 + + @api.depends('kpi_line_ids.kpi_weightage') + def _compute_kpi_weightage(self): + for rec in self: + rec.kra_weightage = sum(rec.kpi_line_ids.mapped('kpi_weightage')) + + def action_open_questions(self): + return { + 'type': 'ir.actions.act_window', + 'name': 'KPI Questions', + 'res_model': 'employee.appraisal.kpi', + 'view_mode': 'list,form', + 'domain': [('kra_id', '=', self.id)], + 'context': { + 'default_kra_id': self.id + }, + 'target': 'new', + } + + +class EmployeeAppraisalKPI(models.Model): + _name = 'employee.appraisal.kpi' + _description = 'Key Performance Indicator' + _order = 'sequence,id' + + sequence = fields.Integer(string="Sequence", default=10) + kra_id = fields.Many2one('employee.appraisal.kra', string="KRA", ondelete='cascade') + name = fields.Text(string="KPI / Question", required=True) + kpi_description = fields.Text('Description') + kpi_weightage = fields.Float(string="KRI Weightage") + kpi_ratings = fields.Integer(string="KRI Ratings", default=5) + kpi_points = fields.Integer(string="KRI Points") + + def action_delete_record(self): + kra_id = self.kra_id.id + self.unlink() + return { + 'type': 'ir.actions.act_window', + 'name': 'KPI Questions', + 'res_model': 'employee.appraisal.kpi', + 'view_mode': 'list,form', + 'domain': [('kra_id', '=', kra_id)], + 'context': { + 'default_kra_id': kra_id + }, + 'target': 'new', + } + + +class EmployeeAppraisalKRALine(models.Model): + _name = 'employee.appraisal.kra.line' + + config_id = fields.Many2one( + 'employee.appraisal.template.config', + ondelete='cascade' + ) + template_empl_rating_bool = fields.Boolean( + related='config_id.template_empl_rating_bool', + readonly=True + ) + template_empl_point_bool = fields.Boolean(related='config_id.template_empl_point_bool', readonly=True) + kra_id = fields.Many2one('employee.appraisal.kra') + name = fields.Text('KRA Name') + sequence = fields.Integer(default=10) + description_line_kra = fields.Text('Description') + kra_line_weightage = fields.Float(string="KRA Point Weightage", compute="_compute_kra_line_weightage", store=True) + kra_line_star_weightage = fields.Integer(string="KRA Star Weightage", compute="_compute_kra_line_star_weightage", + store=True) + kra_line_star_demo_weightage = fields.Selection([ + ('0', '0'), + ('1', '1'), + ('2', '2'), + ('3', '3'), + ('4', '4'), + ('5', '5'), + ], compute='_compute_kra_line_demo_star_weightage', store=True) + kpi_line_ids = fields.One2many('employee.appraisal.kpi.line', 'kra_line_id', string="Questions") + kpi_count_line = fields.Integer(compute="_compute_kpi_count_line") + max_kra_points = fields.Float(string='Maximum KRA Points') + max_kra_stars = fields.Integer(string='Maximum KRA Stars') + employee_kra_points = fields.Float(compute='_compute_employee_kra_points', store=True) + employee_kra_stars = fields.Float(compute='_compute_employee_kra_stars', store=True) + manager_kra_points = fields.Float(compute='_compute_kra_scores', store=True) + hr_kra_points = fields.Float(compute='_compute_kra_scores', store=True) + manager_kra_stars = fields.Float(compute='_compute_kra_scores', store=True) + hr_kra_stars = fields.Float(compute='_compute_kra_scores', store=True) + kra_line_star_display = fields.Char(compute='_compute_kra_line_star_display', string='Rating') + + @api.depends( + 'kpi_line_ids.rating_star', + 'kpi_line_ids.manager_rating_star', + 'kpi_line_ids.hr_rating_star' + ) + def _compute_kra_line_star_display(self): + for rec in self: + + ratings = [] + + ratings += [ + int(kpi.rating_star) + for kpi in rec.kpi_line_ids + if kpi.rating_star + ] + + ratings += [ + int(kpi.manager_rating_star) + for kpi in rec.kpi_line_ids + if kpi.manager_rating_star + ] + + ratings += [ + int(kpi.hr_rating_star) + for kpi in rec.kpi_line_ids + if kpi.hr_rating_star + ] + + if ratings: + avg = round(sum(ratings) / len(ratings)) + stars = '★' * avg + '☆' * (5 - avg) + rec.kra_line_star_display = f"{stars} ({avg})" + else: + rec.kra_line_star_display = "☆☆☆☆☆ (0)" + + @api.depends( + 'kpi_line_ids.employee_score', + 'kpi_line_ids.manager_score', + 'kpi_line_ids.hr_score' + ) + def _compute_kra_line_weightage(self): + for rec in self: + employee_total = sum( + rec.kpi_line_ids.mapped('employee_score') + ) + + manager_total = sum( + rec.kpi_line_ids.mapped('manager_score') + ) + + hr_total = sum( + rec.kpi_line_ids.mapped('hr_score') + ) + + rec.kra_line_weightage = ( + employee_total + + manager_total + + hr_total + ) / 3 + + @api.depends('kpi_line_ids.rating_star') + def _compute_kra_line_star_weightage(self): + for rec in self: + ratings = [ + int(kpi.rating_star) + for kpi in rec.kpi_line_ids + if kpi.rating_star + ] + + rec.kra_line_star_weightage = ( + sum(ratings) / len(ratings) + if ratings else 0 + ) + + @api.depends( + 'kpi_line_ids.employee_score', + 'kpi_line_ids.manager_score', + 'kpi_line_ids.hr_score', + 'kpi_line_ids.rating_star', + 'kpi_line_ids.manager_rating_star', + 'kpi_line_ids.hr_rating_star' + ) + def _compute_kra_scores(self): + for rec in self: + # -------------------------- + # POINTS + # -------------------------- + + rec.employee_kra_points = sum( + rec.kpi_line_ids.mapped('employee_score') + ) + + rec.manager_kra_points = sum( + rec.kpi_line_ids.mapped('manager_score') + ) + + rec.hr_kra_points = sum( + rec.kpi_line_ids.mapped('hr_score') + ) + + # -------------------------- + # STARS + # -------------------------- + + emp_stars = [ + int(x.rating_star) + for x in rec.kpi_line_ids + if x.rating_star + ] + + mgr_stars = [ + int(x.manager_rating_star) + for x in rec.kpi_line_ids + if x.manager_rating_star + ] + + hr_stars = [ + int(x.hr_rating_star) + for x in rec.kpi_line_ids + if x.hr_rating_star + ] + + rec.employee_kra_stars = ( + round(sum(emp_stars) / len(emp_stars), 2) + if emp_stars else 0 + ) + + rec.manager_kra_stars = ( + round(sum(mgr_stars) / len(mgr_stars), 2) + if mgr_stars else 0 + ) + + rec.hr_kra_stars = ( + round(sum(hr_stars) / len(hr_stars), 2) + if hr_stars else 0 + ) + + @api.depends('kpi_line_ids.rating_star') + def _compute_kra_line_demo_star_weightage(self): + for rec in self: + ratings = [ + int(kpi.rating_star) + for kpi in rec.kpi_line_ids + if kpi.rating_star + ] + + if ratings: + avg = round(sum(ratings) / len(ratings)) + rec.kra_line_star_demo_weightage = str(avg) + else: + rec.kra_line_star_demo_weightage = '0' + + def _compute_kpi_count_line(self): + for rec in self: + rec.kpi_count_line = len(rec.kpi_line_ids) + + def action_open_questions_line(self): + self.ensure_one() + + return { + 'type': 'ir.actions.act_window', + 'name': 'KRA Evaluation', + 'res_model': 'employee.appraisal.kra.line', + 'res_id': self.id, + 'view_mode': 'form', + 'target': 'new', + } + + +class EmployeeAppraisalKPILine(models.Model): + _name = 'employee.appraisal.kpi.line' + _rec_name = 'question' + + sequence = fields.Integer(default=10) + kra_line_id = fields.Many2one('employee.appraisal.kra.line', ondelete='cascade') + kra_name = fields.Text() + question = fields.Text(string="KPI") + kpi_line_weightage = fields.Float(string="Given KPI Weightage") + description = fields.Text() + employee_score = fields.Float() + manager_score = fields.Float() + hr_score = fields.Float() + template_empl_rating_bool = fields.Boolean(related='kra_line_id.config_id.template_empl_rating_bool', readonly=True) + template_empl_point_bool = fields.Boolean(related='kra_line_id.config_id.template_empl_point_bool', readonly=True) + manager_kpi_points = fields.Float(string="Maximum KPI Points") + manager_kpi_stars = fields.Integer(string="Maximum KPI Stars") + rating = fields.Selection([ + ('1', '1'), + ('2', '2'), + ('3', '3'), + ('4', '4'), + ('5', '5'), + ], string="Rating") + rating_value = fields.Selection([ + ('unsatisfactory', 'Unsatisfactory'), + ('needs_improvements', 'Needs Improvements'), + ('meet_expectation', 'Meets Expectation'), + ('exceed_expectation', 'Exceeds Expectation'), + ('outstanding', 'Outstanding'), + ], string="Rating Value") + rating_star = fields.Selection([ + ('0', '0'), + ('1', '1'), + ('2', '2'), + ('3', '3'), + ('4', '4'), + ('5', '5'), + ], string="Stars", copy=False) + manager_rating_star = fields.Selection([ + ('0', '0'), + ('1', '1'), + ('2', '2'), + ('3', '3'), + ('4', '4'), + ('5', '5'), + ], string="Stars", copy=False) + hr_rating_star = fields.Selection([ + ('0', '0'), + ('1', '1'), + ('2', '2'), + ('3', '3'), + ('4', '4'), + ('5', '5'), + ], string="Stars", copy=False) + # self_rating = fields.Selection([ + # ('0', '0'), + # ('1', '1'), + # ('2', '2'), + # ('3', '3'), + # ('4', '4'), + # ('5', '5'), + # ], string='Self Rating', compute="_compute_self_rating") + # + # @api.depends('program_id.self_other_ev_line_ids.rating_star') + # def _compute_self_rating(self): + # for line in self: + # other_line = line.program_id.self_other_ev_line_ids.filtered(lambda l: l.name == line.name) + # line.self_rating = other_line.rating_star if other_line else False + + @api.onchange('rating') + def _onchange_rating(self): + for rec in self: + if rec.rating: + rec.rating_value = RATING_VALUATION.get(rec.rating) + rec.rating_star = rec.rating + else: + rec.rating_value = 0 + rec.rating_star = 0 + + @api.onchange('rating_star') + def _onchange_rating_star(self): + for rec in self: + rec.rating_value = RATING_VALUATION.get( + rec.rating_star, + False + ) + + +class ResCompany(models.Model): + _inherit = 'res.company' + + short_code = fields.Char(string="Short Code") + company_registry_placeholder = fields.Char(string="Short Code") diff --git a/addons_extensions/hrms_employee_appraisal/security/ir.model.access.csv b/addons_extensions/hrms_employee_appraisal/security/ir.model.access.csv new file mode 100644 index 000000000..247be6304 --- /dev/null +++ b/addons_extensions/hrms_employee_appraisal/security/ir.model.access.csv @@ -0,0 +1,29 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_employee_appraisal_template_config,employee.appraisal.template.config,model_employee_appraisal_template_config,base.group_user,1,1,1,1 +access_employee_appraisal_year,employee.appraisal.year,model_employee_appraisal_year,base.group_user,1,1,1,1 + + +access_employee_appraisal_template,employee.appraisal.template,model_employee_appraisal_template,base.group_user,1,1,1,1 + +access_employee_appraisal_kra_user,employee.appraisal.kra.user,model_employee_appraisal_kra,base.group_user,1,1,1,1 +access_employee_appraisal_kpi_user,employee.appraisal.kpi.user,model_employee_appraisal_kpi,base.group_user,1,1,1,1 + + +access_employee_appraisal_kra_line_user,employee.appraisal.kra.line.user,model_employee_appraisal_kra_line,base.group_user,1,1,1,1 +access_employee_appraisal_kpi_line_user,employee.appraisal.kpi.line.user,model_employee_appraisal_kpi_line,base.group_user,1,1,1,1 + +access_colleague_feedback_user,colleague.feedback.user,model_colleague_feedback,base.group_user,1,1,1,1 +access_hr_notice_appraisal,hr.notice.appraisal,model_hr_notice_appraisal,base.group_user,1,1,1,1 + +access_employee.appraisal.type,employee.appraisal.type,model_employee_appraisal_type,base.group_user,1,1,1,1 + + + +access_appraisal_postpone_wizard,appraisal_postpone_wizard,model_appraisal_postpone_wizard,base.group_user,1,1,1,1 +access_appraisal_cancel_wizard,appraisal.cancel.wizard,model_appraisal_cancel_wizard,base.group_user,1,1,1,1 + + +access_employee_stage_config,employee.stage.config,model_employee_stage_config,base.group_user,1,1,1,1 + + + diff --git a/addons_extensions/hrms_employee_appraisal/static/description/icon.png b/addons_extensions/hrms_employee_appraisal/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..92fba0e58a6827677e05690f0f7cfab0e698cd26 GIT binary patch literal 6044 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 W3GHruZ571;0000 + + + + + employee.appraisal.template.config.tree + employee.appraisal.template.config + + + + + + + + + + + + + + employee.appraisal.template.config.form + employee.appraisal.template.config + +
+
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 1 = Unsatisfactory +
+
+ 2 = Needs Improvements +
+
+ 3 = Meets Expectations +
+
+ 4 = Exceeds Expectations +
+
+ 5 = Outstanding +
+
+
+ +
+
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + +

+ +

+
+ + + + + + + + + + + + + + + + + +
+ +
+
+ + + + + + + + +