feat(shopfloor): force-lock cron for stale tablet sessions
Every 5 minutes, find active unlock events past 8-hour ceiling and mark them force-locked. SQL bypass of the model's read-only ACL is the only path that can update existing rows (no Python write() works because the model override blocks even sudo writes without the explicit fp_tablet_audit_admin_write context flag). Ceiling configurable via ir.config_parameter[fp.tablet.session_ceiling_hours]. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Placeholder for cron records added in Phase C task C4
|
||||
(force-lock stale tablet sessions). File registered in manifest
|
||||
to keep the data list stable across phase boundaries. -->
|
||||
<data noupdate="1">
|
||||
<record id="ir_cron_force_lock_stale_sessions" model="ir.cron">
|
||||
<field name="name">Tablet: Force-lock Stale PIN Sessions</field>
|
||||
<field name="model_id" ref="model_fp_tablet_session_event"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_force_lock_stale_sessions()</field>
|
||||
<field name="interval_number">5</field>
|
||||
<field name="interval_type">minutes</field>
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -113,3 +113,45 @@ class FpTabletSessionEvent(models.Model):
|
||||
"admin-purge context flag."
|
||||
))
|
||||
return super().unlink()
|
||||
|
||||
@api.model
|
||||
def _cron_force_lock_stale_sessions(self):
|
||||
"""Belt-and-suspenders cron: find any active unlock event past
|
||||
the 8-hour ceiling and mark it force-locked.
|
||||
|
||||
Handles browser crashes, tablet reboots with stale cookies,
|
||||
and any path that bypasses /fp/tablet/lock_session.
|
||||
|
||||
Runs every 5 minutes per fp_tablet_cron.xml.
|
||||
"""
|
||||
from datetime import timedelta
|
||||
ceiling_hours = int(self.env['ir.config_parameter'].sudo().get_param(
|
||||
'fp.tablet.session_ceiling_hours', 8))
|
||||
cutoff = fields.Datetime.now() - timedelta(hours=ceiling_hours)
|
||||
stale = self.search([
|
||||
('event_type', '=', 'unlock'),
|
||||
('session_ended_at', '=', False),
|
||||
('session_started_at', '<', cutoff),
|
||||
])
|
||||
now = fields.Datetime.now()
|
||||
for event in stale:
|
||||
duration = int((now - event.session_started_at).total_seconds())
|
||||
self.sudo().create({
|
||||
'event_type': 'force_lock',
|
||||
'user_id': event.user_id.id,
|
||||
'session_id_hash': event.session_id_hash,
|
||||
'session_started_at': event.session_started_at,
|
||||
'session_ended_at': now,
|
||||
'duration_seconds': duration,
|
||||
'notes': 'Cron force-lock: session exceeded %d-hour ceiling' % ceiling_hours,
|
||||
})
|
||||
# Mark the original unlock event closed so it's not reprocessed
|
||||
# next tick. write() is blocked by the model override — use
|
||||
# direct SQL bypass (this is the documented escape hatch for
|
||||
# the retention/cron path).
|
||||
self.env.cr.execute(
|
||||
"""UPDATE fp_tablet_session_event
|
||||
SET session_ended_at = %s, duration_seconds = %s
|
||||
WHERE id = %s""",
|
||||
(now, duration, event.id),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user