From adc4733e15fa0a6643df4131ec73a827b4ab9707 Mon Sep 17 00:00:00 2001 From: seshikanth Date: Wed, 24 Jun 2026 12:20:06 +0530 Subject: [PATCH 1/8] #fix: Employee Performance Management Module and few HRMS bugs --- .../views/bench_management_view.xml | 15 +- .../disciplinary/models/employee_displane.py | 179 ++++ .../views/disciplinary_complaint_type.xml | 122 +++ .../disciplinary/views/employee_displance.xml | 169 ++++ .../models/payroll_periods.py | 41 +- ...employee_payslip_download_wizard_views.xml | 5 + .../hr_resignation/__manifest__.py | 1 + .../hr_resignation/models/hr_resignation.py | 25 + .../security/resignation_groups.xml | 15 + .../views/hr_resignation_views.xml | 34 +- ...e_resignation_requirements_proceedings.xml | 2 +- .../static/src/css/hrms_emp_dashboard.css | 22 +- .../static/src/js/hrms_emp_dashboard.js | 16 +- .../static/src/xml/hrms_emp_dashboard.xml | 17 +- .../hrms_employee_appraisal/__manifest__.py | 4 + .../models/__init__.py | 3 + .../models/apprasial_conf.py | 126 ++- .../models/employee_appraisal.py | 800 +++++++++++++++--- .../models/employee_pip.py | 89 ++ .../models/hr_head_nofication.py | 121 +++ .../models/hr_notice_appraisal.py | 301 +++++-- .../hrms_employee_appraisal/models/kpi_kra.py | 15 + .../models/setting_config.py | 76 ++ .../security/ir.model.access.csv | 7 + .../security/performace_record_rules.xml | 111 +++ .../security/security_groups.xml | 45 + .../views/employee_appraisal.xml | 196 +++-- .../views/employee_evalutor.xml | 17 +- .../views/employee_pip.xml | 84 ++ .../views/employee_template_appraisal.xml | 38 +- .../views/hr_notice_appraisal.xml | 129 ++- .../views/stage_config.xml | 41 +- .../requisitions/views/hr_requisition.xml | 14 +- 33 files changed, 2503 insertions(+), 377 deletions(-) create mode 100644 addons_extensions/disciplinary/models/employee_displane.py create mode 100644 addons_extensions/disciplinary/views/disciplinary_complaint_type.xml create mode 100644 addons_extensions/disciplinary/views/employee_displance.xml create mode 100644 addons_extensions/hr_resignation/security/resignation_groups.xml create mode 100644 addons_extensions/hrms_employee_appraisal/models/employee_pip.py create mode 100644 addons_extensions/hrms_employee_appraisal/models/hr_head_nofication.py create mode 100644 addons_extensions/hrms_employee_appraisal/models/setting_config.py create mode 100644 addons_extensions/hrms_employee_appraisal/security/performace_record_rules.xml create mode 100644 addons_extensions/hrms_employee_appraisal/security/security_groups.xml create mode 100644 addons_extensions/hrms_employee_appraisal/views/employee_pip.xml diff --git a/addons_extensions/bench_management_system/views/bench_management_view.xml b/addons_extensions/bench_management_system/views/bench_management_view.xml index 606632e12..e75526621 100644 --- a/addons_extensions/bench_management_system/views/bench_management_view.xml +++ b/addons_extensions/bench_management_system/views/bench_management_view.xml @@ -5,7 +5,7 @@ bench.management.line.list bench.management.line - + @@ -41,7 +41,7 @@ bench.management.line.form bench.management.line -
+ @@ -73,9 +73,7 @@ bench.management.line.kanban bench.management.line - - - + @@ -85,17 +83,12 @@ - - -
-
- - Bench Management bench.management.line kanban,list,form @@ -247,6 +239,7 @@ diff --git a/addons_extensions/disciplinary/models/employee_displane.py b/addons_extensions/disciplinary/models/employee_displane.py new file mode 100644 index 000000000..0abcba19e --- /dev/null +++ b/addons_extensions/disciplinary/models/employee_displane.py @@ -0,0 +1,179 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError +from datetime import date + + +class HRDisciplinaryAction(models.Model): + _name = 'hr.employee.disciplinary' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _description = 'Employee Disciplinary Management' + + active = fields.Boolean(default=True) + name = fields.Char('Reference', copy=False, readonly=True, default=lambda x: _('New')) + employee_id = fields.Many2one('hr.employee', string="Employee", required=True) + company_id = fields.Many2one('res.company', string="Company", required=True, default=lambda self: self.env.company) + employee_code = fields.Char(string='Employee Code', related='employee_id.employee_id',tracking=True,required=True) + # unit_id = fields.Many2one('unit.master', string="Unit",tracking=True) + department_id = fields.Many2one('hr.department', string="Department",tracking=True) + designation_id = fields.Many2one('hr.job', string="Designation",tracking=True) + doj = fields.Date(string="Date of Joining",tracking=True) + referred_by_id = fields.Many2one('res.users', string="Referred By",tracking=True) + loss_of_cost = fields.Float(string="Loss of Cost") + # employee_section_id = fields.Many2one('section.master',string='Section') + disciplinary_complaint_line_ids = fields.One2many('hr.disciplinary.complaint.line','disciplinary_id',string = 'Complaint Lines') + disciplinary_action_line_ids = fields.One2many('hr.disciplinary.action.line','disciplinary_id',string = 'Action Lines') + state = fields.Selection([ + ('new', 'New'), + ('submitted', 'Submitted'), + ('pending', 'Pending'), + ('closed', 'Closed'), + ('cancel', 'Cancel') + ], default='new',tracking=True,string='State') + complaint_name = fields.Text('Complaint', compute='_compute_complaint_name', store=True) + name_1 = fields.Char('Name') + disciplinary_id = fields.Many2one('hr.employee.disciplinary', string="Disciplinary") + complaint_date = fields.Date('Complaint Date') + language_id = fields.Many2one('res.lang', 'Language') + complaint_type_id = fields.Many2one('disciplinary.complaint.type', string="Complaint Type") + mistake_type_id = fields.Many2one('disciplinary.mistake.type', string="Mistake Type", required=True) + complaint = fields.Char(string='Complaints') + employee_id_2 = fields.Many2one('hr.employee', string='Employee') + related_record_count = fields.Integer(string="Disciplinary Action Records Count", compute="_compute_related_record_count") + # general_cat = fields.Many2one('general.category', string="General Category", tracking=True) + # cat_id = fields.Many2one('hr.category','Category') + occurrences = fields.Integer('Occurrences', store=True) + severe = fields.Char('Severe') + major = fields.Char('Major') + less_major = fields.Char('Less Major') + negligible = fields.Char('Negligible') + normal = fields.Char('Normal') + total_mistakes = fields.Char('Total Mistakes') + memo = fields.Char('Memo') + explanation = fields.Char('Explanation') + show_cause = fields.Char('Show Cause') + charge_sheet = fields.Char('Charge Sheet') + warning = fields.Char('Warning') + enquiry_notice = fields.Char('Enquiry Notice') + recovery_order = fields.Char('Recovery_ Order') + stoppage_of_increment = fields.Char('Stoppage Of Increment') + demotion = fields.Char('Demotion') + total_actions = fields.Char('Total Actions') + normal_action = fields.Char('Normal Actions') + suspension = fields.Char('Suspension') + total_cost = fields.Float('Total Cost') + + @api.depends('employee_id') + def _compute_related_record_count(self): + for record in self: + record.related_record_count = self.env['hr.employee.disciplinary'].search_count([('employee_id', '=', record.employee_id.id)]) + + def action_open_related_records(self): + return { + 'name': 'Disciplinary Action Records', + 'type': 'ir.actions.act_window', + 'res_model': 'hr.employee.disciplinary', + 'view_mode': 'list', + 'domain': [('employee_id', '=', self.employee_id.id)], + 'context': {'default_employee_id': self.employee_id.id}, + } + + @api.depends('disciplinary_complaint_line_ids.complaint') + def _compute_complaint_name(self): + for record in self: + complaints = record.disciplinary_complaint_line_ids.mapped('complaint') + record.complaint_name = "\n".join(filter(None, complaints)) + + def action_set_submitted(self): + self.state = 'submitted' + + def action_set_pending(self): + self.state = 'pending' + + def action_set_closed(self): + self.state = 'closed' + + def action_set_cancel(self): + self.state = 'cancel' + + def action_reset_to_new(self): + self.state = 'new' + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if not vals.get('name') or vals['name'] == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code('hr.employee.sequence') or _('New') + return super().create(vals_list) + + @api.onchange('employee_id') + def _onchange_employee_id(self): + for rec in self: + if rec.employee_id: + rec.employee_code = rec.employee_id.employee_id or '' + rec.department_id = rec.employee_id.department_id.id + rec.designation_id = rec.employee_id.job_id.id + rec.doj = rec.employee_id.doj + rec.company_id = rec.employee_id.company_id.id + # rec.unit_id = rec.employee_id.unit_name_hr.id if rec.employee_id.unit_name_hr else False + # rec.employee_section_id = rec.employee_id.section_name_hr.id if rec.employee_id.section_name_hr else False + else: + rec.employee_code = False + rec.department_id = False + rec.designation_id = False + rec.doj = False + + +class DisciplinaryComplaintLine(models.Model): + _name = 'hr.disciplinary.complaint.line' + _description = 'Disciplinary Complaint Line' + + name = fields.Char('Name') + disciplinary_id = fields.Many2one('hr.employee.disciplinary',string="Disciplinary") + complaint_date = fields.Date('Complaint Date') + language_id = fields.Many2one('res.lang','Language') + complaint_type_id = fields.Many2one('disciplinary.complaint.type',string="Complaint Type") + mistake_type_id = fields.Many2one('disciplinary.mistake.type',string="Mistake Type") + complaint = fields.Char(string='Complaints') + employee_id = fields.Many2one('hr.employee', string='Employee') + + + + +class DisciplinaryActionLine(models.Model): + _name = 'hr.disciplinary.action.line' + _description = 'Disciplinary Action Line' + + name = fields.Char('Name') + disciplinary_id = fields.Many2one('hr.employee.disciplinary',string="Disciplinary") + action_taken_date = fields.Date('Action On') + action_type_id = fields.Many2one('disciplinary.action.type',string="Action Type") + action = fields.Char(string='Description') + action_name = fields.Char('ActionName') + related_complaint_id = fields.Many2one('hr.disciplinary.complaint.line', string="Related Complaint", + domain="[('disciplinary_id', '=', disciplinary_id)]") + employee_id = fields.Many2one('hr.employee', string='Employee') + + @api.constrains('action_taken_date') + def _check_action_taken_date(self): + for record in self: + if record.action_taken_date and record.action_taken_date > date.today(): + raise ValidationError("The Action On date cannot be in the future.") + + +class DisciplinaryActionType(models.Model): + _name = 'disciplinary.action.type' + _description = 'Action Type' + + name = fields.Char('Name', required=True) + +class DisciplinaryComplaintType(models.Model): + _name = 'disciplinary.complaint.type' + _description = 'Complaint Type' + + name = fields.Char('Name', required=True) + +class DisciplinaryMistakeType(models.Model): + _name = 'disciplinary.mistake.type' + _description = 'Mistake Type' + + name = fields.Char('Name', required=True) diff --git a/addons_extensions/disciplinary/views/disciplinary_complaint_type.xml b/addons_extensions/disciplinary/views/disciplinary_complaint_type.xml new file mode 100644 index 000000000..569a0caf9 --- /dev/null +++ b/addons_extensions/disciplinary/views/disciplinary_complaint_type.xml @@ -0,0 +1,122 @@ + + + + disciplinary.complaint.type + disciplinary.complaint.type + + + + + + + + + disciplinary.complaint.type.form + disciplinary.complaint.type + + + + + + + + + + + + + Employee Disciplinary Complaint Type + disciplinary.complaint.type + list,form + + + + + + + hr.disciplinary.action.line + hr.disciplinary.action.line + + + + + + + + + hr.disciplinary.action.line.form + hr.disciplinary.action.line + +
+ + + + + + + + + + + +
+
+
+ + + Employee Disciplinary Action + hr.disciplinary.action.line + list,form + + + + + + + + disciplinary.action.type + disciplinary.action.type + + + + + + + + + disciplinary.action.type.form + disciplinary.action.type + +
+ + + + + +
+
+
+ + + Employee Disciplinary Action Type + disciplinary.action.type + list,form + + + +
\ No newline at end of file diff --git a/addons_extensions/disciplinary/views/employee_displance.xml b/addons_extensions/disciplinary/views/employee_displance.xml new file mode 100644 index 000000000..f762c695b --- /dev/null +++ b/addons_extensions/disciplinary/views/employee_displance.xml @@ -0,0 +1,169 @@ + + + + employee.disciplinary.form + hr.employee.disciplinary + +
+
+ +
+ +
+ +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + hr.employee.disciplinary.list + hr.employee.disciplinary + + + + + + + + + + + + + + + + hr.employee.disciplinary.search + hr.employee.disciplinary + + + + + + + + + + Employee Disciplinary + hr.employee.disciplinary + list,form + + + + + + + + + +
\ No newline at end of file diff --git a/addons_extensions/employee_it_declaration/models/payroll_periods.py b/addons_extensions/employee_it_declaration/models/payroll_periods.py index ed9bc86c8..5cc558492 100644 --- a/addons_extensions/employee_it_declaration/models/payroll_periods.py +++ b/addons_extensions/employee_it_declaration/models/payroll_periods.py @@ -1,12 +1,13 @@ -from odoo import models, fields, api -from odoo.exceptions import ValidationError -import calendar +from odoo import models, fields, api +from odoo.exceptions import ValidationError +import calendar -class PayrollPeriod(models.Model): +class PayrollPeriod(models.Model): _name = 'payroll.period' _description = 'Payroll Period' _rec_name = 'name' + _order = 'id desc' _sql_constraints = [ ('unique_name', 'unique(name)', 'The name must be unique.') ] @@ -14,22 +15,22 @@ class PayrollPeriod(models.Model): from_date = fields.Date(string="From Date", required=True) to_date = fields.Date(string="To Date", required=True) name = fields.Char(string="Name", required=True) - period_line_ids = fields.One2many('payroll.period.line', 'period_id', string="Monthly Periods") - - @api.model_create_multi - def create(self, vals_list): - periods = super().create(vals_list) - active_investment_types = self.env['it.investment.type'].search([('active', '=', True)]) - if active_investment_types: - active_investment_types.write({ - 'period_ids': [(4, period.id) for period in periods], - }) - return periods - - @api.onchange('from_date', 'to_date') - def onchange_from_to_date(self): - for rec in self: - if rec.from_date and rec.to_date: + period_line_ids = fields.One2many('payroll.period.line', 'period_id', string="Monthly Periods") + + @api.model_create_multi + def create(self, vals_list): + periods = super().create(vals_list) + active_investment_types = self.env['it.investment.type'].search([('active', '=', True)]) + if active_investment_types: + active_investment_types.write({ + 'period_ids': [(4, period.id) for period in periods], + }) + return periods + + @api.onchange('from_date', 'to_date') + def onchange_from_to_date(self): + for rec in self: + if rec.from_date and rec.to_date: rec.name = f"{rec.from_date.year}-{rec.to_date.year}" diff --git a/addons_extensions/employee_it_declaration/views/employee_payslip_download_wizard_views.xml b/addons_extensions/employee_it_declaration/views/employee_payslip_download_wizard_views.xml index cf0b7b0cf..d0ce96181 100644 --- a/addons_extensions/employee_it_declaration/views/employee_payslip_download_wizard_views.xml +++ b/addons_extensions/employee_it_declaration/views/employee_payslip_download_wizard_views.xml @@ -41,6 +41,11 @@ +
+
diff --git a/addons_extensions/hr_resignation/__manifest__.py b/addons_extensions/hr_resignation/__manifest__.py index 8ab0426f3..06b508c07 100644 --- a/addons_extensions/hr_resignation/__manifest__.py +++ b/addons_extensions/hr_resignation/__manifest__.py @@ -34,6 +34,7 @@ 'data': [ 'security/hr_resignation_security.xml', 'security/ir.model.access.csv', + 'security/resignation_groups.xml', 'data/data.xml', 'data/ir_sequence_data.xml', 'data/ir_cron_data.xml', diff --git a/addons_extensions/hr_resignation/models/hr_resignation.py b/addons_extensions/hr_resignation/models/hr_resignation.py index cf6e9cbdb..cd39f8e11 100644 --- a/addons_extensions/hr_resignation/models/hr_resignation.py +++ b/addons_extensions/hr_resignation/models/hr_resignation.py @@ -167,8 +167,33 @@ class HrResignation(models.Model): admin_checklist_submitted = fields.Boolean(tracking=True) hr_checklist_submitted = fields.Boolean(tracking=True) + manager_checklist_status = fields.Selection([('pending', 'Pending'),('completed', 'Completed')], compute='_compute_checklist_status') + it_checklist_status = fields.Selection([('pending', 'Pending'),('completed', 'Completed')], compute='_compute_checklist_status') + finance_checklist_status = fields.Selection([('pending', 'Pending'),('completed', 'Completed') ], compute='_compute_checklist_status') + admin_checklist_status = fields.Selection([('pending', 'Pending'),('completed', 'Completed')], compute='_compute_checklist_status') + hr_checklist_status = fields.Selection([('pending', 'Pending'),('completed', 'Completed')], compute='_compute_checklist_status') relieving_documents = fields.Many2many('ir.attachment') + applied_date = fields.Date(string="Applied Date",default=fields.Date.context_today,readonly=True,tracking=True) + + @api.depends('manager_checklist_submitted','it_checklist_submitted','finance_checklist_submitted','admin_checklist_submitted', 'hr_checklist_submitted') + def _compute_checklist_status(self): + for rec in self: + rec.manager_checklist_status = ( + 'completed' if rec.manager_checklist_submitted else 'pending' + ) + rec.it_checklist_status = ( + 'completed' if rec.it_checklist_submitted else 'pending' + ) + rec.finance_checklist_status = ( + 'completed' if rec.finance_checklist_submitted else 'pending' + ) + rec.admin_checklist_status = ( + 'completed' if rec.admin_checklist_submitted else 'pending' + ) + rec.hr_checklist_status = ( + 'completed' if rec.hr_checklist_submitted else 'pending' + ) @api.depends('employee_id') def _compute_user_rights(self): diff --git a/addons_extensions/hr_resignation/security/resignation_groups.xml b/addons_extensions/hr_resignation/security/resignation_groups.xml new file mode 100644 index 000000000..822086f4b --- /dev/null +++ b/addons_extensions/hr_resignation/security/resignation_groups.xml @@ -0,0 +1,15 @@ + + + Resignation Management + 41 + + + Resignation User + + + + + Resignation Manager + + + \ No newline at end of file diff --git a/addons_extensions/hr_resignation/views/hr_resignation_views.xml b/addons_extensions/hr_resignation/views/hr_resignation_views.xml index 1363c4746..d1f3a1dc1 100644 --- a/addons_extensions/hr_resignation/views/hr_resignation_views.xml +++ b/addons_extensions/hr_resignation/views/hr_resignation_views.xml @@ -150,6 +150,7 @@ + @@ -173,12 +174,30 @@ + + + + + + + - - - - - + + + + + @@ -186,7 +205,8 @@ - + @@ -387,7 +407,7 @@ diff --git a/addons_extensions/hr_resignation/views/pre_resignation_requirements_proceedings.xml b/addons_extensions/hr_resignation/views/pre_resignation_requirements_proceedings.xml index 6589247d8..00277d65f 100644 --- a/addons_extensions/hr_resignation/views/pre_resignation_requirements_proceedings.xml +++ b/addons_extensions/hr_resignation/views/pre_resignation_requirements_proceedings.xml @@ -24,7 +24,7 @@ - + { + window.location.reload(); + }, 500); } catch (error) { console.error(error); this.notification.add("Unable to update attendance", { type: "danger" }); diff --git a/addons_extensions/hrms_emp_dashboard/static/src/xml/hrms_emp_dashboard.xml b/addons_extensions/hrms_emp_dashboard/static/src/xml/hrms_emp_dashboard.xml index ab06a9e15..4f3481021 100644 --- a/addons_extensions/hrms_emp_dashboard/static/src/xml/hrms_emp_dashboard.xml +++ b/addons_extensions/hrms_emp_dashboard/static/src/xml/hrms_emp_dashboard.xml @@ -27,10 +27,21 @@
- +
""" ctx = { 'default_model': 'employee.appraisal.template.config', @@ -549,17 +894,132 @@ class EmployeeAppraisal(models.Model): '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' + self.ensure_one() + + if not self.hr_remarks: + raise ValidationError ('Please Provide the Remarks') + + email_to = self.managerapp_id.work_email or '' + + body_html = f""" +
+

Hello,

+ +

+ HR evaluation has been completed for the appraisal of + {self.employee_appraisal_id.name}. +

+ +

+ Kindly review the appraisal and take the necessary action. +

+ +
+ + + + + + + + + + + + + + +
Employee{self.employee_appraisal_id.name or ''}
Department{self.department_appraisal_id.name or ''}
Template{self.template_id.name or ''}
+ +
+ +

Regards,

+

HR Team

+ +
+ """ + + ctx = { + 'default_model': 'employee.appraisal.template.config', + 'default_res_ids': [self.id], + 'default_composition_mode': 'comment', + 'default_email_to': email_to, + 'default_subject': f'HR Evaluation Completed - {self.employee_appraisal_id.name}', + 'default_body': body_html, + 'move_hr_next_stage': True, + } + + return { + 'type': 'ir.actions.act_window', + 'res_model': 'mail.compose.message', + 'view_mode': 'form', + 'target': 'new', + 'context': ctx, + } + + def action_head_hr(self): + self.ensure_one() + + if not self.hr_head_remarks: + raise ValidationError( + _('Please provide HR Head Remarks.') + ) + + email_to = self.created_by_id.work_email or '' + + body_html = f""" +
+

Hello,

+ +

+ HR Head review has been completed for the appraisal of + {self.employee_appraisal_id.name}. +

+ +

+ Kindly proceed with the next level approval. +

+ + + + + + + + + + + + + + +
Employee{self.employee_appraisal_id.name or ''}
Department{self.department_appraisal_id.name or ''}
HR Head Remarks{self.hr_head_remarks or ''}
+ +
+ +

Regards,

+

HR Head

+
+ """ + + ctx = { + 'default_model': 'employee.appraisal.template.config', + 'default_res_ids': [self.id], + 'default_composition_mode': 'comment', + 'default_email_to': email_to, + 'default_subject': f'HR Head Approval - {self.employee_appraisal_id.name}', + 'default_body': body_html, + 'move_hr_head_next_stage': True, + } + + return { + 'type': 'ir.actions.act_window', + 'res_model': 'mail.compose.message', + 'view_mode': 'form', + 'target': 'new', + 'context': ctx, + } def action_send_colleague_feedback(self): for rec in self: @@ -568,6 +1028,7 @@ class EmployeeAppraisal(models.Model): ('id', '!=', rec.employee_appraisal_id.id) ]) vals = [] + email_list = [] for emp in employees: already_exists = self.env['colleague.feedback'].search([ ('employee_appraisal_feed_id', '=', rec.id), @@ -578,7 +1039,123 @@ class EmployeeAppraisal(models.Model): 'colleague_feed_id': emp.id, })) rec.colleague_feed_ids = vals - rec.state = 'colleague_manager' + if self.managerapp_id and self.managerapp_id.work_email: + email_list.append(self.managerapp_id.work_email) + email_to = ",".join(filter(None, email_list)) + + body_html = f""" +
+

Hello Team,

+ +

+ {self.employee_appraisal_id.name} has completed the self-assessment. + Please provide your feedback as part of the appraisal process. +

+ +

+ Your feedback will help in evaluating the employee's overall performance. +

+ +
+ +

Regards,

+

{self.employee_appraisal_id.name}

+
+ """ + + ctx = { + 'default_model': 'employee.appraisal.template.config', + 'default_res_ids': [self.id], + 'default_composition_mode': 'comment', + 'default_email_to': email_to, + 'default_subject': f'Colleague Feedback Request - {self.employee_appraisal_id.name}', + 'default_body': body_html, + 'mark_colleague_feedback_sent': True, + } + + return { + 'type': 'ir.actions.act_window', + 'res_model': 'mail.compose.message', + 'view_mode': 'form', + 'target': 'new', + 'context': ctx, + } + + def action_initiate_pip(self): + self.ensure_one() + self.write({'invite_pip': True}) + + employee_email = self.employee_appraisal_id.work_email or '' + + body_html = f""" +
+

Dear {self.employee_appraisal_id.name},

+ +

+ Based on the recent performance appraisal review, your overall performance + rating indicates that improvement is required in certain areas. +

+ +

+ Therefore, you are requested to attend a Performance Improvement Plan (PIP) + discussion meeting with Management and HR. +

+ + + + + + + + + + + + + + + + + + +
Employee{self.employee_appraisal_id.name or ''}
Department{self.department_appraisal_id.name or ''}
Performance Period{self.appraisal_period_id.appraisal_name or ''}
Overall Rating{self.overall_rating or ''}
+ +
+ +

+ During this meeting, we will discuss performance concerns, + expectations, improvement objectives, and the Performance + Improvement Plan (PIP) timeline. +

+ +

+ Kindly acknowledge and attend the meeting as scheduled. +

+ +
+ +

Regards,

+

Management Team

+
+ """ + + ctx = { + 'default_model': 'employee.appraisal.template.config', + 'default_res_ids': [self.id], + 'default_composition_mode': 'comment', + 'default_email_to': employee_email, + 'default_subject': 'Performance Improvement Plan (PIP) Meeting Notification', + 'default_body': body_html, + 'mark_invite_pip': True, + } + + return { + 'type': 'ir.actions.act_window', + 'res_model': 'mail.compose.message', + 'view_mode': 'form', + 'target': 'new', + 'context': ctx, + } @api.onchange('template_id') def _onchange_template_id(self): @@ -624,7 +1201,6 @@ class ColleagueFeedBack(models.Model): submitted_date = fields.Datetime() def action_submit_feedback(self): - for rec in self: rec.write({ 'state': 'submitted', diff --git a/addons_extensions/hrms_employee_appraisal/models/employee_pip.py b/addons_extensions/hrms_employee_appraisal/models/employee_pip.py new file mode 100644 index 000000000..905d9c21a --- /dev/null +++ b/addons_extensions/hrms_employee_appraisal/models/employee_pip.py @@ -0,0 +1,89 @@ +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError + + +class EmployeePIP(models.Model): + _name = 'employee.pip' + _description = 'Performance Improvement Plan' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _rec_name = 'employee_id' + + name = fields.Char(string="PIP Reference",default=lambda self: _('New'),readonly=True ) + employee_id = fields.Many2one( 'hr.employee',required=True) + manager_id = fields.Many2one('hr.employee',string="Manager") + appraisal_id = fields.Many2one('employee.appraisal.template.config',string="Appraisal") + objective = fields.Text(string="Improvement Objective",required=True) + timeline = fields.Selection([ + ('30', '30 Days'), + ('60', '60 Days'), + ('90', '90 Days') + ], default='30', required=True) + start_date = fields.Date(default=fields.Date.today) + end_date = fields.Date() + review_date = fields.Date() + employee_acknowledged = fields.Boolean(string="Employee Acknowledged") + state = fields.Selection([ + ('draft', 'Draft'), + ('running', 'In Progress'), + ('review', 'Under Review'), + ('completed', 'Completed'), + ('failed', 'Failed') + ], default='draft', tracking=True) + task_ids = fields.One2many('employee.pip.task','pip_id',string='Improvement Tasks') + progress_percentage = fields.Float(compute='_compute_progress',store=True) + remarks = fields.Text() + + @api.depends('task_ids.state') + def _compute_progress(self): + for rec in self: + + total = len(rec.task_ids) + + completed = len( + rec.task_ids.filtered( + lambda l: l.state == 'done' + ) + ) + + rec.progress_percentage = ( + (completed / total) * 100 + ) if total else 0 + + @api.onchange('timeline', 'start_date') + def _onchange_timeline(self): + for rec in self: + if rec.start_date and rec.timeline: + rec.end_date = fields.Date.add( + rec.start_date, + days=int(rec.timeline) + ) + + def action_start(self): + self.state = 'running' + + def action_review(self): + self.state = 'review' + + def action_complete(self): + self.state = 'completed' + + def action_fail(self): + self.state = 'failed' + + + + +class EmployeePIPTask(models.Model): + _name = 'employee.pip.task' + _description = 'PIP Task' + + pip_id = fields.Many2one('employee.pip',ondelete='cascade') + name = fields.Char(required=True) + description = fields.Text() + target_date = fields.Date() + training_course = fields.Char(string="Suggested Training") + state = fields.Selection([ + ('pending', 'Pending'), + ('progress', 'In Progress'), + ('done', 'Completed') + ], default='pending') \ No newline at end of file diff --git a/addons_extensions/hrms_employee_appraisal/models/hr_head_nofication.py b/addons_extensions/hrms_employee_appraisal/models/hr_head_nofication.py new file mode 100644 index 000000000..38366da68 --- /dev/null +++ b/addons_extensions/hrms_employee_appraisal/models/hr_head_nofication.py @@ -0,0 +1,121 @@ +from odoo import api, fields, models + + +class HrHeadNofication(models.Model): + _name = 'hr.head.notification' + _description = 'HeadNofication' + _inherit = ['mail.thread', 'mail.activity.mixin'] + + @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) + name = fields.Char("Subject") + appraisal_type_id = fields.Many2one('employee.appraisal.type') + appraisal_period_id = fields.Many2one('employee.appraisal.year', + domain="[('appraisal_type_id', '=', appraisal_type_id)]") + body = fields.Html(string="Notice Body", required=True) + start_date = fields.Date() + end_date = fields.Date() + hr_ids = fields.Many2many('hr.employee', string="HR Team") + # hr_employee_domain_ids = fields.Many2many('hr.employee',compute='_compute_hr_employee_domain') + stage_config_ids = fields.Many2many('employee.stage.config', string="Stages") + state = fields.Selection([ + ('draft', 'Draft'), + ('sent', 'Sent') + ], default='draft') + seq = fields.Char(string="Reference", readonly=True, copy=False, default="New") + + @api.model + def _get_hr_users_domain(self): + group = self.env.ref('hrms_employee_appraisal.group_appraisal_hr') + if group: + return [('groups_id', 'in', [group.id])] + return [('id', '=', False)] + + hr_users_ids = fields.Many2many('res.users', string="HR Team", copy=False, domain=_get_hr_users_domain) + + @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) + + + def action_sent_hr(self): + self.ensure_one() + + hr_emails = self.hr_users_ids.mapped('email') + email_to = ",".join(filter(None, hr_emails)) + + body_html = f""" +
+

Hello HR Team,

+ +

{self.body or ''}

+ + + + + + + + + + + + + + + + + + +
Appraisal Type{self.appraisal_type_id.name or ''}
Appraisal Period{self.appraisal_period_id.appraisal_name or ''}
Start Date{self.start_date or ''}
End Date{self.end_date or ''}
+ +
+ +

Please initiate the appraisal process.

+ +

Regards,

+

HR Head

+
+ """ + + ctx = { + 'default_model': 'hr.head.notification', + 'default_res_ids': [self.id], + 'default_composition_mode': 'comment', + 'default_email_to': email_to, + 'default_subject': self.name, + 'default_body': body_html, + 'mark_hr_notification_sent': True, + } + + return { + 'type': 'ir.actions.act_window', + 'res_model': 'mail.compose.message', + 'view_mode': 'form', + 'target': 'new', + 'context': ctx, + } \ No newline at end of file diff --git a/addons_extensions/hrms_employee_appraisal/models/hr_notice_appraisal.py b/addons_extensions/hrms_employee_appraisal/models/hr_notice_appraisal.py index 4432a9b6c..546a6c78d 100644 --- a/addons_extensions/hrms_employee_appraisal/models/hr_notice_appraisal.py +++ b/addons_extensions/hrms_employee_appraisal/models/hr_notice_appraisal.py @@ -2,6 +2,8 @@ from odoo import models, fields, api, _ from odoo.exceptions import ValidationError from random import randint +import logging +_logger = logging.getLogger(__name__) class HrNoticeAppraisal(models.Model): _name = 'hr.notice.appraisal' @@ -15,10 +17,11 @@ class HrNoticeAppraisal(models.Model): return self.env.user.employee_id hr_employee_id = fields.Many2one('hr.employee', string='Employee', default=_default_employee_get) + notification_id = fields.Many2one('hr.head.notification','HR') 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) + start_date = fields.Date(string="Start Date") + end_date = fields.Date(string="End Date") 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([ @@ -45,36 +48,81 @@ class HrNoticeAppraisal(models.Model): 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.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): + @api.constrains('start_date', 'end_date','employee_ids','manager_ids','appraisal_notice_id','state') + def _check_appraisal_validations(self): for rec in self: - if rec.end_date < rec.start_date: - raise ValidationError(_("End Date must be greater than Start Date.")) + if rec.start_date and rec.end_date: + if rec.end_date <= rec.start_date: + raise ValidationError( + _("End Date must be greater than Start Date.") + ) + if rec.state == 'cancelled': + continue + duplicate_period = self.search([ + ('id', '!=', rec.id), + ('appraisal_notice_id', '=', rec.appraisal_notice_id.id), + ('hr_employee_id', '=', rec.hr_employee_id.id), + ('state', '!=', 'cancelled'), + ], limit=1) + if duplicate_period: + raise ValidationError(_( + "An appraisal notification already exists for appraisal period '%s'." + ) % rec.appraisal_notice_id.display_name) + + for employee in rec.employee_ids: + duplicate_employee = self.search([ + ('id', '!=', rec.id), + ('employee_ids', 'in', employee.id), + ('state', '!=', 'cancelled'), + ('start_date', '<=', rec.end_date), + ('end_date', '>=', rec.start_date), + ], limit=1) + _logger.info( + "Duplicate Period Records: %s", + duplicate_period.ids + ) + if duplicate_employee: + raise ValidationError(_( + "Employee '%s' is already assigned in another appraisal notification for the selected period." + ) % employee.name) + + for manager in rec.manager_ids: + duplicate_manager = self.search([ + ('id', '!=', rec.id), + ('manager_ids', 'in', manager.id), + ('state', '!=', 'cancelled'), + ('start_date', '<=', rec.end_date), + ('end_date', '>=', rec.start_date), + ], limit=1) + if duplicate_manager: + raise ValidationError(_( + "Manager '%s' is already assigned in another appraisal notification for the selected period." + ) % manager.name) @api.onchange('employee_ids') def _onchange_employee_ids(self): @@ -89,45 +137,6 @@ class HrNoticeAppraisal(models.Model): 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,

@@ -226,6 +235,7 @@ class StageConfig(models.Model): name = fields.Char(required=True) seq = fields.Integer(required=True) + colour_seq = fields.Integer(required=True) active = fields.Boolean(default=True) color = fields.Integer('Color', default=_get_default_color_stage) @@ -240,26 +250,157 @@ class MailComposeMessage(models.TransientModel): 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'): + template_obj = self.env['employee.appraisal.template'] + for rec in records: + grouped_employees = {} + for employee in rec.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', '=', rec.id), + ('employee_department_id', '=', department_id), + ], limit=1) + if already_exists: + continue + template_obj.create({ + 'name': rec.subject, + 'seq': rec.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': rec.id, + 'start_date': rec.start_date, + 'end_date': rec.end_date, + 'appraisal_period_id': rec.appraisal_notice_id.id, + 'appraisal_period_type_id': rec.appraisal_type_id.id, + 'template_rating_bool': rec.employee_rating, + 'template_point_bool': rec.employee_points, + 'hr_employee_id': rec.hr_employee_id.id, + 'stage_config_ids': [(6, 0, rec.stage_config.ids)], + }) + rec.write({ + 'state': 'sent' + }) + if ( + self.env.context.get('mark_appraisal_sent') + or self.env.context.get('mark_colleague_feedback_sent') + or self.env.context.get('move_hr_next_stage') + or self.env.context.get('move_hr_head_next_stage') + or self.env.context.get('mark_finance_approved') + or self.env.context.get('mark_finance_head_approved') + or self.env.context.get('mark_invite_pip') + ): 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'): + # 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_colleague_feedback_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' + templates = self.env[model].browse(res_ids) + appraisal_config_obj = self.env[ + 'employee.appraisal.template.config' + ] + for rec in templates: + 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, + }) + + appraisal._onchange_template_id() + + rec.write({ + 'employee_state': 'sent' + }) + + if self.env.context.get('mark_hr_notification_sent'): + records = self.env[model].browse(res_ids) + for record in records: + for user in record.hr_users_ids: + employee = self.env['hr.employee'].search([ + ('user_id', '=', user.id) + ], limit=1) + if not employee: + continue + already_exists = self.env[ + 'hr.notice.appraisal' + ].search([ + ('notification_id', '=', record.id), + ('hr_employee_id', '=', employee.id) + ], limit=1) + if already_exists: + continue + self.env['hr.notice.appraisal'].create({ + 'notification_id': record.id, + 'hr_employee_id': employee.id, + 'subject': record.name, + 'appraisal_type_id': + record.appraisal_type_id.id, + 'appraisal_notice_id': + record.appraisal_period_id.id, + 'start_date': record.start_date, + 'end_date': record.end_date, + 'seq': record.seq, + 'body': record.body, + 'stage_config': [ + (6, 0, + record.stage_config_ids.ids) + ], + }) + + record.write({ + 'state': 'sent' }) - return res + + return res \ No newline at end of file diff --git a/addons_extensions/hrms_employee_appraisal/models/kpi_kra.py b/addons_extensions/hrms_employee_appraisal/models/kpi_kra.py index 20edde453..aaa370f3a 100644 --- a/addons_extensions/hrms_employee_appraisal/models/kpi_kra.py +++ b/addons_extensions/hrms_employee_appraisal/models/kpi_kra.py @@ -373,6 +373,21 @@ class EmployeeAppraisalKPILine(models.Model): ('4', '4'), ('5', '5'), ], string="Stars", copy=False) + is_employee_reviewer = fields.Boolean(compute="_compute_user_roles", store=False) + is_manager_reviewer = fields.Boolean(compute="_compute_user_roles", store=False) + is_hr_reviewer = fields.Boolean(compute="_compute_user_roles", store=False) + + def _compute_user_roles(self): + current_user = self.env.user + for rec in self: + rec.is_employee_reviewer = ( + rec.kra_line_id.config_id.employee_appraisal_id.user_id.id == current_user.id) + rec.is_manager_reviewer= ( + rec.kra_line_id.config_id.managerapp_id.user_id.id == current_user.id) + rec.is_hr_reviewer= ( + rec.kra_line_id.config_id.hr_apprai_id.user_id.id == current_user.id + ) + # self_rating = fields.Selection([ # ('0', '0'), # ('1', '1'), diff --git a/addons_extensions/hrms_employee_appraisal/models/setting_config.py b/addons_extensions/hrms_employee_appraisal/models/setting_config.py new file mode 100644 index 000000000..4d7075129 --- /dev/null +++ b/addons_extensions/hrms_employee_appraisal/models/setting_config.py @@ -0,0 +1,76 @@ +from odoo import api, fields, models +from datetime import date + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + + appraisal_reminder_days = fields.Integer( + string="Appraisal Reminder Before (Days)", + config_parameter='hrms_employee_appraisal.appraisal_reminder_days', + default=7 + ) + + appraisal_reminder_enabled = fields.Boolean( + string="Enable Appraisal Reminders", + config_parameter='hrms_employee_appraisal.appraisal_reminder_enabled', + default=True + ) + + + + + + +class EmployeeAppraisal(models.Model): + _inherit = 'employee.appraisal.template.config' + + def cron_send_appraisal_reminder(self): + + enabled = self.env['ir.config_parameter'].sudo().get_param( + 'hrms_employee_appraisal.appraisal_reminder_enabled' + ) + + if not enabled: + return + + reminder_days = int( + self.env['ir.config_parameter'].sudo().get_param( + 'hrms_employee_appraisal.appraisal_reminder_days', + 7 + ) + ) + + today = date.today() + + records = self.search([ + ('end_date', '!=', False) + ]) + + for rec in records: + + days_left = (rec.end_date - today).days + + if days_left == reminder_days: + + if rec.employee_appraisal_id.work_email: + + self.env['mail.mail'].sudo().create({ + 'subject': 'Performance Appraisal Reminder', + 'email_to': rec.employee_appraisal_id.work_email, + 'body_html': f""" +

Dear {rec.employee_appraisal_id.name},

+ +

+ Your appraisal period is ending in + {reminder_days} days. +

+ +

+ Please complete your self appraisal. +

+ +
+

Regards,
HR Team

+ """ + }).send() \ No newline at end of file diff --git a/addons_extensions/hrms_employee_appraisal/security/ir.model.access.csv b/addons_extensions/hrms_employee_appraisal/security/ir.model.access.csv index 247be6304..f6abb9a48 100644 --- a/addons_extensions/hrms_employee_appraisal/security/ir.model.access.csv +++ b/addons_extensions/hrms_employee_appraisal/security/ir.model.access.csv @@ -23,7 +23,14 @@ access_appraisal_postpone_wizard,appraisal_postpone_wizard,model_appraisal_postp access_appraisal_cancel_wizard,appraisal.cancel.wizard,model_appraisal_cancel_wizard,base.group_user,1,1,1,1 +access_hr_head_notification,hr.head.notification,model_hr_head_notification,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 +access_employee_pip,employee.pip,model_employee_pip,base.group_user,1,1,1,1 +access_employee_pip_task,employee.pip.task,model_employee_pip_task,base.group_user,1,1,1,1 + + diff --git a/addons_extensions/hrms_employee_appraisal/security/performace_record_rules.xml b/addons_extensions/hrms_employee_appraisal/security/performace_record_rules.xml new file mode 100644 index 000000000..fa17c46bc --- /dev/null +++ b/addons_extensions/hrms_employee_appraisal/security/performace_record_rules.xml @@ -0,0 +1,111 @@ + + + + HR Notice - HR Access + + + [('hr_employee_id.user_id', '=', user.id)] + + + + + + HR Notice - Management Access + + [(1,'=',1)] + + + + + Appraisal Template Manager + + + [('employee_eva_id.user_id', '=', user.id)] + + + + + + Appraisal Template HR + + + [('hr_employee_id.user_id', '=', user.id)] + + + + + Appraisal Template - Management Access + + [(1,'=',1)] + + + + + Employee Appraisal - employee access + + + [('employee_appraisal_id.user_id','=', user.id)] + + + + + + Manager Access + + + [('managerapp_id.user_id','=',user.id)] + + + + + + + + + + Employee Appraisal - HR Access + + + [('hr_apprai_id.user_id', '=', user.id)] + + + + + + Employee Appraisal - HR Head Access + + [(1,'=',1)] + + + + + Employee Appraisal - Finance Access + + [(1,'=',1)] + + + + + Employee Appraisal - Finance Head Access + + [(1,'=',1)] + + + + + Employee Appraisal - Management Access + + [(1,'=',1)] + + + + + Employee Appraisal - Management Access + + [(1,'=',1)] + + + + + + \ No newline at end of file diff --git a/addons_extensions/hrms_employee_appraisal/security/security_groups.xml b/addons_extensions/hrms_employee_appraisal/security/security_groups.xml new file mode 100644 index 000000000..d9fdcda58 --- /dev/null +++ b/addons_extensions/hrms_employee_appraisal/security/security_groups.xml @@ -0,0 +1,45 @@ + + + + + Performance Management + 40 + + + + Employee + + + + + Appraisal Manager + + + + + Appraisal HR + + + + + Appraisal HR Head + + + + + Appraisal Finance + + + + + Appraisal Finance Head + + + + + Appraisal Management + + + + + \ No newline at end of file diff --git a/addons_extensions/hrms_employee_appraisal/views/employee_appraisal.xml b/addons_extensions/hrms_employee_appraisal/views/employee_appraisal.xml index 7e6ce572a..9d75ce7d2 100644 --- a/addons_extensions/hrms_employee_appraisal/views/employee_appraisal.xml +++ b/addons_extensions/hrms_employee_appraisal/views/employee_appraisal.xml @@ -13,10 +13,31 @@ - + + + + + employee.app.search + employee.appraisal.template.config + + + + + + + + + + + employee.appraisal.template.config.form employee.appraisal.template.config @@ -32,23 +53,42 @@ domain="[('id', 'in', available_stage_ids)]"/> +
- - - - - - - - - - - - + + + + + + + + + + + + + + - + + - - + + +
@@ -146,27 +206,64 @@ - - + + + - + + - - + + + + + + + + + + + + + + + + + + + + + + @@ -174,47 +271,35 @@ - -
+ + diff --git a/addons_extensions/hr_recruitment_extended/views/hr_job_recruitment.xml b/addons_extensions/hr_recruitment_extended/views/hr_job_recruitment.xml index 87d4ab30f..5b5e0a27a 100644 --- a/addons_extensions/hr_recruitment_extended/views/hr_job_recruitment.xml +++ b/addons_extensions/hr_recruitment_extended/views/hr_job_recruitment.xml @@ -429,6 +429,7 @@ Job Positions Recruitment hr.job.recruitment kanban,list,form +

@@ -446,6 +447,7 @@ hr.job.recruitment kanban,list,form,search + {"search_default_open_status":1,"search_default_my_assignments":1}

From 1b34ae2c5c0865dd7285e778195bf2d1581752c9 Mon Sep 17 00:00:00 2001 From: pranaysaidurga Date: Fri, 26 Jun 2026 17:26:33 +0530 Subject: [PATCH 7/8] recruitment bug fixes --- .../hr_recruitment_extended/__manifest__.py | 1 + .../data/mail_template.xml | 6 +- .../models/__init__.py | 7 +- .../models/hr_applicant.py | 24 ++++ .../models/hr_job_recruitment.py | 2 + .../models/submission_share_history.py | 51 +++++++ .../security/ir.model.access.csv | 4 +- .../views/hr_applicant_views.xml | 4 + .../views/hr_job_recruitment.xml | 16 ++- .../views/hr_recruitment.xml | 1 + .../views/submission_share_history.xml | 20 +++ .../client_submission_mail_template_wizard.py | 128 +++++++++++++----- ...client_submission_mail_template_wizard.xml | 2 + 13 files changed, 223 insertions(+), 43 deletions(-) create mode 100644 addons_extensions/hr_recruitment_extended/models/submission_share_history.py create mode 100644 addons_extensions/hr_recruitment_extended/views/submission_share_history.xml diff --git a/addons_extensions/hr_recruitment_extended/__manifest__.py b/addons_extensions/hr_recruitment_extended/__manifest__.py index d48cd2c0d..3a39282b4 100644 --- a/addons_extensions/hr_recruitment_extended/__manifest__.py +++ b/addons_extensions/hr_recruitment_extended/__manifest__.py @@ -29,6 +29,7 @@ 'data/sequence.xml', 'data/mail_template.xml', 'data/templates.xml', + 'views/submission_share_history.xml', 'views/job_category.xml', 'views/hr_location.xml', 'views/stages.xml', diff --git a/addons_extensions/hr_recruitment_extended/data/mail_template.xml b/addons_extensions/hr_recruitment_extended/data/mail_template.xml index 4cc0162b5..10c704b32 100644 --- a/addons_extensions/hr_recruitment_extended/data/mail_template.xml +++ b/addons_extensions/hr_recruitment_extended/data/mail_template.xml @@ -14,7 +14,7 @@ + t-value="', '.join(object.locations.mapped('location_name')) if object.locations else 'N/A'"/>

@@ -669,7 +669,7 @@ + t-value="', '.join(object.hr_job_recruitment.locations.mapped('location_name'))"/> @@ -784,7 +784,7 @@ - +
diff --git a/addons_extensions/hr_recruitment_extended/models/__init__.py b/addons_extensions/hr_recruitment_extended/models/__init__.py index e87225262..0636edb40 100644 --- a/addons_extensions/hr_recruitment_extended/models/__init__.py +++ b/addons_extensions/hr_recruitment_extended/models/__init__.py @@ -1,9 +1,10 @@ +from . import submission_share_history from . import hr_recruitment from . import hr_job_recruitment from . import stages -from . import applicant_request_forms -from . import hr_applicant_stage_comment -from . import hr_applicant +from . import applicant_request_forms +from . import hr_applicant_stage_comment +from . import hr_applicant from . import hr_job from . import res_partner from . import candidate_experience diff --git a/addons_extensions/hr_recruitment_extended/models/hr_applicant.py b/addons_extensions/hr_recruitment_extended/models/hr_applicant.py index 4010d4888..75ecf7f44 100644 --- a/addons_extensions/hr_recruitment_extended/models/hr_applicant.py +++ b/addons_extensions/hr_recruitment_extended/models/hr_applicant.py @@ -61,6 +61,30 @@ class HRApplicant(models.Model): ) stage_comment_count = fields.Integer(compute='_compute_stage_comment_count') stage_comment_tooltips = fields.Json(compute='_compute_stage_comment_tooltips') + submission_tracker = fields.One2many('recruitment.share.tracker','applicant_id') + submission_count = fields.Integer( + string="Submission Count", + compute="_compute_submission_count", + ) + + def _compute_submission_count(self): + for rec in self: + rec.submission_count = len(rec.submission_tracker) + + def action_open_submissions_wizard(self): + self.ensure_one() + + return { + "type": "ir.actions.act_window", + "name": "Submission History", + "res_model": "recruitment.share.tracker", + "view_mode": "list", + "views": [ + (self.env.ref("hr_recruitment_extended.view_recruitment_share_tracker_list_popup").id, "list"), + ], + "domain": [("applicant_id", "=", self.id)], + "target": "new", + } @api.depends('is_on_hold') def _compute_hold_state(self): diff --git a/addons_extensions/hr_recruitment_extended/models/hr_job_recruitment.py b/addons_extensions/hr_recruitment_extended/models/hr_job_recruitment.py index b8b35ba08..6e8ef76ca 100644 --- a/addons_extensions/hr_recruitment_extended/models/hr_job_recruitment.py +++ b/addons_extensions/hr_recruitment_extended/models/hr_job_recruitment.py @@ -175,6 +175,7 @@ class HRJobRecruitment(models.Model): requested_by = fields.Many2one('res.partner', string="Requested By", default=lambda self: self.env.user.partner_id, domain="[('contact_type','=',recruitment_type)]", tracking=True) + submission_tracker = fields.One2many('recruitment.share.tracker','job_recruitment_id') def action_toggle_chatter_visibility(self): for record in self: record.hide_chatter_suggestion = not record.hide_chatter_suggestion @@ -405,6 +406,7 @@ class HRJobRecruitment(models.Model): 'default_template_id': self.env.ref( 'hr_recruitment_extended.job_recruitment_share_email_template' ).id, + 'default_is_job_recruitment': True, }, } diff --git a/addons_extensions/hr_recruitment_extended/models/submission_share_history.py b/addons_extensions/hr_recruitment_extended/models/submission_share_history.py new file mode 100644 index 000000000..b22ce3268 --- /dev/null +++ b/addons_extensions/hr_recruitment_extended/models/submission_share_history.py @@ -0,0 +1,51 @@ +from odoo import api, fields, models + + +class RecruitmentShareTracker(models.Model): + _name = "recruitment.share.tracker" + _description = "Recruitment Share Tracker" + _order = "date desc, id desc" + + date = fields.Datetime( + string="Date", + required=True, + default=fields.Datetime.now, + ) + + share_type = fields.Selection( + [ + ("job", "Job"), + ("applicant", "Applicant"), + ], + string="Share Type", + required=True, + ) + + reference = fields.Char( + string="Reference", + ) + + applicant_id = fields.Many2one( + "hr.applicant", + string="Applicant", + ondelete="set null", + ) + + job_recruitment_id = fields.Many2one( + "hr.job.recruitment", + string="Job Position", + ondelete="set null", + ) + + email_from = fields.Char( + string="Email From", + ) + + email_to = fields.Char( + string="Email To", + ) + + is_client_submission = fields.Boolean( + string="Client Submission", + default=False, + ) \ No newline at end of file diff --git a/addons_extensions/hr_recruitment_extended/security/ir.model.access.csv b/addons_extensions/hr_recruitment_extended/security/ir.model.access.csv index 7ca70add2..391deaad1 100644 --- a/addons_extensions/hr_recruitment_extended/security/ir.model.access.csv +++ b/addons_extensions/hr_recruitment_extended/security/ir.model.access.csv @@ -1,6 +1,7 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_hr_location,hr.location,model_hr_location,base.group_user,1,1,1,1 +access_recruitment_share_tracker_user,recruitment.share.tracker.user,model_recruitment_share_tracker,hr_recruitment.group_hr_recruitment_user,1,1,1,1 access_job_category_user,job.category.user,model_job_category,base.group_user,1,0,0,0 access_job_category_manager,job.category.manager,model_job_category,hr_recruitment.group_hr_recruitment_user,1,1,1,1 @@ -35,4 +36,5 @@ access_applicant_stage_comment_wizard,applicant.stage.comment.wizard.user,model_ access_hr_application_public,hr.applicant.public.access,hr_recruitment.model_hr_applicant,base.group_public,1,0,0,0 access_hr_application_group_hr,hr.applicant.hr.access,hr_recruitment.model_hr_applicant,hr.group_hr_manager,1,1,0,0 access_applicant_request_forms_hr_user,access.applicant.request.forms.hr.user,model_applicant_request_forms,hr.group_hr_user,1,1,1,1 -access_hr_skill,access.hr.skill.user,hr_skills.model_hr_skill,base.group_public,1,0,0,0 \ No newline at end of file +access_hr_skill,access.hr.skill.user,hr_skills.model_hr_skill,base.group_public,1,0,0,0 + diff --git a/addons_extensions/hr_recruitment_extended/views/hr_applicant_views.xml b/addons_extensions/hr_recruitment_extended/views/hr_applicant_views.xml index 3528775de..4dcd1632a 100644 --- a/addons_extensions/hr_recruitment_extended/views/hr_applicant_views.xml +++ b/addons_extensions/hr_recruitment_extended/views/hr_applicant_views.xml @@ -90,6 +90,10 @@ + + diff --git a/addons_extensions/hr_recruitment_extended/views/hr_job_recruitment.xml b/addons_extensions/hr_recruitment_extended/views/hr_job_recruitment.xml index 5b5e0a27a..2a89d280a 100644 --- a/addons_extensions/hr_recruitment_extended/views/hr_job_recruitment.xml +++ b/addons_extensions/hr_recruitment_extended/views/hr_job_recruitment.xml @@ -201,6 +201,20 @@ + + + + + + + + + + + + + + @@ -484,7 +498,7 @@ active="0" sequence="2"/> - {'search_default_my_candidates': 1,'active_test': False} + kanban,list,form,activity + + + recruitment.share.tracker.popup.list + recruitment.share.tracker + + + + + + + + + + + + \ No newline at end of file diff --git a/addons_extensions/hr_recruitment_extended/wizards/client_submission_mail_template_wizard.py b/addons_extensions/hr_recruitment_extended/wizards/client_submission_mail_template_wizard.py index 84bcc41c7..761d36367 100644 --- a/addons_extensions/hr_recruitment_extended/wizards/client_submission_mail_template_wizard.py +++ b/addons_extensions/hr_recruitment_extended/wizards/client_submission_mail_template_wizard.py @@ -1,65 +1,123 @@ from odoo import models, fields, api from odoo.exceptions import UserError + class ClientSubmissionsMailTemplateWizard(models.TransientModel): _name = 'client.submission.mails.template.wizard' _description = 'Client Submission Mails Template Wizard' template_id = fields.Many2one('mail.template', string='Email Template') submit_date = fields.Date(string='Submission Date', required=True, default=fields.Date.today()) - send_email_from_odoo = fields.Boolean(string="Send Email From Odoo", default=False) + is_job_recruitment = fields.Boolean(string='Is Job Recruitment', default=False) + is_client_submission = fields.Boolean(string='Is Client Submission?', default=False) + send_email_from_odoo = fields.Boolean(string="Send Email", default=True) email_from = fields.Char('Email From') email_to = fields.Char('Email To') email_cc = fields.Text('Email CC') email_subject = fields.Char() email_body = fields.Html( - 'Body', render_engine='qweb', render_options={'post_process': True}, - prefetch=True, translate=True, sanitize='email_outgoing', + 'Body', + render_engine='qweb', + render_options={'post_process': True}, + prefetch=True, + translate=True, + sanitize='email_outgoing', ) @api.onchange('template_id') def _onchange_template_id(self): - """ Update the email body and recipients based on the selected template. """ - if self.template_id: - record_id = self.env.context.get('active_id') - if record_id: - record = self.env[self.template_id.model].browse(record_id) + """Update the email fields based on the selected template.""" + if not self.template_id: + return - if not record.exists(): - raise UserError("The record does not exist or is not accessible.") + record_id = self.env.context.get('active_id') + active_model = self.env.context.get('active_model') - # Fetch email template - email_template = self.env['mail.template'].browse(self.template_id.id) + if not record_id: + return - if not email_template: - raise UserError("Email template not found.") + record = self.env[self.template_id.model].browse(record_id) - self.email_from = record.user_id.partner_id.email - self.email_to = record.requested_by.email - self.email_body = email_template.body_html # Assign the rendered email bodyc - self.email_subject = email_template.subject + if not record.exists(): + raise UserError("The record does not exist or is not accessible.") + self.email_from = record.user_id.partner_id.email + + if active_model == 'hr.applicant': + self.email_to = record.hr_job_recruitment.requested_by.email + else: + self.email_to = record.requested_by.email + + self.email_subject = self.template_id.subject + self.email_body = self.template_id.body_html def action_send_email(self): - """ Send email to the selected partners """ + """Send email and create recruitment share tracker.""" + self.ensure_one() + record_id = self.env.context.get('active_id') - for rec in self: - record = self.env[self.template_id.model].browse(record_id) - if rec.send_email_from_odoo: - template = self.env.ref('hr_recruitment_extended.application_client_submission_email_template') - values = { - 'email_from': rec.email_from, - 'email_to': rec.email_to, - 'email_cc': rec.email_cc, - 'subject' : rec.email_subject, - 'body_html': rec.email_body, - } - render_ctx = dict(client_name=record.hr_job_recruitment.requested_by.name) - # Use 'with_context' to override the email template fields dynamically - template.sudo().with_context(**render_ctx).send_mail(self.env.context.get('active_id'),email_values=values, force_send=True) + model_name = self.env.context.get('active_model') + + record = self.env[self.template_id.model].browse(record_id) + + tracker_values = { + 'date': fields.Datetime.now(), + 'is_client_submission': self.is_client_submission, + } + + if model_name == 'hr.applicant': + tracker_values.update({ + 'share_type': 'applicant', + 'reference': "%s/%s" % ( + record.hr_job_recruitment.recruitment_sequence, + record.candidate_id.candidate_sequence, + ), + 'applicant_id': record.id, + }) + else: + tracker_values.update({ + 'share_type': 'job', + 'reference': record.recruitment_sequence, + 'job_recruitment_id': record.id, + }) + + if self.send_email_from_odoo: + # Use the selected template + template = self.template_id + + values = { + 'email_from': self.email_from, + 'email_to': self.email_to, + 'email_cc': self.email_cc, + 'subject': self.email_subject, + 'body_html': self.email_body, + } + + if model_name == 'hr.applicant': + client_name = record.hr_job_recruitment.requested_by.name + else: + client_name = record.requested_by.name + + render_ctx = { + 'client_name': client_name, + } + + tracker_values['email_from'] = self.email_from, + tracker_values['email_to'] = self.email_to, + + template.sudo().with_context(**render_ctx).send_mail( + record.id, + email_values=values, + force_send=True, + ) + + if self.is_client_submission and model_name == 'hr.applicant': record.sudo().write({ 'submitted_to_client': True, - 'client_submission_date': rec.submit_date, + 'client_submission_date': self.submit_date, 'submitted_stage': record.recruitment_stage_id.id, }) - return {'type': 'ir.actions.act_window_close'} # Close wizard after sending + + self.env['recruitment.share.tracker'].sudo().create(tracker_values) + + return {'type': 'ir.actions.act_window_close'} \ No newline at end of file diff --git a/addons_extensions/hr_recruitment_extended/wizards/client_submission_mail_template_wizard.xml b/addons_extensions/hr_recruitment_extended/wizards/client_submission_mail_template_wizard.xml index 05d11d1ac..70553c153 100644 --- a/addons_extensions/hr_recruitment_extended/wizards/client_submission_mail_template_wizard.xml +++ b/addons_extensions/hr_recruitment_extended/wizards/client_submission_mail_template_wizard.xml @@ -9,6 +9,8 @@ + + From cf0b469b2154d28ca2357f674121a2ccba6cdaf2 Mon Sep 17 00:00:00 2001 From: pranaysaidurga Date: Fri, 26 Jun 2026 17:52:45 +0530 Subject: [PATCH 8/8] recruitment mail issue fix --- .../data/mail_template.xml | 4 ++-- .../client_submission_mail_template_wizard.py | 23 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/addons_extensions/hr_recruitment_extended/data/mail_template.xml b/addons_extensions/hr_recruitment_extended/data/mail_template.xml index 10c704b32..612894f91 100644 --- a/addons_extensions/hr_recruitment_extended/data/mail_template.xml +++ b/addons_extensions/hr_recruitment_extended/data/mail_template.xml @@ -676,7 +676,7 @@ -
+

Dear Sir/Madam,

Please find the applicant details below for your review.

@@ -787,7 +787,7 @@ -
+

Dear Sir/Madam,

Please find the job description and hiring details below for your review and sourcing support.

diff --git a/addons_extensions/hr_recruitment_extended/wizards/client_submission_mail_template_wizard.py b/addons_extensions/hr_recruitment_extended/wizards/client_submission_mail_template_wizard.py index 761d36367..3a9ecb5cb 100644 --- a/addons_extensions/hr_recruitment_extended/wizards/client_submission_mail_template_wizard.py +++ b/addons_extensions/hr_recruitment_extended/wizards/client_submission_mail_template_wizard.py @@ -26,7 +26,6 @@ class ClientSubmissionsMailTemplateWizard(models.TransientModel): @api.onchange('template_id') def _onchange_template_id(self): - """Update the email fields based on the selected template.""" if not self.template_id: return @@ -36,10 +35,7 @@ class ClientSubmissionsMailTemplateWizard(models.TransientModel): if not record_id: return - record = self.env[self.template_id.model].browse(record_id) - - if not record.exists(): - raise UserError("The record does not exist or is not accessible.") + record = self.env[active_model].browse(record_id) self.email_from = record.user_id.partner_id.email @@ -48,8 +44,17 @@ class ClientSubmissionsMailTemplateWizard(models.TransientModel): else: self.email_to = record.requested_by.email - self.email_subject = self.template_id.subject - self.email_body = self.template_id.body_html + # Render subject + self.email_subject = self.template_id._render_field( + 'subject', + [record.id], + )[record.id] + + # Render body + self.email_body = self.template_id._render_field( + 'body_html', + [record.id], + )[record.id] def action_send_email(self): """Send email and create recruitment share tracker.""" @@ -102,8 +107,8 @@ class ClientSubmissionsMailTemplateWizard(models.TransientModel): 'client_name': client_name, } - tracker_values['email_from'] = self.email_from, - tracker_values['email_to'] = self.email_to, + tracker_values['email_from'] = self.email_from + tracker_values['email_to'] = self.email_to template.sudo().with_context(**render_ctx).send_mail( record.id,