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. + +