# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) from odoo import models, fields, api class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' # ── Work Schedule ────────────────────────────────────────────────── fclk_default_clock_in_time = fields.Float( string='Default Clock-In Time', config_parameter='fusion_clock.default_clock_in_time', default=9.0, help="Default scheduled clock-in time (24h format, e.g. 9.0 = 9:00 AM).", ) fclk_default_clock_out_time = fields.Float( string='Default Clock-Out Time', config_parameter='fusion_clock.default_clock_out_time', default=17.0, help="Default scheduled clock-out time (24h format, e.g. 17.0 = 5:00 PM).", ) fclk_auto_deduct_break = fields.Boolean( string='Auto-Deduct Break', config_parameter='fusion_clock.auto_deduct_break', default=True, help="Automatically deduct break from worked hours on clock-out.", ) fclk_default_break_minutes = fields.Float( string='Default Break Duration (min)', config_parameter='fusion_clock.default_break_minutes', default=30.0, help="Default unpaid break duration in minutes.", ) fclk_break_threshold_hours = fields.Float( string='Break Threshold (hours)', config_parameter='fusion_clock.break_threshold_hours', default=4.0, help="Only deduct break if shift is longer than this many hours.", ) # ── Attendance Rules ─────────────────────────────────────────────── fclk_enable_auto_clockout = fields.Boolean( string='Enable Auto Clock-Out', config_parameter='fusion_clock.enable_auto_clockout', default=True, help="Automatically clock out employees who forget. Triggers after shift end time plus grace period, or after max shift hours.", ) fclk_grace_period_minutes = fields.Float( string='Grace Period (min)', config_parameter='fusion_clock.grace_period_minutes', default=15.0, help="Minutes allowed after scheduled end before auto clock-out.", ) fclk_max_shift_hours = fields.Float( string='Max Shift Length (hours)', config_parameter='fusion_clock.max_shift_hours', default=12.0, help="Maximum shift length before auto clock-out (safety net).", ) fclk_enable_penalties = fields.Boolean( string='Enable Penalty Tracking', config_parameter='fusion_clock.enable_penalties', default=True, help="Deduct minutes from worked hours when employees clock in late or clock out early.", ) fclk_penalty_grace_minutes = fields.Float( string='Penalty Grace (min)', config_parameter='fusion_clock.penalty_grace_minutes', default=5.0, help="Minutes of grace before a late/early penalty is recorded.", ) fclk_penalty_deduction_minutes = fields.Float( string='Penalty Deduction (min)', config_parameter='fusion_clock.penalty_deduction_minutes', default=15.0, help="Minutes deducted from worked hours per penalty occurrence.", ) fclk_enable_overtime = fields.Boolean( string='Enable Overtime Tracking', config_parameter='fusion_clock.enable_overtime', default=True, help="Calculate and track overtime when net hours exceed the daily or weekly threshold.", ) fclk_daily_overtime_threshold = fields.Float( string='Daily OT Threshold (hours)', config_parameter='fusion_clock.daily_overtime_threshold', default=8.0, help="Net hours beyond this threshold count as daily overtime.", ) fclk_weekly_overtime_threshold = fields.Float( string='Weekly OT Threshold (hours)', config_parameter='fusion_clock.weekly_overtime_threshold', default=40.0, help="Net hours beyond this threshold count as weekly overtime.", ) # ── Notifications ────────────────────────────────────────────────── fclk_office_user_id = fields.Many2one( 'res.users', string='Office User', help="User who receives activity notifications for attendance issues.", ) fclk_very_late_threshold_minutes = fields.Float( string='Very Late Threshold (min)', config_parameter='fusion_clock.very_late_threshold_minutes', default=15.0, help="Minutes late before an activity is scheduled for the office user.", ) fclk_max_monthly_absences = fields.Integer( string='Max Monthly Absences', config_parameter='fusion_clock.max_monthly_absences', default=3, help="Alert office user when an employee reaches this many absences in a month.", ) fclk_enable_employee_notifications = fields.Boolean( string='Enable Employee Notifications', config_parameter='fusion_clock.enable_employee_notifications', default=True, help="Send clock-in/out reminders to employees.", ) fclk_reminder_before_shift_minutes = fields.Float( string='Remind After Shift Start (min)', config_parameter='fusion_clock.reminder_before_shift_minutes', default=30.0, help="Send reminder if employee hasn't clocked in this many minutes after shift start.", ) fclk_reminder_before_end_minutes = fields.Float( string='Remind Before Shift End (min)', config_parameter='fusion_clock.reminder_before_end_minutes', default=15.0, help="Send clock-out reminder this many minutes before shift end.", ) fclk_send_weekly_summary = fields.Boolean( string='Send Weekly Summary', config_parameter='fusion_clock.send_weekly_summary', default=True, help="Send weekly attendance summary to each employee on Monday.", ) # ── Location & Verification ──────────────────────────────────────── fclk_enable_ip_fallback = fields.Boolean( string='Enable IP Fallback', config_parameter='fusion_clock.enable_ip_fallback', default=False, help="Allow IP-based location verification when GPS is unavailable.", ) fclk_enable_photo_verification = fields.Boolean( string='Enable Photo Verification', config_parameter='fusion_clock.enable_photo_verification', default=False, help="Global toggle for selfie verification on clock-in (per-location control).", ) fclk_google_maps_api_key = fields.Char( string='Google Maps API Key', config_parameter='fusion_clock.google_maps_api_key', ) # ── Kiosk & Portal ───────────────────────────────────────────────── fclk_enable_kiosk = fields.Boolean( string='Enable Kiosk Mode', config_parameter='fusion_clock.enable_kiosk', default=False, help="Allow employees to clock in/out from a shared device using their PIN code.", ) fclk_kiosk_pin_required = fields.Boolean( string='Require PIN for Kiosk', config_parameter='fusion_clock.kiosk_pin_required', default=True, help="Require employees to enter a PIN when using kiosk mode.", ) fclk_enable_correction_requests = fields.Boolean( string='Enable Correction Requests', config_parameter='fusion_clock.enable_correction_requests', default=True, help="Allow employees to request timesheet corrections from the portal.", ) fclk_enable_sounds = fields.Boolean( string='Enable Clock Sounds', config_parameter='fusion_clock.enable_sounds', default=True, help="Play audio confirmation sounds when employees clock in or out.", ) # ── Pay Period & Reports ─────────────────────────────────────────── fclk_pay_period_type = fields.Selection( [ ('weekly', 'Weekly'), ('biweekly', 'Bi-Weekly'), ('semi_monthly', 'Semi-Monthly'), ('monthly', 'Monthly'), ], string='Pay Period', config_parameter='fusion_clock.pay_period_type', default='biweekly', help="How often attendance reports are generated.", ) fclk_pay_period_start = fields.Char( string='Pay Period Anchor Date', config_parameter='fusion_clock.pay_period_start', help="Start date for pay period calculations (YYYY-MM-DD format).", ) fclk_auto_generate_reports = fields.Boolean( string='Auto-Generate Reports', config_parameter='fusion_clock.auto_generate_reports', default=True, help="Automatically create attendance reports at the end of each pay period.", ) fclk_send_employee_reports = fields.Boolean( string='Send Employee Copies', config_parameter='fusion_clock.send_employee_reports', default=True, help="Send each employee a copy of their individual attendance report.", ) fclk_report_recipient_user_ids = fields.Many2many( 'res.users', 'fclk_report_recipient_user_rel', 'config_id', 'user_id', string='Internal Report Recipients', help="Internal users who will receive batch reports.", ) fclk_report_recipient_emails = fields.Char( string='Report Recipient Emails', config_parameter='fusion_clock.report_recipient_emails', help="Comma-separated email addresses for batch report delivery.", ) fclk_csv_column_mapping = fields.Char( string='CSV Column Mapping', config_parameter='fusion_clock.csv_column_mapping', help="Custom column names for CSV export (JSON format). Leave blank for defaults.", ) # ── NFC Clock Kiosk ──────────────────────────────────────────────── fclk_enable_nfc_kiosk = fields.Boolean( string='Enable NFC Clock Kiosk', config_parameter='fusion_clock.enable_nfc_kiosk', default=False, help="Enable the tap-to-clock NFC kiosk page at /fusion_clock/kiosk/nfc.", ) fclk_nfc_photo_required = fields.Boolean( string='Require Photo on Tap', config_parameter='fusion_clock.nfc_photo_required', default=True, help="If enabled, the kiosk rejects taps when the front camera is unavailable. " "Recommended for buddy-punch deterrence.", ) fclk_nfc_enroll_password = fields.Char( string='Enroll Mode Password', config_parameter='fusion_clock.nfc_enroll_password', help="Short password the manager types on the kiosk to enter Enroll Mode. " "Leave empty to fall back to manager-group membership only.", ) fclk_nfc_kiosk_debug = fields.Boolean( string='Enable Mock-Tap Debug', config_parameter='fusion_clock.nfc_kiosk_debug', default=False, help="Enables a Ctrl+Shift+T keyboard shortcut on the kiosk page for " "simulating a tap with a configurable UID. Off in production.", ) fclk_nfc_kiosk_location_id = fields.Many2one( related='company_id.x_fclk_nfc_kiosk_location_id', readonly=False, string='NFC Kiosk Location', help="Which clock location is bound to the NFC kiosk for this company. " "Required when the kiosk is enabled.", ) def set_values(self): super().set_values() ICP = self.env['ir.config_parameter'].sudo() if self.fclk_office_user_id: ICP.set_param('fusion_clock.office_user_id', str(self.fclk_office_user_id.id)) else: ICP.set_param('fusion_clock.office_user_id', '0') if self.fclk_report_recipient_user_ids: ICP.set_param('fusion_clock.report_recipient_user_ids', ','.join(str(uid) for uid in self.fclk_report_recipient_user_ids.ids)) else: ICP.set_param('fusion_clock.report_recipient_user_ids', '') @api.model def get_values(self): res = super().get_values() ICP = self.env['ir.config_parameter'].sudo() office_user_id = int(ICP.get_param('fusion_clock.office_user_id', '0')) if office_user_id: res['fclk_office_user_id'] = office_user_id user_ids_str = ICP.get_param('fusion_clock.report_recipient_user_ids', '') if user_ids_str: try: user_ids = [int(x) for x in user_ids_str.split(',') if x.strip()] res['fclk_report_recipient_user_ids'] = [(6, 0, user_ids)] except (ValueError, TypeError): pass return res