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 @@
-
-
-
-