diff --git a/fusion_clock/__manifest__.py b/fusion_clock/__manifest__.py
index c9fb7e0f..5e027958 100644
--- a/fusion_clock/__manifest__.py
+++ b/fusion_clock/__manifest__.py
@@ -5,7 +5,7 @@
{
'name': 'Fusion Clock',
- 'version': '19.0.4.1.0',
+ 'version': '19.0.4.2.0',
'category': 'Human Resources/Attendances',
'summary': 'Complete Employee T&A with Geofencing, Shifts, Penalties, Overtime, Kiosk, Dashboard & Payroll Export',
'description': """
diff --git a/fusion_clock/controllers/clock_api.py b/fusion_clock/controllers/clock_api.py
index 59464870..d50105db 100644
--- a/fusion_clock/controllers/clock_api.py
+++ b/fusion_clock/controllers/clock_api.py
@@ -287,6 +287,11 @@ class FusionClockAPI(http.Controller):
attendance.sudo().write(write_vals)
+ # A successful clock-in resolves any pending missed-clock-out flag,
+ # so the employee is never nagged once they are back on the clock.
+ if employee.x_fclk_pending_reason:
+ employee.sudo().write({'x_fclk_pending_reason': False})
+
# Log clock-in
self._log_activity(
employee, 'clock_in',
@@ -542,7 +547,10 @@ class FusionClockAPI(http.Controller):
'is_checked_in': is_checked_in,
'employee_name': employee.name,
'enable_clock': employee.x_fclk_enable_clock,
- 'pending_reason': employee.x_fclk_pending_reason,
+ # Only nag when there is genuinely something to explain: a flag set,
+ # the employee NOT currently on the clock, and not attendance-exempt.
+ 'pending_reason': (employee.x_fclk_pending_reason and not is_checked_in
+ and not employee._fclk_is_attendance_exempt()),
'ontime_streak': employee.x_fclk_ontime_streak,
}
local_today = get_local_today(request.env, employee)
@@ -728,7 +736,8 @@ class FusionClockAPI(http.Controller):
'is_checked_in': is_checked_in,
'check_in': check_in,
'location_name': location_name,
- 'pending_reason': employee.x_fclk_pending_reason,
+ 'pending_reason': (employee.x_fclk_pending_reason and not is_checked_in
+ and not employee._fclk_is_attendance_exempt()),
'today_hours': today_hours,
'week_hours': week_hours,
'overtime_week': round(employee.x_fclk_overtime_this_week or 0, 2),
diff --git a/fusion_clock/controllers/clock_kiosk.py b/fusion_clock/controllers/clock_kiosk.py
index 3ed5db98..f34eba2e 100644
--- a/fusion_clock/controllers/clock_kiosk.py
+++ b/fusion_clock/controllers/clock_kiosk.py
@@ -137,6 +137,9 @@ class FusionClockKiosk(http.Controller):
'x_fclk_clock_source': 'kiosk',
'x_fclk_check_in_photo': photo_bytes if photo_bytes else False,
})
+ # Back on the clock -> clear any stale missed-clock-out flag.
+ if employee.x_fclk_pending_reason:
+ employee.sudo().write({'x_fclk_pending_reason': False})
api._log_activity(employee, 'clock_in', f"Kiosk clock-in at {location.name}",
attendance=attendance, location=location,
latitude=0, longitude=0, distance=0, source='kiosk')
diff --git a/fusion_clock/controllers/clock_nfc_kiosk.py b/fusion_clock/controllers/clock_nfc_kiosk.py
index c2c05dce..58381d0a 100644
--- a/fusion_clock/controllers/clock_nfc_kiosk.py
+++ b/fusion_clock/controllers/clock_nfc_kiosk.py
@@ -345,6 +345,9 @@ class FusionClockNfcKiosk(http.Controller):
'x_fclk_clock_source': 'nfc_kiosk',
'x_fclk_check_in_photo': photo_bytes if photo_bytes else False,
})
+ # Back on the clock -> clear any stale missed-clock-out flag.
+ if employee.x_fclk_pending_reason:
+ employee.sudo().write({'x_fclk_pending_reason': False})
api._log_activity(
employee, 'clock_in',
f"NFC kiosk clock-in at {location.name}",
diff --git a/fusion_clock/migrations/19.0.4.2.0/post-migrate.py b/fusion_clock/migrations/19.0.4.2.0/post-migrate.py
new file mode 100644
index 00000000..970cc695
--- /dev/null
+++ b/fusion_clock/migrations/19.0.4.2.0/post-migrate.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Copyright 2026 Nexa Systems Inc.
+# License OPL-1 (Odoo Proprietary License v1.0)
+"""One-time reset of stale missed-clock-out flags on upgrade to 19.0.4.1.0.
+
+Background: x_fclk_pending_reason was set by the absence + auto-clock-out crons
+but only cleared by the systray reason dialog -- never by the kiosk / NFC clock
+paths that staff actually use. During the kiosk rollout the absence cron flagged
+essentially the whole company (hundreds of "absent" logs), and those flags then
+nagged everyone forever, even while currently clocked in.
+
+This release clears the flag on every clock-in (all paths), stops absences from
+setting it at all, and exempts owners. The flags already on record are stale
+artifacts of the rollout, so wipe them once here; correct ones re-appear only
+for a genuine forgotten clock-out from now on.
+"""
+
+
+def migrate(cr, version):
+ if not version:
+ return
+ cr.execute(
+ "UPDATE hr_employee SET x_fclk_pending_reason = false "
+ "WHERE x_fclk_pending_reason = true"
+ )
diff --git a/fusion_clock/models/hr_attendance.py b/fusion_clock/models/hr_attendance.py
index 5de4ad64..bb6204cb 100644
--- a/fusion_clock/models/hr_attendance.py
+++ b/fusion_clock/models/hr_attendance.py
@@ -345,6 +345,9 @@ class HrAttendance(models.Model):
continue
employee = att.employee_id
+ # Owners / attendance-exempt employees are never auto-clocked-out or nagged.
+ if employee._fclk_is_attendance_exempt():
+ continue
clock_out_time = effective_deadline
try:
with self.env.cr.savepoint():
@@ -456,6 +459,9 @@ class HrAttendance(models.Model):
for emp in employees:
try:
with self.env.cr.savepoint():
+ # Owners / attendance-exempt employees are never flagged absent.
+ if emp._fclk_is_attendance_exempt():
+ continue
yesterday = get_local_today(self.env, emp) - timedelta(days=1)
# Only days the employee was actually scheduled to work
@@ -498,7 +504,11 @@ class HrAttendance(models.Model):
'source': 'system',
})
- emp.sudo().write({'x_fclk_pending_reason': True})
+ # NOTE: an absence does NOT set x_fclk_pending_reason. That flag
+ # drives the "explain your missed clock-OUT (departure time)"
+ # dialog, which is meaningless for a day with no attendance and
+ # caused a persistent false nag. The absence is logged + the
+ # office is notified on excess; that is the absence remedy.
month_start = yesterday.replace(day=1)
month_boundary_start, _ = get_local_day_boundaries(self.env, month_start, emp)
@@ -546,6 +556,9 @@ class HrAttendance(models.Model):
for emp in employees:
try:
with self.env.cr.savepoint():
+ # Owners / attendance-exempt employees are never reminded.
+ if emp._fclk_is_attendance_exempt():
+ continue
today = get_local_today(self.env, emp)
if not emp._get_fclk_day_plan(today).get('scheduled'):
continue
@@ -610,6 +623,9 @@ class HrAttendance(models.Model):
company_name = company.name or ''
for emp in employees:
+ # Owners / attendance-exempt employees get no weekly summary.
+ if emp._fclk_is_attendance_exempt():
+ continue
if not emp.work_email:
continue
diff --git a/fusion_clock/models/hr_employee.py b/fusion_clock/models/hr_employee.py
index d459f2bc..1b840320 100644
--- a/fusion_clock/models/hr_employee.py
+++ b/fusion_clock/models/hr_employee.py
@@ -40,6 +40,18 @@ class HrEmployee(models.Model):
help="If set, employee must explain a missed clock-out before clocking in again.",
)
+ # Attendance exemption (owners / anyone who works but is not "on the clock").
+ # Exempt employees are skipped by absence detection, auto-clock-out and
+ # reminders, and never see the missed-clock-out reason dialog.
+ x_fclk_exempt_from_attendance = fields.Boolean(
+ string='Exempt from Attendance Tracking',
+ default=False,
+ help="If set, this employee is never flagged absent, auto-clocked-out, "
+ "reminded, or asked to explain a missed clock-out. Use for owners "
+ "and others who work but are not on the clock. The Fusion Clock "
+ "'Owner' role grants this automatically.",
+ )
+
# Kiosk PIN
x_fclk_kiosk_pin = fields.Char(
string='Kiosk PIN',
@@ -122,6 +134,19 @@ class HrEmployee(models.Model):
help="Tracks the last date a reminder was sent to avoid duplicates.",
)
+ def _fclk_is_attendance_exempt(self):
+ """True when this employee is exempt from attendance automation.
+
+ Exempt = the per-employee checkbox is set, OR the linked user holds the
+ Fusion Clock 'Owner' role. Exempt employees are never flagged absent,
+ auto-clocked-out, reminded, or shown the missed-clock-out reason dialog.
+ """
+ self.ensure_one()
+ if self.x_fclk_exempt_from_attendance:
+ return True
+ user = self.user_id
+ return bool(user) and user.has_group('fusion_clock.group_fusion_clock_owner')
+
def _get_fclk_schedule_for_date(self, date):
"""Return this employee's dated Fusion Clock schedule for a local date."""
self.ensure_one()
diff --git a/fusion_clock/security/security.xml b/fusion_clock/security/security.xml
index 4dfc9647..a3101a57 100644
--- a/fusion_clock/security/security.xml
+++ b/fusion_clock/security/security.xml
@@ -49,6 +49,18 @@
Can manage locations, view all attendance, generate reports
+
+
+ Owner
+
+
+ Full Clock management; exempt from attendance tracking, reminders and missed-clock alerts.
+
+