diff options
Diffstat (limited to 'addons/google_calendar/models/calendar_recurrence_rule.py')
| -rw-r--r-- | addons/google_calendar/models/calendar_recurrence_rule.py | 187 |
1 files changed, 187 insertions, 0 deletions
diff --git a/addons/google_calendar/models/calendar_recurrence_rule.py b/addons/google_calendar/models/calendar_recurrence_rule.py new file mode 100644 index 00000000..c9ff1566 --- /dev/null +++ b/addons/google_calendar/models/calendar_recurrence_rule.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import re +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models + +from odoo.addons.google_calendar.utils.google_calendar import GoogleCalendarService + + +class RecurrenceRule(models.Model): + _name = 'calendar.recurrence' + _inherit = ['calendar.recurrence', 'google.calendar.sync'] + + + def _apply_recurrence(self, specific_values_creation=None, no_send_edit=False): + events = self.filtered('need_sync').calendar_event_ids + detached_events = super()._apply_recurrence(specific_values_creation, no_send_edit) + + google_service = GoogleCalendarService(self.env['google.service']) + + # If a synced event becomes a recurrence, the event needs to be deleted from + # Google since it's now the recurrence which is synced. + # Those events are kept in the database and their google_id is updated + # according to the recurrence google_id, therefore we need to keep an inactive copy + # of those events with the original google id. The next sync will then correctly + # delete those events from Google. + vals = [] + for event in events.filtered('google_id'): + if event.active and event.google_id != event.recurrence_id._get_event_google_id(event): + vals += [{ + 'name': event.name, + 'google_id': event.google_id, + 'start': event.start, + 'stop': event.stop, + 'active': False, + 'need_sync': True, + }] + event._google_delete(google_service, event.google_id) + event.google_id = False + self.env['calendar.event'].create(vals) + + self.calendar_event_ids.need_sync = False + return detached_events + + def _get_event_google_id(self, event): + """Return the Google id of recurring event. + Google ids of recurrence instances are formatted as: {recurrence google_id}_{UTC starting time in compacted ISO8601} + """ + if self.google_id: + if event.allday: + time_id = event.start_date.isoformat().replace('-', '') + else: + # '-' and ':' are optional in ISO8601 + start_compacted_iso8601 = event.start.isoformat().replace('-', '').replace(':', '') + # Z at the end for UTC + time_id = '%sZ' % start_compacted_iso8601 + return '%s_%s' % (self.google_id, time_id) + return False + + def _write_events(self, values, dtstart=None): + values.pop('google_id', False) + # If only some events are updated, sync those events. + values['need_sync'] = bool(dtstart) + return super()._write_events(values, dtstart=dtstart) + + def _cancel(self): + self.calendar_event_ids._cancel() + super()._cancel() + + def _get_google_synced_fields(self): + return {'rrule'} + + def _write_from_google(self, gevent, vals): + current_rrule = self.rrule + # event_tz is written on event in Google but on recurrence in Odoo + vals['event_tz'] = gevent.start.get('timeZone') + super()._write_from_google(gevent, vals) + + base_event_time_fields = ['start', 'stop', 'allday'] + new_event_values = self.env["calendar.event"]._odoo_values(gevent) + old_event_values = self.base_event_id and self.base_event_id.read(base_event_time_fields)[0] + if old_event_values and any(new_event_values[key] != old_event_values[key] for key in base_event_time_fields): + # 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 Google side. + # We can't call _cancel because events without user_id would not be deleted + (self.calendar_event_ids - base_event_id).google_id = False + (self.calendar_event_ids - base_event_id).unlink() + base_event_id.write(dict(new_event_values, google_id=False, need_sync=False)) + if self.rrule == current_rrule: + # if the rrule has changed, it will be recalculated below + # There is no detached event now + self._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._write_events(dict({ + field: value + for field, value in new_event_values.items() + if field not in time_fields + }, need_sync=False) + ) + + # We apply the rrule check after the time_field check because the google_id are generated according + # to base_event start datetime. + if self.rrule != current_rrule: + detached_events = self._apply_recurrence() + detached_events.google_id = False + detached_events.unlink() + + def _create_from_google(self, gevents, vals_list): + for gevent, vals in zip(gevents, vals_list): + base_values = dict( + self.env['calendar.event']._odoo_values(gevent), # FIXME default reminders + need_sync=False, + ) + # If we convert a single event into a recurrency on Google, we should reuse this event on Odoo + # Google reuse the event google_id to identify the recurrence in that case + base_event = self.env['calendar.event'].search([('google_id', '=', vals['google_id'])]) + if not base_event: + base_event = self.env['calendar.event'].create(base_values) + else: + # We override the base_event values because they could have been changed in Google interface + # The event google_id will be recalculated once the recurrence is created + base_event.write(dict(base_values, google_id=False)) + vals['base_event_id'] = base_event.id + vals['calendar_event_ids'] = [(4, base_event.id)] + # event_tz is written on event in Google but on recurrence in Odoo + vals['event_tz'] = gevent.start.get('timeZone') + recurrence = super(RecurrenceRule, self.with_context(dont_notify=True))._create_from_google(gevents, vals_list) + recurrence.with_context(dont_notify=True)._apply_recurrence() + if not recurrence._context.get("dont_notify"): + recurrence._notify_attendees() + return recurrence + + def _get_sync_domain(self): + return [('calendar_event_ids.user_id', '=', self.env.user.id)] + + @api.model + def _odoo_values(self, google_recurrence, default_reminders=()): + return { + 'rrule': google_recurrence.rrule, + 'google_id': google_recurrence.id, + } + + def _google_values(self): + event = self._get_first_event() + if not event: + return {} + values = event._google_values() + values['id'] = self.google_id + + if not self._is_allday(): + values['start']['timeZone'] = self.event_tz + values['end']['timeZone'] = self.event_tz + + # DTSTART is not allowed by Google Calendar API. + # Event start and end times are specified in the start and end fields. + rrule = re.sub('DTSTART:[0-9]{8}T[0-9]{1,8}\\n', '', self.rrule) + # UNTIL must be in UTC (appending Z) + # We want to only add a 'Z' to non UTC UNTIL values and avoid adding a second. + # 'RRULE:FREQ=DAILY;UNTIL=20210224T235959;INTERVAL=3 --> match UNTIL=20210224T235959 + # 'RRULE:FREQ=DAILY;UNTIL=20210224T235959 --> match + rrule = re.sub(r"(UNTIL=\d{8}T\d{6})($|;)", r"\1Z\2", rrule) + values['recurrence'] = ['RRULE:%s' % rrule] if 'RRULE:' not in rrule else [rrule] + property_location = 'shared' if event.user_id else 'private' + values['extendedProperties'] = { + property_location: { + '%s_odoo_id' % self.env.cr.dbname: self.id, + }, + } + return values + + def _notify_attendees(self): + recurrences = self.filtered( + lambda recurrence: recurrence.base_event_id.alarm_ids and ( + not recurrence.until or recurrence.until >= fields.Date.today() - relativedelta(days=1) + ) and (max(recurrence.calendar_event_ids.mapped('stop')) >= fields.Datetime.now()) + ) + partners = recurrences.base_event_id.partner_ids + if partners: + self.env['calendar.alarm_manager']._notify_next_alarm(partners.ids) |
