19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:28 +01:00
parent 20ddc1b4a3
commit c0efcc53f5
1162 changed files with 125577 additions and 105287 deletions

View file

@ -6,4 +6,5 @@ from . import test_microsoft_service
from . import test_create_events
from . import test_update_events
from . import test_delete_events
from . import test_sync_odoo2microsoft_mail
from . import test_answer_events

View file

@ -1,3 +1,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import pytz
from datetime import datetime, timedelta
from markupsafe import Markup
@ -8,9 +10,10 @@ from freezegun import freeze_time
from odoo import fields
from odoo.tests.common import HttpCase
from odoo.tools import mute_logger
from odoo.addons.microsoft_calendar.models.microsoft_sync import MicrosoftCalendarSync
from odoo.addons.microsoft_calendar.models.microsoft_sync import MicrosoftSync
from odoo.addons.microsoft_calendar.utils.event_id_storage import combine_ids
def mock_get_token(user):
return f"TOKEN_FOR_USER_{user.id}"
@ -23,14 +26,14 @@ def _modified_date_in_the_future(event):
return (event.write_date + timedelta(seconds=5)).strftime("%Y-%m-%dT%H:%M:%SZ")
def patch_api(func):
@patch.object(MicrosoftSync, '_microsoft_insert', MagicMock())
@patch.object(MicrosoftSync, '_microsoft_delete', MagicMock())
@patch.object(MicrosoftSync, '_microsoft_patch', MagicMock())
@patch.object(MicrosoftCalendarSync, '_microsoft_insert', MagicMock())
@patch.object(MicrosoftCalendarSync, '_microsoft_delete', MagicMock())
@patch.object(MicrosoftCalendarSync, '_microsoft_patch', MagicMock())
def patched(self, *args, **kwargs):
return func(self, *args, **kwargs)
return patched
# By inheriting from TransactionCase, postcommit hooks (so methods tagged with `@after_commit` in MicrosoftSync),
# By inheriting from TransactionCase, postcommit hooks (so methods tagged with `@after_commit` in MicrosoftCalendarSync),
# are not called because no commit is done.
# To be able to manually call these postcommit hooks, we need to inherit from HttpCase.
# Note: as postcommit hooks are called separately, do not forget to invalidate cache for records read during the test.
@ -39,6 +42,11 @@ class TestCommon(HttpCase):
@patch_api
def setUp(self):
super(TestCommon, self).setUp()
m = mute_logger('odoo.addons.auth_signup.models.res_users')
mute_logger.__enter__(m) # noqa: PLC2801
self.addCleanup(mute_logger.__exit__, m, None, None, None)
self.env.user.unpause_microsoft_synchronization()
# prepare users
self.organizer_user = self.env["res.users"].search([("name", "=", "Mike Organizer")])
@ -249,7 +257,8 @@ class TestCommon(HttpCase):
"start": self.start_date,
"stop": self.end_date,
"user_id": self.organizer_user,
"microsoft_id": combine_ids("123", "456"),
"microsoft_id": "123",
"ms_universal_event_id": "456",
"partner_ids": [self.organizer_user.partner_id.id, self.attendee_user.partner_id.id],
}
self.expected_odoo_recurrency_from_outlook = {
@ -266,7 +275,8 @@ class TestCommon(HttpCase):
'fri': False,
'interval': self.recurrent_event_interval,
'month_by': 'date',
'microsoft_id': combine_ids('REC123', 'REC456'),
"microsoft_id": "REC123",
"ms_universal_event_id": "REC456",
'name': "Every %s Days until %s" % (
self.recurrent_event_interval, self.recurrence_end_date.strftime("%Y-%m-%d")
),
@ -405,7 +415,8 @@ class TestCommon(HttpCase):
"stop": self.end_date + timedelta(days=i * self.recurrent_event_interval),
"until": self.recurrence_end_date.date(),
"microsoft_recurrence_master_id": "REC123",
'microsoft_id': combine_ids(f"REC123_EVENT_{i+1}", f"REC456_EVENT_{i+1}"),
"microsoft_id": f"REC123_EVENT_{i+1}",
"ms_universal_event_id": f"REC456_EVENT_{i+1}",
"recurrency": True,
"follow_recurrence": True,
"active": True,
@ -445,7 +456,8 @@ class TestCommon(HttpCase):
self.simple_event = self.env["calendar.event"].with_user(self.organizer_user).create(
dict(
self.simple_event_values,
microsoft_id=combine_ids("123", "456"),
microsoft_id="123",
ms_universal_event_id="456",
)
)
@ -456,7 +468,8 @@ class TestCommon(HttpCase):
dict(
self.simple_event_values,
name=f"event{i}",
microsoft_id=combine_ids(f"e{i}", f"u{i}"),
microsoft_id=f"e{i}",
ms_universal_event_id=f"u{i}"
)
for i in range(1, 4)
])
@ -483,11 +496,13 @@ class TestCommon(HttpCase):
# set ids set by Outlook
if not already_created:
self.recurrence.with_context(dont_notify=True).write({
"microsoft_id": combine_ids("REC123", "REC456"),
"microsoft_id": "REC123",
"ms_universal_event_id": "REC456"
})
for i, e in enumerate(self.recurrence.calendar_event_ids.sorted(key=lambda r: r.start)):
e.with_context(dont_notify=True).write({
"microsoft_id": combine_ids(f"REC123_EVENT_{i+1}", f"REC456_EVENT_{i+1}"),
"microsoft_id": f"REC123_EVENT_{i+1}",
"ms_universal_event_id": f"REC456_EVENT_{i+1}",
"microsoft_recurrence_master_id": "REC123",
})
self.recurrence.invalidate_recordset()

View file

@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import patch, ANY
from datetime import datetime, timedelta
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent
from odoo.addons.microsoft_calendar.models.res_users import User
from odoo.addons.microsoft_calendar.utils.event_id_storage import combine_ids
from odoo.addons.microsoft_calendar.models.res_users import ResUsers
from odoo.addons.microsoft_calendar.tests.common import TestCommon, mock_get_token, _modified_date_in_the_future, patch_api
from odoo.tests import users
@ -13,7 +13,7 @@ import json
from freezegun import freeze_time
@patch.object(User, '_get_microsoft_calendar_token', mock_get_token)
@patch.object(ResUsers, '_get_microsoft_calendar_token', mock_get_token)
class TestAnswerEvents(TestCommon):
@patch_api
@ -26,7 +26,8 @@ class TestAnswerEvents(TestCommon):
self.simple_event = self.env["calendar.event"].with_user(self.organizer_user).create(
dict(
self.simple_event_values,
microsoft_id=combine_ids("123", "456"),
microsoft_id="123",
ms_universal_event_id="456",
)
)
(self.organizer_user | self.attendee_user).microsoft_calendar_token_validity = datetime.now() + timedelta(hours=1)
@ -38,14 +39,14 @@ class TestAnswerEvents(TestCommon):
('event_id', '=', self.simple_event.id),
('partner_id', '=', self.attendee_user.partner_id.id)
])
attendee_ms_organizer_event_id = 100
mock_get_single_event.return_value = (True, {'value': [{'id': attendee_ms_organizer_event_id}]})
mock_get_single_event.return_value = (True, {'value': [{'id': attendee.event_id.microsoft_id}]})
attendee.with_user(self.attendee_user).do_accept()
self.call_post_commit_hooks()
self.simple_event.invalidate_recordset()
mock_answer.assert_called_once_with(
attendee_ms_organizer_event_id,
self.simple_event.microsoft_id,
'accept',
{"comment": "", "sendResponse": True},
token=mock_get_token(self.attendee_user),
@ -59,13 +60,12 @@ class TestAnswerEvents(TestCommon):
('event_id', '=', self.simple_event.id),
('partner_id', '=', self.attendee_user.partner_id.id)
])
attendee_ms_organizer_event_id = 100
mock_get_single_event.return_value = (True, {'value': [{'id': attendee_ms_organizer_event_id}]})
mock_get_single_event.return_value = (True, {'value': [{'id': attendee.event_id.microsoft_id}]})
attendee.with_user(self.attendee_user).do_decline()
self.call_post_commit_hooks()
self.simple_event.invalidate_recordset()
mock_answer.assert_called_once_with(
attendee_ms_organizer_event_id,
self.simple_event.microsoft_id,
'decline',
{"comment": "", "sendResponse": True},
token=mock_get_token(self.attendee_user),

View file

@ -1,15 +1,22 @@
from datetime import datetime, timedelta
from unittest.mock import patch
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import patch, call
from datetime import timedelta, datetime
from freezegun import freeze_time
from odoo import Command, fields
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent
from odoo.addons.microsoft_calendar.models.res_users import User
from odoo.addons.microsoft_calendar.tests.common import TestCommon, mock_get_token
from odoo.addons.microsoft_calendar.models.res_users import ResUsers
from odoo.addons.microsoft_calendar.tests.common import TestCommon, mock_get_token, _modified_date_in_the_future
from odoo.exceptions import ValidationError, UserError
from odoo.tests.common import tagged
@patch.object(User, '_get_microsoft_calendar_token', mock_get_token)
@tagged('post_install', '-at_install')
@patch.object(ResUsers, '_get_microsoft_calendar_token', mock_get_token)
class TestCreateEvents(TestCommon):
@patch.object(MicrosoftCalendarService, 'insert')
@ -193,7 +200,7 @@ class TestCreateEvents(TestCommon):
recurrence.invalidate_recordset()
# assert
self.assertEqual(recurrence.ms_organizer_event_id, event_id)
self.assertEqual(recurrence.microsoft_id, event_id)
self.assertEqual(recurrence.ms_universal_event_id, event_iCalUId)
self.assertEqual(recurrence.need_sync_m, False)
@ -239,7 +246,7 @@ class TestCreateEvents(TestCommon):
# assert
mock_insert.assert_not_called()
self.assertEqual(recurrence.ms_organizer_event_id, False)
self.assertEqual(recurrence.microsoft_id, False)
self.assertEqual(recurrence.ms_universal_event_id, False)
self.assertEqual(recurrence.need_sync_m, False)
@ -307,6 +314,101 @@ class TestCreateEvents(TestCommon):
# Assert that no insert call was made.
mock_insert.assert_not_called()
@patch.object(MicrosoftCalendarService, 'insert')
def test_create_event_with_sync_config_paused(self, mock_insert):
"""
Creates an event with the synchronization paused, the event must have its field 'need_sync_m' as True
for later synchronizing it with Outlook Calendar.
"""
# Set user sync configuration as active and then pause the synchronization.
self.organizer_user.microsoft_synchronization_stopped = False
self.organizer_user.pause_microsoft_synchronization()
# Try to create a simple event in Odoo Calendar.
record = self.env["calendar.event"].with_user(self.organizer_user).create(self.simple_event_values)
self.call_post_commit_hooks()
record.invalidate_recordset()
# Ensure that synchronization is paused, insert wasn't called and record is waiting to be synced.
self.assertFalse(self.organizer_user.microsoft_synchronization_stopped)
self.assertEqual(self.organizer_user._get_microsoft_sync_status(), "sync_paused")
self.assertTrue(record.need_sync_m, "Sync variable must be true for updating event when sync re-activates")
mock_insert.assert_not_called()
@patch.object(MicrosoftCalendarService, 'get_events')
@patch.object(MicrosoftCalendarService, 'insert')
def test_sync_create_update_single_event(self, mock_insert, mock_get_events):
"""
If the synchronization with Outlook is stopped, then records (events and recurrences) created or updated
should not be synced. They must be synced only when created or updated having the synchronization active.
In this test, the synchronization is stopped and an event is created locally. After this, the synchronization
is restarted and the event is updated (this way, syncing it with Outlook Calendar).
"""
# Set last synchronization date for allowing synchronizing events created after this date.
self.organizer_user._set_ICP_first_synchronization_date(fields.Datetime.now())
# Stop the synchronization for clearing the last_sync_date.
self.organizer_user.with_user(self.organizer_user).sudo().stop_microsoft_synchronization()
self.assertEqual(self.organizer_user.microsoft_last_sync_date, False,
"Variable last_sync_date must be False due to sync stop.")
# Create a not synced event (local).
simple_event_values_updated = self.simple_event_values
for date_field in ['start', 'stop']:
simple_event_values_updated[date_field] = simple_event_values_updated[date_field].replace(year=datetime.now().year)
event = self.env["calendar.event"].with_user(self.organizer_user).create(simple_event_values_updated)
# Assert that insert was not called and prepare mock for the synchronization restart.
mock_insert.assert_not_called()
mock_get_events.return_value = ([], None)
ten_minutes_after_creation = event.write_date + timedelta(minutes=10)
with freeze_time(ten_minutes_after_creation):
# Restart the synchronization with Outlook Calendar.
self.organizer_user.with_user(self.organizer_user).sudo().restart_microsoft_synchronization()
# Sync microsoft calendar, considering that ten minutes were passed after the event creation.
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
self.call_post_commit_hooks()
event.invalidate_recordset()
# Assert that insert function was not called and check last_sync_date variable value.
mock_insert.assert_not_called()
self.assertNotEqual(self.organizer_user.microsoft_last_sync_date, False,
"Variable last_sync_date must not be empty after sync.")
self.assertLessEqual(event.write_date, self.organizer_user.microsoft_last_sync_date,
"Event creation must happen before last_sync_date")
# Assert that the local event did not get synced after synchronization restart.
self.assertEqual(event.microsoft_id, False,
"Event should not be synchronized while sync is paused.")
self.assertEqual(event.ms_universal_event_id, False,
"Event should not be synchronized while sync is paused.")
# Update local event information.
event.write({
"name": "New event name"
})
self.call_post_commit_hooks()
# Prepare mock for new synchronization.
event_id = "123"
event_iCalUId = "456"
mock_insert.return_value = (event_id, event_iCalUId)
mock_get_events.return_value = ([], None)
# Synchronize local event with Outlook after updating it locally.
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
self.call_post_commit_hooks()
event.invalidate_recordset()
# Assert that the event got synchronized with Microsoft (through mock).
self.assertEqual(event.microsoft_id, "123")
self.assertEqual(event.ms_universal_event_id, "456")
# Assert that the Microsoft Insert was called once.
mock_insert.assert_called_once()
@patch.object(MicrosoftCalendarService, 'get_events')
@patch.object(MicrosoftCalendarService, 'insert')
def test_create_event_for_another_user(self, mock_insert, mock_get_events):
@ -357,7 +459,7 @@ class TestCreateEvents(TestCommon):
'login': 'portal@user',
'email': 'portal@user',
'name': 'PortalUser',
'groups_id': [Command.set([portal_group.id])],
'group_ids': [Command.set([portal_group.id])],
})
# Mock event from Microsoft and sync event with Odoo through self.attendee_user (synced user).
@ -397,6 +499,203 @@ class TestCreateEvents(TestCommon):
self.assertEqual(len(new_records), 1)
self.assert_odoo_event(new_records, expected_event)
def test_create_event_with_default_and_undefined_sensitivity(self):
""" Check if microsoft events are created in Odoo when 'None' sensitivity setting is defined and also when it is not. """
# Sync events from Microsoft to Odoo after adding the sensitivity (privacy) property.
self.simple_event_from_outlook_organizer.pop('sensitivity')
undefined_privacy_event = {'id': 100, 'iCalUId': 2, **self.simple_event_from_outlook_organizer}
default_privacy_event = {'id': 200, 'iCalUId': 4, 'sensitivity': None, **self.simple_event_from_outlook_organizer}
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent([undefined_privacy_event, default_privacy_event]))
# Ensure that synced events have the correct privacy field in Odoo.
undefined_privacy_odoo_event = self.env['calendar.event'].search([('microsoft_id', '=', 100)])
default_privacy_odoo_event = self.env['calendar.event'].search([('microsoft_id', '=', 200)])
self.assertFalse(undefined_privacy_odoo_event.privacy, "Event with undefined privacy must have False value in privacy field.")
self.assertFalse(default_privacy_odoo_event.privacy, "Event with custom privacy must have False value in privacy field.")
@patch.object(MicrosoftCalendarService, 'get_events')
@patch.object(MicrosoftCalendarService, 'insert')
def test_create_videocall_sync_microsoft_calendar(self, mock_insert, mock_get_events):
"""
Test syncing an event from Odoo to Microsoft Calendar.
Ensures that meeting details are correctly updated after syncing from Microsoft.
"""
record = self.env["calendar.event"].with_user(self.organizer_user).create(self.simple_event_values)
self.assertEqual(record.name, "simple_event", "Event name should be same as simple_event")
# Mock values to simulate Microsoft event creation
event_id = "123"
event_iCalUId = "456"
mock_insert.return_value = (event_id, event_iCalUId)
# Prepare the mock event response from Microsoft
self.response_from_outlook_organizer = {
**self.simple_event_from_outlook_organizer,
'_odoo_id': record.id,
'onlineMeeting': {
'joinUrl': 'https://teams.microsoft.com/l/meetup-join/test',
'conferenceId': '275984951',
'tollNumber': '+1 323-555-0166',
},
'lastModifiedDateTime': _modified_date_in_the_future(record),
'isOnlineMeeting': True,
'onlineMeetingProvider': 'teamsForBusiness',
}
mock_get_events.return_value = (MicrosoftEvent([self.response_from_outlook_organizer]), None)
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
self.call_post_commit_hooks()
record.invalidate_recordset()
# Check that Microsoft insert was called exactly once
mock_insert.assert_called_once()
self.assertEqual(record.microsoft_id, event_id, "The Microsoft ID should be assigned to the event.")
self.assertEqual(record.ms_universal_event_id, event_iCalUId)
self.assertEqual(mock_insert.call_args[0][0].get('isOnlineMeeting'), True,
"The event should be marked as an online meeting.")
self.assertEqual(mock_insert.call_args[0][0].get('onlineMeetingProvider'), 'teamsForBusiness',
"The event's online meeting provider should be set to Microsoft Teams.")
self.assertEqual(record.need_sync_m, False)
# Verify the event's videocall_location is updated in Odoo
event = self.env['calendar.event'].search([('name', '=', self.response_from_outlook_organizer.get('subject'))])
self.assertTrue(event, "The event should exist in the calendar after sync.")
self.assertEqual(event.videocall_location, 'https://teams.microsoft.com/l/meetup-join/test', "The meeting URL should match.")
@patch.object(MicrosoftCalendarService, 'get_events')
@patch.object(MicrosoftCalendarService, 'insert')
def test_no_videocall_hr_holidays(self, mock_insert, mock_get_events):
"""
Test HR holidays synchronization with Microsoft Calendar, ensuring no online meetings
are generated for leave requests.
"""
# Skip test if HR Holidays module isn't installed
if self.env['ir.module.module']._get('hr_holidays').state not in ['installed', 'to upgrade']:
self.skipTest("The 'hr_holidays' module must be installed to run this test.")
self.user_hrmanager = mail_new_test_user(self.env, login='bastien', groups='base.group_user,hr_holidays.group_hr_holidays_manager')
self.user_employee = mail_new_test_user(self.env, login='enguerran', password='enguerran', groups='base.group_user')
self.rd_dept = self.env['hr.department'].with_context(tracking_disable=True).create({
'name': 'Research and Development',
})
self.employee_emp = self.env['hr.employee'].create({
'name': 'Marc Demo',
'user_id': self.user_employee.id,
'department_id': self.rd_dept.id,
})
self.hr_leave_type = self.env['hr.leave.type'].with_user(self.user_hrmanager).create({
'name': 'Time Off Type',
'requires_allocation': False,
})
self.holiday = self.env['hr.leave'].with_context(mail_create_nolog=True, mail_notrack=True).with_user(self.user_employee).create({
'name': 'Time Off Employee',
'employee_id': self.employee_emp.id,
'holiday_status_id': self.hr_leave_type.id,
'request_date_from': datetime(2020, 1, 15),
'request_date_to': datetime(2020, 1, 15),
})
self.holiday.with_user(self.user_hrmanager).action_approve()
# Ensure the event exists in the calendar and is correctly linked to the time off
search_domain = [
('name', 'like', self.holiday.employee_id.name),
('start_date', '>=', self.holiday.request_date_from),
('stop_date', '<=', self.holiday.request_date_to),
]
record = self.env['calendar.event'].search(search_domain)
self.assertTrue(record, "The time off event should exist.")
self.assertEqual(record.name, "Marc Demo on Time Off : 1 days",
"The event name should match the employee's time off description.")
self.assertEqual(record.start_date, datetime(2020, 1, 15).date(),
"The start date should match the time off request.")
self.assertEqual(record.stop_date, datetime(2020, 1, 15).date(),
"The end date should match the time off request.")
# Mock Microsoft API response for event creation
event_id = "123"
event_iCalUId = "456"
mock_insert.return_value = (event_id, event_iCalUId)
mock_get_events.return_value = ([], None)
# Sync calendar with Microsoft
self.user_employee.with_user(self.user_employee).sudo()._sync_microsoft_calendar()
self.call_post_commit_hooks()
record.invalidate_recordset()
mock_insert.assert_called_once()
self.assertEqual(record.microsoft_id, event_id, "The Microsoft ID should be assigned correctly.")
self.assertEqual(record.ms_universal_event_id, event_iCalUId, "The iCalUID should be assigned correctly.")
self.assertEqual(record.need_sync_m, False, "The event should no longer need synchronization.")
self.assertEqual(mock_insert.call_args[0][0].get('isOnlineMeeting'), False,
"Time off events should not be marked as an online meeting.")
self.assertFalse(mock_insert.call_args[0][0].get('onlineMeetingProvider', False))
@patch.object(MicrosoftCalendarService, 'insert')
def test_skip_sync_for_non_synchronized_users_new_events(self, mock_insert):
"""
Skip the synchro of new events by attendees when the organizer is not synchronized with Outlook.
Otherwise, the event ownership will be lost to the attendee and it could generate duplicates in
Odoo, as well cause problems in the future the synchronization of that event for the original owner.
"""
with self.mock_datetime_and_now('2021-09-20 10:00:00'):
# Ensure that the calendar synchronization of the attendee is active. Deactivate organizer's synchronization.
self.attendee_user.microsoft_calendar_token_validity = datetime.now() + timedelta(minutes=60)
self.assertTrue(self.env['calendar.event'].with_user(self.attendee_user)._check_microsoft_sync_status())
self.organizer_user.microsoft_synchronization_stopped = True
# Create an event with the organizer not synchronized and invite the synchronized attendee.
self.simple_event_values['user_id'] = self.organizer_user.id
self.simple_event_values['partner_ids'] = [Command.set([self.organizer_user.partner_id.id, self.attendee_user.partner_id.id])]
event = self.env['calendar.event'].with_user(self.organizer_user).create(self.simple_event_values)
self.assertTrue(event, "The event for the not synchronized owner must be created in Odoo.")
# Synchronize the attendee's calendar, then make sure insert was not called.
event.with_user(self.attendee_user).sudo()._sync_odoo2microsoft()
mock_insert.assert_not_called()
# Prepare mock return for the insertion.
event_id = "123"
event_iCalUId = "456"
mock_insert.return_value = (event_id, event_iCalUId)
# Activate the synchronization of the organizer and ensure that the event is now inserted.
self.organizer_user.microsoft_synchronization_stopped = False
self.organizer_user.microsoft_calendar_token_validity = datetime.now() + timedelta(minutes=60)
self.organizer_user.with_user(self.organizer_user).restart_microsoft_synchronization()
event.with_user(self.organizer_user).sudo()._sync_odoo2microsoft()
self.call_post_commit_hooks()
mock_insert.assert_called()
@patch.object(MicrosoftCalendarService, 'get_events')
@patch.object(MicrosoftCalendarService, 'insert')
def test_create_duplicate_event_microsoft_calendar(self, mock_insert, mock_get_events):
"""
Test syncing an event from Odoo to Microsoft Calendar.
"""
record = self.env["calendar.event"].with_user(self.organizer_user).create(self.simple_event_values)
# Mock values to simulate Microsoft event creation
event_id = "123"
event_iCalUId = "456"
mock_insert.return_value = (event_id, event_iCalUId)
record2 = record.copy()
# Prepare the mock event response from Microsoft
self.response_from_outlook_organizer = {
**self.simple_event_from_outlook_organizer,
'_odoo_id': record.id,
}
self.response_from_outlook_organizer_1 = {
**self.simple_event_from_outlook_organizer,
'_odoo_id': record2.id,
}
mock_get_events.return_value = (MicrosoftEvent([self.response_from_outlook_organizer, self.response_from_outlook_organizer_1]), None)
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
self.call_post_commit_hooks()
record.invalidate_recordset()
record2.invalidate_recordset()
# Check that Microsoft insert was called exactly once
mock_insert.assert_called()
@patch.object(MicrosoftCalendarService, 'get_events')
@patch.object(MicrosoftCalendarService, 'insert')
def test_new_db_skip_odoo2microsoft_sync_previously_created_events(self, mock_insert, mock_get_events):

View file

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import patch, ANY, call
from datetime import timedelta
@ -7,7 +8,7 @@ from odoo import fields
from odoo.exceptions import UserError
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent
from odoo.addons.microsoft_calendar.models.res_users import User
from odoo.addons.microsoft_calendar.models.res_users import ResUsers
from odoo.addons.microsoft_calendar.tests.common import (
TestCommon,
mock_get_token,
@ -15,7 +16,8 @@ from odoo.addons.microsoft_calendar.tests.common import (
patch_api
)
@patch.object(User, '_get_microsoft_calendar_token', mock_get_token)
@patch.object(ResUsers, '_get_microsoft_calendar_token', mock_get_token)
class TestDeleteEvents(TestCommon):
@patch_api
@ -25,7 +27,7 @@ class TestDeleteEvents(TestCommon):
@patch.object(MicrosoftCalendarService, 'delete')
def test_delete_simple_event_from_odoo_organizer_calendar(self, mock_delete):
event_id = self.simple_event.ms_organizer_event_id
event_id = self.simple_event.microsoft_id
self.simple_event.with_user(self.organizer_user).unlink()
self.call_post_commit_hooks()
@ -40,7 +42,7 @@ class TestDeleteEvents(TestCommon):
@patch.object(MicrosoftCalendarService, 'delete')
def test_delete_simple_event_from_odoo_attendee_calendar(self, mock_delete):
event_id = self.simple_event.ms_organizer_event_id
event_id = self.simple_event.microsoft_id
self.simple_event.with_user(self.attendee_user).unlink()
self.call_post_commit_hooks()
@ -55,7 +57,7 @@ class TestDeleteEvents(TestCommon):
@patch.object(MicrosoftCalendarService, 'delete')
def test_archive_simple_event_from_odoo_organizer_calendar(self, mock_delete):
event_id = self.simple_event.ms_organizer_event_id
event_id = self.simple_event.microsoft_id
self.simple_event.with_user(self.organizer_user).write({'active': False})
self.call_post_commit_hooks()
@ -71,7 +73,7 @@ class TestDeleteEvents(TestCommon):
@patch.object(MicrosoftCalendarService, 'delete')
def test_archive_simple_event_from_odoo_attendee_calendar(self, mock_delete):
event_id = self.simple_event.ms_organizer_event_id
event_id = self.simple_event.microsoft_id
self.simple_event.with_user(self.attendee_user).write({'active': False})
self.call_post_commit_hooks()
@ -101,7 +103,7 @@ class TestDeleteEvents(TestCommon):
self.assertFalse(all(e.active for e in several_simple_events))
mock_delete.assert_has_calls([
call(e.ms_organizer_event_id, token=ANY, timeout=ANY)
call(e.microsoft_id, token=ANY, timeout=ANY)
for e in several_simple_events
])
@ -110,7 +112,7 @@ class TestDeleteEvents(TestCommon):
"""
In his Outlook calendar, the organizer cannot delete the event, he can only cancel it.
"""
event_id = self.simple_event.ms_organizer_event_id
event_id = self.simple_event.microsoft_id
mock_get_events.return_value = (
MicrosoftEvent([{
"id": event_id,
@ -142,7 +144,7 @@ class TestDeleteEvents(TestCommon):
return
# arrange
idx = 2
event_id = self.recurrent_events[idx].ms_organizer_event_id
event_id = self.recurrent_events[idx].microsoft_id
# act
self.recurrent_events[idx].with_user(self.organizer_user).unlink()
@ -163,7 +165,7 @@ class TestDeleteEvents(TestCommon):
return
# arrange
idx = 0
event_id = self.recurrent_events[idx].ms_organizer_event_id
event_id = self.recurrent_events[idx].microsoft_id
# act
self.recurrent_events[idx].with_user(self.organizer_user).unlink()
@ -261,7 +263,7 @@ class TestDeleteEvents(TestCommon):
# arrange
mock_get_events.return_value = (
MicrosoftEvent([{
"id": self.recurrence.ms_organizer_event_id,
"id": self.recurrence.microsoft_id,
"@removed": {"reason": "deleted"}
}]),
None
@ -289,7 +291,7 @@ class TestDeleteEvents(TestCommon):
return
# arrange
idx = 0
event_id = self.recurrent_events[idx].ms_organizer_event_id
event_id = self.recurrent_events[idx].microsoft_id
# act
self.recurrent_events[idx].with_user(self.organizer_user).action_mass_archive('self_only')
@ -305,6 +307,26 @@ class TestDeleteEvents(TestCommon):
timeout=ANY
)
@patch.object(MicrosoftCalendarService, 'delete')
def test_delete_synced_event_with_sync_config_paused(self, mock_delete):
"""
Deletes an event with the Outlook Calendar synchronization paused, the event must be archived completely.
"""
# Set user synchronization configuration as active and pause it.
self.organizer_user.microsoft_synchronization_stopped = False
self.organizer_user.pause_microsoft_synchronization()
# Try to delete a simple event in Odoo Calendar.
self.simple_event.with_user(self.organizer_user).unlink()
self.call_post_commit_hooks()
self.simple_event.invalidate_recordset()
# Ensure that synchronization is paused, delete wasn't called and record doesn't exist anymore.
self.assertFalse(self.organizer_user.microsoft_synchronization_stopped)
self.assertEqual(self.organizer_user._get_microsoft_sync_status(), "sync_paused")
self.assertFalse(self.simple_event.exists(), "Event must be deleted from Odoo even though sync configuration is off")
mock_delete.assert_not_called()
@patch.object(MicrosoftCalendarService, 'delete')
def test_delete_recurrence_previously_synced(self, mock_delete):
# Arrange: select recurrent event and update token validity to simulate an active sync environment.

View file

@ -1,7 +1,10 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from dateutil.relativedelta import relativedelta
from pytz import UTC
from odoo.exceptions import UserError
from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent
from odoo.addons.microsoft_calendar.tests.common import TestCommon, patch_api
@ -15,7 +18,7 @@ class TestMicrosoftEvent(TestCommon):
def test_already_mapped_events(self):
# arrange
event_id = self.simple_event.ms_organizer_event_id
event_id = self.simple_event.microsoft_id
event_uid = self.simple_event.ms_universal_event_id
events = MicrosoftEvent([{
"type": "singleInstance",
@ -31,10 +34,69 @@ class TestMicrosoftEvent(TestCommon):
self.assertEqual(len(mapped._events), 1)
self.assertEqual(mapped._events[event_id]["_odoo_id"], self.simple_event.id)
def test_map_an_event_using_global_id(self):
def test_forbid_edit_outlook_recurring_event(self):
"""
Test that no user can edit a recurring event imported from Outlook
(identified by microsoft_recurrence_master_id), but that the sync
mechanism itself can still write through via dont_notify context.
"""
# Give the organizer user a Microsoft token
self.organizer_user.microsoft_calendar_token = "fake_token"
outlook_recurring_event, recurring_event = self.env['calendar.event'].with_context(dont_notify=True).create([
{
'name': 'Outlook Recurring Event',
'start': datetime(2023, 9, 25, 10, 0),
'stop': datetime(2023, 9, 25, 11, 0),
'microsoft_id': 'AAA123:BBB456',
'microsoft_recurrence_master_id': 'MASTER123',
'user_id': self.organizer_user.id,
},
{
'name': 'Regular Recurring Event',
'start': datetime(2023, 9, 25, 10, 0),
'stop': datetime(2023, 9, 25, 11, 0),
'user_id': self.organizer_user.id,
'partner_ids': [(4, self.organizer_user.partner_id.id)],
'recurrency': True,
'follow_recurrence': True,
'rrule_type': 'daily',
'interval': 1,
'end_type': 'count',
'count': 3,
},
])
# No user should be able to edit the Outlook event through Odoo
for user in [self.attendee_user, self.organizer_user]:
with self.assertRaises(UserError):
outlook_recurring_event.with_user(user).with_context(dont_notify=False).write({
'name': 'Trying to change name',
'recurrence_update': 'future_events',
})
self.assertEqual(outlook_recurring_event.name, 'Outlook Recurring Event')
# But changes from Microsoft sync itself should still work
outlook_recurring_event.with_context(dont_notify=True).write({
'name': 'Updated from Outlook',
'recurrence_update': 'future_events'
})
self.assertEqual(outlook_recurring_event.name, 'Updated from Outlook')
# Remove token: organizer is no longer synced to Outlook
self.organizer_user.microsoft_calendar_token = False
# Any user should be able to edit a non-Outlook recurring event
for user in [self.attendee_user, self.organizer_user]:
recurring_event.with_user(user).with_context(dont_notify=False).write({
'name': f'Changed by {user.name}',
'recurrence_update': 'future_events',
})
self.assertEqual(recurring_event.name, f'Changed by {user.name}')
def test_map_an_event_using_global_id(self):
# arrange
event_id = self.simple_event.ms_organizer_event_id
event_id = self.simple_event.microsoft_id
event_uid = self.simple_event.ms_universal_event_id
events = MicrosoftEvent([{
"type": "singleInstance",
@ -55,7 +117,7 @@ class TestMicrosoftEvent(TestCommon):
Here, the Odoo event has an uid but the Outlook event has not.
"""
# arrange
event_id = self.simple_event.ms_organizer_event_id
event_id = self.simple_event.microsoft_id
events = MicrosoftEvent([{
"type": "singleInstance",
"_odoo_id": False,
@ -76,7 +138,7 @@ class TestMicrosoftEvent(TestCommon):
"""
# arrange
event_id = self.simple_event.ms_organizer_event_id
event_id = self.simple_event.microsoft_id
event_uid = self.simple_event.ms_universal_event_id
self.simple_event.ms_universal_event_id = False
events = MicrosoftEvent([{
@ -100,7 +162,7 @@ class TestMicrosoftEvent(TestCommon):
"""
# arrange
event_id = self.simple_event.ms_organizer_event_id
event_id = self.simple_event.microsoft_id
self.simple_event.ms_universal_event_id = False
events = MicrosoftEvent([{
"type": "singleInstance",
@ -120,7 +182,7 @@ class TestMicrosoftEvent(TestCommon):
def test_map_a_recurrence_using_global_id(self):
# arrange
rec_id = self.recurrence.ms_organizer_event_id
rec_id = self.recurrence.microsoft_id
rec_uid = self.recurrence.ms_universal_event_id
events = MicrosoftEvent([{
"type": "seriesMaster",
@ -139,7 +201,7 @@ class TestMicrosoftEvent(TestCommon):
def test_map_a_recurrence_using_instance_id(self):
# arrange
rec_id = self.recurrence.ms_organizer_event_id
rec_id = self.recurrence.microsoft_id
events = MicrosoftEvent([{
"type": "seriesMaster",
"_odoo_id": False,
@ -157,9 +219,9 @@ class TestMicrosoftEvent(TestCommon):
def test_try_to_map_mixed_of_single_events_and_recurrences(self):
# arrange
event_id = self.simple_event.ms_organizer_event_id
event_id = self.simple_event.microsoft_id
event_uid = self.simple_event.ms_universal_event_id
rec_id = self.recurrence.ms_organizer_event_id
rec_id = self.recurrence.microsoft_id
rec_uid = self.recurrence.ms_universal_event_id
events = MicrosoftEvent([
@ -184,7 +246,7 @@ class TestMicrosoftEvent(TestCommon):
def test_match_event_only(self):
# arrange
event_id = self.simple_event.ms_organizer_event_id
event_id = self.simple_event.microsoft_id
event_uid = self.simple_event.ms_universal_event_id
events = MicrosoftEvent([{
"type": "singleInstance",
@ -203,7 +265,7 @@ class TestMicrosoftEvent(TestCommon):
def test_match_recurrence_only(self):
# arrange
rec_id = self.recurrence.ms_organizer_event_id
rec_id = self.recurrence.microsoft_id
rec_uid = self.recurrence.ms_universal_event_id
events = MicrosoftEvent([{
"type": "seriesMaster",
@ -226,7 +288,7 @@ class TestMicrosoftEvent(TestCommon):
recurrence.
"""
# arrange
rec_id = self.recurrence.ms_organizer_event_id
rec_id = self.recurrence.microsoft_id
rec_uid = self.recurrence.ms_universal_event_id
events = MicrosoftEvent([{
"@removed": {
@ -247,9 +309,9 @@ class TestMicrosoftEvent(TestCommon):
def test_match_mix_of_events_and_recurrences(self):
# arrange
event_id = self.simple_event.ms_organizer_event_id
event_id = self.simple_event.microsoft_id
event_uid = self.simple_event.ms_universal_event_id
rec_id = self.recurrence.ms_organizer_event_id
rec_id = self.recurrence.microsoft_id
rec_uid = self.recurrence.ms_universal_event_id
events = MicrosoftEvent([
@ -296,11 +358,10 @@ class TestMicrosoftEvent(TestCommon):
def test_search_set_ms_universal_event_id(self):
not_synced_events = self.env['calendar.event'].search([('ms_universal_event_id', '=', False)])
synced_events = self.env['calendar.event'].search([('ms_universal_event_id', '!=', False)])
self.assertIn(self.simple_event, synced_events)
self.assertNotIn(self.simple_event, not_synced_events)
self.simple_event.ms_universal_event_id = ''
self.simple_event.ms_universal_event_id = False
not_synced_events = self.env['calendar.event'].search([('ms_universal_event_id', '=', False)])
synced_events = self.env['calendar.event'].search([('ms_universal_event_id', '!=', False)])

View file

@ -1,3 +1,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import requests
from unittest.mock import patch, call, MagicMock
@ -5,7 +7,7 @@ from unittest.mock import patch, call, MagicMock
from odoo import fields
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent
from odoo.addons.microsoft_account.models.microsoft_service import MicrosoftService
from odoo.addons.microsoft_account.models.microsoft_service import MicrosoftService, DEFAULT_MICROSOFT_TOKEN_ENDPOINT
from odoo.tests import TransactionCase
@ -28,7 +30,10 @@ class TestMicrosoftService(TransactionCase):
self.fake_next_sync_token_url = f"https://graph.microsoft.com/v1.0/me/calendarView/delta?$deltatoken={self.fake_next_sync_token}"
self.header_prefer = 'outlook.body-content-type="html", odata.maxpagesize=50'
self.header = {'Content-type': 'application/json', 'Authorization': 'Bearer %s' % self.fake_token}
self.delete_header = {
'Authorization': 'Bearer %s' % self.fake_token,
}
self.header = {'Content-type': 'application/json', **self.delete_header}
self.call_with_sync_token = call(
"/v1.0/me/calendarView/delta",
{"$deltatoken": self.fake_sync_token},
@ -365,7 +370,7 @@ class TestMicrosoftService(TransactionCase):
self.assertFalse(res)
mock_do_request.assert_called_with(
f"/v1.0/me/calendar/events/{event_id}",
{}, headers={'Authorization': 'Bearer %s' % self.fake_token}, method="DELETE", timeout=DEFAULT_TIMEOUT
{}, headers=self.delete_header, method="DELETE", timeout=DEFAULT_TIMEOUT
)
@patch.object(MicrosoftService, "_do_request")
@ -384,7 +389,7 @@ class TestMicrosoftService(TransactionCase):
self.assertTrue(res)
mock_do_request.assert_called_with(
f"/v1.0/me/calendar/events/{event_id}",
{}, headers={'Authorization': 'Bearer %s' % self.fake_token}, method="DELETE", timeout=DEFAULT_TIMEOUT
{}, headers=self.delete_header, method="DELETE", timeout=DEFAULT_TIMEOUT
)
@ -398,7 +403,7 @@ class TestMicrosoftService(TransactionCase):
self.assertTrue(res)
mock_do_request.assert_called_with(
f"/v1.0/me/calendar/events/{event_id}",
{}, headers={'Authorization': 'Bearer %s' % self.fake_token}, method="DELETE", timeout=DEFAULT_TIMEOUT
{}, headers=self.delete_header, method="DELETE", timeout=DEFAULT_TIMEOUT
)
def test_answer_token_error(self):
@ -461,3 +466,40 @@ class TestMicrosoftService(TransactionCase):
self.call_with_sync_token,
self.call_without_sync_token
])
@patch.object(MicrosoftService, "_do_request")
def test_refresh_microsoft_calendar_token_uses_correct_endpoint(self, mock_do_request):
# Ensure we use the correct endpoint (useful for single/multi-tenant deployments).
mock_do_request.return_value = self._do_request_result(
{
"access_token": "dummy_access_token",
"token_type": "Bearer",
"expires_in": 3599,
"scope": "Mail.Read User.Read",
"refresh_token": "dummy_refresh_token",
}
)
IrParameter = self.env["ir.config_parameter"].sudo()
IrParameter.set_param("microsoft_calendar_client_id", "dummy_client_id")
IrParameter.set_param("microsoft_calendar_client_secret", "dummy_client_secret")
self.env.user._refresh_microsoft_calendar_token()
custom_token_endpoint = "https://login.microsoftonline.com/dummy_tenant_id/oauth2/v2.0/token"
IrParameter.set_param("microsoft_account.token_endpoint", custom_token_endpoint)
self.env.user._refresh_microsoft_calendar_token()
kwargs = {
"params": {
"client_id": "dummy_client_id",
"client_secret": "dummy_client_secret",
"grant_type": "refresh_token",
"refresh_token": False,
},
"headers": {"Content-type": "application/x-www-form-urlencoded"},
"method": "POST",
"preuri": "",
}
first_call = call(DEFAULT_MICROSOFT_TOKEN_ENDPOINT, **kwargs)
second_call = call(custom_token_endpoint, **kwargs)
mock_do_request.assert_has_calls([first_call, second_call])

View file

@ -1,374 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService, MicrosoftEvent
from odoo.exceptions import ValidationError
import pytz
from datetime import datetime, date
from odoo.tests.common import TransactionCase
from dateutil.relativedelta import relativedelta
class TestSyncMicrosoft2Odoo(TransactionCase):
@property
def now(self):
return pytz.utc.localize(datetime.now()).isoformat()
def setUp(self):
super().setUp()
self.recurrence_id = 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA'
values = [
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAACyq4xQ=="', 'createdDateTime': '2020-05-06T07:03:49.1444085Z', 'lastModifiedDateTime': '2020-05-06T07:00:00Z', 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAACyq4xQ==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000874F057E7423D601000D848B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'busy', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFAAALLLTEAAAA&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2020-05-03', 'endDate': '2020-05-05', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAALKrjF"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX7vTsS0AARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAALKrjF"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAACyy0xAAAABA=', 'start': {'dateTime': '2020-05-04T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-04T16:00:00.0000000', 'timeZone': 'UTC'}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAALKrjF"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX8IdBHsAARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-05T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-05T16:00:00.0000000', 'timeZone': 'UTC'}}
]
self.single_event = [
{
'@odata.type': '#microsoft.graph.event',
'@odata.etag': 'W/"AAAAA"',
'type': 'singleInstance',
'id': "CCCCC",
'start': {
'dateTime': '2020-05-05T14:30:00.0000000',
'timeZone': 'UTC'
},
'end': {
'dateTime': '2020-05-05T16:00:00.0000000',
'timeZone': 'UTC'
},
'location': {
'displayName': "a meeting room at Odoo"
}
}
]
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(values))
self.datetime_future = pytz.utc.localize(datetime.now() + relativedelta(days=1)).isoformat()
def sync(self, events):
self.env['calendar.event']._sync_microsoft2odoo(events)
def test_new_microsoft_recurrence(self):
recurrence = self.env['calendar.recurrence'].search([('microsoft_id', '=', self.recurrence_id)])
events = recurrence.calendar_event_ids
self.assertTrue(recurrence, "It should have created an recurrence")
self.assertEqual(len(events), 3, "It should have created 3 events")
self.assertEqual(recurrence.base_event_id, events[0])
self.assertEqual(events.mapped('name'), ['My recurrent event', 'My recurrent event', 'My recurrent event'])
self.assertFalse(events[0].allday)
self.assertEqual(events[0].start, datetime(2020, 5, 3, 14, 30))
self.assertEqual(events[0].stop, datetime(2020, 5, 3, 16, 00))
self.assertEqual(events[1].start, datetime(2020, 5, 4, 14, 30))
self.assertEqual(events[1].stop, datetime(2020, 5, 4, 16, 00))
self.assertEqual(events[2].start, datetime(2020, 5, 5, 14, 30))
self.assertEqual(events[2].stop, datetime(2020, 5, 5, 16, 00))
def test_microsoft_recurrence_delete_one_event(self):
values = [
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADIaZKQ=="', 'createdDateTime': '2020-05-06T07:03:49.1444085Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADIaZKQ==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000874F057E7423D601000000000000000010000000C6918C4B44D2D84586351FEC8B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'busy', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2020-05-03', 'endDate': '2020-05-05', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMhpkp"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX7vTsS0AARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMhpkp"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX8IdBHsAARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-05T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-05T16:00:00.0000000', 'timeZone': 'UTC'}}
]
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(values))
recurrence = self.env['calendar.recurrence'].search([('microsoft_id', '=', self.recurrence_id)])
events = self.env['calendar.event'].search([('recurrence_id', '=', recurrence.id)], order='start asc')
self.assertTrue(recurrence, "It should keep the recurrence")
self.assertEqual(len(events), 2, "It should keep 2 events")
self.assertEqual(recurrence.base_event_id, events[0])
self.assertEqual(events.mapped('name'), ['My recurrent event', 'My recurrent event'])
def test_microsoft_recurrence_change_name_one_event(self):
values = [
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADIaZKQ=="', 'createdDateTime': '2020-05-06T07:03:49.1444085Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADIaZKQ==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000874F057E7423D601000000000000000010000000C6918C4B44D2D84586351FEC8B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'busy', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2020-05-03', 'endDate': '2020-05-05', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMhpkp"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX7vTsS0AARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADIaZKQ=="', 'createdDateTime': '2020-05-06T08:01:32.4884797Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADIaZKQ==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00807E40504874F057E7423D601000000000000000010000000C6918C4B44D2D84586351FEC8B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event 2', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'originalStart': '2020-05-04T14:30:00Z', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'showAs': 'busy', 'type': 'exception', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAACyy0xAAAABA%3D&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAACyy0xAAAABA=', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-04T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-04T16:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMhpkp"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX8IdBHsAARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-05T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-05T16:00:00.0000000', 'timeZone': 'UTC'}}
]
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(values))
recurrence = self.env['calendar.recurrence'].search([('microsoft_id', '=', self.recurrence_id)])
events = self.env['calendar.event'].search([('recurrence_id', '=', recurrence.id)], order='start asc')
self.assertTrue(recurrence, "It should have created an recurrence")
self.assertEqual(len(events), 3, "It should have created 3 events")
self.assertEqual(recurrence.base_event_id, events[0])
self.assertEqual(events.mapped('name'), ['My recurrent event', 'My recurrent event 2', 'My recurrent event'])
def test_microsoft_recurrence_change_name_all_event(self):
values = [
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADIaZKQ=="', 'createdDateTime': '2020-05-06T07:03:49.1444085Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADIaZKQ==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000874F057E7423D601000000000000000010000000C6918C4B44D2D84586351FEC8B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event 2', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'busy', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2020-05-03', 'endDate': '2020-05-05', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMhpkp"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX7vTsS0AARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMhpkp"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAACyy0xAAAABA=', 'start': {'dateTime': '2020-05-04T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-04T16:00:00.0000000', 'timeZone': 'UTC'}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMhpkp"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX8IdBHsAARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-05T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-05T16:00:00.0000000', 'timeZone': 'UTC'}}
]
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(values))
recurrence = self.env['calendar.recurrence'].search([('microsoft_id', '=', self.recurrence_id)])
events = self.env['calendar.event'].search([('recurrence_id', '=', recurrence.id)], order='start asc')
self.assertTrue(recurrence, "It should keep the recurrence")
self.assertEqual(len(events), 3, "It should keep the 3 events")
self.assertEqual(recurrence.base_event_id, events[0])
self.assertEqual(events.mapped('name'), ['My recurrent event 2', 'My recurrent event 2', 'My recurrent event 2'])
def test_microsoft_recurrence_change_date_one_event(self):
values = [
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADIaZPA=="', 'createdDateTime': '2020-05-06T07:03:49.1444085Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADIaZPA==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000874F057E7423D601000000000000000010000000C6918C4B44D2D84586351FEC8B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'busy', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2020-05-03', 'endDate': '2020-05-05', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMhpk8"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX7vTsS0AARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADIaZPA=="', 'createdDateTime': '2020-05-06T08:41:52.1067613Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADIaZPA==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00807E40504874F057E7423D601000000000000000010000000C6918C4B44D2D84586351FEC8B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'originalStart': '2020-05-04T14:30:00Z', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'showAs': 'busy', 'type': 'exception', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAACyy0xAAAABA%3D&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAACyy0xAAAABA=', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-04T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-04T17:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMhpk8"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX8IdBHsAARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-05T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-05T16:00:00.0000000', 'timeZone': 'UTC'}}
]
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(values))
recurrence = self.env['calendar.recurrence'].search([('microsoft_id', '=', self.recurrence_id)])
events = self.env['calendar.event'].search([('recurrence_id', '=', recurrence.id)], order='start asc')
special_event = self.env['calendar.event'].search([('microsoft_id', '=', 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAACyy0xAAAABA=')])
self.assertTrue(recurrence, "It should have created an recurrence")
self.assertTrue(special_event, "It should have created an special event")
self.assertEqual(len(events), 3, "It should have created 3 events")
self.assertTrue(special_event in events)
self.assertEqual(recurrence.base_event_id, events[0])
self.assertEqual(events.mapped('name'), ['My recurrent event', 'My recurrent event', 'My recurrent event'])
event_not_special = events - special_event
self.assertEqual(event_not_special[0].start, datetime(2020, 5, 3, 14, 30))
self.assertEqual(event_not_special[0].stop, datetime(2020, 5, 3, 16, 00))
self.assertEqual(event_not_special[1].start, datetime(2020, 5, 5, 14, 30))
self.assertEqual(event_not_special[1].stop, datetime(2020, 5, 5, 16, 00))
self.assertEqual(special_event.start, datetime(2020, 5, 4, 14, 30))
self.assertEqual(special_event.stop, datetime(2020, 5, 4, 17, 00))
def test_microsoft_recurrence_delete_first_event(self):
values = [
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADI/Bnw=="', 'createdDateTime': '2020-05-06T07:03:49.1444085Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADI/Bnw==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000874F057E7423D601000000000000000010000000C6918C4B44D2D84586351FEC8B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'busy', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2020-05-03', 'endDate': '2020-05-05', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMj8Gf"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAACyy0xAAAABA=', 'start': {'dateTime': '2020-05-04T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-04T16:00:00.0000000', 'timeZone': 'UTC'}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMj8Gf"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX8IdBHsAARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-05T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-05T16:00:00.0000000', 'timeZone': 'UTC'}}
]
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(values))
recurrence = self.env['calendar.recurrence'].search([('microsoft_id', '=', self.recurrence_id)])
events = self.env['calendar.event'].search([('recurrence_id', '=', recurrence.id)], order='start asc')
self.assertTrue(recurrence, "It should have created an recurrence")
self.assertEqual(len(events), 2, "It should left 2 events")
self.assertEqual(recurrence.base_event_id, events[0])
self.assertEqual(events[0].start, datetime(2020, 5, 4, 14, 30))
self.assertEqual(events[0].stop, datetime(2020, 5, 4, 16, 00))
self.assertEqual(events[1].start, datetime(2020, 5, 5, 14, 30))
self.assertEqual(events[1].stop, datetime(2020, 5, 5, 16, 00))
# Now we delete lastest event in Outlook.
values = [
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADI/Bpg=="', 'createdDateTime': '2020-05-06T07:03:49.1444085Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADI/Bpg==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000874F057E7423D601000000000000000010000000C6918C4B44D2D84586351FEC8B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'busy', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2020-05-03', 'endDate': '2020-05-05', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMj8Gm"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAACyy0xAAAABA=', 'start': {'dateTime': '2020-05-04T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-04T16:00:00.0000000', 'timeZone': 'UTC'}}
]
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(values))
events = self.env['calendar.event'].search([('recurrence_id', '=', recurrence.id)], order='start asc')
self.assertEqual(len(events), 1, "It should have created 1 events")
self.assertEqual(recurrence.base_event_id, events[0])
# Now, we change end datetime of recurrence in Outlook, so all recurrence is recreated (even deleted events)
values = [
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADI/Bqg=="', 'createdDateTime': '2020-05-06T07:03:49.1444085Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADI/Bqg==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000874F057E7423D601000000000000000010000000C6918C4B44D2D84586351FEC8B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'busy', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:30:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2020-05-03', 'endDate': '2020-05-05', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMj8Gq"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX7vTsS0AARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:30:00.0000000', 'timeZone': 'UTC'}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMj8Gq"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAACyy0xAAAABA=', 'start': {'dateTime': '2020-05-04T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-04T16:30:00.0000000', 'timeZone': 'UTC'}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMj8Gq"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX8IdBHsAARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-05T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-05T16:30:00.0000000', 'timeZone': 'UTC'}}
]
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(values))
events = self.env['calendar.event'].search([('recurrence_id', '=', recurrence.id)], order='start asc')
self.assertEqual(len(events), 3, "It should have created 3 events")
self.assertEqual(recurrence.base_event_id, events[0])
self.assertEqual(events.mapped('name'), ['My recurrent event', 'My recurrent event', 'My recurrent event'])
self.assertEqual(events[0].start, datetime(2020, 5, 3, 14, 30))
self.assertEqual(events[0].stop, datetime(2020, 5, 3, 16, 30))
self.assertEqual(events[1].start, datetime(2020, 5, 4, 14, 30))
self.assertEqual(events[1].stop, datetime(2020, 5, 4, 16, 30))
self.assertEqual(events[2].start, datetime(2020, 5, 5, 14, 30))
self.assertEqual(events[2].stop, datetime(2020, 5, 5, 16, 30))
def test_microsoft_recurrence_split_recurrence(self):
values = [
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADI/Dig=="', 'createdDateTime': '2020-05-06T07:03:49.1444085Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADI/Dig==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000874F057E7423D601000000000000000010000000C6918C4B44D2D84586351FEC8B1B7F8C', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'busy', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:30:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2020-05-03', 'endDate': '2020-05-03', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADI/Dkw=="', 'createdDateTime': '2020-05-06T13:24:10.0507138Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADI/Dkw==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E008000000001A4457A0A923D601000000000000000010000000476AE6084FD718418262DA1AE3E41411', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'busy', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAAMkgQrAAAA&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAAMkgQrAAAA', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-04T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-04T17:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2020-05-04', 'endDate': '2020-05-06', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMj8OK"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX7vTsS0AARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAAEA==', 'start': {'dateTime': '2020-05-03T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-03T16:30:00.0000000', 'timeZone': 'UTC'}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMj8OT"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAAMkgQrAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX774WtQAAAEYAAAJAcu19N72jSr9Rp1mE2xWABwBlLa4RUBXJToExnebpwea2AAACAQ0AAABlLa4RUBXJToExnebpwea2AAAADJIEKwAAABA=', 'start': {'dateTime': '2020-05-04T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-04T17:00:00.0000000', 'timeZone': 'UTC'}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"ZS2uEVAVyU6BMZ3m6cHmtgAADI/Dkw=="', 'createdDateTime': '2020-05-06T13:25:05.9240043Z', 'lastModifiedDateTime': self.datetime_future, 'changeKey': 'ZS2uEVAVyU6BMZ3m6cHmtgAADI/Dkw==', 'categories': [], 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00807E405051A4457A0A923D601000000000000000010000000476AE6084FD718418262DA1AE3E41411', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'My recurrent event 2', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'originalStart': '2020-05-05T14:30:00Z', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': True, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAAMkgQrAAAA', 'showAs': 'busy', 'type': 'exception', 'webLink': 'https://outlook.live.com/owa/?itemid=AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX8IdBHsAARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAAMkgQrAAAAEA%3D%3D&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'AllowNewTimeProposals': True, 'IsDraft': False, 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX8IdBHsAARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAAMkgQrAAAAEA==', 'responseStatus': {'response': 'organizer', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2020-05-05T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-05T17:00:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'attendees': [], 'organizer': {'emailAddress': {'name': 'outlook_7BA43549E5FD4413@outlook.com', 'address': 'outlook_7BA43549E5FD4413@outlook.com'}}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAABlLa4RUBXJToExnebpwea2AAAMj8OT"', 'seriesMasterId': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAAMkgQrAAAA', 'type': 'occurrence', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoBUQAICADX8VBriIAARgAAAkBy7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAAMkgQrAAAAEA==', 'start': {'dateTime': '2020-05-06T14:30:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2020-05-06T17:00:00.0000000', 'timeZone': 'UTC'}}
]
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(values))
recurrence_1 = self.env['calendar.recurrence'].search([('microsoft_id', '=', self.recurrence_id)])
recurrence_2 = self.env['calendar.recurrence'].search([('microsoft_id', '=', 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAAMkgQrAAAA')])
events_1 = self.env['calendar.event'].search([('recurrence_id', '=', recurrence_1.id)], order='start asc')
events_2 = self.env['calendar.event'].search([('recurrence_id', '=', recurrence_2.id)], order='start asc')
self.assertTrue(recurrence_1, "It should have created an recurrence")
self.assertTrue(recurrence_2, "It should have created an recurrence")
self.assertEqual(len(events_1), 1, "It should left 1 event")
self.assertEqual(len(events_2), 3, "It should have created 3 events")
self.assertEqual(recurrence_1.base_event_id, events_1[0])
self.assertEqual(recurrence_2.base_event_id, events_2[0])
self.assertEqual(events_1.mapped('name'), ['My recurrent event'])
self.assertEqual(events_2.mapped('name'), ['My recurrent event', 'My recurrent event 2', 'My recurrent event'])
self.assertEqual(events_1[0].start, datetime(2020, 5, 3, 14, 30))
self.assertEqual(events_1[0].stop, datetime(2020, 5, 3, 16, 30))
self.assertEqual(events_2[0].start, datetime(2020, 5, 4, 14, 30))
self.assertEqual(events_2[0].stop, datetime(2020, 5, 4, 17, 00))
self.assertEqual(events_2[1].start, datetime(2020, 5, 5, 14, 30))
self.assertEqual(events_2[1].stop, datetime(2020, 5, 5, 17, 00))
self.assertEqual(events_2[2].start, datetime(2020, 5, 6, 14, 30))
self.assertEqual(events_2[2].stop, datetime(2020, 5, 6, 17, 00))
def test_microsoft_recurrence_delete(self):
recurrence_id = self.env['calendar.recurrence'].search([('microsoft_id', '=', self.recurrence_id)])
event_ids = self.env['calendar.event'].search([('recurrence_id', '=', recurrence_id.id)], order='start asc').ids
values = [{'@odata.type': '#microsoft.graph.event', 'id': 'AQ8PojGtrADQATM3ZmYAZS0yY2MAMC00MDg1LTAwAi0wMAoARgAAA0By7X03vaNKv1GnWYTbFYAHAGUtrhFQFclOgTGd5unB5rYAAAIBDQAAAGUtrhFQFclOgTGd5unB5rYAAAALLLTEAAAA', '@removed': {'reason': 'deleted'}}]
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(values))
recurrence = self.env['calendar.recurrence'].search([('microsoft_id', '=', self.recurrence_id)])
events = self.env['calendar.event'].browse(event_ids).exists()
self.assertFalse(recurrence, "It should remove recurrence")
self.assertFalse(events, "It should remove all events")
def test_attendees_must_have_email(self):
"""
Synching with a partner without mail raises a ValidationError because Microsoft don't accept attendees without one.
"""
MicrosoftCal = MicrosoftCalendarService(self.env['microsoft.service'])
partner = self.env['res.partner'].create({
'name': 'SuperPartner',
})
event = self.env['calendar.event'].create({
'name': "SuperEvent",
'start': datetime(2020, 3, 16, 11, 0),
'stop': datetime(2020, 3, 16, 13, 0),
'partner_ids': [(4, partner.id)],
})
with self.assertRaises(ValidationError):
event._sync_odoo2microsoft(MicrosoftCal)
def test_cancel_occurence_of_recurrent_event(self):
""" The user is invited to a recurrent event. When synced, all events are present, there are three occurrences:
- 07/15/2021, 15:00-15:30
- 07/16/2021, 15:00-15:30
- 07/17/2021, 15:00-15:30
Then, the organizer cancels the second occurrence -> The latter should not be displayed anymore
"""
microsoft_id = 'AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgBGAAADZ59RIxdyh0Kt-MXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAA='
# self.env.user.partner_id.email = "odoo_bf_user01@outlook.com"
first_sync_values = [
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"pynKRnkCyUmnqILQHcLZEQAABElcNQ=="', 'createdDateTime': '2021-07-15T14:47:40.2996962Z', 'lastModifiedDateTime': '2021-07-15T14:47:40.3783507Z', 'changeKey': 'pynKRnkCyUmnqILQHcLZEQAABElcNQ==', 'categories': [], 'transactionId': None, 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000B35B3B5A8879D70100000000000000001000000008A0949F4EC0A1479E4ED178D87EF679', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'Recurrent Event 1646', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': False, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'tentative', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgBGAAADZ59RIxdyh0Kt%2FMXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAA%3D&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'allowNewTimeProposals': True, 'OccurrenceId': None, 'isDraft': False, 'hideAttendees': False, 'CalendarEventClassifications': [], 'AutoRoomBookingOptions': None, 'onlineMeeting': None, 'id': microsoft_id, 'responseStatus': {'response': 'notResponded', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2021-07-15T15:00:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2021-07-15T15:30:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2021-07-15', 'endDate': '2021-07-17', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [{'type': 'required', 'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'}, 'emailAddress': {'name': 'Odoo02 Outlook02', 'address': 'odoo_bf_user02@outlook.com'}}, {'type': 'required', 'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'}, 'emailAddress': {'name': 'Odoo01 Outlook01', 'address': 'odoo_bf_user01@outlook.com'}}], 'organizer': {'emailAddress': {'name': 'Odoo02 Outlook02', 'address': 'odoo_bf_user02@outlook.com'}}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAACnKcpGeQLJSaeogtAdwtkRAAAESVw1"', 'seriesMasterId': ('%s' % microsoft_id), 'type': 'occurrence', 'id': 'AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgFRAAgIANlHI305wABGAAACZ59RIxdyh0Kt-MXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAAQ', 'start': {'dateTime': '2021-07-15T15:00:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2021-07-15T15:30:00.0000000', 'timeZone': 'UTC'}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAACnKcpGeQLJSaeogtAdwtkRAAAESVw1"', 'seriesMasterId': microsoft_id, 'type': 'occurrence', 'id': 'AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgFRAAgIANlH7KejgABGAAACZ59RIxdyh0Kt-MXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAAQ', 'start': {'dateTime': '2021-07-16T15:00:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2021-07-16T15:30:00.0000000', 'timeZone': 'UTC'}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAACnKcpGeQLJSaeogtAdwtkRAAAESVw1"', 'seriesMasterId': microsoft_id, 'type': 'occurrence', 'id': 'AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgFRAAgIANlItdINQABGAAACZ59RIxdyh0Kt-MXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAAQ', 'start': {'dateTime': '2021-07-17T15:00:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2021-07-17T15:30:00.0000000', 'timeZone': 'UTC'}}
]
second_sync_values = [
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"pynKRnkCyUmnqILQHcLZEQAABElcUw=="', 'createdDateTime': '2021-07-15T14:47:40.2996962Z', 'lastModifiedDateTime': '2021-07-15T14:51:25.2560888Z', 'changeKey': 'pynKRnkCyUmnqILQHcLZEQAABElcUw==', 'categories': [], 'transactionId': None, 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00800000000B35B3B5A8879D70100000000000000001000000008A0949F4EC0A1479E4ED178D87EF679', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'Recurrent Event 1646', 'bodyPreview': '', 'importance': 'normal', 'sensitivity': 'normal', 'isAllDay': False, 'isCancelled': False, 'isOrganizer': False, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': None, 'showAs': 'tentative', 'type': 'seriesMaster', 'webLink': 'https://outlook.live.com/owa/?itemid=AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgBGAAADZ59RIxdyh0Kt%2FMXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAA%3D&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'allowNewTimeProposals': True, 'OccurrenceId': None, 'isDraft': False, 'hideAttendees': False, 'CalendarEventClassifications': [], 'id': microsoft_id, 'responseStatus': {'response': 'notResponded', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': ''}, 'start': {'dateTime': '2021-07-15T15:00:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2021-07-15T15:30:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'recurrence': {'pattern': {'type': 'daily', 'interval': 1, 'month': 0, 'dayOfMonth': 0, 'firstDayOfWeek': 'sunday', 'index': 'first'}, 'range': {'type': 'endDate', 'startDate': '2021-07-15', 'endDate': '2021-07-17', 'recurrenceTimeZone': 'Romance Standard Time', 'numberOfOccurrences': 0}}, 'attendees': [{'type': 'required', 'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'}, 'emailAddress': {'name': 'Odoo02 Outlook02', 'address': 'odoo_bf_user02@outlook.com'}}, {'type': 'required', 'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'}, 'emailAddress': {'name': 'Odoo01 Outlook01', 'address': 'odoo_bf_user01@outlook.com'}}], 'organizer': {'emailAddress': {'name': 'Odoo02 Outlook02', 'address': 'odoo_bf_user02@outlook.com'}}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAACnKcpGeQLJSaeogtAdwtkRAAAESVxT"', 'seriesMasterId': microsoft_id, 'type': 'occurrence', 'id': 'AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgFRAAgIANlHI305wABGAAACZ59RIxdyh0Kt-MXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAAQ', 'start': {'dateTime': '2021-07-15T15:00:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2021-07-15T15:30:00.0000000', 'timeZone': 'UTC'}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"pynKRnkCyUmnqILQHcLZEQAABElcUw=="', 'createdDateTime': '2021-07-15T14:51:25.1366139Z', 'lastModifiedDateTime': '2021-07-15T14:51:25.136614Z', 'changeKey': 'pynKRnkCyUmnqILQHcLZEQAABElcUw==', 'categories': [], 'transactionId': None, 'originalStartTimeZone': 'Romance Standard Time', 'originalEndTimeZone': 'Romance Standard Time', 'iCalUId': '040000008200E00074C5B7101A82E00807E50710B35B3B5A8879D70100000000000000001000000008A0949F4EC0A1479E4ED178D87EF679', 'reminderMinutesBeforeStart': 15, 'isReminderOn': True, 'hasAttachments': False, 'subject': 'Canceled: Recurrent Event 1646', 'bodyPreview': '', 'importance': 'high', 'sensitivity': 'normal', 'originalStart': '2021-07-16T15:00:00Z', 'isAllDay': False, 'isCancelled': True, 'isOrganizer': False, 'IsRoomRequested': False, 'AutoRoomBookingStatus': 'None', 'responseRequested': True, 'seriesMasterId': microsoft_id, 'showAs': 'free', 'type': 'exception', 'webLink': 'https://outlook.live.com/owa/?itemid=AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgFRAAgIANlH7KejgABGAAACZ59RIxdyh0Kt%2FMXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAAQ&exvsurl=1&path=/calendar/item', 'onlineMeetingUrl': None, 'isOnlineMeeting': False, 'onlineMeetingProvider': 'unknown', 'allowNewTimeProposals': True, 'OccurrenceId': ('OID.%s.2021-07-16' % microsoft_id), 'isDraft': False, 'hideAttendees': False, 'CalendarEventClassifications': [], 'id': 'AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgFRAAgIANlH7KejgABGAAACZ59RIxdyh0Kt-MXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAAQ', 'responseStatus': {'response': 'notResponded', 'time': '0001-01-01T00:00:00Z'}, 'body': {'contentType': 'html', 'content': '<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8">\r\n<meta name="Generator" content="Microsoft Exchange Server">\r\n<!-- converted from text -->\r\n<style><!-- .EmailQuote { margin-left: 1pt; padding-left: 4pt; border-left: #800000 2px solid; } --></style></head>\r\n<body>\r\n<font size="2"><span style="font-size:11pt;"><div class="PlainText">&nbsp;</div></span></font>\r\n</body>\r\n</html>\r\n'}, 'start': {'dateTime': '2021-07-16T15:00:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2021-07-16T15:30:00.0000000', 'timeZone': 'UTC'}, 'location': {'displayName': '', 'locationType': 'default', 'uniqueIdType': 'unknown', 'address': {}, 'coordinates': {}}, 'locations': [], 'attendees': [{'type': 'required', 'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'}, 'emailAddress': {'name': 'Odoo02 Outlook02', 'address': 'odoo_bf_user02@outlook.com'}}, {'type': 'required', 'status': {'response': 'none', 'time': '0001-01-01T00:00:00Z'}, 'emailAddress': {'name': 'Odoo01 Outlook01', 'address': 'odoo_bf_user01@outlook.com'}}], 'organizer': {'emailAddress': {'name': 'Odoo02 Outlook02', 'address': 'odoo_bf_user02@outlook.com'}}},
{'@odata.type': '#microsoft.graph.event', '@odata.etag': 'W/"DwAAABYAAACnKcpGeQLJSaeogtAdwtkRAAAESVxT"', 'seriesMasterId': microsoft_id, 'type': 'occurrence', 'id': 'AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgFRAAgIANlItdINQABGAAACZ59RIxdyh0Kt-MXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAAQ', 'start': {'dateTime': '2021-07-17T15:00:00.0000000', 'timeZone': 'UTC'}, 'end': {'dateTime': '2021-07-17T15:30:00.0000000', 'timeZone': 'UTC'}}
]
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(first_sync_values))
recurrent_event = self.env['calendar.recurrence'].search([('microsoft_id', '=', 'AQMkADAwATM3ZmYAZS0zZmMyLWYxYjQtMDACLTAwCgBGAAADZ59RIxdyh0Kt-MXfyCpfwAcApynKRnkCyUmnqILQHcLZEQAAAgENAAAApynKRnkCyUmnqILQHcLZEQAAAARKsSQAAAA=')])
self.assertEqual(len(recurrent_event.calendar_event_ids), 3)
# Need to cheat on the write date, otherwise the second sync won't update the events
recurrent_event.write_date = datetime(2021, 7, 15, 14, 00)
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(second_sync_values))
self.assertEqual(len(recurrent_event.calendar_event_ids), 2)
events = recurrent_event.calendar_event_ids.sorted(key=lambda e: e.start)
self.assertEqual(events[0].start, datetime(2021, 7, 15, 15, 00))
self.assertEqual(events[0].stop, datetime(2021, 7, 15, 15, 30))
self.assertEqual(events[1].start, datetime(2021, 7, 17, 15, 00))
self.assertEqual(events[1].stop, datetime(2021, 7, 17, 15, 30))
def test_use_classic_location(self):
ms_event = self.single_event
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(ms_event))
event = self.env['calendar.event'].search([("microsoft_id", "=", ms_event[0]["id"])])
self.assertEqual(event.location, ms_event[0]["location"]["displayName"])
def test_use_url_location(self):
ms_event = self.single_event
ms_event[0]["location"]["displayName"] = "https://mylocation.com/meeting-room"
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(ms_event))
event = self.env['calendar.event'].search([("microsoft_id", "=", ms_event[0]["id"])])
self.assertEqual(event.location, ms_event[0]["location"]["displayName"])
def test_use_specific_virtual_location(self):
"""
If the location of the Outlook event is a specific virtual location (such as a video Teams meeting),
use it as videocall location.
"""
ms_event = self.single_event
ms_event[0]["location"]["displayName"] = "https://teams.microsoft.com/l/meeting/1234"
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(ms_event))
event = self.env['calendar.event'].search([("microsoft_id", "=", ms_event[0]["id"])])
self.assertEqual(event.location, False)
self.assertEqual(event.videocall_location, ms_event[0]["location"]["displayName"])
def test_outlook_event_has_online_meeting_url(self):
ms_event = self.single_event
ms_event[0].update({
'isOnlineMeeting': True,
'onlineMeeting': {'joinUrl': 'https://video-meeting.com/1234'}
})
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(ms_event))
event = self.env['calendar.event'].search([("microsoft_id", "=", ms_event[0]["id"])])
self.assertEqual(event.videocall_location, ms_event[0]["onlineMeeting"]["joinUrl"])
def test_event_reminder_emails_with_microsoft_id(self):
"""
Odoo shouldn't send email reminders for synced events.
Test that events synced to Microsoft (with a `microsoft_id`)
are excluded from email alarm notifications.
"""
now = datetime.now()
start = now - relativedelta(minutes=30)
end = now + relativedelta(hours=2)
alarm = self.env['calendar.alarm'].create({
'name': 'Alarm',
'alarm_type': 'email',
'interval': 'minutes',
'duration': 30,
})
ms_event = self.single_event
ms_event[0].update({
'isOnlineMeeting': True,
'alarm_id': alarm.id,
'start': {
'dateTime': pytz.utc.localize(start).isoformat(),
'timeZone': 'Europe/Brussels'
},
'reminders': {'overrides': [{"method": "email", "minutes": 30}], 'useDefault': False},
'end': {
'dateTime': pytz.utc.localize(end).isoformat(),
'timeZone': 'Europe/Brussels'
},
})
self.env['calendar.event']._sync_microsoft2odoo(MicrosoftEvent(ms_event))
events_by_alarm = self.env['calendar.alarm_manager']._get_events_by_alarm_to_notify('email')
self.assertFalse(events_by_alarm, "Events with microsoft_id should not trigger reminders")

View file

@ -1,109 +0,0 @@
# -*- 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 TransactionCase
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
from odoo.addons.microsoft_calendar.models.res_users import User
from odoo.addons.microsoft_calendar.models.microsoft_sync import MicrosoftSync
from odoo.modules.registry import Registry
from odoo.addons.microsoft_account.models.microsoft_service import TIMEOUT
def patch_api(func):
@patch.object(MicrosoftSync, '_microsoft_insert', MagicMock())
@patch.object(MicrosoftSync, '_microsoft_delete', MagicMock())
@patch.object(MicrosoftSync, '_microsoft_patch', MagicMock())
def patched(self, *args, **kwargs):
return func(self, *args, **kwargs)
return patched
@patch.object(User, '_get_microsoft_calendar_token', lambda user: 'dummy-token')
class TestSyncOdoo2Microsoft(TransactionCase):
def setUp(self):
super().setUp()
self.microsoft_service = MicrosoftCalendarService(self.env['microsoft.service'])
def assertMicrosoftEventInserted(self, values):
MicrosoftSync._microsoft_insert.assert_called_once_with(self.microsoft_service, values)
def assertMicrosoftEventNotInserted(self):
MicrosoftSync._microsoft_insert.assert_not_called()
def assertMicrosoftEventPatched(self, microsoft_id, values, timeout=None):
expected_args = (microsoft_id, values)
expected_kwargs = {'timeout': timeout} if timeout else {}
MicrosoftSync._microsoft_patch.assert_called_once()
args, kwargs = MicrosoftSync._microsoft_patch.call_args
self.assertEqual(args[1:], expected_args) # skip Google service arg
self.assertEqual(kwargs, expected_kwargs)
@patch_api
def test_stop_synchronization(self):
self.env.user.stop_microsoft_synchronization()
self.assertTrue(self.env.user.microsoft_synchronization_stopped, "The microsoft synchronization flag should be switched on")
self.assertFalse(self.env.user._sync_microsoft_calendar(self.microsoft_service), "The microsoft synchronization should be stopped")
year = date.today().year - 1
# If synchronization stopped, creating a new event should not call _google_insert.
self.env['calendar.event'].create({
'name': "Event",
'start': datetime(year, 1, 15, 8, 0),
'stop': datetime(year, 1, 15, 18, 0),
'privacy': 'private',
})
self.assertMicrosoftEventNotInserted()
@patch_api
def test_restart_synchronization(self):
# Test new event created after stopping synchronization are correctly patched when restarting sync.
self.maxDiff = None
microsoft_id = 'aaaaaaaaa'
year = date.today().year
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_microsoft_synchronization()
# In case of full sync, limit to a range of 1y in past and 1y in the future by default
event = self.env['calendar.event'].with_user(user).create({
'microsoft_id': microsoft_id,
'name': "Event",
'start': datetime(year, 1, 15, 8, 0),
'stop': datetime(year, 1, 15, 18, 0),
'partner_ids': [(4, partner.id)],
})
user.with_user(user).restart_microsoft_synchronization()
event.with_user(user)._sync_odoo2microsoft(self.microsoft_service)
microsoft_guid = self.env['ir.config_parameter'].sudo().get_param('microsoft_calendar.microsoft_guid', False)
self.assertMicrosoftEventPatched(event.microsoft_id, {
'id': event.microsoft_id,
'start': {'dateTime': '%s-01-15T08:00:00+00:00' % year, 'timeZone': 'Europe/London'},
'end': {'dateTime': '%s-01-15T18:00:00+00:00' % year, 'timeZone': 'Europe/London'},
'subject': 'Event',
'body': {'content': '', 'contentType': 'html'},
'attendees': [],
'isAllDay': False,
'isOrganizer': True,
'isReminderOn': False,
'sensitivity': 'normal',
'showAs': 'busy',
'location': {'displayName': ''},
'organizer': {'emailAddress': {'address': 'jean-luc@opoo.com', 'name': 'Test user Calendar'}},
'reminderMinutesBeforeStart': 0,
'singleValueExtendedProperties': [{
'id': 'String {%s} Name odoo_id' % microsoft_guid,
'value': str(event.id),
}, {
'id': 'String {%s} Name owner_odoo_id' % microsoft_guid,
'value': str(user.id),
}
]
})

View file

@ -0,0 +1,107 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import patch
from datetime import datetime
from freezegun import freeze_time
from odoo import Command
from odoo.addons.mail.tests.common import MailCase
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
from odoo.addons.microsoft_calendar.models.res_users import ResUsers
from odoo.addons.microsoft_calendar.tests.common import TestCommon
class TestSyncOdoo2MicrosoftMail(TestCommon, MailCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.users = []
for n in range(1, 4):
user = cls.env['res.users'].create({
'name': f'user{n}',
'login': f'user{n}',
'email': f'user{n}@odoo.com',
'microsoft_calendar_rtoken': f'abc{n}',
'microsoft_calendar_token': f'abc{n}',
'microsoft_calendar_token_validity': datetime(9999, 12, 31),
})
user.res_users_settings_id.write({
'microsoft_synchronization_stopped': False,
'microsoft_calendar_sync_token': f'{n}_sync_token',
})
cls.users += [user]
@freeze_time("2020-01-01")
@patch.object(ResUsers, '_get_microsoft_calendar_token', lambda user: user.microsoft_calendar_token)
def test_event_creation_for_user(self):
"""Check that either emails or synchronization happens correctly when creating an event for another user."""
user_root = self.env.ref('base.user_root')
self.assertFalse(user_root.microsoft_calendar_token)
partner = self.env['res.partner'].create({'name': 'Jean-Luc', 'email': 'jean-luc@opoo.com'})
event_values = {
'name': 'Event',
'need_sync_m': True,
'start': datetime(2020, 1, 15, 8, 0),
'stop': datetime(2020, 1, 15, 18, 0),
}
paused_sync_user = self.users[2]
paused_sync_user.write({
'email': 'ms.sync.paused@test.lan',
'microsoft_synchronization_stopped': True,
'name': 'Paused Microsoft Sync User',
'login': 'ms_sync_paused_user',
})
self.assertTrue(paused_sync_user.microsoft_synchronization_stopped)
for create_user, organizer, mail_notified_partners, attendee in [
(user_root, self.users[0], partner + self.users[0].partner_id, partner), # emulates online appointment with user 0
(user_root, None, partner, partner), # emulates online resource appointment
(self.users[0], None, False, partner),
(self.users[0], self.users[0], False, partner),
(self.users[0], self.users[1], False, partner),
# create user has paused sync and organizer can sync -> will not sync because of bug
# only the organizer is notified as we don't notify the author (= create_user.partner_id) on creation
(paused_sync_user, self.users[0], self.users[0].partner_id, paused_sync_user.partner_id),
]:
with self.subTest(create_uid=create_user.name if create_user else None, user_id=organizer.name if organizer else None, attendee=attendee.name):
with self.mock_mail_gateway(), patch.object(MicrosoftCalendarService, 'insert') as mock_insert:
mock_insert.return_value = ('1', '1')
self.env['calendar.event'].with_user(create_user).create({
**event_values,
'partner_ids': [(4, organizer.partner_id.id), (4, attendee.id)] if organizer else [(4, attendee.id)],
'user_id': organizer.id if organizer else False,
})
self.env.cr.postcommit.run()
if not mail_notified_partners:
self.assertNotSentEmail()
mock_insert.assert_called_once()
self.assert_dict_equal(mock_insert.call_args[0][0]['organizer'], {
'emailAddress': {'address': organizer.email if organizer else '', 'name': organizer.name if organizer else ''}
})
else:
mock_insert.assert_not_called()
for notified_partner in mail_notified_partners:
self.assertMailMail(notified_partner, 'sent', author=(organizer or create_user).partner_id)
def test_change_organizer_pure_odoo_event(self):
"""
Test that changing organizer on a pure Odoo event (not synced with Microsoft)
does not archive the event.
"""
self.organizer_user.microsoft_synchronization_stopped = True
event = self.env["calendar.event"].with_user(self.organizer_user).create({
'name': "Pure Odoo Event",
'start': datetime(2024, 1, 1, 10, 0),
'stop': datetime(2024, 1, 1, 11, 0),
'user_id': self.organizer_user.id,
'partner_ids': [Command.set([self.organizer_user.partner_id.id, self.attendee_user.partner_id.id])],
})
self.assertFalse(event.microsoft_id)
self.assertTrue(event.active)
event.write({
'user_id': self.attendee_user.id,
})
self.assertTrue(event.active, "Pure Odoo event should not be archived when changing organizer")
self.assertEqual(event.user_id, self.attendee_user, "Organizer should be updated")

View file

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from dateutil.parser import parse
import logging
@ -8,17 +9,17 @@ from freezegun import freeze_time
from odoo import Command
from odoo.addons.microsoft_calendar.models.microsoft_sync import MicrosoftSync
from odoo.addons.microsoft_calendar.models.microsoft_sync import MicrosoftCalendarSync
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent
from odoo.addons.microsoft_calendar.models.res_users import User
from odoo.addons.microsoft_calendar.utils.event_id_storage import combine_ids
from odoo.addons.microsoft_calendar.models.res_users import ResUsers
from odoo.addons.microsoft_calendar.tests.common import TestCommon, mock_get_token, _modified_date_in_the_future, patch_api
from odoo.exceptions import UserError, ValidationError
_logger = logging.getLogger(__name__)
@patch.object(User, '_get_microsoft_calendar_token', mock_get_token)
@patch.object(ResUsers, '_get_microsoft_calendar_token', mock_get_token)
class TestUpdateEvents(TestCommon):
@patch_api
@ -68,8 +69,8 @@ class TestUpdateEvents(TestCommon):
# assert
self.assertTrue(res)
mock_patch.assert_called_once_with(
self.simple_event.ms_organizer_event_id,
{"subject": "my new simple event"},
self.simple_event.microsoft_id,
{"subject": "my new simple event", "isOnlineMeeting": False},
token=mock_get_token(self.organizer_user),
timeout=ANY,
)
@ -92,8 +93,8 @@ class TestUpdateEvents(TestCommon):
# assert
self.assertTrue(res)
mock_patch.assert_called_once_with(
self.simple_event.ms_organizer_event_id,
{"subject": "my new simple event"},
self.simple_event.microsoft_id,
{"subject": "my new simple event", "isOnlineMeeting": False},
token=mock_get_token(self.organizer_user),
timeout=ANY,
)
@ -123,7 +124,7 @@ class TestUpdateEvents(TestCommon):
# assert
self.assertTrue(res)
mock_patch.assert_called_once_with(
self.recurrent_events[modified_event_id].ms_organizer_event_id,
self.recurrent_events[modified_event_id].microsoft_id,
{'seriesMasterId': 'REC123', 'type': 'exception', "subject": new_name},
token=mock_get_token(self.organizer_user),
timeout=ANY,
@ -157,7 +158,7 @@ class TestUpdateEvents(TestCommon):
# assert
self.assertTrue(res)
mock_patch.assert_called_once_with(
self.recurrent_events[modified_event_id].ms_organizer_event_id,
self.recurrent_events[modified_event_id].microsoft_id,
{
'seriesMasterId': 'REC123',
'type': 'exception',
@ -228,7 +229,7 @@ class TestUpdateEvents(TestCommon):
# assert
self.assertTrue(res)
mock_patch.assert_called_once_with(
self.recurrent_events[modified_event_id].ms_organizer_event_id,
self.recurrent_events[modified_event_id].microsoft_id,
{'seriesMasterId': 'REC123', 'type': 'exception', "subject": new_name},
token=mock_get_token(self.organizer_user),
timeout=ANY,
@ -266,7 +267,7 @@ class TestUpdateEvents(TestCommon):
self.assertEqual(mock_patch.call_count, self.recurrent_events_count - modified_event_id)
for i in range(modified_event_id, self.recurrent_events_count):
mock_patch.assert_any_call(
self.recurrent_events[i].ms_organizer_event_id,
self.recurrent_events[i].microsoft_id,
{'seriesMasterId': 'REC123', 'type': 'exception', "subject": new_name},
token=mock_get_token(self.organizer_user),
timeout=ANY,
@ -304,7 +305,7 @@ class TestUpdateEvents(TestCommon):
existing_recurrences = self.env["calendar.recurrence"].search([])
expected_deleted_event_ids = [
r.ms_organizer_event_id
r.microsoft_id
for i, r in enumerate(self.recurrent_events)
if i in range(modified_event_id + 1, self.recurrent_events_count)
]
@ -337,7 +338,7 @@ class TestUpdateEvents(TestCommon):
# the base event should have been modified
mock_patch.assert_called_once_with(
self.recurrent_events[modified_event_id].ms_organizer_event_id,
self.recurrent_events[modified_event_id].microsoft_id,
{
'seriesMasterId': 'REC123',
'type': 'exception',
@ -373,14 +374,14 @@ class TestUpdateEvents(TestCommon):
existing_recurrences = self.env["calendar.recurrence"].search([])
expected_deleted_event_ids = [
r.ms_organizer_event_id
r.microsoft_id
for i, r in enumerate(self.recurrent_events)
if i in range(modified_event_id + 1, self.recurrent_events_count)
]
# as the test overlap the previous event of the updated event, this previous event
# should be removed too
expected_deleted_event_ids += [self.recurrent_events[modified_event_id - 1].ms_organizer_event_id]
expected_deleted_event_ids += [self.recurrent_events[modified_event_id - 1].microsoft_id]
# act
res = self.recurrent_events[modified_event_id].with_user(self.organizer_user).write({
@ -410,7 +411,7 @@ class TestUpdateEvents(TestCommon):
# the base event should have been modified
mock_patch.assert_called_once_with(
self.recurrent_events[modified_event_id].ms_organizer_event_id,
self.recurrent_events[modified_event_id].microsoft_id,
{
'seriesMasterId': 'REC123',
'type': 'exception',
@ -445,7 +446,7 @@ class TestUpdateEvents(TestCommon):
existing_recurrences = self.env["calendar.recurrence"].search([])
expected_deleted_event_ids = [
r.ms_organizer_event_id
r.microsoft_id
for i, r in enumerate(self.recurrent_events)
if i in range(modified_event_id + 1, self.recurrent_events_count)
]
@ -478,7 +479,7 @@ class TestUpdateEvents(TestCommon):
# the base event should have been modified
mock_patch.assert_called_once_with(
self.recurrent_events[modified_event_id].ms_organizer_event_id,
self.recurrent_events[modified_event_id].microsoft_id,
{
'seriesMasterId': 'REC123',
'type': 'exception',
@ -525,7 +526,7 @@ class TestUpdateEvents(TestCommon):
self.assertEqual(mock_patch.call_count, self.recurrent_events_count)
for i in range(self.recurrent_events_count):
mock_patch.assert_any_call(
self.recurrent_events[i].ms_organizer_event_id,
self.recurrent_events[i].microsoft_id,
{'seriesMasterId': 'REC123', 'type': 'exception', "subject": new_name},
token=mock_get_token(self.organizer_user),
timeout=ANY,
@ -548,7 +549,7 @@ class TestUpdateEvents(TestCommon):
new_date = datetime(2021, 9, 25, 10, 0, 0)
existing_recurrences = self.env["calendar.recurrence"].search([])
expected_deleted_event_ids = [
r.ms_organizer_event_id
r.microsoft_id
for i, r in enumerate(self.recurrent_events)
if i in range(1, self.recurrent_events_count)
]
@ -571,7 +572,7 @@ class TestUpdateEvents(TestCommon):
self.assertEqual(len(new_recurrences.calendar_event_ids), self.recurrent_events_count)
mock_patch.assert_called_once_with(
self.recurrent_events[0].ms_organizer_event_id,
self.recurrent_events[0].microsoft_id,
{
'seriesMasterId': 'REC123',
'type': 'exception',
@ -612,7 +613,7 @@ class TestUpdateEvents(TestCommon):
new_date = datetime(2021, 9, 25, 10, 0, 0)
existing_recurrences = self.env["calendar.recurrence"].search([])
expected_deleted_event_ids = [
r.ms_organizer_event_id
r.microsoft_id
for i, r in enumerate(self.recurrent_events)
if i in range(1, self.recurrent_events_count)
]
@ -635,7 +636,7 @@ class TestUpdateEvents(TestCommon):
self.assertEqual(len(new_recurrences.calendar_event_ids), self.recurrent_events_count)
mock_patch.assert_called_once_with(
self.recurrent_events[0].ms_organizer_event_id,
self.recurrent_events[0].microsoft_id,
{
'seriesMasterId': 'REC123',
'type': 'exception',
@ -739,7 +740,7 @@ class TestUpdateEvents(TestCommon):
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
# assert
updated_event = self.env["calendar.event"].search([('ms_organizer_event_id', '=', ms_event_id)])
updated_event = self.env["calendar.event"].search([('microsoft_id', '=', ms_event_id)])
self.assertEqual(updated_event.name, new_name)
self.assertEqual(updated_event.follow_recurrence, False)
@ -767,7 +768,7 @@ class TestUpdateEvents(TestCommon):
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
# assert
updated_event = self.env["calendar.event"].search([('ms_organizer_event_id', '=', ms_event_id)])
updated_event = self.env["calendar.event"].search([('microsoft_id', '=', ms_event_id)])
self.assertEqual(updated_event.start, new_date)
self.assertEqual(updated_event.follow_recurrence, False)
@ -797,7 +798,7 @@ class TestUpdateEvents(TestCommon):
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
# assert
updated_event = self.env["calendar.event"].search([('ms_organizer_event_id', '=', ms_event_id)])
updated_event = self.env["calendar.event"].search([('microsoft_id', '=', ms_event_id)])
self.assertEqual(updated_event.start, new_date)
self.assertEqual(updated_event.follow_recurrence, False)
@ -829,10 +830,10 @@ class TestUpdateEvents(TestCommon):
# assert
updated_events = self.env["calendar.event"].search([
('ms_organizer_event_id', 'in', tuple(ms_event_ids.keys()))
('microsoft_id', 'in', tuple(ms_event_ids.keys()))
])
for e in updated_events:
self.assertEqual(e.name, ms_event_ids[e.ms_organizer_event_id])
self.assertEqual(e.name, ms_event_ids[e.microsoft_id])
@patch.object(MicrosoftCalendarService, 'get_events')
def test_update_start_of_one_event_and_future_of_recurrence_from_outlook_organizer_calendar(self, mock_get_events):
@ -941,14 +942,15 @@ class TestUpdateEvents(TestCommon):
# new recurrence
self.assertEqual(len(new_recurrences), 1)
self.assertEqual(len(new_events), new_recurrence_event_count)
self.assertEqual(new_recurrences.ms_organizer_event_id, "REC123_new")
self.assertEqual(new_recurrences.microsoft_id, "REC123_new")
self.assertEqual(new_recurrences.ms_universal_event_id, "REC456_new")
for i, e in enumerate(sorted(new_events, key=lambda e: e.id)):
self.assert_odoo_event(e, {
"start": new_rec_first_event_start_date + timedelta(days=i * self.recurrent_event_interval),
"stop": new_rec_first_event_end_date + timedelta(days=i * self.recurrent_event_interval),
"microsoft_id": combine_ids(f'REC123_new_{i+1}', f'REC456_new_{i+1}'),
"microsoft_id": f'REC123_new_{i+1}',
"ms_universal_event_id": f'REC456_new_{i+1}',
"recurrence_id": new_recurrences,
"follow_recurrence": True,
})
@ -1060,14 +1062,15 @@ class TestUpdateEvents(TestCommon):
# new recurrence
self.assertEqual(len(new_recurrences), 1)
self.assertEqual(len(new_events), new_recurrence_event_count)
self.assertEqual(new_recurrences.ms_organizer_event_id, "REC123_new")
self.assertEqual(new_recurrences.microsoft_id, "REC123_new")
self.assertEqual(new_recurrences.ms_universal_event_id, "REC456_new")
for i, e in enumerate(sorted(new_events, key=lambda e: e.id)):
self.assert_odoo_event(e, {
"start": new_rec_first_event_start_date + timedelta(days=i * self.recurrent_event_interval),
"stop": new_rec_first_event_end_date + timedelta(days=i * self.recurrent_event_interval),
"microsoft_id": combine_ids(f'REC123_new_{i+1}', f'REC456_new_{i+1}'),
"microsoft_id": f"REC123_new_{i+1}",
"ms_universal_event_id": f"REC456_new_{i+1}",
"recurrence_id": new_recurrences,
"follow_recurrence": True,
})
@ -1098,10 +1101,10 @@ class TestUpdateEvents(TestCommon):
# assert
updated_events = self.env["calendar.event"].search([
('ms_organizer_event_id', 'in', tuple(ms_events_to_update.keys()))
('microsoft_id', 'in', tuple(ms_events_to_update.keys()))
])
for e in updated_events:
self.assertEqual(e.name, ms_events_to_update[e.ms_organizer_event_id])
self.assertEqual(e.name, ms_events_to_update[e.microsoft_id])
self.assertEqual(e.follow_recurrence, True)
def _prepare_outlook_events_for_all_events_start_date_update(self, nb_of_events):
@ -1182,12 +1185,12 @@ class TestUpdateEvents(TestCommon):
# ----------- ASSERT -----------
updated_events = self.env["calendar.event"].search([
('ms_organizer_event_id', 'in', tuple(ms_events_to_update.keys()))
('microsoft_id', 'in', tuple(ms_events_to_update.keys()))
])
for e in updated_events:
self.assertEqual(
e.start.strftime("%Y-%m-%dT%H:%M:%S.0000000"),
ms_events_to_update[e.ms_organizer_event_id]["dateTime"]
ms_events_to_update[e.microsoft_id]["dateTime"]
)
@patch.object(MicrosoftCalendarService, 'get_events')
@ -1210,14 +1213,13 @@ class TestUpdateEvents(TestCommon):
self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
# ----------- ASSERT -----------
updated_events = self.env["calendar.event"].search([
('ms_organizer_event_id', 'in', tuple(ms_events_to_update.keys()))
('microsoft_id', 'in', tuple(ms_events_to_update.keys()))
])
for e in updated_events:
self.assertEqual(
e.start.strftime("%Y-%m-%dT%H:%M:%S.0000000"),
ms_events_to_update[e.ms_organizer_event_id]["dateTime"]
ms_events_to_update[e.microsoft_id]["dateTime"]
)
@patch.object(MicrosoftCalendarService, 'get_events')
@ -1242,12 +1244,12 @@ class TestUpdateEvents(TestCommon):
# ----------- ASSERT -----------
updated_events = self.env["calendar.event"].search([
('ms_organizer_event_id', 'in', tuple(ms_events_to_update.keys()))
('microsoft_id', 'in', tuple(ms_events_to_update.keys()))
])
for e in updated_events:
self.assertEqual(
e.start.strftime("%Y-%m-%dT%H:%M:%S.0000000"),
ms_events_to_update[e.ms_organizer_event_id]["dateTime"]
ms_events_to_update[e.microsoft_id]["dateTime"]
)
@patch.object(MicrosoftCalendarService, 'get_events')
@ -1287,12 +1289,12 @@ class TestUpdateEvents(TestCommon):
# ----------- ASSERT -----------
updated_events = self.env["calendar.event"].search([
('ms_organizer_event_id', 'in', tuple(ms_events_to_update.keys()))
('microsoft_id', 'in', tuple(ms_events_to_update.keys()))
])
for e in updated_events:
self.assertEqual(
e.start.strftime("%Y-%m-%dT%H:%M:%S.0000000"),
ms_events_to_update[e.ms_organizer_event_id]["dateTime"]
ms_events_to_update[e.microsoft_id]["dateTime"]
)
@patch.object(MicrosoftCalendarService, 'patch')
@ -1326,6 +1328,27 @@ class TestUpdateEvents(TestCommon):
# Assert that no patch call was made due to the recurrence update forbiddance.
mock_patch.assert_not_called()
@patch.object(MicrosoftCalendarService, 'patch')
def test_update_synced_event_with_sync_config_paused(self, mock_patch):
"""
Updates an event with the synchronization paused, the event must have its field 'need_sync_m' as True
for later synchronizing it with Outlook Calendar.
"""
# Set user synchronization configuration as active and pause it.
self.organizer_user.microsoft_synchronization_stopped = False
self.organizer_user.pause_microsoft_synchronization()
# Try to update a simple event in Odoo Calendar.
self.simple_event.with_user(self.organizer_user).write({"name": "updated simple event"})
self.call_post_commit_hooks()
self.simple_event.invalidate_recordset()
# Ensure that synchronization is paused, delete wasn't called and record is waiting to be synced again.
self.assertFalse(self.organizer_user.microsoft_synchronization_stopped)
self.assertEqual(self.organizer_user._get_microsoft_sync_status(), "sync_paused")
self.assertTrue(self.simple_event.need_sync_m, "Sync variable must be true for updating event when sync re-activates")
mock_patch.assert_not_called()
@patch.object(MicrosoftCalendarService, 'get_events')
@patch.object(MicrosoftCalendarService, 'delete')
@patch.object(MicrosoftCalendarService, 'insert')
@ -1340,6 +1363,9 @@ class TestUpdateEvents(TestCommon):
self.simple_event_values['user_id'] = self.organizer_user.id
self.simple_event_values['partner_ids'] = [Command.set([self.organizer_user.partner_id.id])]
event = self.env['calendar.event'].with_user(self.organizer_user).create(self.simple_event_values)
# Simulate sync where the api update the microsoft_id field
event.ms_universal_event_id = "test_id_for_event"
event.microsoft_id = "test_id_for_organizer"
# Deactivate user B's calendar synchronization. Try changing the event organizer to user B.
# A ValidationError must be thrown because user B's calendar is not synced.
@ -1360,8 +1386,6 @@ class TestUpdateEvents(TestCommon):
mock_get_events.return_value = ([], None)
# Change the event organizer: user B (the organizer) is synced and now listed as an attendee.
event.ms_universal_event_id = "test_id_for_event"
event.ms_organizer_event_id = "test_id_for_organizer"
event.with_user(self.organizer_user).write({
'user_id': self.attendee_user.id,
'partner_ids': [Command.set([self.organizer_user.partner_id.id, self.attendee_user.partner_id.id])]
@ -1372,7 +1396,7 @@ class TestUpdateEvents(TestCommon):
# Ensure that the event was deleted and recreated with the new organizer and the organizer listed as attendee.
mock_delete.assert_any_call(
event.ms_organizer_event_id,
event.microsoft_id,
token=mock_get_token(self.attendee_user),
timeout=ANY,
)
@ -1388,12 +1412,16 @@ class TestUpdateEvents(TestCommon):
""" Ensure that sync restart is not blocked when there are recurrence outliers in Odoo database. """
# Stop synchronization, set recurrent events as outliers and restart sync with Outlook.
self.organizer_user.stop_microsoft_synchronization()
self.recurrent_events.with_user(self.organizer_user).write({'microsoft_id': False, 'follow_recurrence': False})
self.recurrent_events.with_user(self.organizer_user).write({
'microsoft_id': False,
'ms_universal_event_id': False,
'follow_recurrence': False
})
self.attendee_user.with_user(self.attendee_user).restart_microsoft_synchronization()
self.organizer_user.with_user(self.organizer_user).restart_microsoft_synchronization()
self.assertTrue(all(ev.need_sync_m for ev in self.recurrent_events))
@patch.object(MicrosoftSync, '_write_from_microsoft')
@patch.object(MicrosoftCalendarSync, '_write_from_microsoft')
@patch.object(MicrosoftCalendarService, 'get_events')
def test_update_old_event_synced_with_outlook(self, mock_get_events, mock_write_from_microsoft):
"""