# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models from odoo.osv import expression class RecurrenceRule(models.Model): _name = 'calendar.recurrence' _inherit = ['calendar.recurrence', 'microsoft.calendar.sync'] # Don't sync by default. Sync only when the recurrence is applied need_sync_m = fields.Boolean(default=False) microsoft_id = fields.Char('Microsoft Calendar Recurrence Id') def _compute_rrule(self): # Note: 'need_sync_m' is set to False to avoid syncing the updated recurrence with # Outlook, as this update may already come from Outlook. If not, this modification will # be already synced through the calendar.event.write() for recurrence in self: if recurrence.rrule != recurrence._rrule_serialize(): recurrence.write({'rrule': recurrence._rrule_serialize()}) def _inverse_rrule(self): # Note: 'need_sync_m' is set to False to avoid syncing the updated recurrence with # Outlook, as this update mainly comes from Outlook (the 'rrule' field is not directly # modified in Odoo but computed from other fields). for recurrence in self.filtered('rrule'): values = self._rrule_parse(recurrence.rrule, recurrence.dtstart) recurrence.with_context(dont_notify=True).write(dict(values, need_sync_m=False)) def _apply_recurrence(self, specific_values_creation=None, no_send_edit=False, generic_values_creation=None): events = self.filtered('need_sync_m').calendar_event_ids detached_events = super()._apply_recurrence(specific_values_creation, no_send_edit, generic_values_creation) # If a synced event becomes a recurrence, the event needs to be deleted from # Microsoft since it's now the recurrence which is synced. vals = [] for event in events._get_synced_events(): if event.active and event.ms_universal_event_id and not event.recurrence_id.ms_universal_event_id: vals += [{ 'name': event.name, 'microsoft_id': event.microsoft_id, 'start': event.start, 'stop': event.stop, 'active': False, 'need_sync_m': True, }] event._microsoft_delete(event.user_id, event.ms_organizer_event_id) event.ms_universal_event_id = False self.env['calendar.event'].create(vals) self.calendar_event_ids.need_sync_m = False return detached_events def _write_events(self, values, dtstart=None): # If only some events are updated, sync those events. # If all events are updated, sync the recurrence instead. values['need_sync_m'] = bool(dtstart) or values.get("need_sync_m", True) return super()._write_events(values, dtstart=dtstart) def _get_organizer(self): return self.base_event_id.user_id def _get_rrule(self, dtstart=None): if not dtstart and self.dtstart: dtstart = self.dtstart return super()._get_rrule(dtstart) def _get_microsoft_synced_fields(self): return {'rrule'} | self.env['calendar.event']._get_microsoft_synced_fields() @api.model def _restart_microsoft_sync(self): self.env['calendar.recurrence'].search(self._get_microsoft_sync_domain()).write({ 'need_sync_m': True, }) def _has_base_event_time_fields_changed(self, new): """ Indicates if at least one time field of the base event has changed, based on provided `new` values. Note: for all day event comparison, hours/minutes are ignored. """ def _convert(value, to_convert): return value.date() if to_convert else value old = self.base_event_id and self.base_event_id.read(['start', 'stop', 'allday'])[0] return old and ( old['allday'] != new['allday'] or any( _convert(new[f], new['allday']) != _convert(old[f], old['allday']) for f in ('start', 'stop') ) ) def _write_from_microsoft(self, microsoft_event, vals): current_rrule = self.rrule # event_tz is written on event in Microsoft but on recurrence in Odoo vals['event_tz'] = microsoft_event.start.get('timeZone') super()._write_from_microsoft(microsoft_event, vals) new_event_values = self.env["calendar.event"]._microsoft_to_odoo_values(microsoft_event) # Edge case: if the base event was deleted manually in 'self_only' update, skip applying recurrence. if self._has_base_event_time_fields_changed(new_event_values) and (new_event_values['start'] >= self.base_event_id.start): # we need to recreate the recurrence, time_fields were modified. base_event_id = self.base_event_id # We archive the old events to recompute the recurrence. These events are already deleted on Microsoft side. # We can't call _cancel because events without user_id would not be deleted (self.calendar_event_ids - base_event_id).microsoft_id = False (self.calendar_event_ids - base_event_id).unlink() base_event_id.with_context(dont_notify=True).write(dict( new_event_values, microsoft_id=False, need_sync_m=False )) if self.rrule == current_rrule: # if the rrule has changed, it will be recalculated below # There is no detached event now self.with_context(dont_notify=True)._apply_recurrence() else: time_fields = ( self.env["calendar.event"]._get_time_fields() | self.env["calendar.event"]._get_recurrent_fields() ) # We avoid to write time_fields because they are not shared between events. self.with_context(dont_notify=True)._write_events(dict({ field: value for field, value in new_event_values.items() if field not in time_fields }, need_sync_m=False) ) # We apply the rrule check after the time_field check because the microsoft ids are generated according # to base_event start datetime. if self.rrule != current_rrule: detached_events = self._apply_recurrence() detached_events.microsoft_id = False detached_events.unlink() def _get_microsoft_sync_domain(self): # Do not sync Odoo recurrences with Outlook Calendar anymore. domain = expression.FALSE_DOMAIN return self._extend_microsoft_domain(domain) def _cancel_microsoft(self): self.calendar_event_ids.with_context(dont_notify=True)._cancel_microsoft() super()._cancel_microsoft() @api.model def _microsoft_to_odoo_values(self, microsoft_recurrence, default_reminders=(), default_values=None, with_ids=False): recurrence = microsoft_recurrence.get_recurrence() if with_ids: recurrence = { **recurrence, 'ms_organizer_event_id': microsoft_recurrence.id, 'ms_universal_event_id': microsoft_recurrence.iCalUId, } return recurrence def _microsoft_values(self, fields_to_sync): """ Get values to update the whole Outlook event recurrence. (done through the first event of the Outlook recurrence). """ return self.base_event_id._microsoft_values(fields_to_sync, initial_values={'type': 'seriesMaster'}) def _ensure_attendees_have_email(self): self.calendar_event_ids.filtered(lambda e: e.active)._ensure_attendees_have_email() def _split_from(self, event, recurrence_values=None): """ When a recurrence is splitted, the base event of the new recurrence already exist and may be already synced with Outlook. In this case, we need to be removed this event on Outlook side to avoid duplicates while posting the new recurrence. """ new_recurrence = super()._split_from(event, recurrence_values) if new_recurrence and new_recurrence.base_event_id.microsoft_id: new_recurrence.base_event_id._microsoft_delete( new_recurrence.base_event_id._get_organizer(), new_recurrence.base_event_id.ms_organizer_event_id ) return new_recurrence def _get_event_user_m(self, user_id=None): """ Get the user who will send the request to Microsoft (organizer if synchronized and current user otherwise). """ self.ensure_one() event = self._get_first_event() if event: return event._get_event_user_m(user_id) return self.env.user