fix(fusion_schedule): stop archiving valid events on @removed=changed
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) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
{
|
{
|
||||||
'name': 'Fusion Schedule',
|
'name': 'Fusion Schedule',
|
||||||
'version': '19.0.2.0.0',
|
'version': '19.0.2.1.0',
|
||||||
'category': 'Services/Appointment',
|
'category': 'Services/Appointment',
|
||||||
'summary': 'Multi-calendar sync, portal booking, and shareable scheduling links',
|
'summary': 'Multi-calendar sync, portal booking, and shareable scheduling links',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -399,12 +399,17 @@ class FusionCalendarAccount(models.Model):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _find_existing_event(self, CalendarEvent, vals):
|
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')
|
start_val = vals.get('start') or vals.get('start_date')
|
||||||
stop_val = vals.get('stop') or vals.get('stop_date')
|
stop_val = vals.get('stop') or vals.get('stop_date')
|
||||||
if not (start_val and stop_val and vals.get('name')):
|
if not (start_val and stop_val and vals.get('name')):
|
||||||
return None
|
return None
|
||||||
domain = [('name', '=', vals['name']), ('active', '=', True)]
|
domain = [('name', '=', vals['name']), ('active', 'in', [True, False])]
|
||||||
if vals.get('allday'):
|
if vals.get('allday'):
|
||||||
domain += [('start_date', '=', start_val), ('stop_date', '=', stop_val)]
|
domain += [('start_date', '=', start_val), ('stop_date', '=', stop_val)]
|
||||||
else:
|
else:
|
||||||
@@ -485,6 +490,8 @@ class FusionCalendarAccount(models.Model):
|
|||||||
|
|
||||||
reuse_event = self._find_existing_event(CalendarEvent, vals)
|
reuse_event = self._find_existing_event(CalendarEvent, vals)
|
||||||
if reuse_event:
|
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)
|
self._upsert_event_link(EventLink, reuse_event.id, external_id, ical_uid)
|
||||||
return 'updated'
|
return 'updated'
|
||||||
|
|
||||||
@@ -677,11 +684,18 @@ class FusionCalendarAccount(models.Model):
|
|||||||
('x_fc_external_id', '=', external_id),
|
('x_fc_external_id', '=', external_id),
|
||||||
], limit=1)
|
], 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:
|
if link and link.x_fc_event_id:
|
||||||
link.x_fc_event_id.with_context(**ctx).write({'active': False})
|
link.x_fc_event_id.with_context(**ctx).write({'active': False})
|
||||||
link.unlink()
|
link.unlink()
|
||||||
return 'deleted'
|
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)
|
vals = self._microsoft_event_to_odoo_vals(event_data)
|
||||||
if not vals:
|
if not vals:
|
||||||
@@ -690,8 +704,11 @@ class FusionCalendarAccount(models.Model):
|
|||||||
ical_uid = event_data.get('iCalUId', '')
|
ical_uid = event_data.get('iCalUId', '')
|
||||||
|
|
||||||
if link:
|
if link:
|
||||||
if link.x_fc_event_id and link.x_fc_event_id.active:
|
if link.x_fc_event_id:
|
||||||
link.x_fc_event_id.with_context(**ctx).write(vals)
|
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()})
|
link.write({'x_fc_last_synced': fields.Datetime.now()})
|
||||||
return 'updated'
|
return 'updated'
|
||||||
|
|
||||||
@@ -701,11 +718,15 @@ class FusionCalendarAccount(models.Model):
|
|||||||
], limit=1) if ical_uid else None
|
], limit=1) if ical_uid else None
|
||||||
|
|
||||||
if existing_link and existing_link.x_fc_event_id:
|
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)
|
self._upsert_event_link(EventLink, existing_link.x_fc_event_id.id, external_id, ical_uid)
|
||||||
return 'updated'
|
return 'updated'
|
||||||
|
|
||||||
reuse_event = self._find_existing_event(CalendarEvent, vals)
|
reuse_event = self._find_existing_event(CalendarEvent, vals)
|
||||||
if reuse_event:
|
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)
|
self._upsert_event_link(EventLink, reuse_event.id, external_id, ical_uid)
|
||||||
return 'updated'
|
return 'updated'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user