feat(fusion_clock): kiosk app + Kiosk Operator role, full-screen PWA, app-integrated permissions
- PWA manifest on the NFC kiosk page so it installs as a full-screen home-screen app (Chrome "Install" / Safari "Add to Home Screen"). - Dedicated "Kiosk Operator" permission + gated "Fusion Clock Kiosk" top-level app (act_url -> /fusion_clock/kiosk/nfc). Kiosk controllers accept Manager OR Kiosk Operator; all kiosk data ops already run sudo. - Fix 403: read the company kiosk location via sudo on page-load and tap (Kiosk Operator has no fusion.clock.location ACL). - Odoo 19 permissions UX: ir.module.category + res.groups.privilege so User/Team Lead/Manager and Kiosk Operator appear as application-access dropdowns on the user form (no developer mode). Short group display names. - Docs: note res.groups.privilege as the Odoo 19 category_id replacement. Deployed live to entech (odoo-entech / LXC 111 on pve-worker5). v19.0.3.6.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
@@ -44,6 +45,12 @@ def _strip_data_url_prefix(b64):
|
||||
return b64.encode('ascii', errors='ignore') if isinstance(b64, str) else b64
|
||||
|
||||
|
||||
def _is_kiosk_operator(user):
|
||||
"""Kiosk surfaces accept a full Clock Manager OR a dedicated Kiosk Operator."""
|
||||
return (user.has_group('fusion_clock.group_fusion_clock_manager')
|
||||
or user.has_group('fusion_clock.group_fusion_clock_kiosk_app'))
|
||||
|
||||
|
||||
class FusionClockNfcKiosk(http.Controller):
|
||||
"""NFC tap-to-clock kiosk controller. Reuses FusionClockAPI helpers."""
|
||||
|
||||
@@ -66,14 +73,14 @@ class FusionClockNfcKiosk(http.Controller):
|
||||
def nfc_kiosk_page(self, **kw):
|
||||
"""Render the NFC kiosk page for a wall-mounted tablet."""
|
||||
user = request.env.user
|
||||
if not user.has_group('fusion_clock.group_fusion_clock_manager'):
|
||||
if not _is_kiosk_operator(user):
|
||||
return request.redirect('/my')
|
||||
|
||||
ICP = request.env['ir.config_parameter'].sudo()
|
||||
if ICP.get_param('fusion_clock.enable_nfc_kiosk', 'False') != 'True':
|
||||
return request.redirect('/my')
|
||||
|
||||
company = request.env.company
|
||||
company = request.env.company.sudo()
|
||||
location = company.x_fclk_nfc_kiosk_location_id
|
||||
company_logo_url = (
|
||||
'/web/image/res.company/%s/logo' % company.id if company.logo else ''
|
||||
@@ -89,6 +96,42 @@ class FusionClockNfcKiosk(http.Controller):
|
||||
}
|
||||
return request.render('fusion_clock.nfc_kiosk_page', values)
|
||||
|
||||
@http.route('/fusion_clock/kiosk/nfc/manifest.webmanifest', type='http', auth='public')
|
||||
def nfc_kiosk_manifest(self, **kw):
|
||||
"""Web App Manifest so the NFC kiosk installs as a full-screen home-screen app.
|
||||
|
||||
On a wall tablet, 'Install' (Chrome) / 'Add to Home Screen' (Safari) then
|
||||
launches the kiosk standalone -- no address bar or browser tabs, like Odoo's
|
||||
own PWA. Public so the icon/splash can load without a session.
|
||||
"""
|
||||
company = request.env.company.sudo()
|
||||
# Square icons via Odoo's on-the-fly resizer (placeholder if the company has no logo).
|
||||
icon_192 = '/web/image/res.company/%s/logo/192x192' % company.id
|
||||
icon_512 = '/web/image/res.company/%s/logo/512x512' % company.id
|
||||
manifest = {
|
||||
'name': 'Fusion Clock Kiosk',
|
||||
'short_name': 'Clock Kiosk',
|
||||
'description': 'Tap-to-clock NFC kiosk',
|
||||
'start_url': '/fusion_clock/kiosk/nfc',
|
||||
'scope': '/',
|
||||
'display': 'fullscreen',
|
||||
'display_override': ['fullscreen', 'standalone'],
|
||||
'background_color': '#0e1116',
|
||||
'theme_color': '#0e1116',
|
||||
'orientation': 'any',
|
||||
'icons': [
|
||||
{'src': icon_192, 'sizes': '192x192', 'type': 'image/png'},
|
||||
{'src': icon_512, 'sizes': '512x512', 'type': 'image/png'},
|
||||
],
|
||||
}
|
||||
return request.make_response(
|
||||
json.dumps(manifest),
|
||||
headers=[
|
||||
('Content-Type', 'application/manifest+json; charset=utf-8'),
|
||||
('Cache-Control', 'public, max-age=3600'),
|
||||
],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _check_enroll_password(env, supplied):
|
||||
"""Verify the enroll-mode password. Empty config = always-allow for managers."""
|
||||
@@ -101,7 +144,7 @@ class FusionClockNfcKiosk(http.Controller):
|
||||
def nfc_enroll(self, employee_id=0, card_uid='', enroll_password='', **kw):
|
||||
"""Bind an NFC card UID to an employee. Manager-gated, password-gated."""
|
||||
user = request.env.user
|
||||
if not user.has_group('fusion_clock.group_fusion_clock_manager'):
|
||||
if not _is_kiosk_operator(user):
|
||||
return {'error': 'access_denied'}
|
||||
|
||||
if not self._check_enroll_password(request.env, enroll_password):
|
||||
@@ -146,7 +189,7 @@ class FusionClockNfcKiosk(http.Controller):
|
||||
def nfc_tap(self, card_uid='', photo_b64='', **kw):
|
||||
"""Toggle attendance state for the employee owning this card UID."""
|
||||
user = request.env.user
|
||||
if not user.has_group('fusion_clock.group_fusion_clock_manager'):
|
||||
if not _is_kiosk_operator(user):
|
||||
return {'error': 'access_denied'}
|
||||
|
||||
ICP = request.env['ir.config_parameter'].sudo()
|
||||
@@ -165,7 +208,7 @@ class FusionClockNfcKiosk(http.Controller):
|
||||
return {'error': 'photo_required', 'message': 'Camera unavailable. Ask IT to check the kiosk.'}
|
||||
photo_bytes = _strip_data_url_prefix(photo_b64) if photo_b64 else b''
|
||||
|
||||
company = request.env.company
|
||||
company = request.env.company.sudo()
|
||||
location = company.x_fclk_nfc_kiosk_location_id
|
||||
if not location:
|
||||
return {'error': 'no_location_configured'}
|
||||
|
||||
Reference in New Issue
Block a user