216 lines
7.9 KiB
Python
216 lines
7.9 KiB
Python
# -*- 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()
|