Privacy/space housekeeping for the kiosk verification selfies. A new daily cron
(_cron_fusion_wipe_old_photos) deletes the photo attachments on attendances
whose clock-in is older than fusion_clock.photo_retention_days (default 60).
Only the images are removed — attendance records, worked hours and penalties
are kept. Clearing the attachment-backed binary reclaims filestore space.
- Configurable in Settings → Fusion Clock → NFC Kiosk ("Auto-Wipe Photos After
(days)"); set 0 to disable.
- Wipes all three photo fields (NFC check-in/out + legacy portal photo),
batched with per-batch savepoints.
- tests/test_photo_retention.py covers wipe-old / keep-recent / retention=0.
Verified live on entech (19.0.3.11.7) via a rollback-only dry run: a 70-day
shift's photos were wiped (record + 8h hours preserved) while a 5-day shift's
photo was kept; nothing persisted. 0 attendances currently exceed 60 days.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
65 lines
2.6 KiB
Python
65 lines
2.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
|
|
|
import base64
|
|
from datetime import timedelta
|
|
|
|
from odoo import fields
|
|
from odoo.tests import tagged
|
|
from odoo.tests.common import TransactionCase
|
|
|
|
|
|
@tagged('-at_install', 'post_install', 'fusion_clock')
|
|
class TestPhotoRetention(TransactionCase):
|
|
"""The daily wipe cron must delete verification photos older than the
|
|
retention window while keeping the attendance records (and recent photos)."""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.Attendance = self.env['hr.attendance']
|
|
self.ICP = self.env['ir.config_parameter'].sudo()
|
|
self.emp = self.env['hr.employee'].create({'name': 'Retention Test Emp'})
|
|
# x_fclk_check_*_photo are plain Binary fields (no image validation),
|
|
# so arbitrary base64 bytes are fine for the test.
|
|
self.photo = base64.b64encode(b'pretend-jpeg-bytes')
|
|
self.ICP.set_param('fusion_clock.photo_retention_days', '60')
|
|
|
|
def _make_attendance(self, days_ago):
|
|
start = fields.Datetime.now() - timedelta(days=days_ago)
|
|
att = self.Attendance.create({
|
|
'employee_id': self.emp.id,
|
|
'check_in': start,
|
|
'check_out': start + timedelta(hours=8),
|
|
})
|
|
att.x_fclk_check_in_photo = self.photo
|
|
att.x_fclk_check_out_photo = self.photo
|
|
return att
|
|
|
|
def test_old_photos_wiped_recent_kept(self):
|
|
old = self._make_attendance(days_ago=70)
|
|
recent = self._make_attendance(days_ago=5)
|
|
self.assertTrue(old.x_fclk_check_in_photo)
|
|
self.assertTrue(recent.x_fclk_check_in_photo)
|
|
|
|
self.Attendance._cron_fusion_wipe_old_photos()
|
|
self.env.invalidate_all() # binary fields are cached; re-read from DB
|
|
|
|
# Old shift: both photos gone, but the record + hours survive.
|
|
self.assertFalse(old.x_fclk_check_in_photo, "Old check-in photo should be wiped")
|
|
self.assertFalse(old.x_fclk_check_out_photo, "Old check-out photo should be wiped")
|
|
self.assertTrue(old.exists(), "Attendance record itself must be kept")
|
|
self.assertGreater(old.worked_hours, 0.0, "Worked hours must be preserved")
|
|
|
|
# Recent shift: photo untouched.
|
|
self.assertTrue(recent.x_fclk_check_in_photo, "Recent photo should be kept")
|
|
|
|
def test_zero_retention_disables_wipe(self):
|
|
self.ICP.set_param('fusion_clock.photo_retention_days', '0')
|
|
old = self._make_attendance(days_ago=200)
|
|
|
|
self.Attendance._cron_fusion_wipe_old_photos()
|
|
self.env.invalidate_all()
|
|
|
|
self.assertTrue(old.x_fclk_check_in_photo, "Retention=0 must disable the wipe")
|