diff options
| author | Lucas Lefèvre <lul@odoo.com> | 2021-02-09 16:42:00 +0000 |
|---|---|---|
| committer | Arnaud Joset <arj@odoo.com> | 2021-03-22 11:00:32 +0000 |
| commit | 374d3c7f95fca9832afb0d34d710f28dbadcdfa6 (patch) | |
| tree | 5ea727d06eece30e4ac69a45e3f8b98c10c15e05 /addons | |
| parent | e6675b075459c446a9115f9695824646c81dcc3f (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>
Diffstat (limited to 'addons')
| -rw-r--r-- | addons/google_calendar/models/calendar.py | 28 | ||||
| -rw-r--r-- | addons/google_calendar/models/calendar_attendee.py | 9 | ||||
| -rw-r--r-- | addons/google_calendar/models/calendar_recurrence_rule.py | 99 | ||||
| -rw-r--r-- | addons/google_calendar/models/google_sync.py | 16 | ||||
| -rw-r--r-- | addons/google_calendar/models/res_users.py | 1 | ||||
| -rw-r--r-- | addons/google_calendar/tests/__init__.py | 1 | ||||
| -rw-r--r-- | addons/google_calendar/tests/test_sync_common.py | 63 | ||||
| -rw-r--r-- | addons/google_calendar/tests/test_sync_google2odoo.py | 311 | ||||
| -rw-r--r-- | addons/google_calendar/tests/test_sync_odoo2google.py | 216 | ||||
| -rw-r--r-- | addons/google_calendar/utils/google_event.py | 32 |
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'] |
