diff --git a/addons_extensions/grace_period/__manifest__.py b/addons_extensions/grace_period/__manifest__.py index 7251fbc6d..e3f0e13d7 100644 --- a/addons_extensions/grace_period/__manifest__.py +++ b/addons_extensions/grace_period/__manifest__.py @@ -12,6 +12,7 @@ 'hr_holidays', 'resource', 'hr_attendance_extended', + 'roster_management', ], 'data': [ @@ -23,7 +24,8 @@ 'views/late_coming_request.xml', 'views/hr_employee_inherit.xml', 'views/late_coming_mail_template.xml', - # 'security/record_rules.xml', + 'views/ot_request.xml', + 'views/ot_mail_template.xml', ], 'installable': True, diff --git a/addons_extensions/grace_period/models/attendance_data.py b/addons_extensions/grace_period/models/attendance_data.py index c7a4d6372..336536635 100644 --- a/addons_extensions/grace_period/models/attendance_data.py +++ b/addons_extensions/grace_period/models/attendance_data.py @@ -1,4 +1,4 @@ -from odoo import api, fields, models, tools +from odoo import api, fields, models, tools, _ class AttendanceAnalytics(models.Model): @@ -12,86 +12,73 @@ class AttendanceAnalytics(models.Model): 'hr.employee', string='Employee' ) - department_id = fields.Many2one( 'hr.department', string='Department' ) - date = fields.Date() - date_end = fields.Date( string="End Date" ) - min_check_in = fields.Datetime() - max_check_out = fields.Datetime() - worked_hours = fields.Float() - out_time = fields.Float() - expected_check_in = fields.Float() - expected_check_out = fields.Float() - required_checkout_time = fields.Float() - compensated_time = fields.Float() - department_grace_period = fields.Integer() - late_minutes = fields.Float() - early_out_minutes = fields.Float() - is_late = fields.Boolean( string="Late" ) - is_early_out = fields.Boolean( string="Early Out" ) - is_compensation_pending = fields.Boolean( string="Compensation Pending" ) - late_time = fields.Char( string="Late Time", compute="_compute_late_time" ) - early_out_time = fields.Char( string="Early Out Time", compute="_compute_early_out_time" ) - holiday_name = fields.Char() - is_holiday = fields.Boolean() - is_week_off = fields.Boolean() - - status_message = fields.Char( - string="Status" - ) - display_label = fields.Char( compute="_compute_display_label", store=False ) - color = fields.Integer( compute="_compute_color" ) - status = fields.Selection([ + ('present', 'Present'), - ('leave', 'Leave'), - ('no_info', 'No Information') - ]) + + ('absent', 'Absent'), + + ('half_day', 'Half Day'), + + ('late_in', 'Late In'), + + ('early_out', 'Early Out'), + + ('on_duty', 'On Duty'), + + ('work_from_home', 'Work From Home'), + + ('holiday', 'Holiday'), + + ('week_off', 'Week Off'), + + ], string="Attendance Status") late_request_id = fields.Many2one( 'late.coming.request', @@ -102,11 +89,26 @@ class AttendanceAnalytics(models.Model): string="Late Approved" ) + hours_per_day = fields.Float() + + allowed_ot_limit = fields.Float() + + ot_eligible = fields.Boolean() + + overtime_hours = fields.Float() + shift_id = fields.Many2one( + 'resource.calendar', + string='Shift' + ) + + shift_name = fields.Char( + string='Shift' + ) + @api.depends('late_minutes') def _compute_late_time(self): for rec in self: - total_minutes = int(rec.late_minutes or 0) hours = total_minutes // 60 @@ -121,7 +123,6 @@ class AttendanceAnalytics(models.Model): def _compute_early_out_time(self): for rec in self: - total_minutes = int( rec.early_out_minutes or 0 ) @@ -135,12 +136,16 @@ class AttendanceAnalytics(models.Model): ) @api.depends( - 'status_message', + 'status', 'late_time', 'early_out_time' ) def _compute_display_label(self): + status_dict = dict( + self._fields['status'].selection + ) + for rec in self: lines = [] @@ -148,8 +153,13 @@ class AttendanceAnalytics(models.Model): if rec.employee_id: lines.append(rec.employee_id.name) - if rec.status_message: - lines.append(rec.status_message) + if rec.status: + lines.append( + status_dict.get( + rec.status, + rec.status + ) + ) if rec.late_minutes: lines.append( @@ -163,36 +173,30 @@ class AttendanceAnalytics(models.Model): rec.display_label = "\n".join(lines) - @api.depends( - 'is_holiday', - 'is_week_off', - 'status', - 'is_compensation_pending', - 'is_late' - ) + + @api.depends('status') def _compute_color(self): for rec in self: - - if rec.is_holiday: - rec.color = 4 - - elif rec.is_week_off: - rec.color = 7 - - elif rec.status == 'leave': - rec.color = 3 - - elif rec.is_compensation_pending: - rec.color = 2 - - elif rec.is_late: - rec.color = 1 - - elif rec.status == 'present': + if rec.status == 'present': rec.color = 10 - - else: + elif rec.status == 'absent': + rec.color = 1 + elif rec.status == 'half_day': + rec.color = 2 + elif rec.status == 'late_in': + rec.color = 3 + elif rec.status == 'early_out': + rec.color = 4 + elif rec.status == 'holiday': + rec.color = 7 + elif rec.status == 'week_off': rec.color = 8 + elif rec.status == 'work_from_home': + rec.color = 5 + elif rec.status == 'on_duty': + rec.color = 6 + else: + rec.color = 0 def action_create_late_request(self): @@ -216,8 +220,6 @@ class AttendanceAnalytics(models.Model): self.is_compensation_pending, 'compensation_minutes': self.early_out_minutes, - 'status_message': - self.status_message, }) return { 'type': 'ir.actions.act_window', @@ -228,6 +230,71 @@ class AttendanceAnalytics(models.Model): 'target': 'current', } + def action_create_ot_request(self): + self.ensure_one() + request = self.env['overtime.request'].search([ + ('employee_id', '=', self.employee_id.id), + ('attendance_date', '=', self.date) + ], limit=1) + if not request: + request = self.env['overtime.request'].create({ + 'employee_id': + self.employee_id.id, + 'attendance_date': + self.date, + 'check_in': + self.min_check_in, + 'check_out': + self.max_check_out, + 'worked_hours': + self.worked_hours, + 'hours_per_day': + self.hours_per_day, + 'allowed_ot_limit': + self.allowed_ot_limit, + 'overtime_hours': + self.overtime_hours, + }) + return { + 'type': 'ir.actions.act_window', + 'name': 'OT Request', + 'res_model': 'overtime.request', + 'res_id': request.id, + 'view_mode': 'form', + 'target': 'current', + } + + def action_create_shiftswap_request(self): + self.ensure_one() + + request = self.env['shift.swap.request'].search([ + ('employee_id', '=', self.employee_id.id), + ('roster_date', '=', self.date), + ('state', '!=', 'approved') + ], limit=1) + + + self.ensure_one() + roster_form = self.env.ref('roster_management.view_shift_swap_form') + if not request: + request = self.env['shift.swap.request'].create({ + 'employee_id': self.employee_id.id, + 'roster_date': self.date, + }) + + return { + 'type': 'ir.actions.act_window', + 'name': 'Shift Swap Request', + 'res_model': 'shift.swap.request', + 'res_id': request.id, + 'view_mode': 'form', + 'view_id': roster_form.id, + 'target': 'current', + 'context': { + 'edit': True, + }, + } + def init(self): tools.drop_view_if_exists( @@ -318,6 +385,23 @@ class AttendanceAnalytics(models.Model): WHERE state = 'approved' ), + + ot_requests AS ( + + SELECT + + id, + + employee_id, + + attendance_date, + + state + + FROM overtime_request + + WHERE state = 'approved' + ), holiday_data AS ( @@ -343,45 +427,73 @@ class AttendanceAnalytics(models.Model): ) SELECT - row_number() OVER() AS id, - ed.employee_id, - ed.department_id, - + rc.id AS shift_id, + rc.name AS shift_name, ed.date, - ed.date AS date_end, - ats.min_check_in, - ats.max_check_out, - COALESCE( ats.worked_hours, 0 ) AS worked_hours, - CASE - WHEN ats.min_check_in IS NOT NULL - AND ats.max_check_out IS NOT NULL - THEN ( - EXTRACT( - EPOCH FROM ( - ats.max_check_out - - ats.min_check_in - ) - ) / 3600 - ) - ats.worked_hours - ELSE 0 -END AS out_time, - -CASE - WHEN ats.worked_hours > 9 - THEN ats.worked_hours - 9 - ELSE 0 -END AS compensated_time, + rc.hours_per_day AS hours_per_day, + ( + rc.hours_per_day + + + COALESCE(rc.over_time_hrs, 0) + ) AS allowed_ot_limit, + + CASE + + WHEN ats.worked_hours > rc.hours_per_day + + THEN + ats.worked_hours - rc.hours_per_day + + ELSE 0 + + END AS overtime_hours, + + CASE + + WHEN ats.worked_hours > + + ( + rc.hours_per_day + + + COALESCE(rc.over_time_hrs, 0) + ) + + THEN TRUE + + ELSE FALSE + + END AS ot_eligible, + CASE + WHEN ats.min_check_in IS NOT NULL + AND ats.max_check_out IS NOT NULL + THEN ( + EXTRACT( + EPOCH FROM ( + ats.max_check_out - + ats.min_check_in + ) + ) / 3600 + ) - ats.worked_hours + ELSE 0 + END AS out_time, + + CASE + WHEN ats.worked_hours > 9 + THEN ats.worked_hours - 9 + ELSE 0 + END AS compensated_time, + lr.id AS late_request_id, hd.holiday_name, @@ -796,104 +908,28 @@ END AS compensated_time, THEN TRUE ELSE FALSE END AS late_approved, - + CASE - + WHEN hd.id IS NOT NULL - THEN 'Holiday' - + THEN 'holiday' + WHEN EXTRACT( DOW FROM ed.date ) IN (0,6) - - THEN 'Week Off' - - WHEN leave.id IS NOT NULL - THEN 'Leave' - + THEN 'week_off' + WHEN ats.min_check_in IS NULL - THEN 'Absent' - - WHEN - ( - ( - - ( - rc.shift_end_time * 60 - ) - - + - - CASE - - WHEN ats.min_check_in - IS NOT NULL - - THEN GREATEST( - - ( - ( - EXTRACT( - HOUR - FROM ats.min_check_in - ) * 60 - ) - + - EXTRACT( - MINUTE - FROM ats.min_check_in - ) - ) - - - - - ( - ( - rc.shift_start_time - * 60 - ) - + - COALESCE( - dg.grace_period, - rc.late_grace_period, - 0 - ) - ), - - 0 - ) - - ELSE 0 - - END - - ) - - > - - ( - ( - EXTRACT( - HOUR - FROM ats.max_check_out - ) * 60 - ) - + - EXTRACT( - MINUTE - FROM ats.max_check_out - ) - ) - ) - - THEN 'Compensation Pending' - + THEN 'absent' + + WHEN ats.worked_hours < (rc.hours_per_day / 2.0) + THEN 'half_day' + WHEN ( ( EXTRACT( - HOUR - FROM ats.min_check_in + HOUR FROM ats.min_check_in ) * 60 ) + @@ -902,9 +938,9 @@ END AS compensated_time, FROM ats.min_check_in ) ) - + > - + ( ( rc.shift_start_time * 60 @@ -916,23 +952,36 @@ END AS compensated_time, 0 ) ) - - THEN 'Late' - - ELSE 'Present' - - END AS status_message, - - CASE - WHEN leave.id IS NOT NULL - THEN 'leave' - - WHEN ats.min_check_in IS NOT NULL - THEN 'present' - - ELSE 'no_info' + + THEN 'late_in' + + WHEN + ( + ( + EXTRACT( + HOUR + FROM ats.max_check_out + ) * 60 + ) + + + EXTRACT( + MINUTE + FROM ats.max_check_out + ) + ) + + < + + ( + rc.shift_end_time * 60 + ) + + THEN 'early_out' + + ELSE 'present' + END AS status - + FROM employee_dates ed LEFT JOIN attendance_summary ats @@ -949,6 +998,10 @@ END AS compensated_time, LEFT JOIN late_requests lr ON lr.employee_id = ed.employee_id AND lr.attendance_date = ed.date + + LEFT JOIN ot_requests otr + ON otr.employee_id = ed.employee_id + AND otr.attendance_date = ed.date LEFT JOIN holiday_data hd ON ed.date BETWEEN @@ -981,7 +1034,7 @@ END AS compensated_time, ('date', '=', today), - ('status', '=', 'no_info'), + ('status', '=', 'absent'), ('is_holiday', '=', False), diff --git a/addons_extensions/grace_period/models/hr_employee_inherit.py b/addons_extensions/grace_period/models/hr_employee_inherit.py index 6915b7e8b..88425b45f 100644 --- a/addons_extensions/grace_period/models/hr_employee_inherit.py +++ b/addons_extensions/grace_period/models/hr_employee_inherit.py @@ -8,6 +8,17 @@ class HREmployee(models.Model): string="Attendance Count", compute="_compute_attendance_analytics_count" ) + attendance_mode = fields.Selection( + [ + ('office', 'Office Based'), + ('remote', 'Remote'), + ('hybrid', 'Hybrid'), + ('shift', 'Shift Based'), + ], + string='Attendance Mode', + default='office', + tracking=True, + ) def _compute_attendance_analytics_count(self): diff --git a/addons_extensions/grace_period/models/ot_request.py b/addons_extensions/grace_period/models/ot_request.py new file mode 100644 index 000000000..69c5757e3 --- /dev/null +++ b/addons_extensions/grace_period/models/ot_request.py @@ -0,0 +1,158 @@ +from odoo import fields, models, _ +from odoo.exceptions import UserError + + +class OvertimeRequest(models.Model): + _name = 'overtime.request' + _description = 'Overtime Request' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _rec_name = 'employee_id' + + employee_id = fields.Many2one( + 'hr.employee', + required=True, + tracking=True + ) + + department_id = fields.Many2one( + 'hr.department', + related='employee_id.department_id', + store=True + ) + + manager_id = fields.Many2one( + 'hr.employee', + related='employee_id.parent_id', + store=True + ) + + attendance_date = fields.Date( + tracking=True + ) + + check_in = fields.Datetime() + + check_out = fields.Datetime() + + worked_hours = fields.Float() + + hours_per_day = fields.Float() + + allowed_ot_limit = fields.Float() + + overtime_hours = fields.Float( + tracking=True + ) + + reason = fields.Text( + tracking=True + ) + + state = fields.Selection([ + + ('draft', 'Draft'), + + ('submitted', 'Submitted'), + + ('approved', 'Approved'), + + ('rejected', 'Rejected') + + ], default='draft', tracking=True) + + def action_submit(self): + + for rec in self: + + if not rec.reason: + raise UserError( + _("Please enter reason.") + ) + + if not rec.manager_id: + raise UserError( + _("Manager not configured.") + ) + + if not rec.manager_id.work_email: + raise UserError( + _("Manager email not configured.") + ) + + rec.state = 'submitted' + + template = self.env.ref( + 'grace_period.email_template_ot_request' + ) + + template.send_mail( + rec.id, + force_send=True + ) + + rec.message_post( + body=_( + "OT Request Submitted" + ) + ) + + def action_approve(self): + + for rec in self: + + if ( + rec.manager_id.user_id != self.env.user + and not self.env.user.has_group( + 'hr.group_hr_manager' + ) + ): + + raise UserError( + _("Only Manager or HR can approve.") + ) + + rec.state = 'approved' + + rec.message_post( + body=_("Overtime Request Approved.") + ) + + def action_reject(self): + + for rec in self: + + if ( + rec.manager_id.user_id != self.env.user + and not self.env.user.has_group( + 'hr.group_hr_manager' + ) + ): + + raise UserError( + _("Only Manager or HR can reject.") + ) + + rec.state = 'rejected' + + template = self.env.ref( + 'grace_period.email_template_ot_rejected' + ) + + template.send_mail( + rec.id, + force_send=True + ) + + rec.message_post( + body=_("Overtime Request Rejected.") + ) + + def action_reset_to_draft(self): + + self.state = 'draft' + + self.message_post( + body=_( + "Reset to Draft" + ) + ) \ No newline at end of file diff --git a/addons_extensions/grace_period/models/resource_calendar_period.py b/addons_extensions/grace_period/models/resource_calendar_period.py index c3b5c10d9..114183196 100644 --- a/addons_extensions/grace_period/models/resource_calendar_period.py +++ b/addons_extensions/grace_period/models/resource_calendar_period.py @@ -24,6 +24,7 @@ class ResourceCalendar(models.Model): shift_end_time = fields.Float(string="End Time") late_grace_period = fields.Integer(default=15) early_out_grace_period = fields.Integer(default=0) + over_time_hrs = fields.Float(default=2) department_grace_ids = fields.One2many( 'resource.calendar.department.grace', 'calendar_id', diff --git a/addons_extensions/grace_period/security/ir.model.access.csv b/addons_extensions/grace_period/security/ir.model.access.csv index b72a540a6..9fcff486c 100644 --- a/addons_extensions/grace_period/security/ir.model.access.csv +++ b/addons_extensions/grace_period/security/ir.model.access.csv @@ -5,3 +5,4 @@ access_attendance_analytics_manager,attendance.analytics.manager,model_attendanc access_attendance_analytics_admin,attendance.analytics.admin,model_attendance_analytics,base.group_system,1,1,1,1 access_late_coming_request,attendance_late_coming_request,model_late_coming_request,base.group_user,1,1,1,1 access_resource_calendar_department_grace,resource_calendar_department_grace,model_resource_calendar_department_grace,base.group_user,1,1,1,1 +access_overtime_request,overtime_request,model_overtime_request,base.group_user,1,1,1,1 diff --git a/addons_extensions/grace_period/views/attendance_data.xml b/addons_extensions/grace_period/views/attendance_data.xml index 2146a5ba8..2ddccc894 100644 --- a/addons_extensions/grace_period/views/attendance_data.xml +++ b/addons_extensions/grace_period/views/attendance_data.xml @@ -1,11 +1,8 @@ - - - attendance.analytics.list attendance.analytics @@ -25,7 +22,15 @@ - + + +