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/__init__.py b/addons_extensions/disciplinary/__init__.py new file mode 100755 index 000000000..5305644df --- /dev/null +++ b/addons_extensions/disciplinary/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models \ No newline at end of file diff --git a/addons_extensions/disciplinary/__manifest__.py b/addons_extensions/disciplinary/__manifest__.py new file mode 100755 index 000000000..266297f2d --- /dev/null +++ b/addons_extensions/disciplinary/__manifest__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +{ + 'name': 'Disciplinary', + 'version': '1.0.0', + 'category': 'Apps', + 'summary': 'Disciplinary', + 'description': 'Employee Disciplinary', + 'sequence': '10', + 'author': '', + 'company': 'FTPROTECH', + 'website': 'https://www.ftprotech.in', + 'depends': ['mail', 'hr', 'base', 'website_hr_recruitment', 'contacts', 'point_of_sale'], + 'demo': [], + 'data': [ + 'data/sequence.xml', + 'security/ir.model.access.csv', + 'views/disciplinary_view.xml', + 'views/employee_displance.xml', + 'views/mistake_type_views.xml', + 'views/incident_sub_type.xml', + 'views/disciplinary_complaint_type.xml', + ], + 'installable': True, + 'application': False, + 'auto_install': False, + 'license': 'LGPL-3', +} diff --git a/addons_extensions/disciplinary/data/sequence.xml b/addons_extensions/disciplinary/data/sequence.xml new file mode 100755 index 000000000..e291002e1 --- /dev/null +++ b/addons_extensions/disciplinary/data/sequence.xml @@ -0,0 +1,28 @@ + + + + + Employee Disciplinary + employee.disciplinary + IR + 5 + + + + + Manage Incident + manage.incident + MI + 5 + + + + + Disciplinary Sequence + hr.employee.sequence + ED + 5 + + + + \ No newline at end of file diff --git a/addons_extensions/disciplinary/models/__init__.py b/addons_extensions/disciplinary/models/__init__.py new file mode 100755 index 000000000..2a55d3a16 --- /dev/null +++ b/addons_extensions/disciplinary/models/__init__.py @@ -0,0 +1,2 @@ +from . import disciplinary +from . import employee_displane \ No newline at end of file diff --git a/addons_extensions/disciplinary/models/disciplinary.py b/addons_extensions/disciplinary/models/disciplinary.py new file mode 100755 index 000000000..3c78c4edd --- /dev/null +++ b/addons_extensions/disciplinary/models/disciplinary.py @@ -0,0 +1,188 @@ +from datetime import datetime, date +from odoo import fields, models, api + +# +# class NameChangeHrEmployee(models.Model): +# _inherit = "hr.employee" +# +# employee_name_ids1 = fields.One2many('employee.disciplinary', 'disp_name') +# employee_self_service_line_ids = fields.One2many('manage.incident', 'emp_incident', domain=[('state', '=', 'closed')]) +# +# def name_get(self): +# result = [] +# for record in self: +# if self.env.context.get('new_custom_name', False): +# result.append((record.id, "{} - {}".format(record.name, record.identification_id))) +# else: +# return super(NameChangeHrEmployee, self).name_get() +# return result + + +class EmployeeDisciplinary(models.Model): + _name = 'employee.disciplinary' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _rec_name = 'incident_type' + + incident_date = fields.Datetime(string='Incident Date & Time', tracking=True, default=datetime.now(), required=True) + incident_type = fields.Many2one('incident.employee', string='Incident Type', tracking=True, required=True) + incident_sub_type = fields.Many2many('incident.sub.employee', string='Incident Sub Type', tracking=True, + required=True) + incident_details = fields.Char(string='Incident Details', tracking=True, required=True) + seized_items = fields.Char(string='Seized Items', tracking=True) + incident_summary = fields.Text(string='Incident Summary', tracking=True, required=True) + attach = fields.Many2many('ir.attachment', string='Attachments', tracking=True) + emp_many_disp = fields.Many2many('hr.employee', 'new_custom_table', string='Employees Involved in the Incident', + tracking=True, required=True) + date_action = fields.Date(string='Date') + employee = fields.Many2one('manage.incident') + employee_code = fields.Many2one("hr.employee", string="Employee Name", required=True) + employee_name = fields.Char(related="employee_code.identification_id") + disp_name = fields.Many2one('hr.employee') + + @api.onchange('incident_type') + def return_incident_sub_type(self): + print(self.incident_type.sub_type) + listed = [] + for recs in self.incident_type.sub_type: + listed.append(recs.id) + return {'domain': {'incident_sub_type': [('id', 'in', listed)]}} + + @api.constrains('incident_type') + def create_manage_incidents(self): + for rec in self: + print('created') + self.env['manage.incident'].create({ + 'employee_disciplinary_id': rec.id, + }) + + @api.constrains('employee') + def holds_hr_employee(self): + for rec in self: + rec.disp_name = rec.employee.employee_code_list1 + + +class IncidentEmployee(models.Model): + _name = 'incident.employee' + + name = fields.Char(string='Incident') + sub_type = fields.Many2many('incident.sub.employee', string='Sub type') + + +class IncidentSubEmployee(models.Model): + _name = 'incident.sub.employee' + + name = fields.Char(string='Incident Sub') + +class DisciplinaryMistakeType(models.Model): + _name = 'disciplinary.mistake.type' + _description = 'Disciplinary Mistake Type' + + name = fields.Char(string="Mistake Type", required=True) + +class IncidentSubEmployee(models.Model): + _name = 'incident.sub.employee' + _description = 'Incident Sub Type' + + name = fields.Char(string="Incident Sub Type", required=True) + +class EmployeeDisciplinaryLines(models.Model): + _name = 'employee.disciplinary.line' + _rec_name = 'hr_emp_many' + + emp_many = fields.Many2one('employee.disciplinary', string='Employee Disp') + hr_emp_many = fields.Many2one('hr.employee', string='Employee Number') + hr_emp_many_name = fields.Char(related='hr_emp_many.name', string='Employee Name') + + +class ManageIncident(models.Model): + _name = 'manage.incident' + _inherit = ['mail.thread', 'mail.activity.mixin'] + + # employee_name_ids = fields.One2many('employee.disciplinary','employee',string="Employee Name") + employee_disciplinary_id = fields.Many2one("employee.disciplinary", string="Employee Disp") + employee_code_list1 = fields.Many2many("hr.employee", string="Employees Involved in the Incident", + related='employee_disciplinary_id.emp_many_disp', tracking=True) + incident_dat = fields.Datetime(related='employee_disciplinary_id.incident_date', string='Incident Date & Time', + tracking=True) + employee_by_code = fields.Many2one(related='employee_disciplinary_id.employee_code', + string="Reported By Employee Name") + incident_sum = fields.Text(related='employee_disciplinary_id.incident_summary', string='Incident Summary', + tracking=True) + incident_typ = fields.Many2one(related='employee_disciplinary_id.incident_type', string="Incident Type", + tracking=True) + incident_sub_typ = fields.Many2many(related='employee_disciplinary_id.incident_sub_type', + string="Incident Sub Type", tracking=True) + # corrective_action_emp_id = fields.Many2one(related='employee_disciplinary_id.corrective_action_id', + # string="Corrective Action", tracking=True) + state = fields.Selection(([ + ('pending_inquiry', 'Pending Inquiry'), + ('in_progress', 'In Process'), + ('closed', 'Closed') + ]), string="Status", default='pending_inquiry', tracking=True) + emp_incident = fields.Many2one('hr.employee') + employee_inquiry = fields.One2many('manage.incident.line', 'employee_inquiry_state') + + def button_in_progress(self): + self.state = 'in_progress' + + # def button_closed(self): + # for rec in self: + # rec.state = 'closed' + + def button_closed(self): + for rec in self: + rec.state = 'closed' + update_into_employee = rec.env['hr.employee'].search([('id', '=', rec.employee_code_list1.id)]) + records = { + } + if records: + update_into_employee.write(records) + + print('triggered 2') + + + + + + +class CorrectiveActions(models.Model): + _name = "corrective.actions" + + name = fields.Char(string="Name") + + +class ManageIncidentLine(models.Model): + _name = 'manage.incident.line' + _inherit = ['mail.thread'] + _description = 'Manage Incident Line' + + corrective_action_id = fields.Many2one('corrective.actions', string="Corrective Action", tracking=True, + required=True) + internal_panel = fields.Many2many('hr.employee', string="Internal Panel Members", tracking=True, + required=True) + external_panel = fields.Char(string="External Panel Members") + due_date = fields.Date(string="Due Date") + last_action_date = fields.Datetime(string="Last Action Date", compute='_compute_last_action_date', + default=date.today()) + recommendation = fields.Char(string="Recommendation", tracking=True, required=True) + venue = fields.Char(string='Venue') + inquiry_summary = fields.Char(string='Inquiry Summary', tracking=True, required=True) + is_guilty = fields.Selection(([ + ('yes', 'Yes'), + ('no', 'No'), + ]), string="Is the Employee Guilt of the Incident", default='no', tracking=True) + inquiry_date = fields.Datetime(string="Inquiry Date and Time", required=True) + employee_inquiry_state = fields.Many2one('manage.incident') + + @api.depends('inquiry_date') + def _compute_last_action_date(self): + for line in self: + if not line.employee_inquiry_state or line == line.employee_inquiry_state.employee_inquiry[0]: + line.last_action_date = False + else: + previous_line = line.employee_inquiry_state.employee_inquiry.filtered(lambda l: l.inquiry_date < line.inquiry_date) + sorted_previous_line = previous_line.sorted(key=lambda l: l.inquiry_date, reverse=True) + if sorted_previous_line: + line.last_action_date = sorted_previous_line[0].inquiry_date + else: + line.last_action_date = False 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/security/ir.model.access.csv b/addons_extensions/disciplinary/security/ir.model.access.csv new file mode 100755 index 000000000..233dcb2b0 --- /dev/null +++ b/addons_extensions/disciplinary/security/ir.model.access.csv @@ -0,0 +1,22 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_employee_disciplinary,employee_disciplinary,model_employee_disciplinary,,1,1,1,1 +access_incident_employee,incident_employee,model_incident_employee,,1,1,1,1 +access_incident_sub_employee,incident_sub_employee,model_incident_sub_employee,,1,1,1,1 +access_employee_disciplinary_line,employee_disciplinary_line,model_employee_disciplinary_line,,1,1,1,1 +access_manage_incident,manage_incident,model_manage_incident,,1,1,1,1 +access_manage_incident_line,manage_incident_line,model_manage_incident_line,,1,1,1,1 +access_corrective_actions,corrective_actions,model_corrective_actions,,1,1,1,1 + +access_hr_employee_disciplinary,hr.employee.disciplinary,model_hr_employee_disciplinary,,1,1,1,1 +access_hr_disciplinary_complaint_line,hr.disciplinary.complaint.line,model_hr_disciplinary_complaint_line,,1,1,1,1 +access_hr_disciplinary_action_line,hr.disciplinary.action.line,model_hr_disciplinary_action_line,,1,1,1,1 +access_disciplinary_action_type,disciplinary.action.type,model_disciplinary_action_type,,1,1,1,1 +access_disciplinary_complaint_type,disciplinary.complaint.type,model_disciplinary_complaint_type,,1,1,1,1 +access_disciplinary_mistake_type,disciplinary.mistake.type,model_disciplinary_mistake_type,,1,1,1,1 + + + + + + + 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/disciplinary_view.xml b/addons_extensions/disciplinary/views/disciplinary_view.xml new file mode 100755 index 000000000..08a3bfdd9 --- /dev/null +++ b/addons_extensions/disciplinary/views/disciplinary_view.xml @@ -0,0 +1,264 @@ + + + + + Employee Disciplinary list + employee.disciplinary + + + + + + + + + + + + Employee Disciplinary form + employee.disciplinary + +
+ + + + + + + + + +
+
+ + + + Manage Incident list + manage.incident + + + + + + + + + + + + + + Manage Incident form + manage.incident + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + incident Employee list + incident.employee + + + + + + + + + + incident Employee form + incident.employee + +
+ + + + + + + +
+ +
+
+ + + + Incident Reporting + employee.disciplinary + list,form + + + + + Manage Incident + manage.incident + list,form + + + + Incident Type + incident.employee + list,form + + + + Employee Disciplinary + hr.employee.disciplinary + 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/disciplinary/views/incident_sub_type.xml b/addons_extensions/disciplinary/views/incident_sub_type.xml new file mode 100644 index 000000000..bd4c35cce --- /dev/null +++ b/addons_extensions/disciplinary/views/incident_sub_type.xml @@ -0,0 +1,45 @@ + + + + + + + incident.sub.employee.list + incident.sub.employee + + + + + + + + + + incident.sub.employee.form + incident.sub.employee + +
+ + + + + +
+
+
+ + + + Incident Sub Type + incident.sub.employee + list,form + + + + + + + + +
+
diff --git a/addons_extensions/disciplinary/views/mistake_type_views.xml b/addons_extensions/disciplinary/views/mistake_type_views.xml new file mode 100644 index 000000000..cbc9efe6c --- /dev/null +++ b/addons_extensions/disciplinary/views/mistake_type_views.xml @@ -0,0 +1,42 @@ + + + + + mistake.type.list + disciplinary.mistake.type + + + + + + + + + + mistake.type.form + disciplinary.mistake.type + +
+ + + + + +
+
+
+ + + + Mistake Type + disciplinary.mistake.type + 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/helpdesk/controllers/portal.py b/addons_extensions/helpdesk/controllers/portal.py index 04d265b04..c0d970109 100644 --- a/addons_extensions/helpdesk/controllers/portal.py +++ b/addons_extensions/helpdesk/controllers/portal.py @@ -179,9 +179,8 @@ class CustomerPortal(portal.CustomerPortal): ('use_website_helpdesk_form', '=', True), '|', ('company_id', '=', False), - ('company_id', 'in', [request.env.company.id]) + ('company_id', 'in', [request.env.user.company_id.id]) ]) - selection_dict = dict( request.env['helpdesk.team']._fields['portal_ticket_type'].selection ) diff --git a/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard.py b/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard.py index 3979d0c06..19760fec8 100644 --- a/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard.py +++ b/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard.py @@ -483,6 +483,10 @@ class HrRecruitmentAutoDocWizard(models.TransientModel): } def _get_jd_required_fields(self): + category_dict = { + str(category['id']): category['category_name'] + for category in self.env['job.category'].sudo().search_read([], ['id', 'category_name']) + } return { "request_id": {"type": "string", "description": "Request or requisition identifier"}, "start_date": {"type": "string", "description": "Requested start date"}, @@ -495,6 +499,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel): "secondary_skills": {"type": "list", "description": "Secondary or nice to have skills"}, "budget": {"type": "string", "description": "Budget or salary range"}, "experience_years": {"type": "float", "description": "Expected total years of experience"}, + "job_category": {"type": "selection", "description": "Based on over all Job description which one matches more accurately give me the key: %s"%(category_dict)} } def _get_resume_prompt(self): @@ -1766,8 +1771,11 @@ class HrRecruitmentAutoDocWizard(models.TransientModel): "secondary_skill_ids": [(6, 0, secondary_skills.ids)] if secondary_skills else False, } job_category = self._match_job_category(parsed_data, parsed_payload) - if job_category: - write_vals["job_category"] = job_category.id + + if job_request and job_request.job_category: + if job_request.job_id and job_request.job_id.job_category != job_request.job_category: + job_request.job_id.job_category = job_request.job_category.id + elif job_category: if job_request.job_id and job_request.job_id.job_category != job_category: job_request.job_id.job_category = job_category.id write_vals = {key: value for key, value in write_vals.items() if value not in (False, None, "")} @@ -1805,7 +1813,14 @@ class HrRecruitmentAutoDocWizard(models.TransientModel): job_category = self._match_job_category(parsed_data, parsed_payload) if job_category and job.job_category != job_category: job.job_category = job_category.id + category_dict = { + str(category['id']): category['category_name'] + for category in self.env['job.category'].search_read([], ['id', 'category_name']) + } + reverse_category = {v: k for k, v in category_dict.items()} + job_category = parsed_data.get("job_category") + job_category_id = reverse_category.get(job_category, job_category) create_vals = { "job_id": job.id, "company_id": self.env.company.id, @@ -1813,12 +1828,17 @@ class HrRecruitmentAutoDocWizard(models.TransientModel): "requirements": parsed_data.get("requirements"), "target_from": self._parse_date_value(parsed_data.get("start_date")), "target_to": self._parse_date_value(parsed_data.get("end_date")), - "job_category": job_category.id if job_category else False, + "job_category": int(job_category_id) if job_category_id and job_category_id.isdigit() else False, + "address_id":False, + "recruitment_type": 'external' + } if request_id: create_vals["recruitment_sequence"] = request_id create_vals = {key: value for key, value in create_vals.items() if value not in (False, None, "")} - return job_request_model.create(create_vals), "created" + job_request = job_request_model.create(create_vals) + job_request.sudo().write({"address_id" : False}) + return job_request, "created" def _match_existing_job_position(self, job_title): hr_job_model = self.env["hr.job"] diff --git a/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard_views.xml b/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard_views.xml index 5bcf5ced6..83038e1b5 100644 --- a/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard_views.xml +++ b/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard_views.xml @@ -5,10 +5,10 @@ hr.recruitment.auto.doc.wizard
-
-
+ + + + @@ -39,7 +39,15 @@ invisible="1"/> - +
+
+ + 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..612894f91 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,14 +669,14 @@ + t-value="', '.join(object.hr_job_recruitment.locations.mapped('location_name'))"/> -
+

Dear Sir/Madam,

Please find the applicant details below for your review.

@@ -784,10 +784,10 @@ - + -
+

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/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 194e84218..2a89d280a 100644 --- a/addons_extensions/hr_recruitment_extended/views/hr_job_recruitment.xml +++ b/addons_extensions/hr_recruitment_extended/views/hr_job_recruitment.xml @@ -110,10 +110,10 @@ - + - + @@ -201,6 +201,20 @@ + + + + + + + + + + + + + + @@ -429,6 +443,7 @@ Job Positions Recruitment hr.job.recruitment kanban,list,form +

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

@@ -482,7 +498,7 @@ active="0" sequence="2"/> - - + @@ -338,6 +338,7 @@ {'search_default_my_candidates': 1,'active_test': False} + kanban,list,form,activity 1 - + diff --git a/addons_extensions/hr_recruitment_extended/views/submission_share_history.xml b/addons_extensions/hr_recruitment_extended/views/submission_share_history.xml new file mode 100644 index 000000000..40fbda714 --- /dev/null +++ b/addons_extensions/hr_recruitment_extended/views/submission_share_history.xml @@ -0,0 +1,20 @@ + + + + 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..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 @@ -1,65 +1,128 @@ 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) + 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[active_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 + 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 + + # 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 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 @@ + + 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_contract.py b/addons_extensions/hr_resignation/models/hr_contract.py index 725cfbcf2..f0f5a931b 100644 --- a/addons_extensions/hr_resignation/models/hr_contract.py +++ b/addons_extensions/hr_resignation/models/hr_contract.py @@ -33,9 +33,9 @@ class HrContract(models.Model): """Get the default notice period from the configuration. :return: The default notice period in days. :rtype: int """ - return self.env['ir.config_parameter'].get_param( + return self.env['ir.config_parameter'].sudo().get_param( 'hr_employee_updation.no_of_days') if self.env[ - 'ir.config_parameter'].get_param( + 'ir.config_parameter'].sudo().get_param( 'hr_employee_updation.notice_period') else 0 notice_days = fields.Integer(string="Notice Period", 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 @@

- +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Employee{self.employee_appraisal_id.name or ''}
Department{self.department_appraisal_id.name or ''}
Current Salary{self.current_salary or 0}
Appraisal Percentage{self.appraisal_percentage or 0}%
Appraisal Amount{self.appraisal_amount or 0}
Revised Salary{self.new_salary or 0}
Finance Remarks{self.finance_remarks or ''}
+ +
+ +

+ Kindly review and proceed with the next level approval. +

+ +
+ +

Regards,

+

Finance 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'Finance Approval - {self.employee_appraisal_id.name}', + 'default_body': body_html, + 'move_next_stage': True, + 'mark_finance_approved': True, + } + + return { + 'type': 'ir.actions.act_window', + 'res_model': 'mail.compose.message', + 'view_mode': 'form', + 'target': 'new', + 'context': ctx, + } + + def action_finance_head(self): + self.ensure_one() + + if not self.finance_head_remarks: + raise ValidationError( + _('Please provide Finance Head Remarks.') + ) + + email_to = ",".join( + filter(None, [ + self.employee_appraisal_id.work_email, + self.managerapp_id.work_email, + self.creator_email + ]) + ) + + body_html = f""" +
+ +

Hello,

+ +

+ Finance Head has completed the appraisal review for + {self.employee_appraisal_id.name}. +

+ +

+ The compensation revision details have been reviewed and approved. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Employee{self.employee_appraisal_id.name or ''}
Department{self.department_appraisal_id.name or ''}
Current Salary{self.current_salary or 0}
Appraisal %{self.appraisal_percentage or 0}%
Increment Amount{self.appraisal_amount or 0}
Revised Salary{self.new_salary or 0}
Finance Head Remarks{self.finance_head_remarks or ''}
+ +
+ +

+ Kindly proceed with the next level approval. +

+ +
+ +

Regards,

+

Finance 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'Finance Head Approval - {self.employee_appraisal_id.name}', + 'default_body': body_html, + 'move_next_stage': True, + 'mark_finance_head_approved': True, + } + + return { + 'type': 'ir.actions.act_window', + 'res_model': 'mail.compose.message', + 'view_mode': 'form', + 'target': 'new', + 'context': ctx, + } + def _send_manager_notification_mail(self): self.ensure_one() email_to = self.manager_email @@ -434,7 +766,7 @@ class EmployeeAppraisal(models.Model): Performance Period - {self.appraisal_period_id.appraisal_type_id.id or ''} + {self.appraisal_period_id.appraisal_name or ''}
@@ -455,81 +787,94 @@ class EmployeeAppraisal(models.Model): } 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 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() + if not self.manager_remarks: + raise ValidationError("Please give the Manager Remarks") + + manager_email = self.managerapp_id.work_email or '' employee_email = self.employee_appraisal_id.work_email or '' creator_email = self.creator_email or '' + emails = ",".join( - filter(None, [employee_email, creator_email]) + filter(None, [ + manager_email, + employee_email, + creator_email + ]) ) + body_html = f""" -
-

Hello,Team

-

- Your appraisal evaluation has been initiated. -

-
- - - +
+

Hello Team,

-
- - - +

+ The employee has completed the self-assessment as part of the performance appraisal process. + Your feedback and evaluation are now requested. +

- - - - +
- - -
- 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 -

-
+ + + + + + + + + + + + + + + + + +
Employee{self.employee_appraisal_id.name or ''}
Department{self.department_appraisal_id.name or ''}
Appraisal Template{self.template_id.name or ''}
Performance Period{self.appraisal_period_id.appraisal_name or ''}
+ +
+ +

+ Kindly review the employee's self-assessment and provide your feedback, + evaluation, and recommendations within the appraisal timeline. +

+ +

+ Your valuable input will contribute to the employee's overall performance review. +

+ +
+ +

+ Regards, +

+ +

+ Manager Team +

+
""" 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..67c2c2712 --- /dev/null +++ b/addons_extensions/hrms_employee_appraisal/models/hr_head_nofication.py @@ -0,0 +1,122 @@ +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) + image_1920 = fields.Image(related='hr_employee_id.image_1920') + 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..667ba5f68 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([ @@ -44,37 +47,83 @@ class HrNoticeAppraisal(models.Model): 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") + image_1920 = fields.Image(related='hr_employee_id.image_1920',string='Employee Image') - @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 +138,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 +236,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 +251,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 @@ - -