from odoo import api, fields, models, tools class AttendanceAnalytics(models.Model): _name = 'attendance.analytics' _description = 'Attendance Analytics' _auto = False _rec_name = 'employee_id' _order = 'date desc' employee_id = fields.Many2one( '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') ]) late_request_id = fields.Many2one( 'late.coming.request', string="Late Request" ) late_approved = fields.Boolean( string="Late Approved" ) @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 minutes = total_minutes % 60 rec.late_time = "%02d:%02d" % ( hours, minutes ) @api.depends('early_out_minutes') def _compute_early_out_time(self): for rec in self: total_minutes = int( rec.early_out_minutes or 0 ) hours = total_minutes // 60 minutes = total_minutes % 60 rec.early_out_time = "%02d:%02d" % ( hours, minutes ) @api.depends( 'status_message', 'late_time', 'early_out_time' ) def _compute_display_label(self): for rec in self: lines = [] if rec.employee_id: lines.append(rec.employee_id.name) if rec.status_message: lines.append(rec.status_message) if rec.late_minutes: lines.append( f"Late: {rec.late_time}" ) if rec.early_out_minutes: lines.append( f"Early Out: {rec.early_out_time}" ) rec.display_label = "\n".join(lines) @api.depends( 'is_holiday', 'is_week_off', 'status', 'is_compensation_pending', 'is_late' ) 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': rec.color = 10 else: rec.color = 8 def action_create_late_request(self): self.ensure_one() request = self.env['late.coming.request'].search([ ('employee_id', '=', self.employee_id.id), ('attendance_date', '=', self.date) ], limit=1) if not request: request = self.env['late.coming.request'].create({ 'employee_id': self.employee_id.id, 'attendance_date': self.date, 'check_in': self.min_check_in, 'late_minutes': self.late_minutes, 'department_grace_period': self.department_grace_period, 'required_checkout_time': self.required_checkout_time, 'compensation_pending': self.is_compensation_pending, 'compensation_minutes': self.early_out_minutes, 'status_message': self.status_message, }) return { 'type': 'ir.actions.act_window', 'name': 'Late Coming Request', 'res_model': 'late.coming.request', 'res_id': request.id, 'view_mode': 'form', 'target': 'current', } def init(self): tools.drop_view_if_exists( self.env.cr, self._table ) self.env.cr.execute(""" CREATE OR REPLACE VIEW attendance_analytics AS ( WITH employee_dates AS ( SELECT emp.id AS employee_id, emp.department_id, emp.resource_calendar_id, generate_series( DATE(emp.create_date), CURRENT_DATE, interval '1 day' )::date AS date FROM hr_employee emp WHERE emp.active = true ), attendance_summary AS ( SELECT att.employee_id, DATE(att.check_in) AS attendance_date, MIN(att.check_in) AS min_check_in, MAX(att.check_out) AS max_check_out, SUM(att.worked_hours) AS worked_hours FROM hr_attendance att GROUP BY att.employee_id, DATE(att.check_in) ), department_grace AS ( SELECT rcg.calendar_id, rcg.department_id, rcg.grace_period FROM resource_calendar_department_grace rcg ), late_requests AS ( SELECT id, employee_id, attendance_date, state FROM late_coming_request WHERE state = 'approved' ), holiday_data AS ( SELECT DISTINCT rl.id, rl.calendar_id, rl.name AS holiday_name, DATE(rl.date_from) AS holiday_start, DATE(rl.date_to) AS holiday_end FROM resource_calendar_leaves rl WHERE rl.date_from IS NOT NULL AND rl.date_to IS NOT NULL AND rl.resource_id IS NULL ) SELECT row_number() OVER() AS id, ed.employee_id, ed.department_id, 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, lr.id AS late_request_id, hd.holiday_name, CASE WHEN hd.id IS NOT NULL THEN TRUE ELSE FALSE END AS is_holiday, CASE WHEN EXTRACT( DOW FROM ed.date ) IN (0, 6) THEN TRUE ELSE FALSE END AS is_week_off, dg.grace_period AS department_grace_period, ( ( rc.shift_start_time * 60 ) + COALESCE( dg.grace_period, rc.late_grace_period, 0 ) ) AS expected_check_in, ( rc.shift_end_time * 60 ) AS expected_check_out, 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 AS late_minutes, CASE WHEN ats.min_check_in IS NOT NULL AND ( ( 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 ) ) THEN TRUE ELSE FALSE END AS is_late, ( ( 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 ) AS required_checkout_time, CASE WHEN ats.max_check_out IS NOT NULL THEN GREATEST( ( ( 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 ) ), 0 ) ELSE 0 END AS early_out_minutes, CASE WHEN ats.max_check_out IS NOT NULL AND ( ( EXTRACT( HOUR FROM ats.max_check_out ) * 60 ) + EXTRACT( MINUTE FROM ats.max_check_out ) ) < ( ( 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 ) THEN TRUE ELSE FALSE END AS is_early_out, CASE 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 TRUE ELSE FALSE END AS is_compensation_pending, CASE WHEN lr.state = 'approved' THEN TRUE ELSE FALSE END AS late_approved, CASE WHEN hd.id IS NOT NULL THEN 'Holiday' WHEN EXTRACT( DOW FROM ed.date ) IN (0,6) THEN 'Week Off' WHEN leave.id IS NOT NULL THEN 'Leave' 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' WHEN ( ( 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 ) ) 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' END AS status FROM employee_dates ed LEFT JOIN attendance_summary ats ON ats.employee_id = ed.employee_id AND ats.attendance_date = ed.date LEFT JOIN hr_leave leave ON leave.employee_id = ed.employee_id AND ed.date BETWEEN leave.request_date_from AND leave.request_date_to AND leave.state = 'validate' LEFT JOIN late_requests lr ON lr.employee_id = ed.employee_id AND lr.attendance_date = ed.date LEFT JOIN holiday_data hd ON ed.date BETWEEN hd.holiday_start AND hd.holiday_end AND ( hd.calendar_id = ed.resource_calendar_id OR hd.calendar_id IS NULL ) LEFT JOIN resource_calendar rc ON rc.id = ed.resource_calendar_id LEFT JOIN department_grace dg ON dg.calendar_id = ed.resource_calendar_id AND dg.department_id = ed.department_id ) """) def action_send_absent_mail(self): today = fields.Date.today() records = self.search([ ('date', '=', today), ('status', '=', 'no_info'), ('is_holiday', '=', False), ('is_week_off', '=', False), ]) template = self.env.ref( 'grace_period.email_template_absent_alert' ) for rec in records: if rec.employee_id.work_email: template.send_mail( rec.id, force_send=True )