feat(fusion_clock): auto-wipe clock-in/out photos after a retention window

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>
This commit is contained in:
gsinghpal
2026-05-30 20:19:22 -04:00
parent e2f7fa6d19
commit ab3e6fa1e2
7 changed files with 142 additions and 1 deletions

View File

@@ -341,6 +341,57 @@ class HrAttendance(models.Model):
att.id, str(e),
)
@api.model
def _cron_fusion_wipe_old_photos(self):
"""Cron job: delete clock-in/out verification photos older than the
configured retention window (``fusion_clock.photo_retention_days``).
Only the images are removed — the attendance records, worked hours and
penalties are kept. The photos are attachment-backed binary fields, so we
unlink the underlying ir.attachment rows directly, which reclaims the
filestore space. Set the retention to 0 to disable the wipe entirely."""
ICP = self.env['ir.config_parameter'].sudo()
retention_days = int(ICP.get_param('fusion_clock.photo_retention_days', '60') or 0)
if retention_days <= 0:
return # 0 / unset → auto-wipe disabled
cutoff = fields.Datetime.now() - timedelta(days=retention_days)
old_attendances = self.sudo().search([('check_in', '<', cutoff)])
if not old_attendances:
return
Attachment = self.env['ir.attachment'].sudo()
photo_fields = [
'x_fclk_check_in_photo', # NFC kiosk clock-in selfie
'x_fclk_check_out_photo', # NFC kiosk clock-out selfie
'x_fclk_checkin_photo', # legacy portal clock-in photo
]
wiped = 0
# Batch the attendances so the res_id IN (...) list stays bounded, and
# isolate each batch in a savepoint so one bad row can't abort the rest.
for offset in range(0, len(old_attendances), 500):
batch_ids = old_attendances[offset:offset + 500].ids
photos = Attachment.search([
('res_model', '=', 'hr.attendance'),
('res_field', 'in', photo_fields),
('res_id', 'in', batch_ids),
])
if not photos:
continue
try:
with self.env.cr.savepoint():
count = len(photos)
photos.unlink()
wiped += count
except Exception as e:
_logger.error("Fusion Clock: Failed to wipe a photo batch: %s", e)
if wiped:
_logger.info(
"Fusion Clock: Wiped %s clock verification photo(s) older than %s days.",
wiped, retention_days,
)
@api.model
def _cron_fusion_check_absences(self):
"""Cron job: check for absent employees (no attendance on workday)."""

View File

@@ -268,6 +268,15 @@ class ResConfigSettings(models.TransientModel):
help="Which clock location is bound to the NFC kiosk for this company. "
"Required when the kiosk is enabled.",
)
fclk_photo_retention_days = fields.Integer(
string='Auto-Wipe Photos After (days)',
config_parameter='fusion_clock.photo_retention_days',
default=60,
help="Clock-in/out verification photos older than this many days are deleted "
"automatically by a daily cron. The attendance record, worked hours and "
"penalties are kept — only the images are removed, reclaiming storage. "
"Set to 0 to disable the auto-wipe.",
)
def set_values(self):
super().set_values()