feat(fusion_clock): province-aware automatic unpaid break (2-tier)

Statutory unpaid break now deducts automatically from worked hours on every path - portal, kiosk, NFC, auto-clock-out cron, AND manual backend entry.

- new fusion.clock.break.rule per-province table (seed Ontario 5h->30, 10h->+30), resolved from the employee's company province with a global default fallback
- x_fclk_break_minutes is now a single idempotent stored compute (statutory(worked_hours) + penalties), replacing the 4 duplicated write sites (_apply_break_deduction x3 callsites + auto-clock-out cron + penalty write)
- retire break_threshold_hours (superseded by per-rule break1_after_hours); post-migrate drops the param and recomputes historical breaks
- 11 tests all green; module install + 19.0.4.1.0 migration verified on modsdev

Bump 19.0.4.0.3 -> 19.0.4.1.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-01 00:15:42 -04:00
parent 96b3f124f8
commit f7ec1e28f9
20 changed files with 383 additions and 68 deletions

View File

@@ -5,7 +5,6 @@
import base64
import math
import logging
import pytz
from datetime import datetime, timedelta
from odoo import http, fields, _
from odoo.http import request
@@ -137,12 +136,6 @@ class FusionClockAPI(http.Controller):
'date': actual_dt.date() if isinstance(actual_dt, datetime) else get_local_today(request.env, employee),
})
# Deduct penalty minutes from attendance (adds to break deduction)
current_break = attendance.x_fclk_break_minutes or 0.0
attendance.sudo().write({
'x_fclk_break_minutes': current_break + deduction,
})
# Log penalty
log_type = 'late_clock_in' if penalty_type == 'late_in' else 'early_clock_out'
request.env['fusion.clock.activity.log'].sudo().create({
@@ -158,32 +151,6 @@ class FusionClockAPI(http.Controller):
if penalty_type == 'late_in':
employee.sudo().write({'x_fclk_ontime_streak': 0})
def _apply_break_deduction(self, attendance, employee):
"""Apply automatic break deduction if configured."""
ICP = request.env['ir.config_parameter'].sudo()
if ICP.get_param('fusion_clock.auto_deduct_break', 'True') != 'True':
return
threshold = float(ICP.get_param('fusion_clock.break_threshold_hours', '4.0'))
worked = attendance.worked_hours or 0.0
if worked >= threshold:
local_date = get_local_today(request.env, employee)
if attendance.check_in:
tz_name = (
employee.resource_id.tz
or (employee.user_id.partner_id.tz if employee.user_id else False)
or employee.company_id.partner_id.tz
or 'UTC'
)
local_date = pytz.UTC.localize(attendance.check_in).astimezone(pytz.timezone(tz_name)).date()
break_min = employee._get_fclk_break_minutes(local_date)
current = attendance.x_fclk_break_minutes or 0.0
# Set to whichever is higher: configured break or existing (penalty-inflated) value
new_val = max(break_min, current)
if new_val != current:
attendance.sudo().write({'x_fclk_break_minutes': new_val})
def _log_activity(self, employee, log_type, description, attendance=None,
location=None, latitude=0, longitude=0, distance=0, source='portal'):
"""Create an activity log entry."""
@@ -405,9 +372,6 @@ class FusionClockAPI(http.Controller):
'x_fclk_out_distance': round(distance, 1),
})
# Apply break deduction
self._apply_break_deduction(attendance, employee)
# Check for early clock-out penalty
if not is_scheduled_off:
_, scheduled_out = self._get_scheduled_times(employee, today)