Files
Odoo-Modules/fusion_clock/security/security.xml
gsinghpal 78fa8f07ee fix(fusion_clock): stop stale missed-clock-in nag; add Owner role + attendance exemption
The "explain your missed clock-out" dialog (driven by hr.employee.
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 the whole
company (hundreds of "absent" logs); those stale flags then nagged everyone
forever, even while currently clocked in.

Fixes:
- Clear x_fclk_pending_reason on every successful clock-in (portal, systray,
  PIN kiosk, NFC kiosk). Back on the clock => no nag.
- get_status / dashboard never report pending while checked-in or exempt; the
  systray also guards the dialog client-side.
- Absence detection no longer sets x_fclk_pending_reason (an absence has no
  "departure time" to explain). It still logs 'absent' + notifies the office.
- One-time migration (19.0.4.2.0) clears existing stale flags.

Owner / attendance exemption:
- New "Owner" role (top of the Fusion Clock access dropdown, implies Manager)
  plus a per-employee "Exempt from Attendance" checkbox.
- hr.employee._fclk_is_attendance_exempt(); the absence, auto-clock-out,
  reminder and weekly-summary crons all skip exempt employees, and the dialog
  is suppressed for them.

Tests: tests/test_pending_reason_exempt.py (13 cases). Full fusion_clock suite
green except pre-existing env-sensitive failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:54:00 -04:00

397 lines
20 KiB
XML

<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ================================================================
App category + privileges (Odoo 19) so Fusion Clock roles appear
as selectable application-access dropdowns on the user form,
exactly like the other Fusion apps (no developer mode needed).
Odoo 19 dropped res.groups.category_id; groups link to a
res.groups.privilege, which carries the category_id.
================================================================ -->
<record id="module_category_fusion_clock" model="ir.module.category">
<field name="name">Fusion Clock</field>
<field name="sequence">45</field>
</record>
<!-- Main role hierarchy (User &lt; Team Lead &lt; Manager) -> one dropdown -->
<record id="res_groups_privilege_fusion_clock" model="res.groups.privilege">
<field name="name">Fusion Clock</field>
<field name="sequence">45</field>
<field name="category_id" ref="module_category_fusion_clock"/>
</record>
<!-- Standalone kiosk-operator role -> its own row under the same header -->
<record id="res_groups_privilege_fusion_clock_kiosk" model="res.groups.privilege">
<field name="name">Fusion Clock Kiosk</field>
<field name="sequence">46</field>
<field name="category_id" ref="module_category_fusion_clock"/>
</record>
<!-- Groups -->
<record id="group_fusion_clock_user" model="res.groups">
<field name="name">User</field>
<field name="privilege_id" ref="res_groups_privilege_fusion_clock"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
<field name="comment">Can clock in/out and view own attendance</field>
</record>
<record id="group_fusion_clock_team_lead" model="res.groups">
<field name="name">Team Lead</field>
<field name="privilege_id" ref="res_groups_privilege_fusion_clock"/>
<field name="implied_ids" eval="[(4, ref('group_fusion_clock_user'))]"/>
<field name="comment">Can view direct reports attendance (read-only)</field>
</record>
<record id="group_fusion_clock_manager" model="res.groups">
<field name="name">Manager</field>
<field name="privilege_id" ref="res_groups_privilege_fusion_clock"/>
<field name="implied_ids" eval="[(4, ref('group_fusion_clock_team_lead'))]"/>
<field name="comment">Can manage locations, view all attendance, generate reports</field>
</record>
<!-- Owner: top of the role ladder. Carries ALL Manager permissions but is
exempt from attendance automation (no absence flags, no auto-clock-out
nag, no reminders, no missed-clock-out dialog). For owners/principals
who work but are not "on the clock". Implies Manager, so it renders as
the highest role in the single Fusion Clock access dropdown. -->
<record id="group_fusion_clock_owner" model="res.groups">
<field name="name">Owner</field>
<field name="privilege_id" ref="res_groups_privilege_fusion_clock"/>
<field name="implied_ids" eval="[(4, ref('group_fusion_clock_manager'))]"/>
<field name="comment">Full Clock management; exempt from attendance tracking, reminders and missed-clock alerts.</field>
</record>
<!-- Dedicated kiosk-operator permission: can run the shared clock kiosk
(NFC tap / PIN) WITHOUT full Clock Manager access. Gates the
"Fusion Clock Kiosk" app menu and is accepted by the kiosk controllers.
Implies only base.group_user, so it does NOT reveal the full Fusion
Clock app (which is gated to group_fusion_clock_user). -->
<record id="group_fusion_clock_kiosk_app" model="res.groups">
<field name="name">Kiosk Operator</field>
<field name="privilege_id" ref="res_groups_privilege_fusion_clock_kiosk"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
<field name="comment">Can open and operate the shared clock kiosk (NFC tap / PIN) without full Clock Manager access. Intended for shared wall-tablet accounts.</field>
</record>
<!-- Auto-assign admin to Manager group -->
<function model="res.users" name="write">
<value eval="[ref('base.user_admin')]"/>
<value eval="{'group_ids': [(4, ref('group_fusion_clock_manager'))]}"/>
</function>
<!-- ================================================================
Record Rules - Clock Location
================================================================ -->
<record id="rule_clock_location_user" model="ir.rule">
<field name="name">Clock Location: User sees active company locations</field>
<field name="model_id" ref="model_fusion_clock_location"/>
<field name="domain_force">[('company_id', 'in', company_ids), ('active', '=', True)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="rule_clock_location_manager" model="ir.rule">
<field name="name">Clock Location: Manager full access</field>
<field name="model_id" ref="model_fusion_clock_location"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_manager'))]"/>
</record>
<!-- ================================================================
Record Rules - Clock Penalty
================================================================ -->
<record id="rule_clock_penalty_user" model="ir.rule">
<field name="name">Clock Penalty: User sees own penalties</field>
<field name="model_id" ref="model_fusion_clock_penalty"/>
<field name="domain_force">[('employee_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="rule_clock_penalty_team_lead" model="ir.rule">
<field name="name">Clock Penalty: Team Lead sees direct reports</field>
<field name="model_id" ref="model_fusion_clock_penalty"/>
<field name="domain_force">['|', ('employee_id.user_id', '=', user.id), ('employee_id.parent_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_team_lead'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="rule_clock_penalty_manager" model="ir.rule">
<field name="name">Clock Penalty: Manager full access</field>
<field name="model_id" ref="model_fusion_clock_penalty"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_manager'))]"/>
</record>
<!-- ================================================================
Record Rules - Clock Report
================================================================ -->
<record id="rule_clock_report_user" model="ir.rule">
<field name="name">Clock Report: User sees own reports</field>
<field name="model_id" ref="model_fusion_clock_report"/>
<field name="domain_force">['|', ('employee_id.user_id', '=', user.id), ('employee_id', '=', False)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="rule_clock_report_manager" model="ir.rule">
<field name="name">Clock Report: Manager full access</field>
<field name="model_id" ref="model_fusion_clock_report"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_manager'))]"/>
</record>
<!-- ================================================================
Record Rules - Activity Log
================================================================ -->
<record id="rule_activity_log_user" model="ir.rule">
<field name="name">Activity Log: User sees own logs</field>
<field name="model_id" ref="model_fusion_clock_activity_log"/>
<field name="domain_force">[('employee_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="rule_activity_log_team_lead" model="ir.rule">
<field name="name">Activity Log: Team Lead sees direct reports</field>
<field name="model_id" ref="model_fusion_clock_activity_log"/>
<field name="domain_force">['|', ('employee_id.user_id', '=', user.id), ('employee_id.parent_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_team_lead'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="rule_activity_log_manager" model="ir.rule">
<field name="name">Activity Log: Manager full access</field>
<field name="model_id" ref="model_fusion_clock_activity_log"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_manager'))]"/>
</record>
<!-- ================================================================
Record Rules - Leave Request
================================================================ -->
<record id="rule_leave_request_user" model="ir.rule">
<field name="name">Leave Request: User sees own</field>
<field name="model_id" ref="model_fusion_clock_leave_request"/>
<field name="domain_force">[('employee_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="rule_leave_request_manager" model="ir.rule">
<field name="name">Leave Request: Manager full access</field>
<field name="model_id" ref="model_fusion_clock_leave_request"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_manager'))]"/>
</record>
<!-- ================================================================
Record Rules - Shift
================================================================ -->
<record id="rule_shift_user" model="ir.rule">
<field name="name">Shift: User reads active</field>
<field name="model_id" ref="model_fusion_clock_shift"/>
<field name="domain_force">[('active', '=', True)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="rule_shift_manager" model="ir.rule">
<field name="name">Shift: Manager full access</field>
<field name="model_id" ref="model_fusion_clock_shift"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_manager'))]"/>
</record>
<!-- ================================================================
Record Rules - Dated Schedules
================================================================ -->
<record id="rule_schedule_user" model="ir.rule">
<field name="name">Schedule: User sees own</field>
<field name="model_id" ref="model_fusion_clock_schedule"/>
<field name="domain_force">[('employee_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="rule_schedule_team_lead" model="ir.rule">
<field name="name">Schedule: Team Lead sees direct reports</field>
<field name="model_id" ref="model_fusion_clock_schedule"/>
<field name="domain_force">['|', ('employee_id.user_id', '=', user.id), ('employee_id.parent_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_team_lead'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="rule_schedule_manager" model="ir.rule">
<field name="name">Schedule: Manager full access</field>
<field name="model_id" ref="model_fusion_clock_schedule"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_manager'))]"/>
</record>
<record id="rule_schedule_audit_manager" model="ir.rule">
<field name="name">Schedule Audit: Manager reads all</field>
<field name="model_id" ref="model_fusion_clock_schedule_audit"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_manager'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<!-- ================================================================
Record Rules - Correction Request
================================================================ -->
<record id="rule_correction_user" model="ir.rule">
<field name="name">Correction: User sees own</field>
<field name="model_id" ref="model_fusion_clock_correction"/>
<field name="domain_force">[('employee_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="rule_correction_team_lead" model="ir.rule">
<field name="name">Correction: Team Lead sees direct reports</field>
<field name="model_id" ref="model_fusion_clock_correction"/>
<field name="domain_force">['|', ('employee_id.user_id', '=', user.id), ('employee_id.parent_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_team_lead'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="rule_correction_manager" model="ir.rule">
<field name="name">Correction: Manager full access</field>
<field name="model_id" ref="model_fusion_clock_correction"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_fusion_clock_manager'))]"/>
</record>
<!-- ================================================================
Portal Access
================================================================ -->
<record id="rule_hr_attendance_portal" model="ir.rule">
<field name="name">HR Attendance: Portal user sees own</field>
<field name="model_id" ref="hr_attendance.model_hr_attendance"/>
<field name="domain_force">[('employee_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="rule_clock_location_portal" model="ir.rule">
<field name="name">Clock Location: Portal user sees active</field>
<field name="model_id" ref="model_fusion_clock_location"/>
<field name="domain_force">[('active', '=', True)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="rule_clock_report_portal" model="ir.rule">
<field name="name">Clock Report: Portal user sees own</field>
<field name="model_id" ref="model_fusion_clock_report"/>
<field name="domain_force">[('employee_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="rule_clock_penalty_portal" model="ir.rule">
<field name="name">Clock Penalty: Portal user sees own</field>
<field name="model_id" ref="model_fusion_clock_penalty"/>
<field name="domain_force">[('employee_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="rule_activity_log_portal" model="ir.rule">
<field name="name">Activity Log: Portal user sees own</field>
<field name="model_id" ref="model_fusion_clock_activity_log"/>
<field name="domain_force">[('employee_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="rule_leave_request_portal" model="ir.rule">
<field name="name">Leave Request: Portal user sees own</field>
<field name="model_id" ref="model_fusion_clock_leave_request"/>
<field name="domain_force">[('employee_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="rule_correction_portal" model="ir.rule">
<field name="name">Correction: Portal user sees own</field>
<field name="model_id" ref="model_fusion_clock_correction"/>
<field name="domain_force">[('employee_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<record id="rule_schedule_portal" model="ir.rule">
<field name="name">Schedule: Portal user sees own</field>
<field name="model_id" ref="model_fusion_clock_schedule"/>
<field name="domain_force">[('employee_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
</odoo>