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'))