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 000000000..92fba0e58
Binary files /dev/null and b/addons_extensions/hrms_employee_appraisal/static/description/icon.png differ
diff --git a/addons_extensions/hrms_employee_appraisal/static/src/css/wizard.css b/addons_extensions/hrms_employee_appraisal/static/src/css/wizard.css
new file mode 100644
index 000000000..74ad25a86
--- /dev/null
+++ b/addons_extensions/hrms_employee_appraisal/static/src/css/wizard.css
@@ -0,0 +1,12 @@
+
+.modal-dialog:has(.appraisal_kra_popup) {
+ max-width: 75vw !important;
+ width: 75vw !important;
+}
+
+.kra_star_display {
+ font-size: 22px !important;
+ font-weight: bold !important;
+ color: #FFD700 !important;
+}
+
diff --git a/addons_extensions/hrms_employee_appraisal/static/src/img/download.png b/addons_extensions/hrms_employee_appraisal/static/src/img/download.png
new file mode 100644
index 000000000..92fba0e58
Binary files /dev/null and b/addons_extensions/hrms_employee_appraisal/static/src/img/download.png differ
diff --git a/addons_extensions/hrms_employee_appraisal/views/employee_appraisal.xml b/addons_extensions/hrms_employee_appraisal/views/employee_appraisal.xml
new file mode 100644
index 000000000..7e6ce572a
--- /dev/null
+++ b/addons_extensions/hrms_employee_appraisal/views/employee_appraisal.xml
@@ -0,0 +1,247 @@
+
+
+
+
+
+ employee.appraisal.template.config.tree
+ employee.appraisal.template.config
+
+
+
+
+
+
+
+
+
+
+
+
+
+ employee.appraisal.template.config.form
+ employee.appraisal.template.config
+
+
+
+
+
+
+
+ Employee Performance Review
+ employee.appraisal.template.config
+ list,form
+
+
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/hrms_employee_appraisal/views/employee_evalutor.xml b/addons_extensions/hrms_employee_appraisal/views/employee_evalutor.xml
new file mode 100644
index 000000000..af47e91bb
--- /dev/null
+++ b/addons_extensions/hrms_employee_appraisal/views/employee_evalutor.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+ employee.appraisal.year.form
+ employee.appraisal.year
+
+
+
+
+
+ employee.appraisal.year.tree
+ employee.appraisal.year
+
+
+
+
+
+
+
+
+
+
+ Performance Evaluation Period
+ employee.appraisal.year
+ list,form
+
+
+
+
+
+
+ employee.appraisal.type.form
+ employee.appraisal.type
+
+
+
+
+
+ employee.appraisal.type.tree
+ employee.appraisal.type
+
+
+
+
+
+
+
+
+ Performance Evaluation Type
+ employee.appraisal.type
+ list,form
+
+
+
\ No newline at end of file
diff --git a/addons_extensions/hrms_employee_appraisal/views/employee_template_appraisal.xml b/addons_extensions/hrms_employee_appraisal/views/employee_template_appraisal.xml
new file mode 100644
index 000000000..fca95d041
--- /dev/null
+++ b/addons_extensions/hrms_employee_appraisal/views/employee_template_appraisal.xml
@@ -0,0 +1,166 @@
+
+
+
+ employee.appraisal.template.form
+ employee.appraisal.template
+
+
+
+
+
+
+ employee.appraisal.template.list
+ employee.appraisal.template
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Employee Appraisal Templates
+ employee.appraisal.template
+ list,form
+
+
+
+
+
+
+
+ employee.appraisal.kpi.list
+ employee.appraisal.kpi
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ employee.appraisal.kpi.form
+ employee.appraisal.kpi
+
+
+
+
+
+
+ res.company
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons_extensions/hrms_employee_appraisal/views/hr_notice_appraisal.xml b/addons_extensions/hrms_employee_appraisal/views/hr_notice_appraisal.xml
new file mode 100644
index 000000000..8751ab333
--- /dev/null
+++ b/addons_extensions/hrms_employee_appraisal/views/hr_notice_appraisal.xml
@@ -0,0 +1,102 @@
+
+
+ hr.notice.appraisal.form
+ hr.notice.appraisal
+
+
+
+
+
+
+ hr.notice.appraisal.list
+ hr.notice.appraisal
+
+
+
+
+
+
+
+
+
+
+
+
+ Performance Cycle Notification
+ hr.notice.appraisal
+ list,form
+
+
+
+
+
\ No newline at end of file
diff --git a/addons_extensions/hrms_employee_appraisal/views/stage_config.xml b/addons_extensions/hrms_employee_appraisal/views/stage_config.xml
new file mode 100644
index 000000000..edb64a9f9
--- /dev/null
+++ b/addons_extensions/hrms_employee_appraisal/views/stage_config.xml
@@ -0,0 +1,40 @@
+
+
+
+ employee.stage.config.form
+ employee.stage.config
+
+
+
+
+
+ employee.stage.config.tree
+ employee.stage.config
+
+
+
+
+
+
+
+
+
+ Employee Stage Configuration
+ employee.stage.config
+ list,form
+
+
+
+
\ No newline at end of file