From d6d6bbe161ce783c8eea698b4243aac75bfe0994 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 31 May 2026 11:51:40 -0400 Subject: [PATCH] =?UTF-8?q?fix(fusion=5Fclock):=20settings=20audit=20?= =?UTF-8?q?=E2=80=94=20remove=202=20dead=20knobs,=20make=20IP-fallback=20+?= =?UTF-8?q?=20all=20Boolean=20toggles=20work?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit of all 41 settings found 3 that were shown but read nowhere, and 17 Boolean toggles that couldn't be turned OFF. - Remove grace_period_minutes (orphaned by the schedule-driven cron rewrite) and weekly_overtime_threshold (never implemented): field + view + seed. - enable_ip_fallback now actually gates _verify_location's IP-whitelist check (default ON to preserve current behaviour). - All 17 fusion_clock Boolean settings now persist explicitly as 'True'/'False' via a _FCLK_BOOL_PARAMS loop in get_values/set_values (config_parameter Booleans can't store False, so OFF never stuck). Add round-trip tests. Bump 3.15.2 -> 3.16.0. Co-Authored-By: Claude Opus 4.8 --- fusion_clock/CLAUDE.md | 7 +- fusion_clock/__manifest__.py | 2 +- fusion_clock/controllers/clock_api.py | 6 +- .../data/ir_config_parameter_data.xml | 12 +--- fusion_clock/models/res_config_settings.py | 68 ++++++++++--------- fusion_clock/tests/__init__.py | 1 + fusion_clock/tests/test_settings.py | 42 ++++++++++++ .../views/res_config_settings_views.xml | 8 --- 8 files changed, 88 insertions(+), 58 deletions(-) create mode 100644 fusion_clock/tests/test_settings.py diff --git a/fusion_clock/CLAUDE.md b/fusion_clock/CLAUDE.md index 912e4c02..1061d4b6 100644 --- a/fusion_clock/CLAUDE.md +++ b/fusion_clock/CLAUDE.md @@ -247,14 +247,12 @@ fusion_clock.default_break_minutes fusion_clock.auto_deduct_break fusion_clock.break_threshold_hours fusion_clock.enable_auto_clockout -fusion_clock.grace_period_minutes fusion_clock.max_shift_hours fusion_clock.enable_penalties fusion_clock.penalty_grace_minutes fusion_clock.penalty_deduction_minutes fusion_clock.enable_overtime fusion_clock.daily_overtime_threshold -fusion_clock.weekly_overtime_threshold fusion_clock.office_user_id fusion_clock.very_late_threshold_minutes fusion_clock.max_monthly_absences @@ -325,8 +323,9 @@ All new JSON endpoints must use `type="jsonrpc"`, not deprecated `type="json"`. - `hr.employee._get_fclk_scheduled_times(date)` returns naive UTC datetimes suitable for Odoo comparisons. - Break deduction is stored as minutes in `hr.attendance.x_fclk_break_minutes`; penalties add to that same field. - `x_fclk_net_hours` is computed from Odoo `worked_hours` minus break minutes. -- Daily overtime currently compares net hours to employee scheduled hours or daily threshold; weekly threshold is configured but not used in `hr.attendance._compute_overtime_hours()`. -- `fusion_clock.enable_ip_fallback` exists in settings, but server-side `_verify_location()` attempts IP whitelist matching whenever a client IP is present. +- Daily overtime compares net hours to the employee's scheduled hours or the daily threshold. (The old `weekly_overtime_threshold` and `grace_period_minutes` settings were removed 2026-05-31 — they were defined/shown but never consumed.) +- `fusion_clock.enable_ip_fallback` is honoured: `_verify_location()` only attempts IP-whitelist matching when the toggle is on (default on). +- **All fusion_clock Boolean settings are persisted explicitly** (`'True'`/`'False'`) via the `_FCLK_BOOL_PARAMS` loop in `res.config.settings.get_values/set_values`, NOT via `config_parameter=`. Reason: a `config_parameter` Boolean can't be turned OFF (Odoo deletes the param row on a falsy value, so `get_param` returns the default and the feature stays on). When adding a new Boolean setting, add it to `_FCLK_BOOL_PARAMS` with its default; don't use `config_parameter=`. - NFC kiosk needs a company-level `x_fclk_nfc_kiosk_location_id`; without it taps return `no_location_configured`. - Kiosk routes are authenticated (`auth='user'`) and manager-gated; wall tablets need a manager-authorised kiosk user. - Portal report download manually streams the PDF binary rather than using `fusion_pdf_preview`. diff --git a/fusion_clock/__manifest__.py b/fusion_clock/__manifest__.py index 480787e2..c51a68e6 100644 --- a/fusion_clock/__manifest__.py +++ b/fusion_clock/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Clock', - 'version': '19.0.3.15.2', + 'version': '19.0.3.16.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 cbf9887e..5a3e2d1e 100644 --- a/fusion_clock/controllers/clock_api.py +++ b/fusion_clock/controllers/clock_api.py @@ -74,9 +74,11 @@ class FusionClockAPI(http.Controller): if dist < nearest_distance: nearest_distance = dist - # IP fallback -- try when GPS is unavailable OR GPS is outside all geofences + # IP fallback -- only when enabled (default on); try when GPS is + # unavailable OR GPS is outside all geofences. ICP = request.env['ir.config_parameter'].sudo() - if client_ip: + ip_fallback_enabled = ICP.get_param('fusion_clock.enable_ip_fallback', 'True') == 'True' + if client_ip and ip_fallback_enabled: for loc in locations: if loc.check_ip_whitelist(client_ip): return loc, 0, None, 'ip' diff --git a/fusion_clock/data/ir_config_parameter_data.xml b/fusion_clock/data/ir_config_parameter_data.xml index 32547754..6bb305ec 100644 --- a/fusion_clock/data/ir_config_parameter_data.xml +++ b/fusion_clock/data/ir_config_parameter_data.xml @@ -25,11 +25,7 @@ 4.0 - - - fusion_clock.grace_period_minutes - 15 - + fusion_clock.enable_auto_clockout True @@ -92,15 +88,11 @@ fusion_clock.daily_overtime_threshold 8.0 - - fusion_clock.weekly_overtime_threshold - 40.0 - fusion_clock.enable_ip_fallback - False + True fusion_clock.enable_photo_verification diff --git a/fusion_clock/models/res_config_settings.py b/fusion_clock/models/res_config_settings.py index 490570f2..8778c682 100644 --- a/fusion_clock/models/res_config_settings.py +++ b/fusion_clock/models/res_config_settings.py @@ -23,7 +23,6 @@ class ResConfigSettings(models.TransientModel): ) 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.", ) @@ -43,15 +42,10 @@ class ResConfigSettings(models.TransientModel): # ── Attendance Rules ─────────────────────────────────────────────── fclk_enable_auto_clockout = fields.Boolean( string='Enable Auto Clock-Out', - config_parameter='fusion_clock.enable_auto_clockout', default=True, - help="Automatically clock out employees who forget. Triggers after shift end time plus grace period, or after max shift hours.", - ) - 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.", + help="Automatically clock out employees who forget — closes an attendance " + "left open past the Max Shift Length safety cap (overtime up to the cap " + "is never cut off).", ) fclk_max_shift_hours = fields.Float( string='Max Shift Length (hours)', @@ -64,7 +58,6 @@ class ResConfigSettings(models.TransientModel): ) fclk_enable_penalties = fields.Boolean( string='Enable Penalty Tracking', - config_parameter='fusion_clock.enable_penalties', default=True, help="Deduct minutes from worked hours when employees clock in late or clock out early.", ) @@ -82,9 +75,8 @@ class ResConfigSettings(models.TransientModel): ) fclk_enable_overtime = fields.Boolean( string='Enable Overtime Tracking', - config_parameter='fusion_clock.enable_overtime', default=True, - help="Calculate and track overtime when net hours exceed the daily or weekly threshold.", + help="Calculate and track overtime when net hours exceed the daily threshold.", ) fclk_daily_overtime_threshold = fields.Float( string='Daily OT Threshold (hours)', @@ -92,12 +84,6 @@ class ResConfigSettings(models.TransientModel): default=8.0, help="Net hours beyond this threshold count as daily overtime.", ) - fclk_weekly_overtime_threshold = fields.Float( - string='Weekly OT Threshold (hours)', - config_parameter='fusion_clock.weekly_overtime_threshold', - default=40.0, - help="Net hours beyond this threshold count as weekly overtime.", - ) # ── Notifications ────────────────────────────────────────────────── fclk_office_user_id = fields.Many2one( @@ -119,7 +105,6 @@ class ResConfigSettings(models.TransientModel): ) fclk_enable_employee_notifications = fields.Boolean( string='Enable Employee Notifications', - config_parameter='fusion_clock.enable_employee_notifications', default=True, help="Send clock-in/out reminders to employees.", ) @@ -137,7 +122,6 @@ class ResConfigSettings(models.TransientModel): ) fclk_send_weekly_summary = fields.Boolean( string='Send Weekly Summary', - config_parameter='fusion_clock.send_weekly_summary', default=True, help="Send weekly attendance summary to each employee on Monday.", ) @@ -145,13 +129,12 @@ class ResConfigSettings(models.TransientModel): # ── Location & Verification ──────────────────────────────────────── fclk_enable_ip_fallback = fields.Boolean( string='Enable IP Fallback', - config_parameter='fusion_clock.enable_ip_fallback', - default=False, - help="Allow IP-based location verification when GPS is unavailable.", + default=True, + help="Allow IP-whitelist location verification when GPS is unavailable " + "or outside all geofences.", ) fclk_enable_photo_verification = fields.Boolean( string='Enable Photo Verification', - config_parameter='fusion_clock.enable_photo_verification', default=False, help="Global toggle for selfie verification on clock-in (per-location control).", ) @@ -163,25 +146,21 @@ class ResConfigSettings(models.TransientModel): # ── Kiosk & Portal ───────────────────────────────────────────────── fclk_enable_kiosk = fields.Boolean( string='Enable Kiosk Mode', - config_parameter='fusion_clock.enable_kiosk', default=False, help="Allow employees to clock in/out from a shared device using their PIN code.", ) fclk_kiosk_pin_required = fields.Boolean( string='Require PIN for Kiosk', - config_parameter='fusion_clock.kiosk_pin_required', default=True, help="Require employees to enter a PIN when using kiosk mode.", ) fclk_enable_correction_requests = fields.Boolean( string='Enable Correction Requests', - config_parameter='fusion_clock.enable_correction_requests', default=True, help="Allow employees to request timesheet corrections from the portal.", ) fclk_enable_sounds = fields.Boolean( string='Enable Clock Sounds', - config_parameter='fusion_clock.enable_sounds', default=True, help="Play audio confirmation sounds when employees clock in or out.", ) @@ -211,13 +190,11 @@ class ResConfigSettings(models.TransientModel): ) fclk_auto_generate_reports = fields.Boolean( string='Auto-Generate Reports', - config_parameter='fusion_clock.auto_generate_reports', default=True, help="Automatically create attendance reports at the end of each pay period.", ) fclk_send_employee_reports = fields.Boolean( string='Send Employee Copies', - config_parameter='fusion_clock.send_employee_reports', default=True, help="Send each employee a copy of their individual attendance report.", ) @@ -243,13 +220,11 @@ class ResConfigSettings(models.TransientModel): # ── NFC Clock Kiosk ──────────────────────────────────────────────── fclk_enable_nfc_kiosk = fields.Boolean( string='Enable NFC Clock Kiosk', - config_parameter='fusion_clock.enable_nfc_kiosk', default=False, help="Enable the tap-to-clock NFC kiosk page at /fusion_clock/kiosk/nfc.", ) fclk_nfc_photo_required = fields.Boolean( string='Require Photo on Tap', - config_parameter='fusion_clock.nfc_photo_required', default=True, help="If enabled, the kiosk rejects taps when the front camera is unavailable. " "Recommended for buddy-punch deterrence.", @@ -262,7 +237,6 @@ class ResConfigSettings(models.TransientModel): ) fclk_nfc_kiosk_debug = fields.Boolean( string='Debug Mode (overlay + mock-tap)', - config_parameter='fusion_clock.nfc_kiosk_debug', default=False, help="Enables two dev/troubleshooting features on the NFC kiosk page: " "(1) a green-text debug overlay at the top of the screen logging every NFC and tap event in real time, " @@ -286,9 +260,35 @@ class ResConfigSettings(models.TransientModel): "Set to 0 to disable the auto-wipe.", ) + # Boolean settings persisted explicitly (NOT via config_parameter): Odoo + # deletes a config param when you write a falsy value, so a config_parameter + # Boolean can never be turned OFF (the row vanishes and get_param returns the + # default). Storing 'True'/'False' strings ourselves makes the toggles work. + _FCLK_BOOL_PARAMS = [ + ('fclk_auto_deduct_break', 'fusion_clock.auto_deduct_break', True), + ('fclk_enable_auto_clockout', 'fusion_clock.enable_auto_clockout', True), + ('fclk_enable_penalties', 'fusion_clock.enable_penalties', True), + ('fclk_enable_overtime', 'fusion_clock.enable_overtime', True), + ('fclk_enable_employee_notifications', 'fusion_clock.enable_employee_notifications', True), + ('fclk_send_weekly_summary', 'fusion_clock.send_weekly_summary', True), + ('fclk_enable_ip_fallback', 'fusion_clock.enable_ip_fallback', True), + ('fclk_enable_photo_verification', 'fusion_clock.enable_photo_verification', False), + ('fclk_enable_kiosk', 'fusion_clock.enable_kiosk', False), + ('fclk_kiosk_pin_required', 'fusion_clock.kiosk_pin_required', True), + ('fclk_enable_correction_requests', 'fusion_clock.enable_correction_requests', True), + ('fclk_enable_sounds', 'fusion_clock.enable_sounds', True), + ('fclk_auto_generate_reports', 'fusion_clock.auto_generate_reports', True), + ('fclk_send_employee_reports', 'fusion_clock.send_employee_reports', True), + ('fclk_enable_nfc_kiosk', 'fusion_clock.enable_nfc_kiosk', False), + ('fclk_nfc_photo_required', 'fusion_clock.nfc_photo_required', True), + ('fclk_nfc_kiosk_debug', 'fusion_clock.nfc_kiosk_debug', False), + ] + def set_values(self): super().set_values() ICP = self.env['ir.config_parameter'].sudo() + for fname, key, _default in self._FCLK_BOOL_PARAMS: + ICP.set_param(key, 'True' if self[fname] else 'False') if self.fclk_office_user_id: ICP.set_param('fusion_clock.office_user_id', str(self.fclk_office_user_id.id)) else: @@ -308,6 +308,8 @@ class ResConfigSettings(models.TransientModel): def get_values(self): res = super().get_values() ICP = self.env['ir.config_parameter'].sudo() + for fname, key, default in self._FCLK_BOOL_PARAMS: + res[fname] = ICP.get_param(key, 'True' if default else 'False') == 'True' office_user_id = int(ICP.get_param('fusion_clock.office_user_id', '0')) if office_user_id: res['fclk_office_user_id'] = office_user_id diff --git a/fusion_clock/tests/__init__.py b/fusion_clock/tests/__init__.py index 2a10a4dd..fc76ef83 100644 --- a/fusion_clock/tests/__init__.py +++ b/fusion_clock/tests/__init__.py @@ -7,3 +7,4 @@ from . import test_photo_retention from . import test_schedule_driven from . import test_dashboard from . import test_pay_period +from . import test_settings diff --git a/fusion_clock/tests/test_settings.py b/fusion_clock/tests/test_settings.py new file mode 100644 index 00000000..0ed877f9 --- /dev/null +++ b/fusion_clock/tests/test_settings.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +from odoo.tests import tagged, TransactionCase + + +@tagged('-at_install', 'post_install', 'fusion_clock') +class TestFusionClockSettings(TransactionCase): + """The fusion_clock Boolean settings are persisted explicitly as + 'True'/'False' so they can actually be turned OFF (a config_parameter + Boolean can't — Odoo deletes the row on a falsy value).""" + + def _save(self, vals): + self.env['res.config.settings'].create(vals).set_values() + + def test_boolean_toggle_off_persists(self): + ICP = self.env['ir.config_parameter'].sudo() + self._save({'fclk_enable_overtime': False}) + self.assertEqual(ICP.get_param('fusion_clock.enable_overtime'), 'False') + # reopening Settings shows it OFF + self.assertFalse(self.env['res.config.settings'].get_values()['fclk_enable_overtime']) + + def test_boolean_toggle_back_on_persists(self): + ICP = self.env['ir.config_parameter'].sudo() + self._save({'fclk_enable_overtime': False}) + self._save({'fclk_enable_overtime': True}) + self.assertEqual(ICP.get_param('fusion_clock.enable_overtime'), 'True') + self.assertTrue(self.env['res.config.settings'].get_values()['fclk_enable_overtime']) + + def test_default_on_boolean_when_param_absent(self): + ICP = self.env['ir.config_parameter'].sudo() + # set_param with a falsy value deletes the row → simulates "never set" + ICP.set_param('fusion_clock.enable_ip_fallback', False) + self.assertTrue(self.env['res.config.settings'].get_values()['fclk_enable_ip_fallback']) + + def test_default_off_boolean_when_param_absent(self): + ICP = self.env['ir.config_parameter'].sudo() + ICP.set_param('fusion_clock.enable_kiosk', False) + self.assertFalse(self.env['res.config.settings'].get_values()['fclk_enable_kiosk']) + + def test_dead_settings_removed(self): + fields = self.env['res.config.settings']._fields + self.assertNotIn('fclk_grace_period_minutes', fields) + self.assertNotIn('fclk_weekly_overtime_threshold', fields) diff --git a/fusion_clock/views/res_config_settings_views.xml b/fusion_clock/views/res_config_settings_views.xml index 3cb52fc5..60591a8e 100644 --- a/fusion_clock/views/res_config_settings_views.xml +++ b/fusion_clock/views/res_config_settings_views.xml @@ -52,10 +52,6 @@
-
-
@@ -83,10 +79,6 @@
-
-