Files
odoo_source/addons/google_calendar/tests/test_sync_odoo2google.py
T
Rémy Baranx (bar) 2fbfbf0dce [FIX] calendar, microsoft_calendar: backport bugfixes from master
As several customers complained about lot of bugs in microsoft_calendar in 14.0,
It has been decided to backport bug fixes of the model layer from master to 14.0,
without the need of an upgrade script (no new field, ...).

In master, we use 2 ids (organizer event id + universal id) instead of only one,
to handle Odoo <-> Outlook sync correctly when several attendees sync their Outlook
calendar with their Odoo calendar. For that, we have added a new field.
To report this bug fix in 14.0, the existing field which stores the organizer event id,
is now a string storing both ids separated by a ':' as follow: 'organizer_event_id:universal_id'.

2 new compute fields have been added to be able to use these 2 ids more easily.

(all commits from the original PR have been squashed to ease forward-port)

closes odoo/odoo#95736

X-original-commit: 5e83318a7240585371efd31e407829793f3e732f
Signed-off-by: Arnaud Joset <arj@odoo.com>
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
2022-07-12 13:21:27 +02:00

558 lines
23 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from dateutil.relativedelta import relativedelta
from unittest.mock import patch
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.tests.test_sync_common import TestSyncGoogle, patch_api
from odoo.tests.common import users, warmup
from odoo.tests import tagged
@tagged('odoo2google')
@patch.object(User, '_get_google_calendar_token', lambda user: 'dummy-token')
class TestSyncOdoo2Google(TestSyncGoogle):
def setUp(self):
super().setUp()
self.google_service = GoogleCalendarService(self.env['google.service'])
# Make sure this test will work for the next 30 years
self.env['ir.config_parameter'].set_param('google_calendar.sync.range_days', 10000)
@patch_api
def test_event_creation(self):
partner = self.env['res.partner'].create({'name': 'Jean-Luc', 'email': 'jean-luc@opoo.com'})
alarm = self.env['calendar.alarm'].create({
'name': 'Notif',
'alarm_type': 'notification',
'interval': 'minutes',
'duration': 18,
})
event = self.env['calendar.event'].create({
'name': "Event",
'start': datetime(2020, 1, 15, 8, 0),
'stop': datetime(2020, 1, 15, 18, 0),
'partner_ids': [(4, partner.id)],
'alarm_ids': [(4, alarm.id)],
'privacy': 'private',
'need_sync': False,
})
event._sync_odoo2google(self.google_service)
self.assertGoogleEventInserted({
'id': False,
'start': {'dateTime': '2020-01-15T08:00:00+00:00'},
'end': {'dateTime': '2020-01-15T18:00:00+00:00'},
'summary': 'Event',
'description': '',
'location': '',
'visibility': 'private',
'guestsCanModify': True,
'reminders': {'useDefault': False, 'overrides': [{'method': 'popup', 'minutes': alarm.duration_minutes}]},
'organizer': {'email': 'odoobot@example.com', 'self': True},
'attendees': [{'email': 'jean-luc@opoo.com', 'responseStatus': 'needsAction'}],
'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: event.id}}
})
@patch_api
@users('__system__')
@warmup
def test_event_creation_perf(self):
EVENT_COUNT = 100
partners = self.env['res.partner'].create([
{'name': 'Jean-Luc %s' % (i), 'email': 'jean-luc-%s@opoo.com' % (i)} for i in range(EVENT_COUNT)])
alarm = self.env['calendar.alarm'].create({
'name': 'Notif',
'alarm_type': 'notification',
'interval': 'minutes',
'duration': 18,
})
partner_model = self.env.ref('base.model_res_partner')
partner = self.env['res.partner'].search([], limit=1)
with self.assertQueryCount(__system__=616):
events = self.env['calendar.event'].create([{
'name': "Event %s" % (i),
'start': datetime(2020, 1, 15, 8, 0),
'stop': datetime(2020, 1, 15, 18, 0),
'partner_ids': [(4, partners[i].id), (4, self.env.user.partner_id.id)],
'alarm_ids': [(4, alarm.id)],
'privacy': 'private',
'need_sync': False,
'res_model_id': partner_model.id,
'res_id': partner.id,
} for i in range(EVENT_COUNT)])
events._sync_odoo2google(self.google_service)
with self.assertQueryCount(__system__=130):
events.unlink()
@patch_api
@users('__system__')
@warmup
def test_recurring_event_creation_perf(self):
partner = self.env['res.partner'].create({'name': 'Jean-Luc', 'email': 'jean-luc@opoo.com'})
alarm = self.env['calendar.alarm'].create({
'name': 'Notif',
'alarm_type': 'notification',
'interval': 'minutes',
'duration': 18,
})
partner_model = self.env.ref('base.model_res_partner')
partner = self.env['res.partner'].search([], limit=1)
with self.assertQueryCount(__system__=72):
event = self.env['calendar.event'].create({
'name': "Event",
'start': datetime(2020, 1, 15, 8, 0),
'stop': datetime(2020, 1, 15, 18, 0),
'partner_ids': [(4, partner.id)],
'alarm_ids': [(4, alarm.id)],
'privacy': 'private',
'need_sync': False,
'interval': 1,
'recurrency': True,
'rrule_type': 'daily',
'end_type': 'forever',
'res_model_id': partner_model.id,
'res_id': partner.id,
})
with self.assertQueryCount(__system__=35):
event.unlink()
def test_event_without_user(self):
event = self.env['calendar.event'].create({
'name': "Event",
'start': datetime(2020, 1, 15, 8, 0),
'stop': datetime(2020, 1, 15, 18, 0),
'user_id': False,
'privacy': 'private',
'need_sync': False,
})
values = event._google_values()
self.assertFalse('%s_owner_id' % self.env.cr.dbname in values.get('extendedProperties', {}).get('shared', {}))
@patch_api
def test_event_allday_creation(self):
event = self.env['calendar.event'].create({
'name': "Event",
'allday': True,
'start': datetime(2020, 1, 15),
'stop': datetime(2020, 1, 15),
'need_sync': False,
})
event._sync_odoo2google(self.google_service)
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': [{'email': 'odoobot@example.com', 'responseStatus': 'accepted'}],
'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: event.id}}
})
@patch_api
def test_inactive_event(self):
event = self.env['calendar.event'].create({
'name': "Event",
'start': datetime(2020, 1, 15),
'stop': datetime(2020, 1, 15),
'active': False,
'need_sync': False,
})
event._sync_odoo2google(self.google_service)
self.assertGoogleEventNotInserted()
self.assertGoogleEventNotDeleted()
@patch_api
def test_synced_inactive_event(self):
google_id = 'aaaaaaaaa'
# writing on synced event will put it in a need_sync state.
# Delete api will not be called but the state of the event will be set as 'cancelled'
event = self.env['calendar.event'].create({
'google_id': google_id,
'name': "Event",
'start': datetime(2020, 1, 15),
'stop': datetime(2020, 1, 15),
'active': False,
'need_sync': True,
})
event._sync_odoo2google(self.google_service)
self.assertGoogleEventDeleted(google_id)
@patch_api
def test_recurrence(self):
google_id = 'aaaaaaaaa'
event = self.env['calendar.event'].create({
'google_id': google_id,
'name': "Event",
'start': datetime(2020, 1, 15),
'stop': datetime(2020, 1, 15),
'allday': True,
'need_sync': False,
})
recurrence = self.env['calendar.recurrence'].create({
'rrule': 'FREQ=WEEKLY;COUNT=2;BYDAY=WE',
'calendar_event_ids': [(4, event.id)],
'need_sync': False,
})
recurrence._sync_odoo2google(self.google_service)
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': [{'email': 'odoobot@example.com', 'responseStatus': 'accepted'}],
'recurrence': ['RRULE:FREQ=WEEKLY;COUNT=2;BYDAY=WE'],
'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: recurrence.id}}
})
@patch_api
def test_event_added_to_recurrence(self):
google_id = 'aaaaaaaaa'
event = self.env['calendar.event'].create({
'google_id': google_id,
'name': "Event",
'start': datetime(2020, 1, 15),
'stop': datetime(2020, 1, 15),
'allday': True,
'need_sync': False,
})
event.write({
'recurrency': True,
'rrule': 'FREQ=WEEKLY;COUNT=2;BYDAY=WE',
})
to_delete = self.env['calendar.event'].with_context(active_test=False).search([('google_id', '=', google_id)])
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': [{'email': 'odoobot@example.com', 'responseStatus': 'accepted'}],
'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
def test_following_event_updated(self):
google_id = 'aaaaaaaaa'
event_1 = self.env['calendar.event'].create({
'name': "Event",
'start': datetime(2020, 1, 15),
'stop': datetime(2020, 1, 15),
'allday': True,
'need_sync': False,
})
event_2 = self.env['calendar.event'].create({
'name': "Event",
'start': datetime(2020, 1, 22),
'stop': datetime(2020, 1, 22),
'allday': True,
'need_sync': False,
})
self.env['calendar.recurrence'].create({
'google_id': google_id,
'rrule': 'FREQ=WEEKLY;COUNT=2;BYDAY=WE',
'calendar_event_ids': [(4, event_1.id), (4, event_2.id)],
'need_sync': False,
})
event = event_2
# Update only some events in the recurrence
event.write({
'name': 'New name',
'recurrence_update': 'future_events',
})
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': 'New name',
'description': '',
'location': '',
'guestsCanModify': True,
'organizer': {'email': 'odoobot@example.com', 'self': True},
'attendees': [{'email': 'odoobot@example.com', 'responseStatus': 'accepted'}],
'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: event.id}},
'reminders': {'overrides': [], 'useDefault': False},
'visibility': 'public',
}, timeout=3)
@patch_api
def test_stop_synchronization(self):
self.env.user.stop_google_synchronization()
self.assertTrue(self.env.user.google_synchronization_stopped, "The google synchronization flag should be switched on")
self.assertFalse(self.env.user._sync_google_calendar(self.google_service), "The google synchronization should be stopped")
# If synchronization stopped, creating a new event should not call _google_insert.
self.env['calendar.event'].create({
'name': "Event",
'start': datetime(2020, 1, 15, 8, 0),
'stop': datetime(2020, 1, 15, 18, 0),
'privacy': 'private',
})
self.assertGoogleEventNotInserted()
@patch_api
def test_restart_synchronization(self):
# Test new event created after stopping synchronization are correctly patched when restarting sync.
google_id = 'aaaaaaaaa'
partner = self.env['res.partner'].create({'name': 'Jean-Luc', 'email': 'jean-luc@opoo.com'})
user = self.env['res.users'].create({
'name': 'Test user Calendar',
'login': 'jean-luc@opoo.com',
'partner_id': partner.id,
})
user.stop_google_synchronization()
event = self.env['calendar.event'].with_user(user).create({
'google_id': google_id,
'name': "Event",
'start': datetime(2020, 1, 15, 8, 0),
'stop': datetime(2020, 1, 15, 18, 0),
'partner_ids': [(4, partner.id)],
})
user.with_user(user).restart_google_synchronization()
self.assertGoogleEventPatched(event.google_id, {
'id': event.google_id,
'start': {'dateTime': '2020-01-15T08:00:00+00:00'},
'end': {'dateTime': '2020-01-15T18:00:00+00:00'},
'summary': 'Event',
'description': '',
'location': '',
'visibility': 'public',
'guestsCanModify': True,
'reminders': {'overrides': [], 'useDefault': False},
'organizer': {'email': 'jean-luc@opoo.com', 'self': True},
'attendees': [{'email': 'jean-luc@opoo.com', 'responseStatus': 'accepted'}],
'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: event.id}}
}, 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': [{'email': 'odoobot@example.com', 'responseStatus': 'accepted'}],
'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.do_decline()
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',
})
@patch_api
def test_all_event_with_tz_updated(self):
google_id = 'aaaaaaaaa'
event = self.env['calendar.event'].create({
'name': "Event",
'start': datetime(2020, 1, 15, 8, 0),
'stop': datetime(2020, 1, 15, 9, 0),
'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': {'dateTime': "2020-01-15T08:00:00+00:00", 'timeZone': 'Europe/Brussels'},
'end': {'dateTime': "2020-01-15T09:00:00+00:00", 'timeZone': 'Europe/Brussels'},
'summary': 'New name',
'description': '',
'location': '',
'guestsCanModify': True,
'organizer': {'email': 'odoobot@example.com', 'self': True},
'attendees': [{'email': 'odoobot@example.com', 'responseStatus': 'accepted'}],
'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)