# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) from datetime import datetime, timedelta from odoo import models, fields, api from .tz_utils import get_local_today, get_local_day_boundaries class HrEmployee(models.Model): _inherit = 'hr.employee' x_fclk_default_location_id = fields.Many2one( 'fusion.clock.location', string='Default Clock Location', help="The default location shown on this employee's clock page.", ) x_fclk_enable_clock = fields.Boolean( string='Enable Fusion Clock', default=True, help="If unchecked, this employee cannot use the Fusion Clock portal/systray.", ) x_fclk_break_minutes = fields.Float( string='Custom Break (min)', default=0.0, help="Override default break duration for this employee. 0 = use shift or company default.", ) # Shift scheduling x_fclk_shift_id = fields.Many2one( 'fusion.clock.shift', string='Work Shift', help="Assigned shift schedule. Leave empty to use global defaults.", ) # Pending reason enforcement x_fclk_pending_reason = fields.Boolean( string='Pending Reason Required', default=False, help="If set, employee must explain a missed clock-out before clocking in again.", ) # Kiosk PIN x_fclk_kiosk_pin = fields.Char( string='Kiosk PIN', help="PIN code for kiosk clock-in/out identification.", groups="fusion_clock.group_fusion_clock_manager", ) # On-time streak x_fclk_ontime_streak = fields.Integer( string='On-Time Streak', default=0, help="Consecutive workdays clocked in on time.", ) # Absence tracking (computed) x_fclk_absences_this_month = fields.Integer( string='Absences This Month', compute='_compute_absence_counts', ) x_fclk_absences_this_year = fields.Integer( string='Absences This Year', compute='_compute_absence_counts', ) # Overtime tracking (computed) x_fclk_overtime_this_week = fields.Float( string='Overtime This Week (h)', compute='_compute_overtime', ) x_fclk_overtime_this_month = fields.Float( string='Overtime This Month (h)', compute='_compute_overtime', ) # Activity log relation x_fclk_activity_log_ids = fields.One2many( 'fusion.clock.activity.log', 'employee_id', string='Activity Logs', ) # Leave request relation x_fclk_leave_request_ids = fields.One2many( 'fusion.clock.leave.request', 'employee_id', string='Leave Requests', ) # Correction request relation x_fclk_correction_ids = fields.One2many( 'fusion.clock.correction', 'employee_id', string='Correction Requests', ) # Reminder tracking x_fclk_last_reminder_date = fields.Date( string='Last Reminder Date', help="Tracks the last date a reminder was sent to avoid duplicates.", ) def _get_fclk_break_minutes(self): """Return effective break minutes for this employee. Priority: employee override > shift > global setting. """ self.ensure_one() if self.x_fclk_break_minutes > 0: return self.x_fclk_break_minutes if self.x_fclk_shift_id and self.x_fclk_shift_id.break_minutes > 0: return self.x_fclk_shift_id.break_minutes return float( self.env['ir.config_parameter'].sudo().get_param( 'fusion_clock.default_break_minutes', '30' ) ) def _get_fclk_scheduled_times(self, date): """Return (scheduled_in_dt, scheduled_out_dt) for a given date. Uses employee shift if assigned, otherwise global settings. The configured hours are interpreted in the employee's local timezone and converted to naive-UTC datetimes so they can be compared with Odoo's UTC-based ``fields.Datetime.now()``. """ import pytz self.ensure_one() if self.x_fclk_shift_id: in_hour = self.x_fclk_shift_id.start_time out_hour = self.x_fclk_shift_id.end_time else: ICP = self.env['ir.config_parameter'].sudo() in_hour = float(ICP.get_param('fusion_clock.default_clock_in_time', '9.0')) out_hour = float(ICP.get_param('fusion_clock.default_clock_out_time', '17.0')) in_h = int(in_hour) in_m = int((in_hour - in_h) * 60) out_h = int(out_hour) out_m = int((out_hour - out_h) * 60) tz_name = ( self.resource_id.tz or (self.user_id.partner_id.tz if self.user_id else False) or self.company_id.partner_id.tz or 'UTC' ) local_tz = pytz.timezone(tz_name) utc = pytz.UTC local_in = local_tz.localize( datetime.combine(date, datetime.min.time().replace(hour=in_h, minute=in_m)) ) local_out = local_tz.localize( datetime.combine(date, datetime.min.time().replace(hour=out_h, minute=out_m)) ) scheduled_in = local_in.astimezone(utc).replace(tzinfo=None) scheduled_out = local_out.astimezone(utc).replace(tzinfo=None) return scheduled_in, scheduled_out def _get_fclk_scheduled_hours(self): """Return the expected work hours for this employee's shift.""" self.ensure_one() if self.x_fclk_shift_id: return self.x_fclk_shift_id.scheduled_hours ICP = self.env['ir.config_parameter'].sudo() in_hour = float(ICP.get_param('fusion_clock.default_clock_in_time', '9.0')) out_hour = float(ICP.get_param('fusion_clock.default_clock_out_time', '17.0')) break_hrs = self._get_fclk_break_minutes() / 60.0 return max((out_hour - in_hour) - break_hrs, 0.0) def _compute_absence_counts(self): ActivityLog = self.env['fusion.clock.activity.log'].sudo() for emp in self: today = get_local_today(self.env, emp) month_start = today.replace(day=1) year_start = today.replace(month=1, day=1) month_start_utc, _ = get_local_day_boundaries(self.env, month_start, emp) year_start_utc, _ = get_local_day_boundaries(self.env, year_start, emp) emp.x_fclk_absences_this_month = ActivityLog.search_count([ ('employee_id', '=', emp.id), ('log_type', '=', 'absent'), ('log_date', '>=', month_start_utc), ]) emp.x_fclk_absences_this_year = ActivityLog.search_count([ ('employee_id', '=', emp.id), ('log_type', '=', 'absent'), ('log_date', '>=', year_start_utc), ]) def _compute_overtime(self): Attendance = self.env['hr.attendance'].sudo() for emp in self: today = get_local_today(self.env, emp) week_start = today - timedelta(days=today.weekday()) month_start = today.replace(day=1) week_start_utc, _ = get_local_day_boundaries(self.env, week_start, emp) month_start_utc, _ = get_local_day_boundaries(self.env, month_start, emp) week_atts = Attendance.search([ ('employee_id', '=', emp.id), ('check_in', '>=', week_start_utc), ('check_out', '!=', False), ]) emp.x_fclk_overtime_this_week = sum(a.x_fclk_overtime_hours or 0 for a in week_atts) month_atts = Attendance.search([ ('employee_id', '=', emp.id), ('check_in', '>=', month_start_utc), ('check_out', '!=', False), ]) emp.x_fclk_overtime_this_month = sum(a.x_fclk_overtime_hours or 0 for a in month_atts)