1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
|
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
import logging
from odoo import api, models, Command
from odoo.tools import email_normalize
from odoo.addons.google_calendar.utils.google_calendar import GoogleCalendarService
_logger = logging.getLogger(__name__)
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, generic_values_creation=None):
events = self.filtered('need_sync').calendar_event_ids
detached_events = super()._apply_recurrence(specific_values_creation, no_send_edit,
generic_values_creation)
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.with_user(event._get_event_user())._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)
# Events will be updated by patch requests, do not sync events for avoiding spam.
values['need_sync'] = False
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'}
@api.model
def _restart_google_sync(self):
self.env['calendar.recurrence'].search(self._get_sync_domain()).write({
'need_sync': True,
})
def _write_from_google(self, gevent, vals):
current_rrule = self.rrule
current_parsed_rrule = self._rrule_parse(current_rrule, self.dtstart)
# 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)
new_parsed_rrule = self._rrule_parse(self.rrule, self.dtstart)
# We update the attendee status for all events in the recurrence
google_attendees = gevent.attendees or []
emails = [a.get('email') for a in google_attendees]
partners = self._get_sync_partner(emails)
existing_attendees = self.calendar_event_ids.attendee_ids
for attendee in zip(emails, partners, google_attendees):
email = attendee[0]
if email in existing_attendees.mapped('email'):
# Update existing attendees
existing_attendees.filtered(lambda att: att.email == email).write({'state': attendee[2].get('responseStatus')})
else:
# Create new attendees
if attendee[2].get('self'):
partner = self.env.user.partner_id
elif attendee[1]:
partner = attendee[1]
else:
continue
self.calendar_event_ids.write({'attendee_ids': [(0, 0, {'state': attendee[2].get('responseStatus'), 'partner_id': partner.id})]})
if attendee[2].get('displayName') and not partner.name:
partner.name = attendee[2].get('displayName')
organizers_partner_ids = [event.user_id.partner_id for event in self.calendar_event_ids if event.user_id]
for odoo_attendee_email in set(existing_attendees.mapped('email')):
# Sometimes, several partners have the same email. Remove old attendees except organizer, otherwise the events will disappear.
if email_normalize(odoo_attendee_email) not in emails:
attendees = existing_attendees.exists().filtered(lambda att: att.email == email_normalize(odoo_attendee_email) and att.partner_id not in organizers_partner_ids)
self.calendar_event_ids.write({'need_sync': False, 'partner_ids': [Command.unlink(att.partner_id.id) for att in attendees]})
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.get(key) and 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
non_equal_values = [
(key, old_event_values[key] and old_event_values[key].strftime('%m/%d/%Y, %H:%M:%S'), '-->',
new_event_values[key] and new_event_values[key].strftime('%m/%d/%Y, %H:%M:%S')
) for key in ['start', 'stop'] if new_event_values[key] != old_event_values[key]
]
log_msg = f"Recurrence {self.id} {self.rrule} has all events ({len(self.calendar_event_ids.ids)}) deleted because of base event value change: {non_equal_values}"
_logger.info(log_msg)
# 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.with_context(dont_notify=True).write(dict(new_event_values, google_id=False, need_sync=False))
if new_parsed_rrule == current_parsed_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._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 new_parsed_rrule != current_parsed_rrule:
detached_events = self._apply_recurrence()
detached_events.google_id = False
log_msg = f"Recurrence #{self.id} | current rule: {current_rrule} | new rule: {self.rrule} | remaining: {len(self.calendar_event_ids)} | removed: {len(detached_events)}"
_logger.info(log_msg)
detached_events.unlink()
def _create_from_google(self, gevents, vals_list):
attendee_values = {}
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')
attendee_values[base_event.id] = {'attendee_ids': base_values.get('attendee_ids')}
recurrence = super(RecurrenceRule, self.with_context(dont_notify=True))._create_from_google(gevents, vals_list)
generic_values_creation = {
rec.id: attendee_values[rec.base_event_id.id]
for rec in recurrence if attendee_values.get(rec.base_event_id.id)
}
recurrence.with_context(dont_notify=True)._apply_recurrence(generic_values_creation=generic_values_creation)
return recurrence
def _get_sync_domain(self):
# Empty rrule may exists in historical data. It is not a desired behavior but it could have been created with
# older versions of the module. When synced, these recurrency may come back from Google after database cleaning
# and trigger errors as the records are not properly populated.
# We also prevent sync of other user recurrent events.
return [('calendar_event_ids.user_id', '=', self.env.user.id), ('rrule', '!=', False)]
@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 or 'Etc/UTC'
values['end']['timeZone'] = self.event_tz or 'Etc/UTC'
# 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 _get_event_user(self):
self.ensure_one()
event = self._get_first_event()
if event:
return event._get_event_user()
return self.env.user
|