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
|
# -*- 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)
|