fix(fusion_clock): settings audit — remove 2 dead knobs, make IP-fallback + all Boolean toggles work
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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`.
|
||||
|
||||
@@ -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': """
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -25,11 +25,7 @@
|
||||
<field name="value">4.0</field>
|
||||
</record>
|
||||
|
||||
<!-- Grace Period & Auto Clock-Out -->
|
||||
<record id="config_grace_period_minutes" model="ir.config_parameter">
|
||||
<field name="key">fusion_clock.grace_period_minutes</field>
|
||||
<field name="value">15</field>
|
||||
</record>
|
||||
<!-- Auto Clock-Out -->
|
||||
<record id="config_enable_auto_clockout" model="ir.config_parameter">
|
||||
<field name="key">fusion_clock.enable_auto_clockout</field>
|
||||
<field name="value">True</field>
|
||||
@@ -92,15 +88,11 @@
|
||||
<field name="key">fusion_clock.daily_overtime_threshold</field>
|
||||
<field name="value">8.0</field>
|
||||
</record>
|
||||
<record id="config_weekly_overtime_threshold" model="ir.config_parameter">
|
||||
<field name="key">fusion_clock.weekly_overtime_threshold</field>
|
||||
<field name="value">40.0</field>
|
||||
</record>
|
||||
|
||||
<!-- Location & Verification -->
|
||||
<record id="config_enable_ip_fallback" model="ir.config_parameter">
|
||||
<field name="key">fusion_clock.enable_ip_fallback</field>
|
||||
<field name="value">False</field>
|
||||
<field name="value">True</field>
|
||||
</record>
|
||||
<record id="config_enable_photo_verification" model="ir.config_parameter">
|
||||
<field name="key">fusion_clock.enable_photo_verification</field>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
42
fusion_clock/tests/test_settings.py
Normal file
42
fusion_clock/tests/test_settings.py
Normal file
@@ -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)
|
||||
@@ -52,10 +52,6 @@
|
||||
<field name="fclk_enable_auto_clockout"/>
|
||||
<div class="content-group" invisible="not fclk_enable_auto_clockout">
|
||||
<div class="row mt16">
|
||||
<label for="fclk_grace_period_minutes" string="Grace (min)" class="col-lg-5 o_light_label"/>
|
||||
<field name="fclk_grace_period_minutes"/>
|
||||
</div>
|
||||
<div class="row mt8">
|
||||
<label for="fclk_max_shift_hours" string="Max Shift" class="col-lg-5 o_light_label"/>
|
||||
<field name="fclk_max_shift_hours" widget="float_time"/>
|
||||
</div>
|
||||
@@ -83,10 +79,6 @@
|
||||
<label for="fclk_daily_overtime_threshold" string="Daily Limit" class="col-lg-5 o_light_label"/>
|
||||
<field name="fclk_daily_overtime_threshold" widget="float_time"/>
|
||||
</div>
|
||||
<div class="row mt8">
|
||||
<label for="fclk_weekly_overtime_threshold" string="Weekly Limit" class="col-lg-5 o_light_label"/>
|
||||
<field name="fclk_weekly_overtime_threshold" widget="float_time"/>
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
</block>
|
||||
|
||||
Reference in New Issue
Block a user