# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) from datetime import timedelta 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).", ) # -- Break -- 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_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_break_threshold_hours = fields.Float( string='Break Threshold (hours)', config_parameter='fusion_clock.break_threshold_hours', default=5.0, help="Only deduct break if shift is longer than this many hours.", ) # -- Grace Period & Auto Clock-Out -- 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_enable_auto_clockout = fields.Boolean( string='Enable Auto Clock-Out', config_parameter='fusion_clock.enable_auto_clockout', default=True, ) 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).", ) # -- Penalties -- fclk_enable_penalties = fields.Boolean( string='Enable Penalty Tracking', config_parameter='fusion_clock.enable_penalties', default=True, ) 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.", ) # -- Pay Period -- 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 pay periods repeat. Semi-Monthly uses 1st-15th and 16th-end; " "Weekly and Bi-Weekly use the anchor date below.", ) fclk_pay_period_start = fields.Date( string='Pay Period Anchor Date', help="The first day of any real pay period. All periods are calculated " "forward and backward from this date. For example, if your biweekly " "pay period runs Jan 17 - Jan 30, set this to Jan 17.", ) fclk_pay_period_preview = fields.Char( string='Current Period Preview', compute='_compute_pay_period_preview', help="Shows the current pay period based on today's date and your settings.", ) # -- Reports -- fclk_auto_generate_reports = fields.Boolean( string='Auto-Generate Reports', config_parameter='fusion_clock.auto_generate_reports', default=True, ) 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_send_employee_reports = fields.Boolean( string='Send Employee Copies', config_parameter='fusion_clock.send_employee_reports', default=True, help="Send individual report copies to each employee's work email.", ) # -- Google Maps -- fclk_google_maps_api_key = fields.Char( string='Google Maps API Key', config_parameter='fusion_clock.google_maps_api_key', ) # -- Sounds -- fclk_enable_sounds = fields.Boolean( string='Enable Clock Sounds', config_parameter='fusion_clock.enable_sounds', default=True, ) @api.depends('fclk_pay_period_type', 'fclk_pay_period_start') def _compute_pay_period_preview(self): HrAtt = self.env['hr.attendance'] today = fields.Date.context_today(self) for rec in self: schedule = rec.fclk_pay_period_type or 'biweekly' anchor = rec.fclk_pay_period_start if not anchor and schedule in ('weekly', 'biweekly'): rec.fclk_pay_period_preview = 'Set anchor date to see preview' continue period_start, period_end = HrAtt._calc_period(schedule, anchor, today) rec.fclk_pay_period_preview = ( f"{period_start.strftime('%b %d, %Y')} - " f"{period_end.strftime('%b %d, %Y')}" ) def action_backfill_reports(self): """Generate reports for all historical pay periods without sending email.""" count = self.env['fusion.clock.report'].sudo()._backfill_historical_reports() return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'Historical Reports', 'message': f'Created {count} reports for past pay periods.', 'type': 'success', 'sticky': False, }, } def action_backfill_breaks(self): """Apply break deduction to all past attendance records missing it, then regenerate any existing report PDFs so they reflect the new totals.""" count = self.env['hr.attendance'].sudo().action_backfill_breaks() # Regenerate existing report PDFs so stored files match updated totals if count: reports = self.env['fusion.clock.report'].sudo().search([ ('state', 'in', ['generated', 'sent']), ]) for report in reports: try: report._generate_pdf() except Exception: pass return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'Break Backfill', 'message': f'Applied break deduction to {count} attendance records. Report PDFs regenerated.', 'type': 'success', 'sticky': False, }, } @api.model def get_values(self): res = super().get_values() ICP = self.env['ir.config_parameter'].sudo() anchor_str = ICP.get_param('fusion_clock.pay_period_start', '') if anchor_str: try: res['fclk_pay_period_start'] = fields.Date.from_string(anchor_str) except Exception: res['fclk_pay_period_start'] = False return res def set_values(self): super().set_values() ICP = self.env['ir.config_parameter'].sudo() val = self.fclk_pay_period_start ICP.set_param( 'fusion_clock.pay_period_start', fields.Date.to_string(val) if val else '', ) # Recompute all pay periods so existing records match current settings self.env['hr.attendance'].sudo().search([ ('check_in', '!=', False), ])._compute_pay_period()