summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLucas Lefèvre <lul@odoo.com>2021-02-09 16:42:00 +0000
committerArnaud Joset <arj@odoo.com>2021-03-22 11:00:32 +0000
commit374d3c7f95fca9832afb0d34d710f28dbadcdfa6 (patch)
tree5ea727d06eece30e4ac69a45e3f8b98c10c15e05
parente6675b075459c446a9115f9695824646c81dcc3f (diff)
[FIX] google_calendar: events sync issues
Before this commit: The google synchronization dis not properly synced event in some conditions. Some use cases were not properly tested. Recurrent events were regularly badly synchronized with Google. Several issues occured: - events not follow recurrence were duplicated on both Google or Odoo and sometimes deleted when the recurrence was reapplied. - the base_event (first event of the recurrence) was duplicated - miscalculation of the event google_id when they were part of a recurrence but did not followed the rrule. - improper data handling from google. Odoo objets were created with incorrect values - attendee state was not properly sync from Odoo to Google - whole recurrence deletions from google were not properly sync in Odoo - when time fields of a recurrence were modified on Google, modifications were ignored on Odoo - Event timezone were not properly saved on the recurrency. (Odoo saves it on the recurrency and Google on the event) - Odoo considered that all public events are writable bu Odoo users. That would trigger errors as Google implement an access right model on public events - lack of tests - a lot of weird behaviors resulting from these problems. closes odoo/odoo#66080 Taskid: 2456498 Opw: 2299834 Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com> Co-authored-by: Lucas Lefèvre <lul@odoo.com> Co-authored-by: Arnaud Joset <arj@odoo.com>
-rw-r--r--addons/google_calendar/models/calendar.py28
-rw-r--r--addons/google_calendar/models/calendar_attendee.py9
-rw-r--r--addons/google_calendar/models/calendar_recurrence_rule.py99
-rw-r--r--addons/google_calendar/models/google_sync.py16
-rw-r--r--addons/google_calendar/models/res_users.py1
-rw-r--r--addons/google_calendar/tests/__init__.py1
-rw-r--r--addons/google_calendar/tests/test_sync_common.py63
-rw-r--r--addons/google_calendar/tests/test_sync_google2odoo.py311
-rw-r--r--addons/google_calendar/tests/test_sync_odoo2google.py216
-rw-r--r--addons/google_calendar/utils/google_event.py32
10 files changed, 689 insertions, 87 deletions
diff --git a/addons/google_calendar/models/calendar.py b/addons/google_calendar/models/calendar.py
index 0106bf00baf..ba455fa2111 100644
--- a/addons/google_calendar/models/calendar.py
+++ b/addons/google_calendar/models/calendar.py
@@ -23,7 +23,7 @@ class Meeting(models.Model):
# Event if the event is moved, the google_id remains the same.
for event in self:
google_recurrence_id = event.recurrence_id._get_event_google_id(event)
- if google_recurrence_id:
+ if not event.google_id and google_recurrence_id:
event.google_id = google_recurrence_id
elif not event.google_id:
event.google_id = False
@@ -36,7 +36,7 @@ class Meeting(models.Model):
@api.model_create_multi
def create(self, vals_list):
return super().create([
- dict(vals, need_sync=False) if vals.get('recurrency') else vals
+ dict(vals, need_sync=False) if vals.get('recurrence_id') or vals.get('recurrency') else vals
for vals in vals_list
])
@@ -44,7 +44,10 @@ class Meeting(models.Model):
recurrence_update_setting = values.get('recurrence_update')
if recurrence_update_setting in ('all_events', 'future_events') and len(self) == 1:
values = dict(values, need_sync=False)
- return super().write(values)
+ res = super().write(values)
+ if recurrence_update_setting in ('all_events',) and len(self) == 1 and values.keys() & self._get_google_synced_fields():
+ self.recurrence_id.need_sync = True
+ return res
def _get_sync_domain(self):
return [('partner_ids.user_ids', 'in', self.env.user.id)]
@@ -196,11 +199,24 @@ class Meeting(models.Model):
}
if self.privacy:
values['visibility'] = self.privacy
- if self.user_id and self.user_id != self.env.user:
- values['extendedProperties']['shared']['%s_owner_id' % self.env.cr.dbname] = self.user_id.id
-
if not self.active:
values['status'] = 'cancelled'
+ if self.user_id and self.user_id != self.env.user:
+ values['extendedProperties']['shared']['%s_owner_id' % self.env.cr.dbname] = self.user_id.id
+ elif not self.user_id:
+ # We don't store the real owner identity (mail)
+ # We can't store on the shared properties in that case without getting a 403
+ # If several odoo users are attendees but the owner is not in odoo, the event will be duplicated on odoo database
+ # if we are not the owner, we should change the post values to avoid errors because we don't have enough rights
+ # See https://developers.google.com/calendar/concepts/sharing
+ keep_keys = ['id', 'attendees', 'start', 'end', 'reminders']
+ values = {key: val for key, val in values.items() if key in keep_keys}
+ # values['extendedProperties']['private] should be used if the owner is not an odoo user
+ values['extendedProperties'] = {
+ 'private': {
+ '%s_odoo_id' % self.env.cr.dbname: self.id,
+ },
+ }
return values
def _cancel(self):
diff --git a/addons/google_calendar/models/calendar_attendee.py b/addons/google_calendar/models/calendar_attendee.py
index d62702f8972..3e3af0f5a36 100644
--- a/addons/google_calendar/models/calendar_attendee.py
+++ b/addons/google_calendar/models/calendar_attendee.py
@@ -4,6 +4,7 @@
from odoo import models
from odoo.addons.google_calendar.models.google_sync import google_calendar_token
+from odoo.addons.google_calendar.utils.google_calendar import GoogleCalendarService
class Attendee(models.Model):
_name = 'calendar.attendee'
@@ -17,3 +18,11 @@ class Attendee(models.Model):
with google_calendar_token(self.env.user.sudo()) as token:
if not token:
super()._send_mail_to_attendees(template_xmlid, force_send, ignore_recurrence)
+
+ def write(self, vals):
+ res = super().write(vals)
+ if vals.get('state'):
+ # When the state is changed, the corresponding event must be sync with google
+ google_service = GoogleCalendarService(self.env['google.service'])
+ self.event_id.filtered('google_id')._sync_odoo2google(google_service)
+ return res
diff --git a/addons/google_calendar/models/calendar_recurrence_rule.py b/addons/google_calendar/models/calendar_recurrence_rule.py
index b3cd06db329..055359fa8f9 100644
--- a/addons/google_calendar/models/calendar_recurrence_rule.py
+++ b/addons/google_calendar/models/calendar_recurrence_rule.py
@@ -13,11 +13,8 @@ class RecurrenceRule(models.Model):
_inherit = ['calendar.recurrence', 'google.calendar.sync']
- # Don't sync by default. Sync only when the recurrence is applied
- need_sync = fields.Boolean(default=False)
-
def _apply_recurrence(self, specific_values_creation=None, no_send_edit=False):
- events = self.calendar_event_ids
+ 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'])
@@ -43,12 +40,6 @@ class RecurrenceRule(models.Model):
event.google_id = False
self.env['calendar.event'].create(vals)
- for recurrence in self:
- values = recurrence._google_values()
- if not recurrence.google_id:
- recurrence._google_insert(google_service, values)
- else:
- recurrence._google_patch(google_service, recurrence.google_id, values)
self.calendar_event_ids.need_sync = False
return detached_events
@@ -70,34 +61,88 @@ class RecurrenceRule(models.Model):
def _write_events(self, values, dtstart=None):
values.pop('google_id', False)
# If only some events are updated, sync those events.
- # If all events are updated, sync the recurrence instead.
values['need_sync'] = bool(dtstart)
- if not dtstart:
- self.need_sync = True
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 _sync_google2odoo(self, *args, **kwargs):
- synced_recurrences = super()._sync_google2odoo(*args, **kwargs)
- detached_events = synced_recurrences._apply_recurrence()
- detached_events.unlink()
- return synced_recurrences
+ 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()._create_from_google(gevents, vals_list)
+ recurrence._apply_recurrence()
+ 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=()):
- base_values = dict(self.env['calendar.event']._odoo_values(google_recurrence, default_reminders), need_sync=False)
- base_event = self.env['calendar.event'].create(base_values)
return {
'rrule': google_recurrence.rrule,
'google_id': google_recurrence.id,
- 'base_event_id': base_event.id,
- 'calendar_event_ids': [(4, base_event.id)],
}
def _google_values(self):
@@ -114,9 +159,15 @@ class RecurrenceRule(models.Model):
# 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'] = {
- 'shared': {
+ property_location: {
'%s_odoo_id' % self.env.cr.dbname: self.id,
},
}
diff --git a/addons/google_calendar/models/google_sync.py b/addons/google_calendar/models/google_sync.py
index c5c549b752f..e1486bac47e 100644
--- a/addons/google_calendar/models/google_sync.py
+++ b/addons/google_calendar/models/google_sync.py
@@ -155,8 +155,7 @@ class GoogleSync(models.AbstractModel):
dict(self._odoo_values(e, default_reminders), need_sync=False)
for e in new
]
- new_odoo = self.create(odoo_values)
-
+ new_odoo = self._create_from_google(new, odoo_values)
cancelled = existing.cancelled()
cancelled_odoo = self.browse(cancelled.odoo_ids(self.env))
cancelled_odoo._cancel()
@@ -170,7 +169,7 @@ class GoogleSync(models.AbstractModel):
# Migration from 13.4 does not fill write_date. Therefore, we force the update from Google.
if not odoo_record.write_date or updated >= pytz.utc.localize(odoo_record.write_date):
vals = dict(self._odoo_values(gevent, default_reminders), need_sync=False)
- odoo_record.write(vals)
+ odoo_record._write_from_google(gevent, vals)
synced_records |= odoo_record
return synced_records
@@ -180,7 +179,9 @@ class GoogleSync(models.AbstractModel):
with google_calendar_token(self.env.user.sudo()) as token:
if token:
google_service.delete(google_id, token=token, timeout=timeout)
- self.need_sync = False
+ # When the record has been deleted on our side, we need to delete it on google but we don't want
+ # to raise an error because the record don't exists anymore.
+ self.exists().need_sync = False
@after_commit
def _google_patch(self, google_service: GoogleCalendarService, google_id, values, timeout=TIMEOUT):
@@ -217,6 +218,13 @@ class GoogleSync(models.AbstractModel):
]])
return self.with_context(active_test=False).search(domain)
+ def _write_from_google(self, gevent, vals):
+ self.write(vals)
+
+ @api.model
+ def _create_from_google(self, gevents, vals_list):
+ return self.create(vals_list)
+
@api.model
def _odoo_values(self, google_event: GoogleEvent, default_reminders=()):
"""Implements this method to return a dict of Odoo values corresponding
diff --git a/addons/google_calendar/models/res_users.py b/addons/google_calendar/models/res_users.py
index 416cc9f8667..9b7b21e4b84 100644
--- a/addons/google_calendar/models/res_users.py
+++ b/addons/google_calendar/models/res_users.py
@@ -89,6 +89,7 @@ class User(models.Model):
self.google_calendar_sync_token = next_sync_token
# Google -> Odoo
+ events.clear_type_ambiguity(self.env)
recurrences = events.filter(lambda e: e.is_recurrence())
synced_recurrences = self.env['calendar.recurrence']._sync_google2odoo(recurrences)
synced_events = self.env['calendar.event']._sync_google2odoo(events - recurrences, default_reminders=default_reminders)
diff --git a/addons/google_calendar/tests/__init__.py b/addons/google_calendar/tests/__init__.py
index 760fa8a0d4c..7eafa0298f7 100644
--- a/addons/google_calendar/tests/__init__.py
+++ b/addons/google_calendar/tests/__init__.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from . import test_sync_common
from . import test_sync_google2odoo
from . import test_sync_odoo2google
diff --git a/addons/google_calendar/tests/test_sync_common.py b/addons/google_calendar/tests/test_sync_common.py
new file mode 100644
index 00000000000..d3d6062d90f
--- /dev/null
+++ b/addons/google_calendar/tests/test_sync_common.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import datetime, date
+from dateutil.relativedelta import relativedelta
+from unittest.mock import MagicMock, patch
+
+from odoo.tests.common import SavepointCase
+from odoo.addons.google_calendar.utils.google_calendar import GoogleCalendarService
+from odoo.addons.google_calendar.models.res_users import User
+from odoo.addons.google_calendar.models.google_sync import GoogleSync
+from odoo.addons.google_account.models.google_service import TIMEOUT
+
+
+def patch_api(func):
+ @patch.object(GoogleSync, '_google_insert', MagicMock())
+ @patch.object(GoogleSync, '_google_delete', MagicMock())
+ @patch.object(GoogleSync, '_google_patch', MagicMock())
+ def patched(self, *args, **kwargs):
+ return func(self, *args, **kwargs)
+ return patched
+
+@patch.object(User, '_get_google_calendar_token', lambda user: 'dummy-token')
+class TestSyncGoogle(SavepointCase):
+
+ def setUp(self):
+ super().setUp()
+ self.google_service = GoogleCalendarService(self.env['google.service'])
+
+ def assertGoogleEventDeleted(self, google_id):
+ GoogleSync._google_delete.assert_called()
+ args, kwargs = GoogleSync._google_delete.call_args
+ self.assertEqual(args[1], google_id, "Event should have been deleted")
+
+ def assertGoogleEventNotDeleted(self):
+ GoogleSync._google_delete.assert_not_called()
+
+ def assertGoogleEventInserted(self, values, timeout=None):
+ expected_args = (values,)
+ expected_kwargs = {'timeout': timeout} if timeout else {}
+ GoogleSync._google_insert.assert_called_once()
+ args, kwargs = GoogleSync._google_insert.call_args
+ self.assertEqual(args[1:], expected_args) # skip Google service arg
+ self.assertEqual(kwargs, expected_kwargs)
+
+ def assertGoogleEventNotInserted(self):
+ GoogleSync._google_insert.assert_not_called()
+
+ def assertGoogleEventPatched(self, google_id, values, timeout=None):
+ expected_args = (google_id, values)
+ expected_kwargs = {'timeout': timeout} if timeout else {}
+ GoogleSync._google_patch.assert_called_once()
+ args, kwargs = GoogleSync._google_patch.call_args
+ self.assertEqual(args[1:], expected_args) # skip Google service arg
+ self.assertEqual(kwargs, expected_kwargs)
+
+ def assertGoogleEventNotPatched(self):
+ GoogleSync._google_patch.assert_not_called()
+
+ def assertGoogleAPINotCalled(self):
+ self.assertGoogleEventNotPatched()
+ self.assertGoogleEventNotInserted()
+ self.assertGoogleEventNotDeleted()
diff --git a/addons/google_calendar/tests/test_sync_google2odoo.py b/addons/google_calendar/tests/test_sync_google2odoo.py
index fe497910723..d6c998fcbf3 100644
--- a/addons/google_calendar/tests/test_sync_google2odoo.py
+++ b/addons/google_calendar/tests/test_sync_google2odoo.py
@@ -4,20 +4,24 @@
from odoo.addons.google_calendar.utils.google_calendar import GoogleEvent
import pytz
from datetime import datetime, date
-from odoo.tests.common import SavepointCase, new_test_user
+from dateutil.relativedelta import relativedelta
+from odoo.tests.common import new_test_user
+from odoo.addons.google_calendar.tests.test_sync_common import TestSyncGoogle, patch_api
-class TestSyncGoogle2Odoo(SavepointCase):
+class TestSyncGoogle2Odoo(TestSyncGoogle):
@property
def now(self):
return pytz.utc.localize(datetime.now()).isoformat()
def sync(self, events):
+ events.clear_type_ambiguity(self.env)
google_recurrence = events.filter(GoogleEvent.is_recurrence)
self.env['calendar.recurrence']._sync_google2odoo(google_recurrence)
self.env['calendar.event']._sync_google2odoo(events - google_recurrence)
+ @patch_api
def test_new_google_event(self):
values = {
'id': 'oj44nep1ldf8a3ll02uip0c9aa',
@@ -53,7 +57,9 @@ class TestSyncGoogle2Odoo(SavepointCase):
self.assertEqual('Mitchell Admin', admin_attendee.partner_id.name)
self.assertEqual(event.partner_ids, event.attendee_ids.partner_id)
self.assertEqual('needsAction', admin_attendee.state)
+ self.assertGoogleAPINotCalled()
+ @patch_api
def test_invalid_owner_property(self):
values = {
'id': 'oj44nep1ldf8a3ll02uip0c9aa',
@@ -78,7 +84,9 @@ class TestSyncGoogle2Odoo(SavepointCase):
self.env['calendar.event']._sync_google2odoo(GoogleEvent([values]))
event = self.env['calendar.event'].search([('google_id', '=', values.get('id'))])
self.assertEqual(event.user_id, self.env.user)
+ self.assertGoogleAPINotCalled()
+ @patch_api
def test_valid_owner_property(self):
user = new_test_user(self.env, login='calendar-user')
values = {
@@ -104,7 +112,9 @@ class TestSyncGoogle2Odoo(SavepointCase):
self.env['calendar.event']._sync_google2odoo(GoogleEvent([values]))
event = self.env['calendar.event'].search([('google_id', '=', values.get('id'))])
self.assertEqual(event.user_id, user)
+ self.assertGoogleAPINotCalled()
+ @patch_api
def test_cancelled(self):
google_id = 'oj44nep1ldf8a3ll02uip0c9aa'
event = self.env['calendar.event'].create({
@@ -113,6 +123,7 @@ class TestSyncGoogle2Odoo(SavepointCase):
'stop': date(2020, 1, 6),
'google_id': google_id,
'user_id': self.env.user.id,
+ 'need_sync': False,
'partner_ids': [(6, 0, self.env.user.partner_id.ids)] # current user is attendee
})
gevent = GoogleEvent([{
@@ -121,14 +132,18 @@ class TestSyncGoogle2Odoo(SavepointCase):
}])
self.sync(gevent)
self.assertFalse(event.exists())
+ self.assertGoogleAPINotCalled()
+ @patch_api
def test_attendee_cancelled(self):
google_id = 'oj44nep1ldf8a3ll02uip0c9aa'
event = self.env['calendar.event'].create({
'name': 'coucou',
'start': date(2020, 1, 6),
'stop': date(2020, 1, 6),
+ 'allday': True,
'google_id': google_id,
+ 'need_sync': False,
'user_id': False, # Not the current user
'partner_ids': [(6, 0, self.env.user.partner_id.ids)] # current user is attendee
})
@@ -141,7 +156,18 @@ class TestSyncGoogle2Odoo(SavepointCase):
user_attendee = event.attendee_ids
self.assertTrue(user_attendee)
self.assertEqual(user_attendee.state, 'declined')
+ # To avoid 403 errors, we send a limited dictionnary when we don't have write access.
+ # guestsCanModify property is not properly handled yet
+ self.assertGoogleEventPatched(event.google_id, {
+ 'id': event.google_id,
+ 'start': {'date': str(event.start_date)},
+ 'end': {'date': str(event.stop_date + relativedelta(days=1))},
+ 'attendees': [{'email': 'odoobot@example.com', 'responseStatus': 'declined'}],
+ 'extendedProperties': {'private': {'%s_odoo_id' % self.env.cr.dbname: event.id}},
+ 'reminders': {'overrides': [], 'useDefault': False},
+ })
+ @patch_api
def test_attendee_removed(self):
user = new_test_user(self.env, login='calendar-user')
google_id = 'oj44nep1ldf8a3ll02uip0c9aa'
@@ -151,6 +177,7 @@ class TestSyncGoogle2Odoo(SavepointCase):
'stop': date(2020, 1, 6),
'google_id': google_id,
'user_id': False, # user is not owner
+ 'need_sync': False,
'partner_ids': [(6, 0, user.partner_id.ids)], # but user is attendee
})
gevent = GoogleEvent([{
@@ -177,7 +204,9 @@ class TestSyncGoogle2Odoo(SavepointCase):
# User attendee removed but gevent owner might be added after synch.
self.assertNotEqual(event.attendee_ids.partner_id, user.partner_id)
self.assertNotEqual(event.partner_ids, user.partner_id)
+ self.assertGoogleAPINotCalled()
+ @patch_api
def test_recurrence(self):
recurrence_id = 'oj44nep1ldf8a3ll02uip0c9aa'
values = {
@@ -206,7 +235,9 @@ class TestSyncGoogle2Odoo(SavepointCase):
self.assertEqual(events[0].google_id, '%s_20200106' % recurrence_id)
self.assertEqual(events[1].google_id, '%s_20200113' % recurrence_id)
self.assertEqual(events[2].google_id, '%s_20200120' % recurrence_id)
+ self.assertGoogleAPINotCalled()
+ @patch_api
def test_recurrence_datetime(self):
recurrence_id = 'oj44nep1ldf8a3ll02uip0c9aa'
values = {
@@ -232,7 +263,9 @@ class TestSyncGoogle2Odoo(SavepointCase):
self.assertEqual(events[0].google_id, '%s_20200106T170000Z' % recurrence_id)
self.assertEqual(events[1].google_id, '%s_20200113T170000Z' % recurrence_id)
self.assertEqual(events[2].google_id, '%s_20200120T170000Z' % recurrence_id)
+ self.assertGoogleAPINotCalled()
+ @patch_api
def test_recurrence_exdate(self):
recurrence_id = 'oj44nep1ldf8a3ll02uip0c9aa'
events = GoogleEvent([{
@@ -257,7 +290,9 @@ class TestSyncGoogle2Odoo(SavepointCase):
self.assertEqual(len(events), 2, "it should have created a recurrence with 2 events")
self.assertEqual(events[0].start_date, date(2020, 1, 6))
self.assertEqual(events[1].start_date, date(2020, 1, 20))
+ self.assertGoogleAPINotCalled()
+ @patch_api
def test_recurrence_first_exdate(self):
recurrence_id = "4c0de517evkk3ra294lmut57vm"
events = GoogleEvent([{
@@ -284,7 +319,9 @@ class TestSyncGoogle2Odoo(SavepointCase):
self.assertEqual(len(events), 2, "it should have created a recurrence with 2 events")
self.assertEqual(events[0].start_date, date(2020, 1, 13))
self.assertEqual(events[1].start_date, date(2020, 1, 20))
+ self.assertGoogleAPINotCalled()
+ @patch_api
def test_recurrencde_first_updated(self):
recurrence_id = "4c0de517evkk3ra294lmut57vm"
events = GoogleEvent([{
@@ -313,13 +350,23 @@ class TestSyncGoogle2Odoo(SavepointCase):
self.assertEqual(events[0].name, 'edited')
self.assertEqual(events[1].name, 'rrule')
self.assertEqual(events[2].name, 'rrule')
+ self.assertGoogleAPINotCalled()
+ @patch_api
def test_existing_recurrence_first_updated(self):
recurrence_id = "4c0de517evkk3ra294lmut57vm"
+ base_event = self.env['calendar.event'].create({
+ 'name': 'coucou',
+ 'allday': True,
+ 'start': datetime(2020, 1, 6),
+ 'stop': datetime(2020, 1, 6),
+ 'need_sync': False,
+ })
recurrence = self.env['calendar.recurrence'].create({
'google_id': recurrence_id,
'rrule': 'FREQ=WEEKLY;WKST=SU;COUNT=3;BYDAY=MO',
- 'base_event_id': self.env['calendar.event'].create({'name': 'coucou', 'allday': True, 'start': datetime(2020, 1, 6), 'stop': datetime(2020, 1, 6)}).id,
+ 'need_sync': False,
+ 'base_event_id': base_event.id,
})
recurrence._apply_recurrence()
values = [{
@@ -339,7 +386,9 @@ class TestSyncGoogle2Odoo(SavepointCase):
self.assertEqual(events[0].name, 'edited')
self.assertEqual(events[1].name, 'coucou')
self.assertEqual(events[2].name, 'coucou')
+ self.assertGoogleAPINotCalled()
+ @patch_api
def test_recurrence_outlier(self):
recurrence_id = 'oj44nep1ldf8a3ll02uip0c9aa'
events = GoogleEvent([{
@@ -368,20 +417,28 @@ class TestSyncGoogle2Odoo(SavepointCase):
self.assertEqual(events[0].start_date, date(2020, 1, 6))
self.assertEqual(events[1].start_date, date(2020, 1, 18), "It should not be in sync with the recurrence")
self.assertEqual(events[2].start_date, date(2020, 1, 20))
+ self.assertGoogleAPINotCalled()
+ @patch_api
def test_recurrence_moved(self):
- recurrence_id = 'aaaaaa'
- event = self.env['calendar.event'].create({
+ google_id = 'oj44nep1ldf8a3ll02uip0c9aa'
+ base_event = self.env['calendar.event'].create({
'name': 'coucou',
- 'start': date(2020, 1, 6),
- 'stop': date(2020, 1, 6),
- 'rrule': 'FREQ=WEEKLY;COUNT=2;BYDAY=MO',
- 'recurrency': True,
'allday': True,
+ 'start': datetime(2020, 1, 6),
+ 'stop': datetime(2020, 1, 6),
+ 'need_sync': False,
+ })
+ recurrence = self.env['calendar.recurrence'].create({
+ 'google_id': google_id,
+ 'rrule': 'FREQ=WEEKLY;COUNT=2;BYDAY=MO',
+ 'need_sync': False,
+ 'base_event_id': base_event.id,
+ 'calendar_event_ids': [(4, base_event.id)],
})
- event.recurrence_id.google_id = recurrence_id
+ recurrence._apply_recurrence()
values = {
- 'id': recurrence_id,
+ 'id': google_id,
'summary': 'coucou',
'recurrence': ['RRULE:FREQ=WEEKLY;COUNT=2;BYDAY=WE'], # Now wednesday
'start': {'date': '2020-01-08'},
@@ -390,10 +447,236 @@ class TestSyncGoogle2Odoo(SavepointCase):
'updated': self.now,
}
self.env['calendar.recurrence']._sync_google2odoo(GoogleEvent([values]))
- recurrence = self.env['calendar.recurrence'].search([('google_id', '=', recurrence_id)])
events = recurrence.calendar_event_ids.sorted('start')
+ self.assertEqual(len(events), 2)
self.assertEqual(recurrence.rrule, 'FREQ=WEEKLY;COUNT=2;BYDAY=WE')
self.assertEqual(events[0].start_date, date(2020, 1, 8))
self.assertEqual(events[1].start_date, date(2020, 1, 15))
- self.assertEqual(events[0].google_id, '%s_20200108' % recurrence_id)
- self.assertEqual(events[1].google_id, '%s_20200115' % recurrence_id)
+ self.assertEqual(events[0].google_id, '%s_20200108' % google_id)
+ self.assertEqual(events[1].google_id, '%s_20200115' % google_id)
+ self.assertGoogleAPINotCalled()
+
+ @patch_api
+ def test_recurrence_name_updated(self):
+ google_id = 'oj44nep1ldf8a3ll02uip0c9aa'
+ base_event = self.env['calendar.event'].create({
+ 'name': 'coucou',
+ 'allday': True,
+ 'start': datetime(2020, 1, 6),
+ 'stop': datetime(2020, 1, 6),
+ 'need_sync': False,
+ })
+ recurrence = self.env['calendar.recurrence'].create({
+ 'google_id': google_id,
+ 'rrule': 'FREQ=WEEKLY;COUNT=2;BYDAY=MO',
+ 'need_sync': False,
+ 'base_event_id': base_event.id,
+ 'calendar_event_ids': [(4, base_event.id)],
+ })
+ recurrence._apply_recurrence()
+
+ values = {
+ 'id': google_id,
+ 'summary': 'coucou again',
+ 'recurrence': ['RRULE:FREQ=WEEKLY;COUNT=2;BYDAY=MO'],
+ 'start': {'date': '2020-01-06'},
+ 'end': {'date': '2020-01-07'},
+ 'reminders': {'useDefault': True},
+ 'updated': self.now,
+ }
+ self.env['calendar.recurrence']._sync_google2odoo(GoogleEvent([values]))
+ events = recurrence.calendar_event_ids.sorted('start')
+ self.assertEqual(len(events), 2)
+ self.assertEqual(recurrence.rrule, 'FREQ=WEEKLY;COUNT=2;BYDAY=MO')
+ self.assertEqual(events.mapped('name'), ['coucou again','coucou again'])
+ self.assertEqual(events[0].start_date, date(2020, 1, 6))
+ self.assertEqual(events[1].start_date, date(2020, 1, 13))
+ self.assertEqual(events[0].google_id, '%s_20200106' % google_id)
+ self.assertEqual(events[1].google_id, '%s_20200113' % google_id)
+ self.assertGoogleAPINotCalled()
+
+ @patch_api
+ def test_recurrence_write_with_outliers(self):
+ google_id = 'oj44nep1ldf8a3ll02uip0c9aa'
+ base_event = self.env['calendar.event'].create({
+ 'name': 'coucou',
+ 'start': datetime(2021, 2, 15, 8, 0, 0),
+ 'stop': datetime(2021, 2, 15, 10, 0, 0),
+ 'need_sync': False,
+ })
+ recurrence = self.env['calendar.recurrence'].create({
+ 'google_id': google_id,
+ 'rrule': 'FREQ=WEEKLY;COUNT=3;BYDAY=MO',
+ 'need_sync': False,
+ 'base_event_id': base_event.id,
+ 'calendar_event_ids': [(4, base_event.id)],
+ })
+ recurrence._apply_recurrence()
+ events = recurrence.calendar_event_ids.sorted('start')
+ self.assertEqual(events[0].google_id, '%s_20210215T080000Z' % google_id)
+ self.assertEqual(events[1].google_id, '%s_20210222T080000Z' % google_id)
+ self.assertEqual(events[2].google_id, '%s_20210301T080000Z' % google_id)
+ # Modify start of one of the events.
+ middle_event = recurrence.calendar_event_ids.filtered(lambda e: e.start == datetime(2021, 2, 22, 8, 0, 0))
+ middle_event.write({
+ 'start': datetime(2021, 2, 22, 16, 0, 0),
+ 'need_sync': False,
+ })
+
+ values = {
+ 'id': google_id,
+ 'summary': 'coucou again',
+ 'recurrence': ['RRULE:FREQ=WEEKLY;COUNT=3;BYDAY=MO'],
+ 'start': {'dateTime': '2021-02-15T09:00:00+01:00'}, # 8:00 UTC
+ 'end': {'dateTime': '2021-02-15-T11:00:00+01:00'},
+ 'reminders': {'useDefault': True},
+ 'updated': self.now,
+ }
+ self.env['calendar.recurrence']._sync_google2odoo(GoogleEvent([values]))
+ events = recurrence.calendar_event_ids.sorted('start')
+ self.assertEqual(len(events), 3)
+ self.assertEqual(recurrence.rrule, 'FREQ=WEEKLY;COUNT=3;BYDAY=MO')
+ self.assertEqual(events.mapped('name'), ['coucou again', 'coucou again', 'coucou again'])
+ self.assertEqual(events[0].start, datetime(2021, 2, 15, 8, 0, 0))
+ self.assertEqual(events[1].start, datetime(2021, 2, 22, 16, 0, 0))
+ self.assertEqual(events[2].start, datetime(2021, 3, 1, 8, 0, 0))
+ # the google_id of recurrent events should not be modified when events start is modified.
+ # the original start date or datetime should always be present.
+ self.assertEqual(events[0].google_id, '%s_20210215T080000Z' % google_id)
+ self.assertEqual(events[1].google_id, '%s_20210222T080000Z' % google_id)
+ self.assertEqual(events[2].google_id, '%s_20210301T080000Z' % google_id)
+ self.assertGoogleAPINotCalled()
+
+
+ @patch_api
+ def test_recurrence_write_time_fields(self):
+ google_id = 'oj44nep1ldf8a3ll02uip0c9aa'
+ base_event = self.env['calendar.event'].create({
+ 'name': 'coucou',
+ 'start': datetime(2021, 2, 15, 8, 0, 0),
+ 'stop': datetime(2021, 2, 15, 10, 0, 0),
+ 'need_sync': False,
+ })
+ recurrence = self.env['calendar.recurrence'].create({
+ 'google_id': google_id,
+ 'rrule': 'FREQ=WEEKLY;COUNT=3;BYDAY=MO',
+ 'need_sync': False,
+ 'base_event_id': base_event.id,
+ 'calendar_event_ids': [(4, base_event.id)],
+ })
+ recurrence._apply_recurrence()
+ # Google modifies the start/stop of the base event
+ # When the start/stop or all day values are updated, the recurrence should reapplied.
+
+ values = {
+ 'id': google_id,
+ 'summary': "It's me again",
+ 'recurrence': ['RRULE:FREQ=WEEKLY;COUNT=4;BYDAY=MO'],
+ 'start': {'dateTime': '2021-02-15T12:00:00+01:00'}, # 11:00 UTC
+ 'end': {'dateTime': '2021-02-15-T15:00:00+01:00'},
+ 'reminders': {'useDefault': True},
+ 'updated': self.now,
+ }
+
+ self.env['calendar.recurrence']._sync_google2odoo(GoogleEvent([values]))
+ events = recurrence.calendar_event_ids.sorted('start')
+ self.assertEqual(events[0].start, datetime(2021, 2, 15, 11, 0, 0))
+ self.assertEqual(events[1].start, datetime(2021, 2, 22, 11, 0, 0))
+ self.assertEqual(events[2].start, datetime(2021, 3, 1, 11, 0, 0))
+ self.assertEqual(events[3].start, datetime(2021, 3, 8, 11, 0, 0))
+ # We ensure that our modifications are pushed
+ self.assertGoogleAPINotCalled()
+
+ @patch_api
+ def test_recurrence_deleted(self):
+ google_id = 'oj44nep1ldf8a3ll02uip0c9aa'
+ base_event = self.env['calendar.event'].create({
+ 'name': 'coucou',
+ 'start': datetime(2021, 2, 15, 8, 0, 0),
+ 'stop': datetime(2021, 2, 15, 10, 0, 0),
+ 'need_sync': False,
+ })
+ recurrence = self.env['calendar.recurrence'].create({
+ 'google_id': google_id,
+ 'rrule': 'FREQ=WEEKLY;COUNT=3;BYDAY=MO',
+ 'need_sync': False,
+ 'base_event_id': base_event.id,
+ 'calendar_event_ids': [(4, base_event.id)],
+ })
+ recurrence._apply_recurrence()
+ events = recurrence.calendar_event_ids
+ values = {
+ 'id': google_id,
+ 'status': 'cancelled',
+ }
+ self.sync(GoogleEvent([values]))
+ self.assertFalse(recurrence.exists(), "The recurrence should be deleted")
+ self.assertFalse(events.exists(), "All events should be deleted")
+ self.assertGoogleAPINotCalled()
+
+ @patch_api
+ def test_recurrence_timezone(self):
+ """ Ensure that the timezone of the base_event is saved on the recurrency
+ Google save the TZ on the event and we save it on the recurrency.
+ """
+ recurrence_id = 'oj44nep1ldf8a3ll02uip0c9aa'
+ values = {
+ 'id': recurrence_id,
+ 'description': '',
+ 'organizer': {'email': 'odoocalendarref@gmail.com', 'self': True},
+ 'summary': 'Event with ',
+ 'visibility': 'public',
+ 'recurrence': ['RRULE:FREQ=WEEKLY;WKST=SU;COUNT=3;BYDAY=MO'],
+ 'reminders': {'useDefault': True},
+ 'start': {'dateTime': '2020-01-06T18:00:00+01:00', 'timeZone': 'Pacific/Auckland'},
+ 'end': {'dateTime': '2020-01-06T19:00:00+01:00', 'timeZone': 'Pacific/Auckland'},
+ }
+ self.env['calendar.recurrence']._sync_google2odoo(GoogleEvent([values]))
+ recurrence = self.env['calendar.recurrence'].search([('google_id', '=', values.get('id'))])
+ self.assertEqual(recurrence.event_tz, 'Pacific/Auckland', "The Google event Timezone should be saved on the recurrency")
+ self.assertGoogleAPINotCalled()
+
+ @patch_api
+ def test_simple_event_into_recurrency(self):
+ """ Synched single events should be converted in recurrency without problems"""
+ google_id = 'aaaaaaaaaaaa'
+ values = {
+ 'id': google_id,
+ 'description': 'Small mini desc',
+ 'organizer': {'email': 'odoocalendarref@gmail.com', 'self': True},
+ 'summary': 'Pricing new update',
+ 'visibility': 'public',
+ 'attendees': [{
+ 'displayName': 'Mitchell Admin',
+ 'email': 'admin@yourcompany.example.com',
+ 'responseStatus': 'needsAction'
+ }, ],
+ 'reminders': {'useDefault': True},
+ 'start': {
+ 'dateTime': '2020-01-06T18:00:00+01:00',
+ 'timeZone': 'Europe/Brussels'
+ },
+ 'end': {
+ 'dateTime': '2020-01-13T19:55:00+01:00',
+ 'timeZone': 'Europe/Brussels'
+ },
+ }
+ self.env['calendar.event']._sync_google2odoo(GoogleEvent([values]))
+ # The event is transformed into a recurrency on google
+ values = {
+ 'id': google_id,
+ 'description': '',
+ 'organizer': {'email': 'odoocalendarref@gmail.com', 'self': True},
+ 'summary': 'Event with ',
+ 'visibility': 'public',
+ 'recurrence': ['RRULE:FREQ=WEEKLY;WKST=SU;COUNT=3;BYDAY=MO'],
+ 'reminders': {'useDefault': True},
+ 'start': {'dateTime': '2020-01-06T18:00:00+01:00', 'timeZone': 'Europe/Brussels'},
+ 'end': {'dateTime': '2020-01-06T19:00:00+01:00', 'timeZone': 'Europe/Brussels'},
+ }
+ recurrence = self.env['calendar.recurrence']._sync_google2odoo(GoogleEvent([values]))
+ events = recurrence.calendar_event_ids.sorted('start')
+ self.assertEqual(len(events), 3, "it should have created a recurrence with 3 events")
+ event = self.env['calendar.event'].search([('google_id', '=', values.get('id'))])
+ self.assertFalse(event.exists(), "The old event should not exits anymore")
+ self.assertGoogleAPINotCalled()
diff --git a/addons/google_calendar/tests/test_sync_odoo2google.py b/addons/google_calendar/tests/test_sync_odoo2google.py
index 3319d414fd2..3571b6bda39 100644
--- a/addons/google_calendar/tests/test_sync_odoo2google.py
+++ b/addons/google_calendar/tests/test_sync_odoo2google.py
@@ -5,51 +5,21 @@ from datetime import datetime, date
from dateutil.relativedelta import relativedelta
from unittest.mock import MagicMock, patch
-from odoo.tests.common import SavepointCase
from odoo.addons.google_calendar.utils.google_calendar import GoogleCalendarService
from odoo.addons.google_calendar.models.res_users import User
from odoo.addons.google_calendar.models.google_sync import GoogleSync
from odoo.modules.registry import Registry
from odoo.addons.google_account.models.google_service import TIMEOUT
+from odoo.addons.google_calendar.tests.test_sync_common import TestSyncGoogle, patch_api
-def patch_api(func):
- @patch.object(GoogleSync, '_google_insert', MagicMock())
- @patch.object(GoogleSync, '_google_delete', MagicMock())
- @patch.object(GoogleSync, '_google_patch', MagicMock())
- def patched(self, *args, **kwargs):
- return func(self, *args, **kwargs)
- return patched
-
@patch.object(User, '_get_google_calendar_token', lambda user: 'dummy-token')
-class TestSyncOdoo2Google(SavepointCase):
+class TestSyncOdoo2Google(TestSyncGoogle):
def setUp(self):
super().setUp()
self.google_service = GoogleCalendarService(self.env['google.service'])
- def assertGoogleEventDeleted(self, google_id):
- GoogleSync._google_delete.assert_called()
- args, kwargs = GoogleSync._google_delete.call_args
- self.assertEqual(args[1], google_id, "Event should have been deleted")
-
- def assertGoogleEventNotDeleted(self):
- GoogleSync._google_delete.assert_not_called()
-
- def assertGoogleEventInserted(self, values):
- GoogleSync._google_insert.assert_called_once_with(self.google_service, values)
-
- def assertGoogleEventNotInserted(self):
- GoogleSync._google_insert.assert_not_called()
-
- def assertGoogleEventPatched(self, google_id, values, timeout=None):
- expected_args = (google_id, values)
- expected_kwargs = {'timeout': timeout} if timeout else {}
- GoogleSync._google_patch.assert_called_once()
- args, kwargs = GoogleSync._google_patch.call_args
- self.assertEqual(args[1:], expected_args) # skip Google service arg
- self.assertEqual(kwargs, expected_kwargs)
-
@patch_api
def test_event_creation(self):
partner = self.env['res.partner'].create({'name': 'Jean-Luc', 'email': 'jean-luc@opoo.com'})
@@ -200,6 +170,22 @@ class TestSyncOdoo2Google(SavepointCase):
self.assertTrue(to_delete)
self.assertFalse(to_delete.active)
self.assertFalse(event.google_id, "The google id will be set after the API call")
+ self.assertGoogleEventInserted({
+ 'id': False,
+ 'start': {'date': '2020-01-15'},
+ 'end': {'date': '2020-01-16'},
+ 'summary': 'Event',
+ 'description': '',
+ 'location': '',
+ 'visibility': 'public',
+ 'guestsCanModify': True,
+ 'reminders': {'overrides': [], 'useDefault': False},
+ 'organizer': {'email': 'odoobot@example.com', 'self': True},
+ 'attendees': [],
+ 'recurrence': ['RRULE:FREQ=WEEKLY;COUNT=2;BYDAY=WE'],
+ 'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: event.recurrence_id.id}}
+ }, timeout=3)
+
self.assertGoogleEventDeleted(google_id)
@patch_api
@@ -246,3 +232,169 @@ class TestSyncOdoo2Google(SavepointCase):
'reminders': {'overrides': [], 'useDefault': False},
'visibility': 'public',
}, timeout=3)
+
+ @patch_api
+ def test_all_event_updated(self):
+ google_id = 'aaaaaaaaa'
+ event = self.env['calendar.event'].create({
+ 'name': "Event",
+ 'start': datetime(2020, 1, 15),
+ 'stop': datetime(2020, 1, 15),
+ 'allday': True,
+ 'need_sync': False,
+ })
+ recurrence = self.env['calendar.recurrence'].create({
+ 'google_id': google_id,
+ 'rrule': 'FREQ=WEEKLY;COUNT=2;BYDAY=WE',
+ 'base_event_id': event.id,
+ 'need_sync': False,
+ })
+ recurrence._apply_recurrence()
+ event.write({
+ 'name': 'New name',
+ 'recurrence_update': 'all_events',
+ })
+ self.assertGoogleEventPatched(recurrence.google_id, {
+ 'id': recurrence.google_id,
+ 'start': {'date': str(event.start_date)},
+ 'end': {'date': str(event.stop_date + relativedelta(days=1))},
+ 'summary': 'New name',
+ 'description': '',
+ 'location': '',
+ 'guestsCanModify': True,
+ 'organizer': {'email': 'odoobot@example.com', 'self': True},
+ 'attendees': [],
+ 'recurrence': ['RRULE:FREQ=WEEKLY;COUNT=2;BYDAY=WE'],
+ 'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: recurrence.id}},
+ 'reminders': {'overrides': [], 'useDefault': False},
+ 'visibility': 'public',
+ }, timeout=3)
+
+ @patch_api
+ def test_event_need_sync(self):
+ event = self.env['calendar.event'].create({
+ 'name': "Event",
+ 'start': datetime(2020, 1, 15),
+ 'stop': datetime(2020, 1, 15),
+ 'allday': True,
+ 'recurrence_id': False,
+ 'recurrency': True,
+ })
+ self.assertFalse(event.need_sync,
+ "Event created with True recurrency should not be synched to avoid "
+ "duplicate event on google")
+
+ recurrence = self.env['calendar.recurrence'].create({
+ 'google_id': False,
+ 'rrule': 'FREQ=WEEKLY;COUNT=2;BYDAY=WE',
+ 'base_event_id': event.id,
+ 'need_sync': False,
+ })
+ event_2 = self.env['calendar.event'].create({
+ 'name': "Event",
+ 'start': datetime(2020, 1, 15),
+ 'stop': datetime(2020, 1, 15),
+ 'allday': True,
+ 'recurrence_id': recurrence.id,
+ })
+ self.assertFalse(event_2.need_sync,
+ "Event created with recurrence_id should not be synched to avoid "
+ "duplicate event on google")
+
+ self.assertGoogleEventNotInserted()
+ self.assertGoogleEventNotDeleted()
+
+
+ @patch_api
+ def test_event_until_utc(self):
+ """ UNTIl rrule value must be in UTC: ending with a 'Z """
+ google_id = 'aaaaaaaaa'
+ event = self.env['calendar.event'].create({
+ 'name': "Event",
+ 'start': datetime(2020, 1, 15),
+ 'stop': datetime(2020, 1, 15),
+ 'allday': True,
+ 'need_sync': False,
+ })
+ recurrence = self.env['calendar.recurrence'].create({
+ 'google_id': google_id,
+ 'rrule': 'FREQ=DAILY;UNTIL=20200117T235959',
+ 'base_event_id': event.id,
+ 'need_sync': False,
+ })
+ recurrence._apply_recurrence()
+ self.assertEqual(recurrence._google_values()['recurrence'][0], 'RRULE:FREQ=DAILY;UNTIL=20200117T235959Z',
+ "The rrule sent to google should be in UTC: end with Z")
+ # Add it even if it is not the end of the string
+ recurrence.write({'rrule': 'FREQ=DAILY;UNTIL=20200118T235959;INTERVAL=3'})
+ recurrence._apply_recurrence()
+ self.assertEqual(recurrence._google_values()['recurrence'][0],
+ 'RRULE:FREQ=DAILY;UNTIL=20200118T235959Z;INTERVAL=3',
+ "The rrule sent to google should be in UTC: end with Z and preserve the following parameters")
+ # Don't add two Z at the end of the UNTIL value
+ recurrence.write({'rrule': 'FREQ=DAILY;UNTIL=20200119T235959Z'})
+ recurrence._apply_recurrence()
+ self.assertEqual(recurrence._google_values()['recurrence'][0], 'RRULE:FREQ=DAILY;UNTIL=20200119T235959Z',
+ "The rrule sent to google should be in UTC: end with one Z")
+
+ @patch_api
+ def test_write_unsynced_field(self):
+ google_id = 'aaaaaaaaa'
+ event = self.env['calendar.event'].create({
+ 'name': "Event",
+ 'start': datetime(2021, 3, 10),
+ 'stop': datetime(2021, 3, 10),
+ 'allday': True,
+ 'need_sync': False,
+ })
+ recurrence = self.env['calendar.recurrence'].create({
+ 'google_id': google_id,
+ 'rrule': 'FREQ=WEEKLY;COUNT=2;BYDAY=WE',
+ 'base_event_id': event.id,
+ 'need_sync': False,
+ })
+ recurrence._apply_recurrence()
+ event.write({
+ 'start': datetime(2021, 3, 11),
+ 'stop': datetime(2021, 3, 11),
+ 'need_sync': False,
+ })
+ event_type = self.env['calendar.event.type'].create({'name': 'type'})
+ event.write({
+ 'recurrence_update': 'all_events',
+ 'categ_ids': [(4, event_type.id)]
+ })
+ self.assertTrue(all(e.categ_ids == event_type for e in recurrence.calendar_event_ids))
+ self.assertGoogleAPINotCalled()
+
+ @patch_api
+ def test_attendee_state(self):
+ "Sync attendee state immediately"
+ partner = self.env['res.partner'].create({'name': 'Jean-Luc', 'email': 'jean-luc@opoo.com'})
+ event = self.env['calendar.event'].create({
+ 'name': "Event with attendees",
+ 'start': datetime(2020, 1, 15),
+ 'stop': datetime(2020, 1, 15),
+ 'allday': True,
+ 'need_sync': False,
+ 'partner_ids': [(4, partner.id)],
+ 'google_id': 'aaaaaaaaa',
+ })
+ self.assertEqual(event.attendee_ids.state, 'needsAction',
+ "The attendee state should be 'needsAction")
+
+ event.attendee_ids.write({'state': 'declined'})
+ self.assertGoogleEventPatched(event.google_id, {
+ 'id': event.google_id,
+ 'start': {'date': str(event.start_date)},
+ 'end': {'date': str(event.stop_date + relativedelta(days=1))},
+ 'summary': 'Event with attendees',
+ 'description': '',
+ 'location': '',
+ 'guestsCanModify': True,
+ 'organizer': {'email': 'odoobot@example.com', 'self': True},
+ 'attendees': [{'email': 'jean-luc@opoo.com', 'responseStatus': 'declined'}],
+ 'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: event.id}},
+ 'reminders': {'overrides': [], 'useDefault': False},
+ 'visibility': 'public',
+ })
diff --git a/addons/google_calendar/utils/google_event.py b/addons/google_calendar/utils/google_event.py
index 8c11af3ce41..ed691dc6ef1 100644
--- a/addons/google_calendar/utils/google_event.py
+++ b/addons/google_calendar/utils/google_event.py
@@ -3,6 +3,7 @@
from odoo.api import model
from odoo.tools.sql import existing_tables
import pytz
+import logging
from typing import Iterator, Mapping
from collections import abc
from dateutil.parser import parse
@@ -11,6 +12,8 @@ from dateutil.relativedelta import relativedelta
from odoo import _
+_logger = logging.getLogger(__name__)
+
class GoogleEvent(abc.Set):
"""This helper class holds the values of a Google event.
@@ -76,7 +79,7 @@ class GoogleEvent(abc.Set):
"""Returns the Odoo id stored in the Google Event metadata.
This id might not actually exists in the database.
"""
- properties = self.extendedProperties and self.extendedProperties.get('shared', {}) or {}
+ properties = self.extendedProperties and (self.extendedProperties.get('shared', {}) or self.extendedProperties.get('private', {})) or {}
o_id = properties.get('%s_odoo_id' % dbname)
if o_id:
return int(o_id)
@@ -85,15 +88,15 @@ class GoogleEvent(abc.Set):
ids = tuple(e._odoo_id for e in self if e._odoo_id)
if len(ids) == len(self):
return ids
- found = self._load_odoo_ids_from_db(env)
+ model = self._get_model(env)
+ found = self._load_odoo_ids_from_db(env, model)
unsure = self - found
if unsure:
- unsure._load_odoo_ids_from_metadata(env)
+ unsure._load_odoo_ids_from_metadata(env, model)
return tuple(e._odoo_id for e in self)
- def _load_odoo_ids_from_metadata(self, env):
- model = self._get_model(env)
+ def _load_odoo_ids_from_metadata(self, env, model):
unsure_odoo_ids = tuple(e._meta_odoo_id(env.cr.dbname) for e in self)
odoo_events = model.browse(_id for _id in unsure_odoo_ids if _id)
@@ -107,8 +110,7 @@ class GoogleEvent(abc.Set):
if odoo_id in o_ids:
e._events[e.id]['_odoo_id'] = odoo_id
- def _load_odoo_ids_from_db(self, env):
- model = self._get_model(env)
+ def _load_odoo_ids_from_db(self, env, model):
odoo_events = model.with_context(active_test=False)._from_google_ids(self.ids)
mapping = {e.google_id: e.id for e in odoo_events} # {google_id: odoo_id}
existing_google_ids = odoo_events.mapped('google_id')
@@ -149,7 +151,17 @@ class GoogleEvent(abc.Set):
def filter(self, func) -> 'GoogleEvent':
return GoogleEvent(e for e in self if func(e))
+ def clear_type_ambiguity(self, env):
+ ambiguous_events = self.filter(GoogleEvent._is_type_ambiguous)
+ recurrences = ambiguous_events._load_odoo_ids_from_db(env, env['calendar.recurrence'])
+ for recurrence in recurrences:
+ self._events[recurrence.id]['recurrence'] = True
+ for event in ambiguous_events - recurrences:
+ self._events[event.id]['recurrence'] = False
+
def is_recurrence(self):
+ if self._is_type_ambiguous():
+ _logger.warning("Ambiguous event type: cannot accurately tell whether a cancelled event is a recurrence or not")
return bool(self.recurrence)
def is_recurrent(self):
@@ -172,6 +184,12 @@ class GoogleEvent(abc.Set):
return self.filter(lambda e: e._odoo_id)
+ def _is_type_ambiguous(self):
+ """For cancelled events/recurrences, Google only send the id and
+ the cancelled status. There is no way to know if it was a recurrence
+ or simple event."""
+ return self.is_cancelled() and 'recurrence' not in self._events[self.id]
+
def _get_model(self, env):
if all(e.is_recurrence() for e in self):
return env['calendar.recurrence']