feat(fusion_clock): NFC tap photo capture + photo-required gate

- Add _strip_data_url_prefix() helper to clean data-URL prefix from base64 photo payloads
- Gate nfc_tap on fusion_clock.nfc_photo_required ICP param (default True): rejects with error='photo_required' when photo absent
- Write x_fclk_check_in_photo / x_fclk_check_out_photo on clock-in/out attendance records
- Add TestTapPhotoHandling (3 tests): photo saved, required-rejects-missing, optional-succeeds-without

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-14 01:09:27 -04:00
parent ef885c66dc
commit f4c9ed3d24
2 changed files with 89 additions and 0 deletions

View File

@@ -32,6 +32,17 @@ def _is_debounced(uid):
return False
def _strip_data_url_prefix(b64):
"""Strip 'data:image/...;base64,' prefix from a data URL, returning raw base64."""
if not b64:
return b''
if isinstance(b64, str) and b64.startswith('data:'):
comma = b64.find(',')
if comma >= 0:
return b64[comma + 1:].encode('ascii', errors='ignore')
return b64.encode('ascii', errors='ignore') if isinstance(b64, str) else b64
class FusionClockNfcKiosk(http.Controller):
"""NFC tap-to-clock kiosk controller. Reuses FusionClockAPI helpers."""
@@ -144,6 +155,11 @@ class FusionClockNfcKiosk(http.Controller):
if _is_debounced(normalized):
return {'error': 'debounce'}
photo_required = ICP.get_param('fusion_clock.nfc_photo_required', 'True') == 'True'
if photo_required and not photo_b64:
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
location = company.x_fclk_nfc_kiosk_location_id
if not location:
@@ -179,6 +195,7 @@ class FusionClockNfcKiosk(http.Controller):
'x_fclk_location_id': location.id,
'x_fclk_in_distance': 0.0,
'x_fclk_clock_source': 'nfc_kiosk',
'x_fclk_check_in_photo': photo_bytes if photo_bytes else False,
})
api._log_activity(
employee, 'clock_in',
@@ -200,6 +217,7 @@ class FusionClockNfcKiosk(http.Controller):
else:
attendance.sudo().write({
'x_fclk_out_distance': 0.0,
'x_fclk_check_out_photo': photo_bytes if photo_bytes else False,
})
api._apply_break_deduction(attendance, employee)
_, scheduled_out = api._get_scheduled_times(employee, today)