From f1cea2fb352a5c59662a7c2cbab9962b15e4fda0 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Wed, 20 May 2026 23:21:15 -0400 Subject: [PATCH] fix(fusion_schedule): stop archiving valid events on @removed=changed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Microsoft Graph's delta API returns @removed={reason:'changed'} when an event drifts outside the original delta-query window — the event still exists upstream. The old code treated any truthy @removed the same as a real delete and archived the local calendar.event. Combined with _find_existing_event filtering by active=True, every subsequent sync recreated a duplicate (then archived it on the next pass), accumulating 5x duplicates and emptying the user's calendar. - _process_microsoft_event: only archive on isCancelled or @removed.reason='deleted'; skip on @removed.reason='changed' - _process_microsoft_event link path: reactivate when MS Graph confirms a previously-archived event still exists - _process_microsoft_event iCalUId path: same reactivation - _find_existing_event: include archived records so wrongly-archived duplicates are reused instead of piling up - callers reactivate the matched archived record Co-Authored-By: Claude Opus 4.7 (1M context) --- fusion_schedule/__manifest__.py | 2 +- .../models/fusion_calendar_account.py | 31 ++++++++++++++++--- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/fusion_schedule/__manifest__.py b/fusion_schedule/__manifest__.py index bcd7e9bb..896b1bbb 100644 --- a/fusion_schedule/__manifest__.py +++ b/fusion_schedule/__manifest__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- { 'name': 'Fusion Schedule', - 'version': '19.0.2.0.0', + 'version': '19.0.2.1.0', 'category': 'Services/Appointment', 'summary': 'Multi-calendar sync, portal booking, and shareable scheduling links', 'description': """ diff --git a/fusion_schedule/models/fusion_calendar_account.py b/fusion_schedule/models/fusion_calendar_account.py index 4cfd157a..1672a471 100644 --- a/fusion_schedule/models/fusion_calendar_account.py +++ b/fusion_schedule/models/fusion_calendar_account.py @@ -399,12 +399,17 @@ class FusionCalendarAccount(models.Model): } def _find_existing_event(self, CalendarEvent, vals): - """Find an existing calendar event matching name+start+stop to avoid duplicates.""" + """Find an existing calendar event matching name+start+stop to avoid duplicates. + + Includes archived records so prior wrongly-archived duplicates get + reused (the caller is expected to reactivate them) instead of new + copies piling up on every sync iteration. + """ start_val = vals.get('start') or vals.get('start_date') stop_val = vals.get('stop') or vals.get('stop_date') if not (start_val and stop_val and vals.get('name')): return None - domain = [('name', '=', vals['name']), ('active', '=', True)] + domain = [('name', '=', vals['name']), ('active', 'in', [True, False])] if vals.get('allday'): domain += [('start_date', '=', start_val), ('stop_date', '=', stop_val)] else: @@ -485,6 +490,8 @@ class FusionCalendarAccount(models.Model): reuse_event = self._find_existing_event(CalendarEvent, vals) if reuse_event: + if not reuse_event.active: + reuse_event.with_context(**ctx).write({'active': True}) self._upsert_event_link(EventLink, reuse_event.id, external_id, ical_uid) return 'updated' @@ -677,11 +684,18 @@ class FusionCalendarAccount(models.Model): ('x_fc_external_id', '=', external_id), ], limit=1) - if event_data.get('@removed') or event_data.get('isCancelled'): + removed = event_data.get('@removed') + removed_reason = removed.get('reason') if isinstance(removed, dict) else None + if event_data.get('isCancelled') or removed_reason == 'deleted': if link and link.x_fc_event_id: link.x_fc_event_id.with_context(**ctx).write({'active': False}) link.unlink() return 'deleted' + if removed: + # @removed with reason != 'deleted' (typically 'changed') means the + # event drifted outside the original delta query window — it still + # exists upstream. Leave the local copy alone so it stays visible. + return 'skipped' vals = self._microsoft_event_to_odoo_vals(event_data) if not vals: @@ -690,8 +704,11 @@ class FusionCalendarAccount(models.Model): ical_uid = event_data.get('iCalUId', '') if link: - if link.x_fc_event_id and link.x_fc_event_id.active: - link.x_fc_event_id.with_context(**ctx).write(vals) + if link.x_fc_event_id: + update_vals = dict(vals) + if not link.x_fc_event_id.active: + update_vals['active'] = True + link.x_fc_event_id.with_context(**ctx).write(update_vals) link.write({'x_fc_last_synced': fields.Datetime.now()}) return 'updated' @@ -701,11 +718,15 @@ class FusionCalendarAccount(models.Model): ], limit=1) if ical_uid else None if existing_link and existing_link.x_fc_event_id: + if not existing_link.x_fc_event_id.active: + existing_link.x_fc_event_id.with_context(**ctx).write({'active': True}) self._upsert_event_link(EventLink, existing_link.x_fc_event_id.id, external_id, ical_uid) return 'updated' reuse_event = self._find_existing_event(CalendarEvent, vals) if reuse_event: + if not reuse_event.active: + reuse_event.with_context(**ctx).write({'active': True}) self._upsert_event_link(EventLink, reuse_event.id, external_id, ical_uid) return 'updated'