diff --git a/addons/microsoft_calendar/data/neutralize.sql b/addons/microsoft_calendar/data/neutralize.sql
index e05b7ebfac1..ab2ca0faedd 100644
--- a/addons/microsoft_calendar/data/neutralize.sql
+++ b/addons/microsoft_calendar/data/neutralize.sql
@@ -1,5 +1,9 @@
-- neutralization of Microsoft calendar
UPDATE res_users
SET microsoft_calendar_token = NULL,
- microsoft_calendar_rtoken = NULL,
- microsoft_synchronization_stopped = True;
+ microsoft_calendar_rtoken = NULL;
+
+UPDATE microsoft_calendar_credentials
+ SET calendar_sync_token = NULL,
+ synchronization_stopped = True,
+ last_sync_date = NULL;
diff --git a/addons/microsoft_calendar/models/__init__.py b/addons/microsoft_calendar/models/__init__.py
index a87bf35b73d..74bf6a5c4b0 100644
--- a/addons/microsoft_calendar/models/__init__.py
+++ b/addons/microsoft_calendar/models/__init__.py
@@ -7,3 +7,4 @@ from . import calendar
from . import calendar_recurrence_rule
from . import res_users
from . import calendar_attendee
+from . import microsoft_credentials
diff --git a/addons/microsoft_calendar/models/calendar.py b/addons/microsoft_calendar/models/calendar.py
index 2ac3a9a1bf3..de9d36e1aa0 100644
--- a/addons/microsoft_calendar/models/calendar.py
+++ b/addons/microsoft_calendar/models/calendar.py
@@ -4,7 +4,7 @@
import logging
import pytz
import re
-from datetime import datetime
+from datetime import datetime, timedelta
from dateutil.parser import parse
from dateutil.relativedelta import relativedelta
@@ -12,6 +12,7 @@ from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
from odoo.tools import is_html_empty, email_normalize
from odoo.addons.microsoft_calendar.utils.event_id_storage import combine_ids
+from odoo.osv import expression
ATTENDEE_CONVERTER_O2M = {
'needsAction': 'notresponded',
@@ -53,7 +54,14 @@ class Meeting(models.Model):
@api.model
def _restart_microsoft_sync(self):
- self.env['calendar.event'].search(self._get_microsoft_sync_domain()).write({
+ domain = self._get_microsoft_sync_domain()
+
+ # Sync only events created/updated after last sync date (with 5 min of time acceptance).
+ if self.env.user.microsoft_last_sync_date:
+ time_offset = timedelta(minutes=5)
+ domain = expression.AND([domain, [('write_date', '>=', self.env.user.microsoft_last_sync_date - time_offset)]])
+
+ self.env['calendar.event'].search(domain).write({
'need_sync_m': True,
})
diff --git a/addons/microsoft_calendar/models/microsoft_credentials.py b/addons/microsoft_calendar/models/microsoft_credentials.py
new file mode 100644
index 00000000000..a5d3a649c4b
--- /dev/null
+++ b/addons/microsoft_calendar/models/microsoft_credentials.py
@@ -0,0 +1,16 @@
+import logging
+
+from odoo import fields, models
+
+_logger = logging.getLogger(__name__)
+
+class MicrosoftCredentials(models.Model):
+ """"Microsoft Account of res_users"""
+
+ _name = 'microsoft.calendar.credentials'
+ _description = 'Microsoft Calendar Account Data'
+
+ user_ids = fields.One2many('res.users', 'microsoft_calendar_account_id', required=True)
+ calendar_sync_token = fields.Char('Microsoft Next Sync Token', copy=False)
+ synchronization_stopped = fields.Boolean('Outlook Synchronization stopped', copy=False)
+ last_sync_date = fields.Datetime('Last Sync Date', copy=False, help='Last synchronization date with Outlook Calendar')
diff --git a/addons/microsoft_calendar/models/microsoft_sync.py b/addons/microsoft_calendar/models/microsoft_sync.py
index de479e96de3..8673c0b73a5 100644
--- a/addons/microsoft_calendar/models/microsoft_sync.py
+++ b/addons/microsoft_calendar/models/microsoft_sync.py
@@ -6,6 +6,7 @@ from contextlib import contextmanager
from functools import wraps
import pytz
from dateutil.parser import parse
+from datetime import timedelta
from odoo import api, fields, models, registry
from odoo.exceptions import UserError
@@ -519,4 +520,8 @@ class MicrosoftSync(models.AbstractModel):
'&', ('ms_universal_event_id', '=', False), is_active_clause,
('need_sync_m', '=', True),
]])
+ # Sync only events created/updated after last sync date (with 5 min of time acceptance).
+ if self.env.user.microsoft_last_sync_date:
+ time_offset = timedelta(minutes=5)
+ domain = expression.AND([domain, [('write_date', '>=', self.env.user.microsoft_last_sync_date - time_offset)]])
return domain
diff --git a/addons/microsoft_calendar/models/res_users.py b/addons/microsoft_calendar/models/res_users.py
index a13e126f52c..cf75c2268b8 100644
--- a/addons/microsoft_calendar/models/res_users.py
+++ b/addons/microsoft_calendar/models/res_users.py
@@ -6,7 +6,7 @@ import requests
from odoo.addons.microsoft_calendar.models.microsoft_sync import microsoft_calendar_token
from datetime import timedelta
-from odoo import api, fields, models, _
+from odoo import api, fields, models, _, Command
from odoo.exceptions import UserError
from odoo.loglevels import exception_to_unicode
from odoo.addons.microsoft_account.models.microsoft_service import DEFAULT_MICROSOFT_TOKEN_ENDPOINT
@@ -19,16 +19,22 @@ _logger = logging.getLogger(__name__)
class User(models.Model):
_inherit = 'res.users'
- microsoft_calendar_sync_token = fields.Char('Microsoft Next Sync Token', copy=False)
- microsoft_synchronization_stopped = fields.Boolean('Outlook Synchronization stopped', copy=False)
+ microsoft_calendar_account_id = fields.Many2one('microsoft.calendar.credentials')
+ microsoft_calendar_sync_token = fields.Char(related='microsoft_calendar_account_id.calendar_sync_token')
+ microsoft_synchronization_stopped = fields.Boolean(related='microsoft_calendar_account_id.synchronization_stopped', readonly=False)
+ microsoft_last_sync_date = fields.Datetime(related='microsoft_calendar_account_id.last_sync_date', readonly=False)
+
+ _sql_constraints = [
+ ('microsoft_token_uniq', 'unique (microsoft_calendar_account_id)', "The user has already a microsoft account"),
+ ]
@property
def SELF_READABLE_FIELDS(self):
- return super().SELF_READABLE_FIELDS + ['microsoft_synchronization_stopped']
+ return super().SELF_READABLE_FIELDS + ['microsoft_synchronization_stopped', 'microsoft_calendar_account_id', 'microsoft_last_sync_date']
@property
def SELF_WRITEABLE_FIELDS(self):
- return super().SELF_WRITEABLE_FIELDS + ['microsoft_synchronization_stopped']
+ return super().SELF_WRITEABLE_FIELDS + ['microsoft_synchronization_stopped', 'microsoft_calendar_account_id', 'microsoft_last_sync_date']
def _microsoft_calendar_authenticated(self):
return bool(self.sudo().microsoft_calendar_rtoken)
@@ -79,7 +85,9 @@ class User(models.Model):
'microsoft_calendar_rtoken': False,
'microsoft_calendar_token': False,
'microsoft_calendar_token_validity': False,
- 'microsoft_calendar_sync_token': False,
+ })
+ self.microsoft_calendar_account_id.write({
+ 'calendar_sync_token': False
})
self.env.cr.commit()
error_key = error.response.json().get("error", "nc")
@@ -100,6 +108,10 @@ class User(models.Model):
def _sync_microsoft_calendar(self):
self.ensure_one()
+ # Create the Calendar Credentials if it is not defined yet and define the last sync date after creation.
+ if not self.microsoft_calendar_account_id and self._check_microsoft_calendar_credentials():
+ self.microsoft_last_sync_date = fields.datetime.now()
+
if self._get_microsoft_sync_status() != "sync_active":
return False
calendar_service = self.env["calendar.event"]._get_microsoft_service()
@@ -110,7 +122,7 @@ class User(models.Model):
except InvalidSyncToken:
events, next_sync_token = calendar_service.get_events(token=token)
full_sync = True
- self.microsoft_calendar_sync_token = next_sync_token
+ self.microsoft_calendar_account_id.calendar_sync_token = next_sync_token
# Microsoft -> Odoo
synced_events, synced_recurrences = self.env['calendar.event']._sync_microsoft2odoo(events) if events else (self.env['calendar.event'], self.env['calendar.recurrence'])
@@ -123,6 +135,7 @@ class User(models.Model):
events = self.env['calendar.event']._get_microsoft_records_to_sync(full_sync=full_sync)
(events - synced_events)._sync_odoo2microsoft()
+ self.microsoft_last_sync_date = fields.datetime.now()
return bool(events | synced_events) or bool(recurrences | synced_recurrences)
@@ -139,12 +152,23 @@ class User(models.Model):
_logger.exception("[%s] Calendar Synchro - Exception : %s!", user, exception_to_unicode(e))
self.env.cr.rollback()
+ def _check_microsoft_calendar_credentials(self):
+ # Create the Calendar Credentials for the current user if not already created.
+ self.ensure_one()
+ if not self.microsoft_calendar_account_id:
+ self.microsoft_calendar_account_id = self.env['microsoft.calendar.credentials'].sudo().create([{'user_ids': [Command.set(self.ids)]}])
+ return True
+
def stop_microsoft_synchronization(self):
self.ensure_one()
+ self._check_microsoft_calendar_credentials()
self.microsoft_synchronization_stopped = True
+ self.microsoft_last_sync_date = None
def restart_microsoft_synchronization(self):
self.ensure_one()
+ self._check_microsoft_calendar_credentials()
+ self.microsoft_last_sync_date = fields.datetime.now()
self.microsoft_synchronization_stopped = False
self.env['calendar.recurrence']._restart_microsoft_sync()
self.env['calendar.event']._restart_microsoft_sync()
diff --git a/addons/microsoft_calendar/security/ir.model.access.csv b/addons/microsoft_calendar/security/ir.model.access.csv
index 478501e7e4a..7518fba6e35 100644
--- a/addons/microsoft_calendar/security/ir.model.access.csv
+++ b/addons/microsoft_calendar/security/ir.model.access.csv
@@ -1,2 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
microsoft_calendar_account_reset,microsoft_calendar_account_reset_access_right,model_microsoft_calendar_account_reset,base.group_system,1,1,1,0
+microsoft_calendar.access_microsoft_calendar_credentials,access_microsoft_calendar_credentials,microsoft_calendar.model_microsoft_calendar_credentials,base.group_user,1,1,0,0
+microsoft_calendar_manager,access_microsoft_calendar_credentials_manager,microsoft_calendar.model_microsoft_calendar_credentials,base.group_system,1,1,0,0
diff --git a/addons/microsoft_calendar/security/microsoft_calendar_security.xml b/addons/microsoft_calendar/security/microsoft_calendar_security.xml
new file mode 100644
index 00000000000..518e9d5d509
--- /dev/null
+++ b/addons/microsoft_calendar/security/microsoft_calendar_security.xml
@@ -0,0 +1,30 @@
+
+
+
+ Microsoft Calendar NOT own token access rule
+
+ [('user_ids', 'not in', user.id)]
+
+
+
+
+
+
+
+ Microsoft Calendar own token access rule
+
+ [('user_ids', 'in', user.id)]
+
+
+
+
+
+
+
+ Microsoft Calendar manage all tokens
+
+ [(1,'=',1)]
+
+
+
+
diff --git a/addons/microsoft_calendar/tests/test_create_events.py b/addons/microsoft_calendar/tests/test_create_events.py
index 40aaa3e1a78..f64c82317d0 100644
--- a/addons/microsoft_calendar/tests/test_create_events.py
+++ b/addons/microsoft_calendar/tests/test_create_events.py
@@ -1,5 +1,5 @@
-from datetime import datetime, timedelta
from unittest.mock import patch
+from datetime import timedelta, datetime
from odoo.addons.microsoft_calendar.utils.microsoft_calendar import MicrosoftCalendarService
from odoo.addons.microsoft_calendar.utils.microsoft_event import MicrosoftEvent
@@ -299,3 +299,83 @@ class TestCreateEvents(TestCommon):
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).
+ """
+ # 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)
+
+ # Restart the synchronization with Outlook Calendar.
+ self.organizer_user.with_user(self.organizer_user).sudo().restart_microsoft_synchronization()
+ # Last_sync_date is manually updated here due to simulate real use in the test environment.
+ self.organizer_user.microsoft_calendar_account_id.last_sync_date = event.write_date + timedelta(minutes=10)
+ # 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.ms_organizer_event_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.")
+
+ # Synchronize after updating last_sync_date and assert that insert was not called.
+ # Last_sync_date is manually updated here due to simulate real use in the test environment.
+ self.organizer_user.microsoft_calendar_account_id.last_sync_date = event.write_date + timedelta(minutes=10)
+ self.organizer_user.with_user(self.organizer_user).sudo()._sync_microsoft_calendar()
+ self.call_post_commit_hooks()
+ mock_insert.assert_not_called()
+
+ # 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.
+ # Last_sync_date is manually updated here due to simulate real use in the test environment.
+ self.organizer_user.microsoft_calendar_account_id.last_sync_date = event.write_date - timedelta(minutes=10)
+ 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.ms_organizer_event_id, "123")
+ self.assertEqual(event.ms_universal_event_id, "456")
+
+ # Assert that the Microsoft Insert was called once.
+ mock_insert.assert_called_once()
diff --git a/addons/microsoft_calendar/views/res_users_views.xml b/addons/microsoft_calendar/views/res_users_views.xml
index d41a385884d..0e7066f8e9c 100644
--- a/addons/microsoft_calendar/views/res_users_views.xml
+++ b/addons/microsoft_calendar/views/res_users_views.xml
@@ -16,6 +16,7 @@
+