From ec42e2736c03a86c5a3a87d3442af4b592b39bf3 Mon Sep 17 00:00:00 2001 From: administrator Date: Wed, 8 Jan 2025 15:01:54 +0530 Subject: [PATCH] Pranay : addons extra --- addons_extensions/flutter_odoo/__init__.py | 2 + .../flutter_odoo/__manifest__.py | 27 + .../flutter_odoo/controllers/__init__.py | 0 .../flutter_odoo/models/__init__.py | 1 + .../flutter_odoo/models/hr_employee.py | 100 ++++ .../hr_attendance_extended/__init__.py | 1 + .../hr_attendance_extended/__manifest__.py | 30 + .../hr_attendance_extended/data/cron.xml | 16 + .../hr_attendance_extended/models/__init__.py | 1 + .../models/hr_attendance.py | 106 ++++ .../security/ir.model.access.csv | 3 + .../views/hr_attendance.xml | 87 +++ .../hr_employee_appraisal/__init__.py | 1 + .../hr_employee_appraisal/__manifest__.py | 32 ++ .../hr_employee_appraisal/data/cron.xml | 16 + .../data/default_data.xml | 44 ++ .../data/mail_template_data.xml | 242 +++++++++ .../hr_employee_appraisal/models/__init__.py | 4 + .../models/apprisal_period_master.py | 27 + .../models/employee_appraisal.py | 512 ++++++++++++++++++ .../models/kpi_kra_master.py | 53 ++ .../models/res_config_settings.py | 20 + .../security/ir.model.access.csv | 25 + .../security/security.xml | 62 +++ .../static/description/icon.png | Bin 0 -> 2318 bytes .../static/src/img/icon.png | Bin 0 -> 2318 bytes .../views/appraisal_period_master.xml | 64 +++ .../views/employee_appraisal.xml | 247 +++++++++ .../views/kpi_kra_master.xml | 97 ++++ .../views/res_config_settings.xml | 50 ++ .../hr_employee_extended/__init__.py | 1 + .../hr_employee_extended/__manifest__.py | 30 + .../hr_employee_extended/models/__init__.py | 2 + .../models/hr_employee.py | 82 +++ .../models/work_location_history.py | 31 ++ .../security/ir.model.access.csv | 3 + .../views/hr_employee.xml | 64 +++ .../hr_employee_extended/wizards/__init__.py | 1 + .../wizards/work_location_wizard.py | 37 ++ .../wizards/work_location_wizard.xml | 22 + .../hr_timeoff_extended/__init__.py | 1 + .../hr_timeoff_extended/__manifest__.py | 29 + .../hr_timeoff_extended/data/cron.xml | 9 + .../hr_timeoff_extended/models/__init__.py | 2 + .../hr_timeoff_extended/models/hr_employee.py | 6 + .../hr_timeoff_extended/models/hr_timeoff.py | 256 +++++++++ .../hr_timeoff_extended/views/hr_employee.xml | 19 + .../hr_timeoff_extended/views/hr_timeoff.xml | 233 ++++++++ 48 files changed, 2698 insertions(+) create mode 100644 addons_extensions/flutter_odoo/__init__.py create mode 100644 addons_extensions/flutter_odoo/__manifest__.py create mode 100644 addons_extensions/flutter_odoo/controllers/__init__.py create mode 100644 addons_extensions/flutter_odoo/models/__init__.py create mode 100644 addons_extensions/flutter_odoo/models/hr_employee.py create mode 100644 addons_extensions/hr_attendance_extended/__init__.py create mode 100644 addons_extensions/hr_attendance_extended/__manifest__.py create mode 100644 addons_extensions/hr_attendance_extended/data/cron.xml create mode 100644 addons_extensions/hr_attendance_extended/models/__init__.py create mode 100644 addons_extensions/hr_attendance_extended/models/hr_attendance.py create mode 100644 addons_extensions/hr_attendance_extended/security/ir.model.access.csv create mode 100644 addons_extensions/hr_attendance_extended/views/hr_attendance.xml create mode 100644 addons_extensions/hr_employee_appraisal/__init__.py create mode 100644 addons_extensions/hr_employee_appraisal/__manifest__.py create mode 100644 addons_extensions/hr_employee_appraisal/data/cron.xml create mode 100644 addons_extensions/hr_employee_appraisal/data/default_data.xml create mode 100644 addons_extensions/hr_employee_appraisal/data/mail_template_data.xml create mode 100644 addons_extensions/hr_employee_appraisal/models/__init__.py create mode 100644 addons_extensions/hr_employee_appraisal/models/apprisal_period_master.py create mode 100644 addons_extensions/hr_employee_appraisal/models/employee_appraisal.py create mode 100644 addons_extensions/hr_employee_appraisal/models/kpi_kra_master.py create mode 100644 addons_extensions/hr_employee_appraisal/models/res_config_settings.py create mode 100644 addons_extensions/hr_employee_appraisal/security/ir.model.access.csv create mode 100644 addons_extensions/hr_employee_appraisal/security/security.xml create mode 100644 addons_extensions/hr_employee_appraisal/static/description/icon.png create mode 100644 addons_extensions/hr_employee_appraisal/static/src/img/icon.png create mode 100644 addons_extensions/hr_employee_appraisal/views/appraisal_period_master.xml create mode 100644 addons_extensions/hr_employee_appraisal/views/employee_appraisal.xml create mode 100644 addons_extensions/hr_employee_appraisal/views/kpi_kra_master.xml create mode 100644 addons_extensions/hr_employee_appraisal/views/res_config_settings.xml create mode 100644 addons_extensions/hr_employee_extended/__init__.py create mode 100644 addons_extensions/hr_employee_extended/__manifest__.py create mode 100644 addons_extensions/hr_employee_extended/models/__init__.py create mode 100644 addons_extensions/hr_employee_extended/models/hr_employee.py create mode 100644 addons_extensions/hr_employee_extended/models/work_location_history.py create mode 100644 addons_extensions/hr_employee_extended/security/ir.model.access.csv create mode 100644 addons_extensions/hr_employee_extended/views/hr_employee.xml create mode 100644 addons_extensions/hr_employee_extended/wizards/__init__.py create mode 100644 addons_extensions/hr_employee_extended/wizards/work_location_wizard.py create mode 100644 addons_extensions/hr_employee_extended/wizards/work_location_wizard.xml create mode 100644 addons_extensions/hr_timeoff_extended/__init__.py create mode 100644 addons_extensions/hr_timeoff_extended/__manifest__.py create mode 100644 addons_extensions/hr_timeoff_extended/data/cron.xml create mode 100644 addons_extensions/hr_timeoff_extended/models/__init__.py create mode 100644 addons_extensions/hr_timeoff_extended/models/hr_employee.py create mode 100644 addons_extensions/hr_timeoff_extended/models/hr_timeoff.py create mode 100644 addons_extensions/hr_timeoff_extended/views/hr_employee.xml create mode 100644 addons_extensions/hr_timeoff_extended/views/hr_timeoff.xml diff --git a/addons_extensions/flutter_odoo/__init__.py b/addons_extensions/flutter_odoo/__init__.py new file mode 100644 index 000000000..38718f084 --- /dev/null +++ b/addons_extensions/flutter_odoo/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import controllers \ No newline at end of file diff --git a/addons_extensions/flutter_odoo/__manifest__.py b/addons_extensions/flutter_odoo/__manifest__.py new file mode 100644 index 000000000..4283a00ce --- /dev/null +++ b/addons_extensions/flutter_odoo/__manifest__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Odoo Flutter", + + 'summary': 'Integrating flutter with odoo', + + 'description': """ + Creating the required functions according to our needs to add it in the flutter; + """, + + 'author': "Pranay", + 'website': "https://www.ftprotech.com", + + # Categories can be used to filter modules in modules listing + # Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml + # for the full list + 'category': 'Flutter', + 'version': '0.1', + + # any module necessary for this one to work correctly + 'depends': ['base','hr','hr_attendance','hr_employee_extended'], + + # always loaded + 'data': [ + ], +} + diff --git a/addons_extensions/flutter_odoo/controllers/__init__.py b/addons_extensions/flutter_odoo/controllers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/addons_extensions/flutter_odoo/models/__init__.py b/addons_extensions/flutter_odoo/models/__init__.py new file mode 100644 index 000000000..d25661f9c --- /dev/null +++ b/addons_extensions/flutter_odoo/models/__init__.py @@ -0,0 +1 @@ +from . import hr_employee \ No newline at end of file diff --git a/addons_extensions/flutter_odoo/models/hr_employee.py b/addons_extensions/flutter_odoo/models/hr_employee.py new file mode 100644 index 000000000..86d02b7ef --- /dev/null +++ b/addons_extensions/flutter_odoo/models/hr_employee.py @@ -0,0 +1,100 @@ +from odoo import _, api, fields, models + + +class HrEmployeeBase(models.Model): + _inherit = "hr.employee" + + def fetch_all_employees_info(self, user_id=None): + if user_id: + employee = self.env['hr.employee'].sudo().search([('user_id','=',user_id)], limit=1) + + if employee: + + data = { + 'id': str(employee.id), + 'employee_code': str(employee.employee_id), + 'name': str(employee.name), + 'work_email': str(employee.work_email), + 'work_phone': str(employee.work_phone), + 'job_title': str(employee.job_title), + 'department_name': str(employee.department_id.name), + 'work_location_name': str(employee.work_location_id.name), + 'manager': str(employee.parent_id.name), + 'address': str(", ".join( + filter(None, [ + employee.private_street, + employee.private_street2, + employee.private_city, + employee.private_state_id.name if employee.private_state_id else False, + employee.private_zip, + employee.private_country_id.name if employee.private_country_id else False + ]) + ) or 'N/A'), + 'bank_account_id': str(employee.bank_account_id.acc_number if employee.bank_account_id else 'N/A'), + 'bank_name': str(employee.bank_account_id.bank_id.name if employee.bank_account_id and employee.bank_account_id.bank_id else 'N/A'), + 'bank_ifsc': str(employee.bank_account_id.bank_id.bic if employee.bank_account_id and employee.bank_account_id.bank_id else 'N/A'), + 'doj': str(fields.Date.from_string(employee.doj).strftime('%d-%m-%Y') if employee.doj else False), + 'birthday': str(fields.Date.from_string(employee.birthday).strftime('%d-%m-%Y') if employee.birthday else False), + 'gender': str(employee.gender).upper(), + 'marital': str(employee.marital).upper(), + 'aadhar_no': str(employee.identification_id), + 'pan_no': str(employee.pan_no), + 'profile_pic': employee.image_1920, + } + return data + + def mobile_check_in_out(self,is_check_in,longitude=None,latitude=None,in_ip_address=None): + """ + Check in or check out logic. + :param latitude: Latitude from the mobile app. + :param longitude: Longitude from the mobile app. + :param is_check_in: Boolean to check whether it is check-in or check-out. + """ + employee = self.env.user.employee_id + attendance_obj = self.env['hr.attendance'] + + # Check if there is an existing attendance record with no checkout + attendance = attendance_obj.search([ + ('employee_id', '=', employee.id), + ('check_out', '=', False) + ], limit=1) + + if not is_check_in: + # If it's check-in, create a new attendance record + if not attendance: + record = attendance_obj.create({ + 'employee_id': employee.id, + 'check_in': fields.Datetime.now(), + 'in_latitude': latitude if latitude and latitude!=0 else False, + 'in_longitude': longitude if longitude and longitude!=0 else False, + 'in_mode': 'systray' if latitude != 0 else 'manual', + 'in_ip_address': in_ip_address if in_ip_address else False, + 'in_browser': 'Mobile' + + }) + return { + 'message': 'Checked in successfully', + 'attendance_id': record.id + } + else: + return { + 'message': 'Already checked in', + } + else: + # If it's check-out, update the existing attendance record + if attendance: + attendance.write({ + 'check_out': fields.Datetime.now(), + 'out_latitude': latitude if latitude and latitude != 0 else False, + 'out_longitude': longitude if longitude and longitude != 0 else False, + 'out_mode': 'systray' if latitude != 0 else 'manual', + 'out_ip_address': in_ip_address if in_ip_address else False, + 'out_browser': 'Mobile' + }) + return { + 'message': 'Checked out successfully', + } + else: + return { + 'message': 'No active check-in found', + } \ No newline at end of file diff --git a/addons_extensions/hr_attendance_extended/__init__.py b/addons_extensions/hr_attendance_extended/__init__.py new file mode 100644 index 000000000..9a7e03ede --- /dev/null +++ b/addons_extensions/hr_attendance_extended/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/addons_extensions/hr_attendance_extended/__manifest__.py b/addons_extensions/hr_attendance_extended/__manifest__.py new file mode 100644 index 000000000..c779deeee --- /dev/null +++ b/addons_extensions/hr_attendance_extended/__manifest__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Attendance Extended", + + 'summary': 'Customized Attendance', + + 'description': """ + Customized Attendance data, reports + """, + + 'author': "Pranay", + 'website': "https://www.ftprotech.com", + + # Categories can be used to filter modules in modules listing + # Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml + # for the full list + 'category': 'Human Resources/Attendances', + 'version': '0.1', + + # any module necessary for this one to work correctly + 'depends': ['base','hr','hr_attendance','hr_holidays','hr_employee_extended'], + + # always loaded + 'data': [ + 'security/ir.model.access.csv', + 'data/cron.xml', + 'views/hr_attendance.xml' + ], +} + diff --git a/addons_extensions/hr_attendance_extended/data/cron.xml b/addons_extensions/hr_attendance_extended/data/cron.xml new file mode 100644 index 000000000..02d8f8c6a --- /dev/null +++ b/addons_extensions/hr_attendance_extended/data/cron.xml @@ -0,0 +1,16 @@ + + + + + Attendance: Check Out Update + + code + model.update_hr_attendance_check_out() + + 1 + days + + + + + \ No newline at end of file diff --git a/addons_extensions/hr_attendance_extended/models/__init__.py b/addons_extensions/hr_attendance_extended/models/__init__.py new file mode 100644 index 000000000..0d1e0f5a5 --- /dev/null +++ b/addons_extensions/hr_attendance_extended/models/__init__.py @@ -0,0 +1 @@ +from . import hr_attendance \ No newline at end of file diff --git a/addons_extensions/hr_attendance_extended/models/hr_attendance.py b/addons_extensions/hr_attendance_extended/models/hr_attendance.py new file mode 100644 index 000000000..196917d8b --- /dev/null +++ b/addons_extensions/hr_attendance_extended/models/hr_attendance.py @@ -0,0 +1,106 @@ +from odoo import fields, api, models +from datetime import datetime + + +class AttendanceAttendance(models.Model): + _name = "attendance.attendance" + + date = fields.Date(string='Date', default=fields.Date.today, required=True) + work_location_id = fields.Many2one('hr.work.location', default=lambda self: self.env.company.id) + attendance_data = fields.One2many('attendance.data','attendance_id', string="Attendance Data") + + _sql_constraints = [ + ('name_date_work_location_id_unique', 'unique(date, work_location_id)', 'The combination of date and work_location_id should be unique.') + ] + + def get_attendance_for_date(self, rec_date, employee_id): + # Convert rec.date to a datetime object for filtering + start_of_day = fields.Datetime.to_string( + fields.Datetime.from_string(rec_date).replace(hour=0, minute=0, second=0)) + end_of_day = fields.Datetime.to_string( + fields.Datetime.from_string(rec_date).replace(hour=23, minute=59, second=59)) + + # Filter attendance records for the given employee and date range + attendance_records = self.env['hr.attendance'].sudo().search([ + ('employee_id', '=', employee_id), + ('check_in', '>=', start_of_day), + ('check_in', '<=', end_of_day) + ],order='check_in asc') + if not attendance_records: + return False, False, False + + # Initialize variables to track min check_in and max check_out + min_check_in = None + max_check_out = None + + for record in attendance_records: + if min_check_in is None or record.check_in < min_check_in: + min_check_in = record.check_in + if max_check_out is None or (record.check_out and record.check_out > max_check_out): + max_check_out = record.check_out + + # If no check_out found, set max_check_out to current time + if not max_check_out: + max_check_out = False + total_worked_hours = sum(record.worked_hours for record in attendance_records) + + # Return the min_check_in and max_check_out + return min_check_in, max_check_out, total_worked_hours + + def update_attendance_data(self): + for rec in self: + if rec.date and rec.work_location_id: + rec.attendance_data.unlink() + emp_work_location_history = self.env['emp.work.location.history'].sudo().search([('employee_id.company_id','=',self.env.company.id),('employee_id.work_location_id','=',rec.work_location_id.id),('start_date','<=',rec.date),'|',('end_date','=',False),('end_date','>=',rec.date)]).employee_id.ids + employees = self.env['hr.employee'].sudo().search([('id','not in',emp_work_location_history),('work_location_id','=',rec.work_location_id.id)]).ids + all_employees = self.env['hr.employee'].sudo().search([('id','in',list(set(emp_work_location_history) | set(employees)))]) + attendance_data = list() + for emp in all_employees: + leaves = self.env['hr.leave'].search([('employee_id','=',emp.id),('request_date_from', '<=', rec.date),('request_date_to', '>=', rec.date),('state','=','validate')]) + check_in, check_out, worked_hours = self.get_attendance_for_date(rec.date,emp.id) + if leaves: + status = 'leave' + elif check_in or check_out: + status = 'present' + else: + status = 'no_info' + if not leaves and (check_in and check_out): + # Calculate total hours between check_in and check_out in float format + total_hours = (check_out - check_in).total_seconds() / 3600 # Convert seconds to hours + + # Calculate out_time as the difference between total_hours and worked_hours + out_time = total_hours - (worked_hours if worked_hours else 0) + else: + total_hours = 0 + out_time = 0 + + data = self.env['attendance.data'].sudo().create({ + 'employee_id': emp.id, + 'min_check_in': check_in if check_in else False, + 'max_check_out': check_out if check_out else False, + 'worked_hours': worked_hours if worked_hours else False, + 'out_time': out_time if out_time else False, + 'status': status, + 'attendance_id': rec.id + }) + attendance_data.append(data.id) + rec.attendance_data = [(6, 0, attendance_data)] + + + def update_hr_attendance_check_out(self): + attendance = self.env['hr.attendance'].sudo().search([('check_in','!=', False),('check_out','=',False)]) + for rec in attendance: + rec.check_out = fields.Datetime.to_string( + fields.Datetime.from_string(rec.check_in).replace(hour=21, minute=0, second=59)) + +class AttendanceData(models.Model): + _name = 'attendance.data' + + employee_id = fields.Many2one('hr.employee') + min_check_in = fields.Datetime(string="Check In") + max_check_out = fields.Datetime(string="Check Out") + out_time = fields.Float() + worked_hours = fields.Float() + # extra_hours = fields.Float() + status = fields.Selection([('leave','On Leave'),('present','Present'),('no_info','No Information')]) + attendance_id = fields.Many2one('attendance.attendance') diff --git a/addons_extensions/hr_attendance_extended/security/ir.model.access.csv b/addons_extensions/hr_attendance_extended/security/ir.model.access.csv new file mode 100644 index 000000000..417af57fa --- /dev/null +++ b/addons_extensions/hr_attendance_extended/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_attendance_attendance_user,attendance.attendance.access,model_attendance_attendance,base.group_user,1,1,1,1 +access_attendance_data_user,attendance.data.access,model_attendance_data,base.group_user,1,1,1,1 diff --git a/addons_extensions/hr_attendance_extended/views/hr_attendance.xml b/addons_extensions/hr_attendance_extended/views/hr_attendance.xml new file mode 100644 index 000000000..b250affe0 --- /dev/null +++ b/addons_extensions/hr_attendance_extended/views/hr_attendance.xml @@ -0,0 +1,87 @@ + + + + + + attendance.attendance.list + attendance.attendance + + + + + + + + + + attendance.attendance.form + attendance.attendance + +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + attendance.data.list + attendance.data + + + + + + + + + + + + + + + + + + Attendance + attendance.attendance + list,form + +

+ Create your first attendance record +

+
+
+ + + + + + +
+
diff --git a/addons_extensions/hr_employee_appraisal/__init__.py b/addons_extensions/hr_employee_appraisal/__init__.py new file mode 100644 index 000000000..9a7e03ede --- /dev/null +++ b/addons_extensions/hr_employee_appraisal/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/addons_extensions/hr_employee_appraisal/__manifest__.py b/addons_extensions/hr_employee_appraisal/__manifest__.py new file mode 100644 index 000000000..eebf70654 --- /dev/null +++ b/addons_extensions/hr_employee_appraisal/__manifest__.py @@ -0,0 +1,32 @@ +{ + 'name': 'Employee Appraisal', + 'version': '1.0', + 'category': 'Human Resources', + 'summary': 'Module for managing employee appraisals and performance reviews.', + 'description': """ + This module allows you to manage employee appraisals and performance reviews. It provides functionality to create and track appraisals, assign reviewers, and evaluate employee performance based on defined criteria. + """, + 'author': 'Pranay', + 'website': 'https://www.ftprotech.com', + 'depends': ['hr','hr_employee_extended'], # Assuming 'hr' and 'hr_appraisal' modules are required + 'data': [ + 'security/security.xml', + 'security/ir.model.access.csv', + # 'data/default_data.xml', + 'data/mail_template_data.xml', + 'views/appraisal_period_master.xml', + 'views/kpi_kra_master.xml', + 'views/employee_appraisal.xml', + 'views/res_config_settings.xml', + ], + 'assets': { + 'web.assets_backend': [ + '/hr_employee_appraisal/static/description/*', + ], + }, + 'images': ['static/description/icon.png'], + 'installable': True, + 'auto_install': False, + 'application': True, + 'license': 'LGPL-3', +} diff --git a/addons_extensions/hr_employee_appraisal/data/cron.xml b/addons_extensions/hr_employee_appraisal/data/cron.xml new file mode 100644 index 000000000..0065f6ab4 --- /dev/null +++ b/addons_extensions/hr_employee_appraisal/data/cron.xml @@ -0,0 +1,16 @@ + + + + + APPRAISAL: Activation date + + code + model.employee_appraisal_activation_date_update() + + 1 + days + + + + + \ No newline at end of file diff --git a/addons_extensions/hr_employee_appraisal/data/default_data.xml b/addons_extensions/hr_employee_appraisal/data/default_data.xml new file mode 100644 index 000000000..251f2b91d --- /dev/null +++ b/addons_extensions/hr_employee_appraisal/data/default_data.xml @@ -0,0 +1,44 @@ + + + + + + Productivity + + + + Dependability + + + + Interpersonal Relationships + + + + Initiative + + + + Knowledge of Job + + + + Self Organizing and Time Management + + + + Punctuality/Leave Management + + + + Quality of Deliverables + + + Active participation in Company Events/Programs + + + Adherence to the Policies + + + + diff --git a/addons_extensions/hr_employee_appraisal/data/mail_template_data.xml b/addons_extensions/hr_employee_appraisal/data/mail_template_data.xml new file mode 100644 index 000000000..099748368 --- /dev/null +++ b/addons_extensions/hr_employee_appraisal/data/mail_template_data.xml @@ -0,0 +1,242 @@ + + + + + + + + PEP-Employee + {{ object.name.sudo().parent_id.work_email }} + Goal Setting Notification + {{ object.name.sudo().work_email }} + {{ object.appraisal_hr_id.sudo().email }} + + + +

+ Dear Employee, + +

+ You have received a notification from + + your manager, Manager, + + + your manager + + regarding goal setting for FY + + Period. + + +

+ Please click here to review and respond. + +

+ Thank you, +
+ + Manager + +

+
+
+ + + PEP-Employee + {{ object.name.sudo().parent_id.work_email }} + Goal Setting Notification + {{ object.name.sudo().work_email }} + {{ object.appraisal_hr_id.sudo().email }} + + + +

+ Dear Employee, + +

+ You have received a notification from + + your manager, Manager, + + + your manager + + regarding submitting the rating for FY + + Period. + + +

+ Please click here to review and respond. + +

+ Thank you, +
+ + Manager + +

+
+
+ + + + + PEP-Manager + {{ object.name.sudo().work_email }} + Response to Goal Setting + {{ object.name.sudo().parent_id.work_email }} + {{ object.appraisal_hr_id.sudo().email }} + + + +

+ Dear Manager, + +

+ You have received a response from + + Employee + + + the employee + + regarding goal setting. + +

+ Please click here to review and take the necessary action. + +

+ Thank you, +
+ Employee +

+
+
+ + + + PEP-Manager + {{ object.name.sudo().work_email }} + Response to rating Submission + {{ object.name.sudo().parent_id.work_email }} + {{ object.appraisal_hr_id.sudo().email }} + + + +

+ Dear Manager, + +

+ You have received a response from + + Employee + + + the employee + + regarding Rating Submission. + +

+ Please click here to review and take the necessary action. + +

+ Thank you, +
+ Employee +

+
+
+ + + + PEP-HR + {{ object.name.sudo().parent_id.work_email }} + PEP Review Submission + {{ object.appraisal_hr_id.sudo().email }} + {{ object.name.sudo().work_email }} + + + +

+ Dear HR, + +

+ The PEP review for + + Employee + + has been finalized and submitted for your review. + +

+ Please click here to proceed. + +

+ Thank you, +
+ Manager +

+
+
+ + + + PEP-HR-Manager + {{ object.appraisal_hr_id.sudo().email }} + Goal Setting Feedback + {{ object.name.sudo().parent_id.work_email }} + + + +

+ Dear Manager, + +

+ You have received feedback from HR regarding goal setting. + +

+ Please click here to review and respond. + +

+ Thank you, +
+ HR +

+
+
+ + + + + + PEP-MD + {{ object.appraisal_hr_id.sudo().email }} + PEP Review Submitted + {{ object.appraisal_md_id.sudo().email }} + + + +

+ Dear MD, + +

+ The PEP review for + + Employee + + has been finalized and submitted for your review. + +

+ Please click here to review and take action. + +

+ Thank you, +
+ HR +

+
+
+ +
+
diff --git a/addons_extensions/hr_employee_appraisal/models/__init__.py b/addons_extensions/hr_employee_appraisal/models/__init__.py new file mode 100644 index 000000000..899be879b --- /dev/null +++ b/addons_extensions/hr_employee_appraisal/models/__init__.py @@ -0,0 +1,4 @@ +from . import apprisal_period_master +from . import kpi_kra_master +from . import employee_appraisal +from . import res_config_settings \ No newline at end of file diff --git a/addons_extensions/hr_employee_appraisal/models/apprisal_period_master.py b/addons_extensions/hr_employee_appraisal/models/apprisal_period_master.py new file mode 100644 index 000000000..c943080fb --- /dev/null +++ b/addons_extensions/hr_employee_appraisal/models/apprisal_period_master.py @@ -0,0 +1,27 @@ +# models.py +from odoo import models, fields, api +from datetime import datetime, timedelta +import datetime + +class AppraisalPeriod(models.Model): + _name = 'appraisal.period' + _description = 'Appraisal Period' + _rec_name = 'name' + _inherit = ['mail.thread'] + + name = fields.Char('Appraisal Period', required=True, track_visibility='onchange', copy=True) + date_from = fields.Date('Date From', track_visibility='onchange', copy=True) + date_to = fields.Date('Date To', track_visibility='onchange', copy=True) + active = fields.Boolean('Active', track_visibility='onchange', copy=True) + activation_date = fields.Date('Activation Date', required=True) + + @api.onchange('date_to') + def onchange_activation_date(self): + for rec in self: + rec.activation_date = rec.date_to + timedelta(days=1) + + _defaults = { + 'color_name': 'red', + 'active': True, + } + diff --git a/addons_extensions/hr_employee_appraisal/models/employee_appraisal.py b/addons_extensions/hr_employee_appraisal/models/employee_appraisal.py new file mode 100644 index 000000000..dbcaefb10 --- /dev/null +++ b/addons_extensions/hr_employee_appraisal/models/employee_appraisal.py @@ -0,0 +1,512 @@ + +from odoo import api, fields, models, _, tools +from odoo.exceptions import UserError, AccessError, ValidationError + +import datetime + +from dateutil import relativedelta + +class employee_appraisal(models.Model): + _name = "employee.appraisal" + _description = "Employee" + + _inherit = ['mail.thread', ] + + + def employee_appraisal_activation_date_update(self): + appraisal_periods = self.env['appraisal.period'].sudo().search([('activation_date','<=',fields.Date.today())]) + employee_appraisals = self.env['employee.appraisal'].sudo().search([('appraisal_period_id','in',appraisal_periods.ids)]) + for appraisal in employee_appraisals: + if appraisal.state == 'sent': + appraisal.button_send_employee() + # @api.depends('employee_rating_id.self_rating', 'employee_rating_id.manager_rating') + # def _avg_jr_rating(self): + # """ + # Compute the total amounts of the SO. + # """ + # for rec in self: + # leng_rating = len(rec.employee_rating_id) + # if leng_rating != 0: + # for rating in rec: + # emp_tot_rating = self_rating = manager_rating = 0 + # for line in rating.employee_rating_id: + # self_rating += int(line.self_rating) + # manager_rating += int(line.manager_rating) + # # return self_rating + # rating.update({ + # # 'self_rating':rating.round(self_rating), + # # 'manager_rating':rating.round(manager_rating), + # 'emp_avg_jr_rating': float(self_rating) / leng_rating, + # 'mng_avg_jr_rating': float(manager_rating) / leng_rating + # }) + # else: + # rec.emp_avg_jr_rating = 0.0 + # rec.mng_avg_jr_rating = 0.0 + # + @api.depends('employee_job_requirement_id.emp_rating', 'employee_job_requirement_id.man_rating', + 'employee_job_requirement_id.weightage') + def _avg_kra_rating(self): + """ + Compute the weighted average ratings for Employee and Manager based on the weightage of each KRA. + """ + for rec in self: + total_emp_rating = 0 + total_man_rating = 0 + total_weightage = 0 + + # Loop through each KRA + for line in rec.employee_job_requirement_id: + if line.weightage > 0: # Check if weightage is greater than 0 + total_emp_rating += int(line.emp_rating) * line.weightage + total_man_rating += int(line.man_rating) * line.weightage + total_weightage += line.weightage + + # Avoid division by zero if the total weightage is zero + if total_weightage != 0: + rec.emp_avg_kra_rating = total_emp_rating / total_weightage + rec.mng_avg_kra_rating = total_man_rating / total_weightage + else: + rec.emp_avg_kra_rating = 0.0 + rec.mng_avg_kra_rating = 0.0 + + # def _get_total(self, cr, uid, ids, field_names, args, context=None): + # res = {} + # for line in self.browse(cr, uid, ids, context=context): + # res[line.id] = (line.mng_avg_jr_rating + line.mng_avg_kra_rating) / 2 + # return res + # + @api.depends('employee_job_requirement_id.weightage') + def _tot_weightage(self): + """ + Compute the total amounts of the SO. + """ + for rec in self: + weightage = 0 + for line in rec.employee_job_requirement_id: + weightage += line.weightage + rec.update({ + 'total_weightage': weightage + }) + + # we need a related field in order to be able to sort the employee by name + name = fields.Many2one('hr.employee', 'Employee Name', track_visibility='onchange', copy=False) + state = fields.Selection([('draft', 'Draft Appraisal'), ('to_emp', 'Send to Employee'), + ('sent', 'Sent to Manager'), + ('emp_rating','Sent for Employee Rating'), + ('manager_rating','Sent for Manager Rating'), + ('to_approve', 'Sent to HR'), + ('to_md', 'Sent to MD'), + ], default='draft', string='Status', readonly=True, index=True, copy=False, + track_visibility='onchange') + appraisal_period_id = fields.Many2one('appraisal.period', 'Appraisal Period', + track_visibility='onchange', copy=False) + location = fields.Char('Location', track_visibility='onchange', copy=True) + designation = fields.Many2one('hr.job', 'Designation', track_visibility='onchange', copy=False, required=True) + user_id = fields.Many2one('res.users', related='name.user_id', string='Users', store=True, + track_visibility='onchange', copy=False) + department_id = fields.Many2one('hr.department', 'Department', track_visibility='onchange', copy=False) + reviewers_name = fields.Many2one('hr.employee', 'Reviewers Name', related="name.parent_id",store=True, track_visibility='onchange', copy=False) + todays_date = fields.Date(string='Date', default=fields.Date.today(), track_visibility='onchange', copy=False) + appraisal_active = fields.Boolean('Appraisal Active', copy=True) + # emp_tot_rating = fields.Integer('Emp Tot Rating', track_visibility='onchange', copy=False) + # emp_avg_jr_rating = fields.Float(string='Emp JR Average Rating', compute='_avg_jr_rating', + # track_visibility='onchange', copy=False) + # mng_avg_jr_rating = fields.Float(string='Manager JR Average Rating', compute='_avg_jr_rating', + # track_visibility='onchange', copy=False) + emp_avg_kra_rating = fields.Float(string='Employee KRA Average Rating', compute='_avg_kra_rating', + track_visibility='onchange', copy=False) + mng_avg_kra_rating = fields.Float(string='Manager KRA Average Rating', compute='_avg_kra_rating', + track_visibility='onchange', copy=False) + total_weightage = fields.Float(string='Total Weightage', compute='_tot_weightage', track_visibility='onchange', + copy=False) + comments_by_hr = fields.Char('Comments By HR', track_visibility='onchange', copy=False) + comments_by_md = fields.Char('Comments By MD', track_visibility='onchange', copy=False) + employee_comment_set = fields.Boolean('Employee comment set', track_visibility='onchange', copy=False) + employee_rating_id = fields.One2many('employee.rating', 'appraisal_id', 'Appraisal', track_visibility='onchange', + copy=True) + # 'overall_rating':fields.function(_get_total, string='Overall Manager Rating', type='float', track_visibility='onchange'), + overall_evaluation_of_performance = fields.Text('Overall Evaluation of Performance', track_visibility='onchange', + copy=False) + time_in_current_position = fields.Char('Time in current position', track_visibility='onchange', copy=False) + time_with_company = fields.Char('Time with Company', track_visibility='onchange', copy=False) + date = fields.Date('Date', track_visibility='onchange', copy=False) + evaluation_list = fields.Selection([ + ('far_exceeds_expectation', 'Far Exceeds Expectations'), + ('exceed_expectation', 'Exceed Expectations'), + ('met_expectation', 'Met Expectations'), + ('below_expectation', 'Below Expectations'), + ('never_met', 'Never Met'), + ], string='Overall Evaluation', index=True, copy=False, track_visibility='onchange') + current_salary = fields.Float('Current Salary', copy=False) + proposed_merit_increase = fields.Integer('Proposed Merit Increase (%)', copy=False) + proposed_salary = fields.Float('Proposed Salary', copy=False) + proposed_promotion_list = fields.Selection([ + ('yes', 'Yes'), + ('no', 'No'), + ], string='Proposed Promotion', index=True, copy=False) + proposed_designation = fields.Char('Proposed Designation', copy=False) + employee_job_requirement_id = fields.One2many('employee.job.requirement', 'appraisal_id', 'Job Requirement', + track_visibility='onchange', copy=True) + appraisal_hr_id = fields.Many2one('res.users', string="HR Manager", compute='_compute_appraisal_hr_md') + appraisal_md_id = fields.Many2one('res.users', string="Managing Director", compute='_compute_appraisal_hr_md') + + + @api.depends('name') + def _compute_appraisal_hr_md(self): + hr_id = self.env['ir.config_parameter'].sudo().get_param('hr_employee_appraisal.appraisal_hr_id') + self.appraisal_hr_id = self.env['res.users'].sudo().browse(int(hr_id)) if hr_id else False + md_id = self.env['ir.config_parameter'].sudo().get_param('hr_employee_appraisal.appraisal_md_id') + self.appraisal_md_id = self.env['res.users'].sudo().browse(int(md_id)) if md_id else False + + @api.constrains('total_weightage') + def constrain_total_weightage(self): + for record in self: + if record.total_weightage == 100: + return True + else: + raise UserError(_("Error: Total Weightage should be equal to 100")) + return True + + # @api.model + # def default_get(self, default_fields): + # res = {} + # # srd = self.env['employee.rating'] + # # ids = [] + # # active_id = self._context.get('active_ids', []) + # # items_list = self.env['kpi.kra'].search([('kpi_kra_type','in',['kpi','both'])]) + # # for item in items_list: + # # result = {'appraisal_id': active_id, 'name': item.id, 'state': 'draft'} + # # sr = srd.create(result) + # # ids.append(sr.id) + # # res['employee_rating_id'] = [(6, 0, ids)] + # res['state'] = 'draft' + # return res + + @api.onchange('name') + def onchange_employee_name(self): + if self.name: + # self.reviewers_name = self.name.parent_id.id + self.department_id = self.name.department_id.id + self.designation = self.name.job_id.id + self.location = self.name.work_location_id.name + + @api.onchange('designation') + def onchange_designation_id(self): + for rec in self: + # Clear existing records + rec.sudo().employee_job_requirement_id.unlink() + + if rec.designation and rec.designation.kpi_kra_ids: + employee_job_requirement_ids = [] + for kra in rec.designation.kpi_kra_ids: + # Create employee.job.requirement record + kra_data = {'appraisal_id': rec.id, 'kra_1': kra.id} + kra_create = self.env['employee.job.requirement'].create(kra_data) + + # Create employee.rating records linked to the created employee.job.requirement + kpi_ids = [] + for kpi in kra.kpi_question_ids: + kpi_data = { + 'job_requirement_id': kra_create.id, # Link to the created employee.job.requirement + 'appraisal_id': rec.id, + 'name': kpi.id, + } + kpi_create = self.env['employee.rating'].create(kpi_data) + kpi_ids.append(kpi_create.id) + + kra_create.write({'employee_rating_id': [(6, 0, kpi_ids)]}) + employee_job_requirement_ids.append(kra_create.id) + + rec.employee_job_requirement_id = [(6, 0, employee_job_requirement_ids)] + + def button_send_employee(self): + for rec in self: + if rec.state == 'draft' and self.env.user.id != rec.appraisal_hr_id.id: + raise ValidationError(_("Only HR can send Appraisal to the employee")) + if rec.state == 'sent' and self.env.user.id != rec.reviewers_name.user_id.id: + raise ValidationError(_("Only Manager can send Appraisal to the employee in this stage")) + + if rec.state=='draft': + if self.total_weightage == 100: + self.state = 'to_emp' + for line in self.employee_job_requirement_id: + line.status = 'to_emp' + template = self.env.ref('hr_employee_appraisal.employee_email_template') + self.env['mail.template'].browse(template.id).send_mail(self.id) + return True + else: + raise UserError(_('Error: Total Weightage should be equal to 100')) + elif rec.state == 'sent': + rec.state = 'emp_rating' + for line in self.employee_job_requirement_id: + line.status = 'emp_rating' + template = self.env.ref('hr_employee_appraisal.employee_email_submit_rating_template') + self.env['mail.template'].browse(template.id).send_mail(self.id) + return True + + def button_refuse(self): + if self.state == 'sent': + self.state = 'to_emp' + for line in self.employee_job_requirement_id: + line.status = 'to_emp' + elif self.state == 'manager_rating': + self.state = 'emp_rating' + for line in self.employee_job_requirement_id: + line.status = 'emp_rating' + elif self.state == 'to_approve': + self.state = 'manager_rating' + for line in self.employee_job_requirement_id: + line.status = 'manager_rating' + elif self.state == 'to_md': + self.state = 'to_approve' + for line in self.employee_job_requirement_id: + line.status = 'to_approve' + + # elif self.state=='to_approve': + # self.state='to_emp' + # for line in self.employee_rating_id: + # line.state='to_emp' + # for line in self.employee_job_requirement_id: + # line.status='to_emp' + + # if self.state ='to_emp': + # state=manager_rating + # refuse = manager_rating + + # if self.state ='mgr': + # statr=hr + # refuse=hr + + def button_send_manager(self): + if self.env.user.id != self.name.user_id.id: + raise ValidationError(_("Only the Employee can submit to manager")) + for record in self.employee_job_requirement_id: + for rec in record.employee_rating_id: + if not rec.emp_kri_target: + raise ValidationError(_(f"Please provide your targets before submitting to Manager ({record.kra_1.name})")) + if not record.emp_kra_target: + raise ValidationError(_("Please provide your targets before submitting to Manager")) + for rec in self: + if rec.state == 'to_emp': + rec.state = 'sent' + template = self.env.ref('hr_employee_appraisal.manager_targets_submitted_email_template') + self.env['mail.template'].browse(template.id).send_mail(self.id) + # When the state is 'emp_rating', we need to ensure all fields are filled + if rec.state == 'emp_rating': + for record in rec.employee_job_requirement_id: + # Check if employee has filled the rating and comments in the job requirement (KRA) + if not record.emp_rating: + raise UserError(_('Please fill Employee Rating in the KRA: {}').format(record.kra_1.name)) + if not record.sop: + raise UserError(_('Please fill Standards of Performance (SOP) in the KRA: {}').format( + record.kra_1.name)) + + for rating in record.employee_rating_id: # Iterate through related employee ratings + # Check if self-rating and employee comments are filled in the ratings + if not rating.self_rating: + raise UserError(_(f'Please fill your Self Rating for KPI: {rating.name.name}')) + if not rating.employee_comments: + raise UserError(_('Please fill your Employee Comments for KPI: {}').format(rating.name.name)) + + # After all checks, change the state to 'manager_rating' + self.state = 'manager_rating' + + # Update the status for all job requirements to 'manager_rating' + for line in self.employee_job_requirement_id: + line.status = 'manager_rating' + + # Send email to manager using the template + template = self.env.ref('hr_employee_appraisal.manager_email_template') + self.env['mail.template'].browse(template.id).send_mail(self.id) + + return True + + def button_send_hr(self): + # Ensure only the manager can send the appraisal to HR + if self.env.user.id != self.reviewers_name.user_id.id: + raise ValidationError(_("Only Manager can send to HR")) + + # Check if all manager comments and ratings are filled in the employee ratings and job requirements + for record in self.employee_job_requirement_id: + for rating in record.employee_rating_id: + if not rating.manager_comments: + raise UserError(_(f'Please fill Manager Comments for KPI: {rating.name.name}')) + if not rating.manager_rating: + raise UserError(_('Please fill Manager Rating for KPI: {}').format(rating.name.name)) + + if not record.man_rating: + raise UserError(_('Please fill Manager Rating in the KRA: {}').format(record.kra_1.name)) + if not record.results: + raise UserError(_('Please fill Results Achieved (Manager) for KRA: {}').format(record.kra_1.name)) + + # Change the state to 'to_approve' and update status for all ratings and job requirements + self.state = 'to_approve' + for line in self.employee_job_requirement_id: + line.status = 'to_approve' + + # Send an email to HR + template = self.env.ref('hr_employee_appraisal.hr_email_template') + self.env['mail.template'].browse(template.id).send_mail(self.id) + + return True + + def button_send_md(self): + self.state = 'to_md' + for line in self.employee_rating_id: + line.state = 'to_md' + for line in self.employee_job_requirement_id: + line.status = 'to_md' + + def button_back(self): + if self.state == 'to_approve': + self.state = 'sent' + for line in self.employee_rating_id: + line.state = 'sent' + for line in self.employee_job_requirement_id: + line.status = 'sent' + template = self.env.ref('hr_employee_appraisal.hr_reject_email_template') + self.env['mail.template'].browse(template.id).send_mail(self.id) + + @api.onchange('current_salary', 'proposed_merit_increase') + def onchange_proposed_salary(self): + pro_merit = float(self.proposed_merit_increase) + if self.current_salary or pro_merit: + self.proposed_salary = self.current_salary + (self.current_salary * (pro_merit / 100)) + + + @api.onchange('date') + def onchange_time_in_current_position(self): + # pdb.set_trace() + if self.date: + + fmt = '%Y-%m-%d' + if self.name.doj: + d1 = self.name.doj + d2 = self.date + Diff = relativedelta.relativedelta(d2, d1) + self.time_in_current_position = str(Diff.years) + " Years " + str(Diff.months) + " Months " + str( + Diff.days) + " Days " + + +class employee_rating(models.Model): + _name = "employee.rating" + _description = "Part A" + _inherit = ['mail.thread', ] + + + + state = fields.Selection([('draft', 'Draft Appraisal'), ('to_emp', 'Send to Employee'), + ('sent', 'Sent to Manager'), + ('emp_rating','Sent for Employee Rating'), + ('manager_rating','Sent for Manager Rating'), + ('to_approve', 'Sent to HR'), + ('to_md', 'Sent to MD'), ], + string='Status', related='job_requirement_id.status', track_visibility='onchange', copy=False) + appr_active_rel = fields.Boolean(related='appraisal_id.appraisal_active', string='Appraisal Active Related', + store=True, copy=False) + name = fields.Many2one('kpi.question', 'KPIs', track_visibility='onchange', copy=True) + emp_kri_target = fields.Text('Employee Target') + self_rating = fields.Selection([(str(num), str(num)) for num in range(1, 6)],'Self Rating', track_visibility='onchange', copy=False) + employee_comments = fields.Char('Employee Comments', track_visibility='onchange', copy=False) + manager_rating = fields.Selection([(str(num), str(num)) for num in range(1, 6)],'Manager Rating', track_visibility='onchange', copy=False) + manager_comments = fields.Char('Manager Comments', track_visibility='onchange', copy=False) + appraisal_id = fields.Many2one('employee.appraisal', 'Appraisal Id', related='job_requirement_id.appraisal_id',track_visibility='onchange', copy=True) + job_requirement_id = fields.Many2one('employee.job.requirement','Requirement Id', track_visibility='onchange', copy=True) + + + @api.constrains('self_rating') + def _self_rating(self): + record = self + if int(record.self_rating) <= 5 and int(record.self_rating) >= 1: + return True + else: + raise UserError(_('Error: Invalid JR Employee Rating')) + return True + + @api.constrains('manager_rating') + def _manager_rating(self): + record = self + + if int(record.manager_rating) <= 5 and int(record.manager_rating) >= 1: + return True + else: + raise UserError(_('Error: Invalid JR Manager Rating')) + return True + + + +class employee_job_requirement(models.Model): + _name = "employee.job.requirement" + _description = "Part B" + + @api.depends('employee_rating_id.self_rating', 'employee_rating_id.manager_rating') + def _avg_jr_rating(self): + """ + Compute the total amounts of the SO. + """ + for rec in self: + leng_rating = len(rec.employee_rating_id) + if leng_rating != 0: + for rating in rec: + emp_tot_rating = self_rating = manager_rating = 0 + for line in rating.employee_rating_id: + self_rating += int(line.self_rating) + manager_rating += int(line.manager_rating) + # return self_rating + rating.update({ + # 'self_rating':rating.round(self_rating), + # 'manager_rating':rating.round(manager_rating), + 'emp_avg_jr_rating': float(self_rating) / leng_rating, + 'mng_avg_jr_rating': float(manager_rating) / leng_rating + }) + else: + rec.emp_avg_jr_rating = 0.0 + rec.mng_avg_jr_rating = 0.0 + + status = fields.Selection( + [('draft', 'Draft Appraisal'), ('to_emp', 'Send to Employee'), + ('sent', 'Sent to Manager'), + ('emp_rating','Sent for Employee Rating'), + ('manager_rating','Sent for Manager Rating'), + ('to_approve', 'Sent to HR'), + ('to_md', 'Sent to MD'),], + string='Status',related='appraisal_id.state',track_visibility='onchange', copy=False) + + appr_active_kra_rel = fields.Boolean(related='appraisal_id.appraisal_active', + string='Appraisal Active KRA Related', store=True, copy=False) + kra_type = fields.Many2one('kra.types',related='kra_1.kra_type', string='KRA Type', index=True, track_visibility='onchange', copy=True) + kra_1 = fields.Many2one('kpi.kra','Job Requirements(KRA)', + help="(List each major job requirement and describe the key responsibilities of the function)", + track_visibility='onchange', copy=True) + emp_kra_target = fields.Text('Target', track_visibility='onchange', copy=True) + weightage = fields.Integer('Weightage', track_visibility='onchange', copy=False) + emp_rating = fields.Selection([(str(num), str(num)) for num in range(1, 6)],string='Employee Rating (Rating between 1-5)', help="(Rating should be between 1 and 5)", + track_visibility='onchange', copy=False) + sop = fields.Text('Standards of Performance (Employee)', + help="(Indicate the quality of work that you have exhibited against the Key Roles that are delegated to you)", + track_visibility='onchange', copy=False) + man_rating = fields.Selection([(str(num), str(num)) for num in range(1, 6)],'Manager Rating (Rating between 1-5)', help="(Rating should be between 1 and 5)", + track_visibility='onchange', copy=False) + results = fields.Text('Results Achieved (Manager)', + help="(Describe the extent to which the employee has met the standards of performance expected for each major job function.)", + track_visibility='onchange', copy=False) + appraisal_id = fields.Many2one('employee.appraisal', 'Job Requirement Id', track_visibility='onchange', + copy=True) + employee_rating_id = fields.One2many('employee.rating', 'job_requirement_id', 'KPIs', track_visibility='onchange', + copy=True) + + + emp_avg_jr_rating = fields.Float(string='Emp JR Average Rating', compute='_avg_jr_rating', + track_visibility='onchange', copy=False) + mng_avg_jr_rating = fields.Float(string='Manager JR Average Rating', compute='_avg_jr_rating', + track_visibility='onchange', copy=False) + + @api.constrains('man_rating') + def _weightage(self): + # pdb.set_trace() + for record in self: + if record.weightage != 0: + return True + else: + raise UserError(_('Error: Weightage should not be 0')) + return {} diff --git a/addons_extensions/hr_employee_appraisal/models/kpi_kra_master.py b/addons_extensions/hr_employee_appraisal/models/kpi_kra_master.py new file mode 100644 index 000000000..52e9b5c3e --- /dev/null +++ b/addons_extensions/hr_employee_appraisal/models/kpi_kra_master.py @@ -0,0 +1,53 @@ +from odoo import models, fields + + +class kraTypes(models.Model): + _name = 'kra.types' + _description = 'KRA Types' + _rec_name = 'name' + + name = fields.Char('Name') + + +class KpiQuestion(models.Model): + _name = 'kpi.question' + _description = 'KPI Questions' + _rec_name = 'name' + + name = fields.Char(string='Question', required=True) + kra_id = fields.Many2one('kpi.kra', string='KRA', ondelete='cascade', required=True) + + _sql_constraints = [ + ('name_kra_unique', 'unique(name, kra_id)', 'The combination of question name and KRA should be unique.') + ] + + +class KpiKra(models.Model): + _name = 'kpi.kra' + _description = 'Master Record for KPIs and KRAs' + _rec_name = 'name' + + name = fields.Char(string='KRA Name', required=True) + kra_type = fields.Many2one('kra.types',string="Type") + # One2many field to link multiple KPI questions to each KRA + kpi_question_ids = fields.One2many( + 'kpi.question', # Related model + 'kra_id', # Field in kpi.question model + string='KPI Questions' + ) + job_id = fields.Many2one('hr.job') + + _sql_constraints = [ + ('name_job_id_unique', 'unique(name, job_id)', 'The combination of question name and Job Position should be unique.') + ] + + +class HrJob(models.Model): + _inherit = 'hr.job' # Inherit the hr.job model + + # One2many field to link multiple KRAs to each job role + kpi_kra_ids = fields.One2many( + 'kpi.kra', # Related model (KpiKra) + 'job_id', # Field in kpi.kra model (you'll add this field to the KpiKra model) + string='KRAs' + ) diff --git a/addons_extensions/hr_employee_appraisal/models/res_config_settings.py b/addons_extensions/hr_employee_appraisal/models/res_config_settings.py new file mode 100644 index 000000000..6c2ea93cb --- /dev/null +++ b/addons_extensions/hr_employee_appraisal/models/res_config_settings.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, api, _ + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + appraisal_hr_id = fields.Many2one('res.users',config_parameter='hr_employee_appraisal.appraisal_hr_id', string='Appraisal HR', + domain=lambda self: [ + ('groups_id', 'in', self.env.ref('hr_employee_appraisal.group_appraisal_manager').id)]) + + appraisal_md_id = fields.Many2one('res.users', config_parameter='hr_employee_appraisal.appraisal_md_id', + string='Appraisal MD', + domain=lambda self: [ + ('groups_id', 'in', + self.env.ref('hr_employee_appraisal.group_appraisal_administrator').id)]) + + # requisition_md_id = fields.Many2one('res.users', string='Requisition MD') \ No newline at end of file diff --git a/addons_extensions/hr_employee_appraisal/security/ir.model.access.csv b/addons_extensions/hr_employee_appraisal/security/ir.model.access.csv new file mode 100644 index 000000000..516ae524a --- /dev/null +++ b/addons_extensions/hr_employee_appraisal/security/ir.model.access.csv @@ -0,0 +1,25 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +model_appraisal_period_manager_access,appraisal.period manager access,model_appraisal_period,group_appraisal_manager,1,1,1,1 +model_appraisal_period_user_access,appraisal.period user access,model_appraisal_period,base.group_user,1,0,0,0 + +access_kra_types_user,kra.types.user,model_kra_types,base.group_user,1,0,0,0 +access_kra_types_manager,kra.types.manager,model_kra_types,group_appraisal_manager,1,1,1,1 + + +access_kpi_kra_user,kpi.kra user,model_kpi_kra,base.group_user,1,0,0,0 +access_kpi_kra_manager,kpi.kra manager,model_kpi_kra,group_appraisal_manager,1,1,1,1 + +access_kpi_question_user,kpi.question.user,model_kpi_question,base.group_user,1,0,0,0 +access_kpi_question_manager,kpi.question.manager,model_kpi_question,group_appraisal_manager,1,1,1,1 + +access_employee_appraisal_user,employee.appraisal user,model_employee_appraisal,base.group_user,1,1,0,0 +access_employee_appraisal_manager,employee.appraisal manager,model_employee_appraisal,group_appraisal_manager,1,1,1,1 +access_employee_appraisal_officer,employee.appraisal officer,model_employee_appraisal,group_appraisal_officer,1,1,0,0 + +access_rating_user,employee.rating,model_employee_rating,base.group_user,1,1,0,0 +access_rating_hr_user,employee.rating.hr,model_employee_rating,group_appraisal_manager,1,1,1,1 +access_rating_officer,employee.rating.officer,model_employee_rating,group_appraisal_officer,1,1,0,0 + +access_job_req_user,employee.job.requirement,model_employee_job_requirement,base.group_user,1,1,0,0 +access_job_req_hr_user,employee.job.requirement.hr,model_employee_job_requirement,group_appraisal_manager,1,1,1,1 +access_job_req_officer,employee.job.requirement.officer,model_employee_job_requirement,group_appraisal_officer,1,1,0,0 diff --git a/addons_extensions/hr_employee_appraisal/security/security.xml b/addons_extensions/hr_employee_appraisal/security/security.xml new file mode 100644 index 000000000..4bd0676dc --- /dev/null +++ b/addons_extensions/hr_employee_appraisal/security/security.xml @@ -0,0 +1,62 @@ + + + + + + + + + Appraisal + 50 + + + + + Appraisal Officer + + + + + + Appraisal HR Manager + + + + + + Appraisal Administrator + + + + + + + + User can only see his/her own appraisals + + + [('user_id','=',user.id),('state','!=','draft')] + + + User can only see the records of people under him/her + + + [('reviewers_name.user_id','=',user.id),('state','!=','draft')] + + + + User can only see the all the appraisal records where he/she is set as HR + + + [('appraisal_hr_id','=',user.id)] + + + User can only see the all the appraisal records where he/she is set as MD + + + [('appraisal_md_id','=',user.id)] + + + + + diff --git a/addons_extensions/hr_employee_appraisal/static/description/icon.png b/addons_extensions/hr_employee_appraisal/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a9845d4557f8561ccdc771f81e699557f100c3c8 GIT binary patch literal 2318 zcmV+p3Gw!cP)dUT-W;if{wEfy@DVs+OiJcHv z21P=?%V85j3NK}|h#9dG!Y>xGDXV_5hYXjA^}bZ>q{K={Z74+xI1YO~o-=-AtLB4eSUL^RjQRlj)yXi#a=hpQwZmnVGK7r{ai%O zgBCpjqk!Ao{%gjHj>GgPTDQg>A;OCeEA=)Z8by3g>Zaem`{anHK zw3ni!OxM!(8Vj${(7dX9|04{LaM1w(^bol(#kZ)I*Pf$sUy;U8XSNSz>^ zkq#Yen+#ukZsW7h#xZnYANEhIW|$TI>B=HHNXD}WK%jfSP+eRR9Xt%6FWraSBYSb} z?L6+@&AEUkibu;h3da6jg6OxIe2Jd=j%6?A4k;CLSFXTbF?E=d{dP8svu-D zk{hK6N$-6B=_gs-c=rGzga=v3n+YX^qNGak88|${;HIipu11%-IQ@HxJ^kM*LJ)oh z6mu@HJJ8G{2ghNxsirnIQjw%CTF9#@Zoh<(gsuJsy+-#`-5glw0=Whg|Xs)zQBcs^cKUAJ!Lpb}B$hF!>; zk=oYMoa>A@DFO=cnyOlPouXu8-S^1O8;suxTjsVRb?CFT!2fK3a%usR8QZ@PgS&RY zFZH^Gx?^GdppruHANJJcB}hd6;HOar%f)Z1YUReeFcI+A!5!2iCDH3 zC{?whG<8Wt&1jUP3Q6R~#ynM3y%4?0)hmxe7NQY^0*rjWESLUFh8n<8$UYe>p}gtYFD>mukrJg?PbRAtrt3DKRZS_!h5 z#cdX&#?U8wX_9M2c(!4X)YVttjE@j;Hj39{#Uyvv@I#0izp0BoU8zC=kw!?+O(#`| z)D`taSe+0L6W^Ssp2I65b-nH(b*Y;43L=Y;u$xY*xK+*d+=o{+=@qCG(q-anTB>TL z_e7Y-Pe`|kuW6~O6_L8SO@viVdIj23mV#8pTP{-Qwwa}B(koCWq|L-v*8Vu$2VcA# zg0nN6*Cw|vjyM~?0!eZ@r7l&IUV+vcFXa9>r$=~&F;uwYyqtlvII~!4&E@5PPS2L< z*^npw1!h^#(3Or+RV1G3zF)rQZe|_*#rxm8FHnR!A!6d&6n|nN53oPJC4ut`^m!?R z`t_NMrLGI=>BYvGIaRup(Gc`E@#q!E$4SQ6F}le-e8>^0>(Tse?AyQdYn4rU1==FS z<0QB7%p(ie>Ep}vW##nZ3l)Tbt}i}S#6`#<3S?uNgZ+}C&3%Wru7X7BdiJ?1NcV5O z>Q~jIx91%RLoq*{W!At4^uUgYO{XryL%#j7LU(70V{_^BVyOznP-??YYGamo-Tkrc zAGLidUT-W;if{wEfy@DVs+OiJcHv z21P=?%V85j3NK}|h#9dG!Y>xGDXV_5hYXjA^}bZ>q{K={Z74+xI1YO~o-=-AtLB4eSUL^RjQRlj)yXi#a=hpQwZmnVGK7r{ai%O zgBCpjqk!Ao{%gjHj>GgPTDQg>A;OCeEA=)Z8by3g>Zaem`{anHK zw3ni!OxM!(8Vj${(7dX9|04{LaM1w(^bol(#kZ)I*Pf$sUy;U8XSNSz>^ zkq#Yen+#ukZsW7h#xZnYANEhIW|$TI>B=HHNXD}WK%jfSP+eRR9Xt%6FWraSBYSb} z?L6+@&AEUkibu;h3da6jg6OxIe2Jd=j%6?A4k;CLSFXTbF?E=d{dP8svu-D zk{hK6N$-6B=_gs-c=rGzga=v3n+YX^qNGak88|${;HIipu11%-IQ@HxJ^kM*LJ)oh z6mu@HJJ8G{2ghNxsirnIQjw%CTF9#@Zoh<(gsuJsy+-#`-5glw0=Whg|Xs)zQBcs^cKUAJ!Lpb}B$hF!>; zk=oYMoa>A@DFO=cnyOlPouXu8-S^1O8;suxTjsVRb?CFT!2fK3a%usR8QZ@PgS&RY zFZH^Gx?^GdppruHANJJcB}hd6;HOar%f)Z1YUReeFcI+A!5!2iCDH3 zC{?whG<8Wt&1jUP3Q6R~#ynM3y%4?0)hmxe7NQY^0*rjWESLUFh8n<8$UYe>p}gtYFD>mukrJg?PbRAtrt3DKRZS_!h5 z#cdX&#?U8wX_9M2c(!4X)YVttjE@j;Hj39{#Uyvv@I#0izp0BoU8zC=kw!?+O(#`| z)D`taSe+0L6W^Ssp2I65b-nH(b*Y;43L=Y;u$xY*xK+*d+=o{+=@qCG(q-anTB>TL z_e7Y-Pe`|kuW6~O6_L8SO@viVdIj23mV#8pTP{-Qwwa}B(koCWq|L-v*8Vu$2VcA# zg0nN6*Cw|vjyM~?0!eZ@r7l&IUV+vcFXa9>r$=~&F;uwYyqtlvII~!4&E@5PPS2L< z*^npw1!h^#(3Or+RV1G3zF)rQZe|_*#rxm8FHnR!A!6d&6n|nN53oPJC4ut`^m!?R z`t_NMrLGI=>BYvGIaRup(Gc`E@#q!E$4SQ6F}le-e8>^0>(Tse?AyQdYn4rU1==FS z<0QB7%p(ie>Ep}vW##nZ3l)Tbt}i}S#6`#<3S?uNgZ+}C&3%Wru7X7BdiJ?1NcV5O z>Q~jIx91%RLoq*{W!At4^uUgYO{XryL%#j7LU(70V{_^BVyOznP-??YYGamo-Tkrc zAGLi + + + + + + appraisal.period.form + appraisal.period + +
+ + + + + + + + + + + + + + + + + +
+
+
+ + + appraisal.period.list + appraisal.period + + + + + + + + + + + + Appraisal Period + appraisal.period + list,form + + + + + + + + + + +
+ +
+
\ No newline at end of file diff --git a/addons_extensions/hr_employee_appraisal/views/employee_appraisal.xml b/addons_extensions/hr_employee_appraisal/views/employee_appraisal.xml new file mode 100644 index 000000000..7fcd27a38 --- /dev/null +++ b/addons_extensions/hr_employee_appraisal/views/employee_appraisal.xml @@ -0,0 +1,247 @@ + + + + + employee.appraisal.form + employee.appraisal + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Rating Guide + +
5Far Exceeds ExpectationsConsistently exceeds all requirements; superior performance
4Exceeds ExpectationsGenerally exceeds requirements with a minimum of guidance; Well Above Average + Performance +
3Met ExpectationsResponsibilities met in a wholly satisfactory manner; normal guidance and + supervision required +
2Below ExpectationsImprovement needed in some key job areas; considerable guidance and supervision + required +
1Never MetMajor shortcomings in performance; performance improvement plan required to + improve performance within a set time frame +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + employee.appraisal.search + employee.appraisal + + + + + + + + + + + + + + + employee.appraisal.list + employee.appraisal + + + + + + + + + + + + + + + Employees Appraisal + employee.appraisal + + list,form + [] + {} + + + +

+ Click to add a new Employee Appraisal. +

+

+

+
+
+ + + + + + + +
diff --git a/addons_extensions/hr_employee_appraisal/views/kpi_kra_master.xml b/addons_extensions/hr_employee_appraisal/views/kpi_kra_master.xml new file mode 100644 index 000000000..1312ee28d --- /dev/null +++ b/addons_extensions/hr_employee_appraisal/views/kpi_kra_master.xml @@ -0,0 +1,97 @@ + + + + + + + hr.job.form.inherit + hr.job + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+
+
+
+
+ + + + Job Positions + hr.job + list,form + + + + + + + kra.types.list + kra.types + + + + + + + + + + + + + + + + + + + + + + + + + + KRA Types + kra.types + list + current + + + + + + + + + +
+
\ No newline at end of file diff --git a/addons_extensions/hr_employee_appraisal/views/res_config_settings.xml b/addons_extensions/hr_employee_appraisal/views/res_config_settings.xml new file mode 100644 index 000000000..ebb5b6fc8 --- /dev/null +++ b/addons_extensions/hr_employee_appraisal/views/res_config_settings.xml @@ -0,0 +1,50 @@ + + + + res.config.settings.view.form.inherit.appraisal + res.config.settings + + + + + + + + + + + + + + + + + + + + + + Settings + res.config.settings + form + inline + {'module' : 'hr_appraisal', 'bin_size': False} + + + + + + diff --git a/addons_extensions/hr_employee_extended/__init__.py b/addons_extensions/hr_employee_extended/__init__.py new file mode 100644 index 000000000..473833440 --- /dev/null +++ b/addons_extensions/hr_employee_extended/__init__.py @@ -0,0 +1 @@ +from . import models,wizards \ No newline at end of file diff --git a/addons_extensions/hr_employee_extended/__manifest__.py b/addons_extensions/hr_employee_extended/__manifest__.py new file mode 100644 index 000000000..dbb1c272b --- /dev/null +++ b/addons_extensions/hr_employee_extended/__manifest__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Employee Extended", + + 'summary': 'Customized employee information', + + 'description': """ + Customized Employee Info + """, + + 'author': "Pranay", + 'website': "https://www.ftprotech.com", + + # Categories can be used to filter modules in modules listing + # Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml + # for the full list + 'category': 'Human Resources/Employees', + 'version': '0.1', + + # any module necessary for this one to work correctly + 'depends': ['base','hr'], + + # always loaded + 'data': [ + 'security/ir.model.access.csv', + 'views/hr_employee.xml', + 'wizards/work_location_wizard.xml' + ], +} + diff --git a/addons_extensions/hr_employee_extended/models/__init__.py b/addons_extensions/hr_employee_extended/models/__init__.py new file mode 100644 index 000000000..52d4ff8bc --- /dev/null +++ b/addons_extensions/hr_employee_extended/models/__init__.py @@ -0,0 +1,2 @@ +from . import hr_employee +from . import work_location_history \ No newline at end of file diff --git a/addons_extensions/hr_employee_extended/models/hr_employee.py b/addons_extensions/hr_employee_extended/models/hr_employee.py new file mode 100644 index 000000000..bcf7fa571 --- /dev/null +++ b/addons_extensions/hr_employee_extended/models/hr_employee.py @@ -0,0 +1,82 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import _, api, fields, models +from dateutil.relativedelta import relativedelta +from odoo.exceptions import ValidationError + + +class HrEmployeeBase(models.AbstractModel): + _inherit = "hr.employee.base" + + employee_id = fields.Char(string='Employee Code') + doj = fields.Date(string='Date of Joining') + pan_no = fields.Char(string='PAN No') + # Add computed field for experience + previous_exp = fields.Integer(string='Previous Experience (Months)', default=0) + + current_company_exp = fields.Char(string='Current Experience', compute='_compute_experience', store=True) + + total_exp = fields.Char(string='Total Experience', compute='_compute_total_experience', store=True) + + @api.constrains('identification_id') + def _check_identification_id(self): + for record in self: + if not record.identification_id.isdigit(): + raise ValidationError("Identification ID should only contain numbers.") + + @api.depends('doj') + def _compute_experience(self): + for record in self: + if record.doj: + # Get the current date + current_date = fields.Date.today() + + # Calculate the difference between current date and doj + delta = relativedelta(current_date, record.doj) + + # Format the experience as 'X years Y months Z days' + experience_str = f"{delta.years} years {delta.months} months {delta.days} days" + record.current_company_exp = experience_str + else: + record.current_company_exp = '0 years 0 months 0 days' + + + @api.depends('doj', 'previous_exp') + def _compute_total_experience(self): + for record in self: + if record.doj: + # Get the current date + current_date = fields.Date.today() + + # Calculate the current experience in years, months, and days + delta = relativedelta(current_date, record.doj) + current_years = delta.years + current_months = delta.months + current_days = delta.days + + # Convert previous experience (in months) to years, months, and days + previous_years = record.previous_exp // 12 + previous_months = record.previous_exp % 12 + previous_days = 0 # No days in previous_exp, it's already in months + + # Add the previous experience to the current experience + total_years = current_years + previous_years + total_months = current_months + previous_months + total_days = current_days + previous_days + + # Adjust if months > 12 or days > 30 + if total_months >= 12: + total_years += total_months // 12 + total_months = total_months % 12 + if total_days >= 30: + total_months += total_days // 30 + total_days = total_days % 30 + + # Format the total experience as 'X years Y months Z days' + total_experience_str = f"{total_years} years {total_months} months {total_days} days" + record.total_exp = total_experience_str + else: + # If there's no DOJ, total experience is just the previous experience + total_years = record.previous_exp // 12 + total_months = record.previous_exp % 12 + record.total_exp = f"{total_years} years {total_months} months 0 days" \ No newline at end of file diff --git a/addons_extensions/hr_employee_extended/models/work_location_history.py b/addons_extensions/hr_employee_extended/models/work_location_history.py new file mode 100644 index 000000000..304a2c635 --- /dev/null +++ b/addons_extensions/hr_employee_extended/models/work_location_history.py @@ -0,0 +1,31 @@ +from odoo import models, fields, api + + +class HREmployee(models.Model): + _inherit='hr.employee' + + work_loc_history = fields.One2many('emp.work.location.history','employee_id',string="Work Location History") + + def open_work_location_wizard(self): + """Open the wizard to manage work location history.""" + return { + 'name': 'Work Location Update', + 'type': 'ir.actions.act_window', + 'res_model': 'work.location.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'default_work_location_id': self.work_location_id.id, + 'default_employee_id': self.id, + 'default_start_date': fields.Date.today() + }, + } +class EmpWorkLocationHistory(models.Model): + _name = 'emp.work.location.history' + + + work_location_type = fields.Many2one('hr.work.location') + start_date = fields.Date(string="Start Date") + end_date = fields.Date(string="End Date") + note = fields.Text(string="Notes") + employee_id = fields.Many2one('hr.employee') \ No newline at end of file diff --git a/addons_extensions/hr_employee_extended/security/ir.model.access.csv b/addons_extensions/hr_employee_extended/security/ir.model.access.csv new file mode 100644 index 000000000..d53e01e31 --- /dev/null +++ b/addons_extensions/hr_employee_extended/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_emp_work_location_history,emp.work.location.history,model_emp_work_location_history,base.group_user,1,1,1,1 +access_work_location_wizard,work.location.wizard,model_work_location_wizard,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/addons_extensions/hr_employee_extended/views/hr_employee.xml b/addons_extensions/hr_employee_extended/views/hr_employee.xml new file mode 100644 index 000000000..f907b70e7 --- /dev/null +++ b/addons_extensions/hr_employee_extended/views/hr_employee.xml @@ -0,0 +1,64 @@ + + + + + hr.employee.form.inherit + hr.employee + + + + + + + + + + + + + AADHAR No + + + + + +