From 8ef57a4bb1d7ed306df92d39bb33918c8a7e21b4 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Wed, 20 May 2026 22:29:48 -0400 Subject: [PATCH] fix(task_sync): defend against silent sync_id integrity violations The cross-instance sync silently drops tasks when x_fc_tech_sync_id is missing on the technician, and silently collapses duplicates via dict comprehension. Both make sync break in ways that are invisible until someone notices a missing task on the other instance. - _get_remote_tech_map / _get_local_syncid_to_uid: warn on duplicates - _push_tasks_to_remote: info-log when a task is skipped because the tech has no sync_id or no remote counterpart - res.users onchange: warn in the form when entering a sync_id that is already used by another active field staff Co-Authored-By: Claude Opus 4.7 (1M context) --- fusion_tasks/__manifest__.py | 2 +- fusion_tasks/models/res_users.py | 25 ++++++++++++++++++++++- fusion_tasks/models/task_sync.py | 34 ++++++++++++++++++++++++++------ 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/fusion_tasks/__manifest__.py b/fusion_tasks/__manifest__.py index 717e31ce..3521493c 100644 --- a/fusion_tasks/__manifest__.py +++ b/fusion_tasks/__manifest__.py @@ -4,7 +4,7 @@ { 'name': 'Fusion Tasks', - 'version': '19.0.1.0.0', + 'version': '19.0.1.1.0', 'category': 'Services/Field Service', 'summary': 'Technician scheduling, route planning, GPS tracking, and cross-instance sync.', 'author': 'Nexa Systems Inc.', diff --git a/fusion_tasks/models/res_users.py b/fusion_tasks/models/res_users.py index 7d82b551..d622dac7 100644 --- a/fusion_tasks/models/res_users.py +++ b/fusion_tasks/models/res_users.py @@ -2,7 +2,7 @@ # Copyright 2024-2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) -from odoo import models, fields +from odoo import models, fields, api class ResUsers(models.Model): @@ -24,3 +24,26 @@ class ResUsers(models.Model): 'Must be the same value on all instances for the same person.', copy=False, ) + + @api.onchange('x_fc_tech_sync_id') + def _onchange_x_fc_tech_sync_id_dup_warning(self): + if not self.x_fc_tech_sync_id: + return + dup = self.env['res.users'].sudo().search([ + ('id', '!=', self._origin.id or self.id), + ('x_fc_tech_sync_id', '=', self.x_fc_tech_sync_id), + ('x_fc_is_field_staff', '=', True), + ('active', '=', True), + ], limit=1) + if dup: + return { + 'warning': { + 'title': "Duplicate Tech Sync ID", + 'message': ( + f"Tech Sync ID {self.x_fc_tech_sync_id!r} is already used " + f"by {dup.login} ({dup.partner_id.name}). Cross-instance " + f"task sync only routes to ONE user per sync ID — " + f"pick a unique value or only one tech's tasks will sync." + ), + } + } diff --git a/fusion_tasks/models/task_sync.py b/fusion_tasks/models/task_sync.py index 99ee3684..6a02f093 100644 --- a/fusion_tasks/models/task_sync.py +++ b/fusion_tasks/models/task_sync.py @@ -135,11 +135,18 @@ class FusionTaskSyncConfig(models.Model): ], {'fields': ['id', 'x_fc_tech_sync_id']}) if not remote_users: return {} - return { - ru['x_fc_tech_sync_id']: ru['id'] - for ru in remote_users - if ru.get('x_fc_tech_sync_id') - } + by_sync_id = {} + for ru in remote_users: + sid = ru.get('x_fc_tech_sync_id') + if not sid: + continue + if sid in by_sync_id: + _logger.warning( + "Task sync: duplicate x_fc_tech_sync_id %r on remote %s " + "(uids %d and %d) — only the last seen will be reachable", + sid, self.name, by_sync_id[sid], ru['id']) + by_sync_id[sid] = ru['id'] + return by_sync_id def _get_local_syncid_to_uid(self): """Build {x_fc_tech_sync_id: local_user_id} for local field staff.""" @@ -148,7 +155,16 @@ class FusionTaskSyncConfig(models.Model): ('x_fc_tech_sync_id', '!=', False), ('active', '=', True), ]) - return {u.x_fc_tech_sync_id: u.id for u in techs} + by_sync_id = {} + for u in techs: + sid = u.x_fc_tech_sync_id + if sid in by_sync_id: + _logger.warning( + "Task sync: duplicate x_fc_tech_sync_id %r locally " + "(uids %d and %d) — only the last seen will be reachable", + sid, by_sync_id[sid], u.id) + by_sync_id[sid] = u.id + return by_sync_id # ------------------------------------------------------------------ # Connection test @@ -219,9 +235,15 @@ class FusionTaskSyncConfig(models.Model): for task in tasks: sync_id = local_map.get(task.technician_id.id) if not sync_id: + _logger.info( + "Task sync: skipping task %s — technician %s has no x_fc_tech_sync_id", + task.name, task.technician_id.login or task.technician_id.id) continue remote_tech_uid = remote_map.get(sync_id) if not remote_tech_uid: + _logger.info( + "Task sync: skipping task %s — sync_id %r has no matching tech on %s", + task.name, sync_id, self.name) continue # Map additional technicians to remote user IDs