diff --git a/addons/google_calendar/__manifest__.py b/addons/google_calendar/__manifest__.py
index a743f7528a2..8e90d83ac8c 100644
--- a/addons/google_calendar/__manifest__.py
+++ b/addons/google_calendar/__manifest__.py
@@ -14,6 +14,7 @@
'wizard/reset_account_views.xml',
'views/res_config_settings_views.xml',
'views/res_users_views.xml',
+ 'views/google_calendar_views.xml',
'views/google_calendar_templates.xml',
],
'demo': [],
diff --git a/addons/google_calendar/controllers/main.py b/addons/google_calendar/controllers/main.py
index 99ed0b0b2a1..bd76d4dd9c1 100644
--- a/addons/google_calendar/controllers/main.py
+++ b/addons/google_calendar/controllers/main.py
@@ -13,7 +13,7 @@ class GoogleCalendarController(http.Controller):
""" This route/function is called when we want to synchronize Odoo
calendar with Google Calendar.
Function return a dictionary with the status : need_config_from_admin, need_auth,
- need_refresh, success if not calendar_event
+ need_refresh, sync_stopped, success if not calendar_event
The dictionary may contains an url, to allow Odoo Client to redirect user on
this URL for authorization for example
"""
@@ -42,6 +42,13 @@ class GoogleCalendarController(http.Controller):
}
# If App authorized, and user access accepted, We launch the synchronization
need_refresh = request.env.user.sudo()._sync_google_calendar(GoogleCal)
+
+ # If synchronization has been stopped
+ if not need_refresh and request.env.user.google_synchronization_stopped:
+ return {
+ "status": "sync_stopped",
+ "url": ''
+ }
return {
"status": "need_refresh" if need_refresh else "no_new_event_from_google",
"url": ''
diff --git a/addons/google_calendar/models/calendar.py b/addons/google_calendar/models/calendar.py
index 765473509ba..b2aeae22984 100644
--- a/addons/google_calendar/models/calendar.py
+++ b/addons/google_calendar/models/calendar.py
@@ -33,6 +33,12 @@ class Meeting(models.Model):
return {'name', 'description', 'allday', 'start', 'date_end', 'stop',
'attendee_ids', 'alarm_ids', 'location', 'privacy', 'active'}
+ @api.model
+ def _restart_google_sync(self):
+ self.env['calendar.event'].search(self._get_sync_domain()).write({
+ 'need_sync': True,
+ })
+
@api.model_create_multi
def create(self, vals_list):
return super().create([
diff --git a/addons/google_calendar/models/calendar_recurrence_rule.py b/addons/google_calendar/models/calendar_recurrence_rule.py
index b3cd06db329..1bc94084faa 100644
--- a/addons/google_calendar/models/calendar_recurrence_rule.py
+++ b/addons/google_calendar/models/calendar_recurrence_rule.py
@@ -79,6 +79,12 @@ class RecurrenceRule(models.Model):
def _get_google_synced_fields(self):
return {'rrule'}
+ @api.model
+ def _restart_google_sync(self):
+ self.env['calendar.recurrence'].search(self._get_sync_domain()).write({
+ 'need_sync': True,
+ })
+
@api.model
def _sync_google2odoo(self, *args, **kwargs):
synced_recurrences = super()._sync_google2odoo(*args, **kwargs)
diff --git a/addons/google_calendar/models/google_sync.py b/addons/google_calendar/models/google_sync.py
index a16cce527f4..06c09bc48b5 100644
--- a/addons/google_calendar/models/google_sync.py
+++ b/addons/google_calendar/models/google_sync.py
@@ -72,7 +72,7 @@ class GoogleSync(models.AbstractModel):
if 'google_id' in vals:
self._from_google_ids.clear_cache(self)
synced_fields = self._get_google_synced_fields()
- if 'need_sync' not in vals and vals.keys() & synced_fields:
+ if 'need_sync' not in vals and vals.keys() & synced_fields and not self.env.user.google_synchronization_stopped:
vals['need_sync'] = True
result = super().write(vals)
@@ -86,6 +86,9 @@ class GoogleSync(models.AbstractModel):
def create(self, vals_list):
if any(vals.get('google_id') for vals in vals_list):
self._from_google_ids.clear_cache(self)
+ if self.env.user.google_synchronization_stopped:
+ for vals in vals_list:
+ vals.update({'need_sync': False})
records = super().create(vals_list)
google_service = GoogleCalendarService(self.env['google.service'])
@@ -244,3 +247,10 @@ class GoogleSync(models.AbstractModel):
marks the record to be re-synchronized.
"""
raise NotImplementedError()
+
+ @api.model
+ def _restart_google_sync(self):
+ """ Turns on the google synchronization for all the events of
+ a given user.
+ """
+ raise NotImplementedError()
diff --git a/addons/google_calendar/models/res_users.py b/addons/google_calendar/models/res_users.py
index 416cc9f8667..1530c19f20e 100644
--- a/addons/google_calendar/models/res_users.py
+++ b/addons/google_calendar/models/res_users.py
@@ -23,7 +23,8 @@ class User(models.Model):
google_calendar_token_validity = fields.Datetime('Token Validity', copy=False)
google_calendar_sync_token = fields.Char('Next Sync Token', copy=False)
google_calendar_cal_id = fields.Char('Calendar ID', copy=False, help='Last Calendar ID who has been synchronized. If it is changed, we remove all links between GoogleID and Odoo Google Internal ID')
-
+ google_synchronization_stopped = fields.Boolean('Google Synchronization stopped', copy=False)
+
def _set_auth_tokens(self, access_token, refresh_token, ttl):
self.write({
'google_calendar_rtoken': refresh_token,
@@ -79,6 +80,8 @@ class User(models.Model):
def _sync_google_calendar(self, calendar_service: GoogleCalendarService):
self.ensure_one()
+ if self.google_synchronization_stopped:
+ return False
full_sync = not bool(self.google_calendar_sync_token)
with google_calendar_token(self) as token:
try:
@@ -106,7 +109,7 @@ class User(models.Model):
@api.model
def _sync_all_google_calendar(self):
""" Cron job """
- users = self.env['res.users'].search([('google_calendar_rtoken', '!=', False)])
+ users = self.env['res.users'].search([('google_calendar_rtoken', '!=', False), ('google_synchronization_stopped', '=', False)])
google = GoogleCalendarService(self.env['google.service'])
for user in users:
_logger.info("Calendar Synchro - Starting synchronization for %s", user)
@@ -114,3 +117,13 @@ class User(models.Model):
user.with_user(user).sudo()._sync_google_calendar(google)
except Exception as e:
_logger.exception("[%s] Calendar Synchro - Exception : %s !", user, exception_to_unicode(e))
+
+ def stop_google_synchronization(self):
+ self.ensure_one()
+ self.sudo().google_synchronization_stopped = True
+
+ def restart_google_synchronization(self):
+ self.ensure_one()
+ self.sudo().google_synchronization_stopped = False
+ self.env['calendar.recurrence']._restart_google_sync()
+ self.env['calendar.event']._restart_google_sync()
diff --git a/addons/google_calendar/static/src/js/google_calendar.js b/addons/google_calendar/static/src/js/google_calendar.js
index cfa2437bb51..00f75118822 100644
--- a/addons/google_calendar/static/src/js/google_calendar.js
+++ b/addons/google_calendar/static/src/js/google_calendar.js
@@ -9,6 +9,7 @@ const CalendarRenderer = require('calendar.CalendarRenderer');
const CalendarController = require('calendar.CalendarController');
const CalendarModel = require('calendar.CalendarModel');
const viewRegistry = require('web.view_registry');
+const session = require('web.session');
var _t = core._t;
@@ -63,7 +64,7 @@ const GoogleCalendarModel = CalendarModel.include({
local_context: context, // LUL TODO remove this local_context
}
}, {shadow}).then(function (result) {
- if (result.status === "need_config_from_admin" || result.status === "need_auth") {
+ if (["need_config_from_admin", "need_auth", "sync_stopped"].includes(result.status)) {
self.google_is_sync = false;
} else if (result.status === "no_new_event_from_google" || result.status === "need_refresh") {
self.google_is_sync = true;
@@ -71,11 +72,22 @@ const GoogleCalendarModel = CalendarModel.include({
return result
});
},
+
+ archiveRecords: function (ids, model) {
+ return this._rpc({
+ model: model,
+ method: 'action_archive',
+ args: [ids],
+ context: session.user_context,
+ });
+ },
})
const GoogleCalendarController = CalendarController.include({
custom_events: _.extend({}, CalendarController.prototype.custom_events, {
syncGoogleCalendar: '_onGoogleSyncCalendar',
+ stopGoogleSynchronization: '_onStopGoogleSynchronization',
+ archiveRecord: '_onArchiveRecord',
}),
@@ -94,7 +106,7 @@ const GoogleCalendarController = CalendarController.include({
_onGoogleSyncCalendar: function (event) {
var self = this;
- return this.model._syncGoogleCalendar().then(function (o) {
+ return this._restartGoogleSynchronization().then(() => {return this.model._syncGoogleCalendar();}).then(function (o) {
if (o.status === "need_auth") {
Dialog.alert(self, _t("You will be redirected to Google to authorize access to your calendar!"), {
confirm_callback: function() {
@@ -117,20 +129,95 @@ const GoogleCalendarController = CalendarController.include({
}
} else if (o.status === "need_refresh") {
self.reload();
+ return event.data.on_refresh();
}
}).then(event.data.on_always, event.data.on_always);
- }
+ },
+
+ _onStopGoogleSynchronization: function (event) {
+ var self = this;
+ Dialog.confirm(this, _t("You are about to stop the synchronization of your calendar with Google. Are you sure you want to continue?"), {
+ confirm_callback: function() {
+ return self._rpc({
+ model: 'res.users',
+ method: 'stop_google_synchronization',
+ args: [[self.context.uid]],
+ }).then(() => {
+ self.displayNotification({
+ title: _t("Success"),
+ message: _t("The synchronization with Google calendar was successfully stopped."),
+ type: 'success',
+ });
+ }).then(event.data.on_confirm);
+ },
+ title: _t('Confirmation'),
+ });
+
+ return event.data.on_always();
+ },
+
+ _restartGoogleSynchronization: function () {
+ return this._rpc({
+ model: 'res.users',
+ method: 'restart_google_synchronization',
+ args: [[this.context.uid]],
+ });
+ },
+
+ _onArchiveRecord: function (event) {
+ var self = this;
+ Dialog.confirm(this, _t("Are you sure you want to archive this record ?"), {
+ confirm_callback: function () {
+ self.model.archiveRecords([event.data.id], self.modelName).then(function () {
+ self.reload();
+ });
+ }
+ });
+ },
});
const GoogleCalendarRenderer = CalendarRenderer.include({
+ custom_events: _.extend({}, CalendarRenderer.prototype.custom_events, {
+ archive_event: '_onArchiveEvent',
+ }),
+
events: _.extend({}, CalendarRenderer.prototype.events, {
'click .o_google_sync_button': '_onGoogleSyncCalendar',
+ 'click .o_stop_google_sync_button': '_onStopGoogleSynchronization',
}),
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
+ _initGooglePillButton: function() {
+ this.$googleStopButton.css({"cursor":"pointer", "font-size":"0.9em"});
+ var switchBadgeClass = (elem) => {elem.toggleClass('badge-success'); elem.toggleClass('badge-danger');};
+ this.$('.o_stop_google_sync_button').hover(() => {
+ switchBadgeClass(this.$googleStopButton);
+ this.$googleStopButton.html("".concat(_t("Stop the Synchronization")));
+ }, () => {
+ switchBadgeClass(this.$googleStopButton);
+ this.$googleStopButton.html("".concat(_t("Synched with Google")));
+ });
+ },
+
+ _getGoogleButton: function () {
+ return $('', {
+ type: 'button',
+ html: _t("Sync with Google"),
+ class: 'o_google_sync_button w-100 m-auto btn btn-secondary'
+ });
+ },
+
+ _getGoogleStopButton: function () {
+ return $('', {
+ html: _t("Synched with Google"),
+ class: 'w-100 badge badge-pill badge-success border-0 o_stop_google_sync_button'
+ })
+ .prepend($('', {class: "fa mr-2 fa-check"}));
+ },
+
/**
* Adds the Sync with Google button in the sidebar
*
@@ -140,21 +227,13 @@ const GoogleCalendarRenderer = CalendarRenderer.include({
var self = this;
this._super.apply(this, arguments);
this.$googleButton = $();
+ this.$googleStopButton = $();
if (this.model === "calendar.event") {
if (this.state.google_is_sync) {
- this.$googleButton = $('', {
- html: _t("Synched with Google"),
- class: 'w-100 badge badge-pill badge-success border-0'
- })
- .prepend($('', {class: "fa mr-2 fa-check"}))
- .appendTo(self.$sidebar);
+ this.$googleStopButton = this._getGoogleStopButton().appendTo(self.$sidebar);
+ this._initGooglePillButton();
} else {
- this.$googleButton = $('', {
- type: 'button',
- html: _t("Sync with Google"),
- class: 'o_google_sync_button w-100 m-auto btn btn-secondary'
- })
- .appendTo(self.$sidebar);
+ this.$googleButton = this._getGoogleButton().appendTo(self.$sidebar);
}
}
},
@@ -176,8 +255,36 @@ const GoogleCalendarRenderer = CalendarRenderer.include({
on_always: function () {
self.$googleButton.prop('disabled', false);
},
+ on_refresh: function () {
+ if (_.isEmpty(self.$googleStopButton)) {
+ self.$googleStopButton = self._getGoogleStopButton();
+ }
+ self.$googleButton.replaceWith(self.$googleStopButton);
+ self._initGooglePillButton();
+ }
});
},
+
+ _onStopGoogleSynchronization: function() {
+ var self = this;
+ this.$googleStopButton.prop('disabled', true);
+ this.trigger_up('stopGoogleSynchronization' , {
+ on_confirm: function () {
+ if (_.isEmpty(self.$googleButton)) {
+ self.$googleButton = self._getGoogleButton();
+ }
+ self.$googleStopButton.replaceWith(self.$googleButton);
+ },
+ on_always: function() {
+ self.$googleStopButton.prop('disabled', false);
+ }
+ });
+ },
+
+ _onArchiveEvent: function (event) {
+ this._unselectEvent();
+ this.trigger_up('archiveRecord', {id: parseInt(event.data.id, 10)});
+ },
});
return {
diff --git a/addons/google_calendar/static/src/js/google_calendar_popover.js b/addons/google_calendar/static/src/js/google_calendar_popover.js
new file mode 100644
index 00000000000..aae4a1897eb
--- /dev/null
+++ b/addons/google_calendar/static/src/js/google_calendar_popover.js
@@ -0,0 +1,26 @@
+odoo.define('google_calendar.GoogleCalendarPopover', function(require) {
+ "use strict";
+
+ const CalendarPopover = require('web.CalendarPopover');
+
+ const GoogleCalendarPopover = CalendarPopover.include({
+ events: _.extend({}, CalendarPopover.prototype.events, {
+ 'click .o_cw_popover_archive_g': '_onClickPopoverGArchive',
+ }),
+
+ isGEventSyncedAndArchivable() {
+ return this.event.extendedProps.record.google_id;
+ },
+
+ isEventDeletable() {
+ return !this.isGEventSyncedAndArchivable() && this._super();
+ },
+
+ _onClickPopoverGArchive: function (ev) {
+ ev.preventDefault();
+ this.trigger_up('archive_event', {id: this.event.id});
+ },
+ });
+
+ return GoogleCalendarPopover;
+});
diff --git a/addons/google_calendar/static/src/xml/google_calendar_popover.xml b/addons/google_calendar/static/src/xml/google_calendar_popover.xml
new file mode 100644
index 00000000000..53dc371b306
--- /dev/null
+++ b/addons/google_calendar/static/src/xml/google_calendar_popover.xml
@@ -0,0 +1,8 @@
+
+
+
+
+ Archive
+
+
+
diff --git a/addons/google_calendar/tests/test_sync_odoo2google.py b/addons/google_calendar/tests/test_sync_odoo2google.py
index a78d19f60a7..7028d797470 100644
--- a/addons/google_calendar/tests/test_sync_odoo2google.py
+++ b/addons/google_calendar/tests/test_sync_odoo2google.py
@@ -234,3 +234,53 @@ class TestSyncOdoo2Google(TransactionCase):
'reminders': {'overrides': [], 'useDefault': False},
'visibility': 'public',
}, timeout=3)
+
+ @patch_api
+ def test_stop_synchronization(self):
+ self.env.user.stop_google_synchronization()
+ self.assertTrue(self.env.user.google_synchronization_stopped, "The google synchronization flag should be switched on")
+ self.assertFalse(self.env.user._sync_google_calendar(self.google_service), "The google synchronization should be stopped")
+
+ # If synchronization stopped, creating a new event should not call _google_insert.
+ self.env['calendar.event'].create({
+ 'name': "Event",
+ 'start': datetime(2020, 1, 15, 8, 0),
+ 'stop': datetime(2020, 1, 15, 18, 0),
+ 'privacy': 'private',
+ })
+ self.assertGoogleEventNotInserted()
+
+ @patch_api
+ def test_restart_synchronization(self):
+ # Test new event created after stopping synchronization are correctly patched when restarting sync.
+ google_id = 'aaaaaaaaa'
+ partner = self.env['res.partner'].create({'name': 'Jean-Luc', 'email': 'jean-luc@opoo.com'})
+ user = self.env['res.users'].create({
+ 'name': 'Test user Calendar',
+ 'login': 'jean-luc@opoo.com',
+ 'partner_id': partner.id,
+ })
+ user.stop_google_synchronization()
+ event = self.env['calendar.event'].with_user(user).create({
+ 'google_id': google_id,
+ 'name': "Event",
+ 'start': datetime(2020, 1, 15, 8, 0),
+ 'stop': datetime(2020, 1, 15, 18, 0),
+ 'partner_ids': [(4, partner.id)],
+ })
+
+ user.with_user(user).restart_google_synchronization()
+ self.assertGoogleEventPatched(event.google_id, {
+ 'id': event.google_id,
+ 'start': {'dateTime': '2020-01-15T08:00:00+00:00'},
+ 'end': {'dateTime': '2020-01-15T18:00:00+00:00'},
+ 'summary': 'Event',
+ 'description': '',
+ 'location': '',
+ 'visibility': 'public',
+ 'guestsCanModify': True,
+ 'reminders': {'overrides': [], 'useDefault': False},
+ 'organizer': {'email': 'jean-luc@opoo.com', 'self': True},
+ 'attendees': [{'email': 'jean-luc@opoo.com', 'responseStatus': 'accepted'}],
+ 'extendedProperties': {'shared': {'%s_odoo_id' % self.env.cr.dbname: event.id}}
+ }, timeout=3)
diff --git a/addons/google_calendar/views/google_calendar_templates.xml b/addons/google_calendar/views/google_calendar_templates.xml
index a5f3816b938..dbf8fab6f6b 100644
--- a/addons/google_calendar/views/google_calendar_templates.xml
+++ b/addons/google_calendar/views/google_calendar_templates.xml
@@ -2,6 +2,7 @@
+
diff --git a/addons/google_calendar/views/google_calendar_views.xml b/addons/google_calendar/views/google_calendar_views.xml
new file mode 100644
index 00000000000..1d22a10e548
--- /dev/null
+++ b/addons/google_calendar/views/google_calendar_views.xml
@@ -0,0 +1,13 @@
+
+
+
+ google_calendar.event.calendar
+ calendar.event
+
+
+
+
+
+
+
+
diff --git a/addons/microsoft_calendar/__manifest__.py b/addons/microsoft_calendar/__manifest__.py
index 03dd4d1fb56..54ee5428ce4 100644
--- a/addons/microsoft_calendar/__manifest__.py
+++ b/addons/microsoft_calendar/__manifest__.py
@@ -14,6 +14,7 @@
'wizard/reset_account_views.xml',
'views/res_config_settings_views.xml',
'views/res_users_views.xml',
+ 'views/microsoft_calendar_views.xml',
'views/microsoft_calendar_templates.xml',
],
'demo': [],
diff --git a/addons/microsoft_calendar/controllers/main.py b/addons/microsoft_calendar/controllers/main.py
index 5e10e6a254e..954d9af3103 100644
--- a/addons/microsoft_calendar/controllers/main.py
+++ b/addons/microsoft_calendar/controllers/main.py
@@ -13,7 +13,7 @@ class MicrosoftCalendarController(http.Controller):
""" This route/function is called when we want to synchronize Odoo
calendar with Microsoft Calendar.
Function return a dictionary with the status : need_config_from_admin, need_auth,
- need_refresh, success if not calendar_event
+ need_refresh, sync_stopped, success if not calendar_event
The dictionary may contains an url, to allow Odoo Client to redirect user on
this URL for authorization for example
"""
@@ -42,6 +42,13 @@ class MicrosoftCalendarController(http.Controller):
}
# If App authorized, and user access accepted, We launch the synchronization
need_refresh = request.env.user.sudo()._sync_microsoft_calendar(MicrosoftCal)
+
+ # If synchronization has been stopped
+ if not need_refresh and request.env.user.microsoft_synchronization_stopped:
+ return {
+ "status": "sync_stopped",
+ "url": ''
+ }
return {
"status": "need_refresh" if need_refresh else "no_new_event_from_microsoft",
"url": ''
diff --git a/addons/microsoft_calendar/models/calendar.py b/addons/microsoft_calendar/models/calendar.py
index 51e7e720cba..e21fe67e89a 100644
--- a/addons/microsoft_calendar/models/calendar.py
+++ b/addons/microsoft_calendar/models/calendar.py
@@ -36,6 +36,12 @@ class Meeting(models.Model):
'user_id', 'privacy',
'attendee_ids', 'alarm_ids', 'location', 'show_as', 'active'}
+ @api.model
+ def _restart_microsoft_sync(self):
+ self.env['calendar.event'].search(self._get_microsoft_sync_domain()).write({
+ 'need_sync_m': True,
+ })
+
@api.model_create_multi
def create(self, vals_list):
return super().create([
diff --git a/addons/microsoft_calendar/models/calendar_recurrence_rule.py b/addons/microsoft_calendar/models/calendar_recurrence_rule.py
index 0044e3d499b..55c735fa03b 100644
--- a/addons/microsoft_calendar/models/calendar_recurrence_rule.py
+++ b/addons/microsoft_calendar/models/calendar_recurrence_rule.py
@@ -70,6 +70,12 @@ class RecurrenceRule(models.Model):
def _get_microsoft_synced_fields(self):
return {'rrule'} | self.env['calendar.event']._get_microsoft_synced_fields()
+ @api.model
+ def _restart_microsoft_sync(self):
+ self.env['calendar.recurrence'].search(self._get_microsoft_sync_domain()).write({
+ 'need_sync_m': True,
+ })
+
def _get_microsoft_sync_domain(self):
return [('calendar_event_ids.user_id', '=', self.env.user.id)]
diff --git a/addons/microsoft_calendar/models/microsoft_sync.py b/addons/microsoft_calendar/models/microsoft_sync.py
index c93a6ae26d5..ccfde004118 100644
--- a/addons/microsoft_calendar/models/microsoft_sync.py
+++ b/addons/microsoft_calendar/models/microsoft_sync.py
@@ -74,7 +74,7 @@ class MicrosoftSync(models.AbstractModel):
if 'microsoft_id' in vals:
self._from_microsoft_ids.clear_cache(self)
synced_fields = self._get_microsoft_synced_fields()
- if 'need_sync_m' not in vals and vals.keys() & synced_fields:
+ if 'need_sync_m' not in vals and vals.keys() & synced_fields and not self.env.user.microsoft_synchronization_stopped:
fields_to_sync = [x for x in vals.keys() if x in synced_fields]
if fields_to_sync:
vals['need_sync_m'] = True
@@ -95,6 +95,9 @@ class MicrosoftSync(models.AbstractModel):
def create(self, vals_list):
if any(vals.get('microsoft_id') for vals in vals_list):
self._from_microsoft_ids.clear_cache(self)
+ if self.env.user.microsoft_synchronization_stopped:
+ for vals in vals_list:
+ vals.update({'need_sync_m': False})
records = super().create(vals_list)
microsoft_service = MicrosoftCalendarService(self.env['microsoft.service'])
@@ -361,3 +364,10 @@ class MicrosoftSync(models.AbstractModel):
marks the record to be re-synchronized.
"""
raise NotImplementedError()
+
+ @api.model
+ def _restart_microsoft_sync(self):
+ """ Turns on the microsoft synchronization for all the events of
+ a given user.
+ """
+ raise NotImplementedError()
diff --git a/addons/microsoft_calendar/models/res_users.py b/addons/microsoft_calendar/models/res_users.py
index dec51d171de..5898ffb79b1 100644
--- a/addons/microsoft_calendar/models/res_users.py
+++ b/addons/microsoft_calendar/models/res_users.py
@@ -19,6 +19,7 @@ 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)
def _microsoft_calendar_authenticated(self):
return bool(self.sudo().microsoft_calendar_rtoken)
@@ -67,6 +68,8 @@ class User(models.Model):
def _sync_microsoft_calendar(self, calendar_service: MicrosoftCalendarService):
self.ensure_one()
+ if self.microsoft_synchronization_stopped:
+ return False
full_sync = not bool(self.microsoft_calendar_sync_token)
with microsoft_calendar_token(self) as token:
try:
@@ -94,7 +97,7 @@ class User(models.Model):
@api.model
def _sync_all_microsoft_calendar(self):
""" Cron job """
- users = self.env['res.users'].search([('microsoft_calendar_rtoken', '!=', False)])
+ users = self.env['res.users'].search([('microsoft_calendar_rtoken', '!=', False), ('microsoft_synchronization_stopped', '=', False)])
microsoft = MicrosoftCalendarService(self.env['microsoft.service'])
for user in users:
_logger.info("Calendar Synchro - Starting synchronization for %s", user)
@@ -102,3 +105,13 @@ class User(models.Model):
user.with_user(user).sudo()._sync_microsoft_calendar(microsoft)
except Exception as e:
_logger.exception("[%s] Calendar Synchro - Exception : %s !", user, exception_to_unicode(e))
+
+ def stop_microsoft_synchronization(self):
+ self.ensure_one()
+ self.sudo().microsoft_synchronization_stopped = True
+
+ def restart_microsoft_synchronization(self):
+ self.ensure_one()
+ self.sudo().microsoft_synchronization_stopped = False
+ self.env['calendar.recurrence']._restart_microsoft_sync()
+ self.env['calendar.event']._restart_microsoft_sync()
diff --git a/addons/microsoft_calendar/static/src/js/microsoft_calendar.js b/addons/microsoft_calendar/static/src/js/microsoft_calendar.js
index bd0e33f8dc1..209ab8681dd 100644
--- a/addons/microsoft_calendar/static/src/js/microsoft_calendar.js
+++ b/addons/microsoft_calendar/static/src/js/microsoft_calendar.js
@@ -9,6 +9,7 @@ const CalendarRenderer = require('calendar.CalendarRenderer');
const CalendarController = require('calendar.CalendarController');
const CalendarModel = require('calendar.CalendarModel');
const viewRegistry = require('web.view_registry');
+const session = require('web.session');
var _t = core._t;
@@ -60,7 +61,7 @@ const MicrosoftCalendarModel = CalendarModel.include({
fromurl: window.location.href,
}
}, {shadow}).then(function (result) {
- if (result.status === "need_config_from_admin" || result.status === "need_auth") {
+ if (["need_config_from_admin", "need_auth", "sync_stopped"].includes(result.status)) {
self.microsoft_is_sync = false;
} else if (result.status === "no_new_event_from_microsoft" || result.status === "need_refresh") {
self.microsoft_is_sync = true;
@@ -68,11 +69,22 @@ const MicrosoftCalendarModel = CalendarModel.include({
return result
});
},
+
+ archiveRecords: function (ids, model) {
+ return this._rpc({
+ model: model,
+ method: 'action_archive',
+ args: [ids],
+ context: session.user_context,
+ });
+ },
});
const MicrosoftCalendarController = CalendarController.include({
custom_events: _.extend({}, CalendarController.prototype.custom_events, {
syncMicrosoftCalendar: '_onSyncMicrosoftCalendar',
+ stopMicrosoftSynchronization: '_onStopMicrosoftSynchronization',
+ archiveRecord: '_onArchiveRecord',
}),
@@ -91,7 +103,7 @@ const MicrosoftCalendarController = CalendarController.include({
_onSyncMicrosoftCalendar: function (event) {
var self = this;
- return this.model._syncMicrosoftCalendar().then(function (o) {
+ return this._restartMicrosoftSynchronization().then(() => {return this.model._syncMicrosoftCalendar();}).then(function (o) {
if (o.status === "need_auth") {
Dialog.alert(self, _t("You will be redirected to Outlook to authorize the access to your calendar."), {
confirm_callback: function() {
@@ -114,20 +126,95 @@ const MicrosoftCalendarController = CalendarController.include({
}
} else if (o.status === "need_refresh") {
self.reload();
+ return event.data.on_refresh();
}
}).then(event.data.on_always, event.data.on_always);
- }
+ },
+
+ _onStopMicrosoftSynchronization: function (event) {
+ var self = this;
+ Dialog.confirm(this, _t("You are about to stop the synchronization of your calendar with Outlook. Are you sure you want to continue?"), {
+ confirm_callback: function() {
+ return self._rpc({
+ model: 'res.users',
+ method: 'stop_microsoft_synchronization',
+ args: [[self.context.uid]],
+ }).then(() => {
+ self.displayNotification({
+ title: _t("Success"),
+ message: _t("The synchronization with Outlook calendar was successfully stopped."),
+ type: 'success',
+ });
+ }).then(event.data.on_confirm);
+ },
+ title: _t('Confirmation'),
+ });
+
+ return event.data.on_always();
+ },
+
+ _restartMicrosoftSynchronization: function () {
+ return this._rpc({
+ model: 'res.users',
+ method: 'restart_microsoft_synchronization',
+ args: [[this.context.uid]],
+ });
+ },
+
+ _onArchiveRecord: function (event) {
+ var self = this;
+ Dialog.confirm(this, _t("Are you sure you want to archive this record ?"), {
+ confirm_callback: function () {
+ self.model.archiveRecords([event.data.id], self.modelName).then(function () {
+ self.reload();
+ });
+ }
+ });
+ },
});
const MicrosoftCalendarRenderer = CalendarRenderer.include({
+ custom_events: _.extend({}, CalendarRenderer.prototype.custom_events, {
+ archive_event: '_onArchiveEvent',
+ }),
+
events: _.extend({}, CalendarRenderer.prototype.events, {
'click .o_microsoft_sync_button': '_onSyncMicrosoftCalendar',
+ 'click .o_stop_microsoft_sync_button': '_onStopMicrosoftSynchronization',
}),
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
+ _initMicrosoftPillButton: function() {
+ this.$microsoftStopButton.css({"cursor":"pointer", "font-size":"0.9em"});
+ var switchBadgeClass = (elem) => {elem.toggleClass('badge-success'); elem.toggleClass('badge-danger');};
+ this.$('.o_stop_microsoft_sync_button').hover(() => {
+ switchBadgeClass(this.$microsoftStopButton);
+ this.$microsoftStopButton.html("".concat(_t("Stop the Synchronization")));
+ }, () => {
+ switchBadgeClass(this.$microsoftStopButton);
+ this.$microsoftStopButton.html("".concat(_t("Synched with Outlook")));
+ });
+ },
+
+ _getMicrosoftButton: function () {
+ return $('', {
+ type: 'button',
+ html: _t("Sync with Outlook"),
+ class: 'o_microsoft_sync_button w-100 m-auto btn btn-secondary'
+ });
+ },
+
+ _getMicrosoftStopButton: function () {
+ return $('', {
+ html: _t("Synched with Outlook"),
+ class: 'w-100 badge badge-pill badge-success border-0 o_stop_microsoft_sync_button'
+ })
+ .prepend($('', {class: "fa mr-2 fa-check"}));
+ },
+
/**
* Adds the Sync with Outlook button in the sidebar
*
@@ -137,21 +224,13 @@ const MicrosoftCalendarRenderer = CalendarRenderer.include({
var self = this;
this._super.apply(this, arguments);
this.$microsoftButton = $();
+ this.$microsoftStopButton = $();
if (this.model === "calendar.event") {
if (this.state.microsoft_is_sync) {
- this.$microsoftButton = $('', {
- html: _t("Synched with Outlook"),
- class: 'w-100 badge badge-pill badge-success border-0'
- })
- .prepend($('', {class: "fa mr-2 fa-check"}))
- .appendTo(self.$sidebar);
+ this.$microsoftStopButton = this._getMicrosoftStopButton().appendTo(self.$sidebar);
+ this._initMicrosoftPillButton();
} else {
- this.$microsoftButton = $('', {
- type: 'button',
- html: _t("Sync with Outlook"),
- class: 'o_microsoft_sync_button w-100 m-auto btn btn-secondary'
- })
- .appendTo(self.$sidebar);
+ this.$microsoftButton = this._getMicrosoftButton().appendTo(self.$sidebar);
}
}
},
@@ -173,8 +252,36 @@ const MicrosoftCalendarRenderer = CalendarRenderer.include({
on_always: function () {
self.$microsoftButton.prop('disabled', false);
},
+ on_refresh: function () {
+ if (_.isEmpty(self.$microsoftStopButton)) {
+ self.$microsoftStopButton = self._getMicrosoftStopButton();
+ }
+ self.$microsoftButton.replaceWith(self.$microsoftStopButton);
+ self._initMicrosoftPillButton();
+ }
});
},
+
+ _onStopMicrosoftSynchronization: function() {
+ var self = this;
+ this.$microsoftStopButton.prop('disabled', true);
+ this.trigger_up('stopMicrosoftSynchronization' , {
+ on_confirm: function () {
+ if (_.isEmpty(self.$microsoftButton)) {
+ self.$microsoftButton = self._getMicrosoftButton();
+ }
+ self.$microsoftStopButton.replaceWith(self.$microsoftButton);
+ },
+ on_always: function() {
+ self.$microsoftStopButton.prop('disabled', false);
+ }
+ });
+ },
+
+ _onArchiveEvent: function (event) {
+ this._unselectEvent();
+ this.trigger_up('archiveRecord', {id: parseInt(event.data.id, 10)});
+ },
});
return {
diff --git a/addons/microsoft_calendar/static/src/js/microsoft_calendar_popover.js b/addons/microsoft_calendar/static/src/js/microsoft_calendar_popover.js
new file mode 100644
index 00000000000..76c64b5f224
--- /dev/null
+++ b/addons/microsoft_calendar/static/src/js/microsoft_calendar_popover.js
@@ -0,0 +1,34 @@
+odoo.define('microsoft_calendar.MicrosoftCalendarPopover', function(require) {
+ "use strict";
+
+ const CalendarPopover = require('web.CalendarPopover');
+
+ const MicrosoftCalendarPopover = CalendarPopover.include({
+ events: _.extend({}, CalendarPopover.prototype.events, {
+ 'click .o_cw_popover_archive_m': '_onClickPopoverArchive',
+ }),
+
+ /**
+ * We only want one 'Archive' button in the popover
+ * so if Google Sync is active, it takes precedence
+ * over this popover.
+ */
+ isMEventSyncedAndArchivable() {
+ if (this.event.extendedProps.record.google_id === undefined) {
+ return this.event.extendedProps.record.microsoft_id;
+ }
+ return !this.event.extendedProps.record.google_id && this.event.extendedProps.record.microsoft_id
+ },
+
+ isEventDeletable() {
+ return !this.isMEventSyncedAndArchivable() && this._super();
+ },
+
+ _onClickPopoverArchive: function (ev) {
+ ev.preventDefault();
+ this.trigger_up('archive_event', {id: this.event.id});
+ },
+ });
+
+ return MicrosoftCalendarPopover;
+});
diff --git a/addons/microsoft_calendar/static/src/xml/microsoft_calendar_popover.xml b/addons/microsoft_calendar/static/src/xml/microsoft_calendar_popover.xml
new file mode 100644
index 00000000000..b8fa3206279
--- /dev/null
+++ b/addons/microsoft_calendar/static/src/xml/microsoft_calendar_popover.xml
@@ -0,0 +1,8 @@
+
+
+
+
+ Archive
+
+
+
diff --git a/addons/microsoft_calendar/tests/__init__.py b/addons/microsoft_calendar/tests/__init__.py
index fb1d7b300ae..0a906aa7123 100644
--- a/addons/microsoft_calendar/tests/__init__.py
+++ b/addons/microsoft_calendar/tests/__init__.py
@@ -2,3 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_sync_microsoft2odoo
+from . import test_sync_odoo2microsoft
diff --git a/addons/microsoft_calendar/tests/test_sync_odoo2microsoft.py b/addons/microsoft_calendar/tests/test_sync_odoo2microsoft.py
new file mode 100644
index 00000000000..689e136604c
--- /dev/null
+++ b/addons/microsoft_calendar/tests/test_sync_odoo2microsoft.py
@@ -0,0 +1,106 @@
+# -*- 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")
+
+ # If synchronization stopped, creating a new event should not call _google_insert.
+ self.env['calendar.event'].create({
+ 'name': "Event",
+ 'start': datetime(2020, 1, 15, 8, 0),
+ 'stop': datetime(2020, 1, 15, 18, 0),
+ 'privacy': 'private',
+ })
+ self.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'
+ 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()
+ event = self.env['calendar.event'].with_user(user).create({
+ 'microsoft_id': microsoft_id,
+ 'name': "Event",
+ 'start': datetime(2020, 1, 15, 8, 0),
+ 'stop': datetime(2020, 1, 15, 18, 0),
+ 'partner_ids': [(4, partner.id)],
+ })
+
+ user.with_user(user).restart_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': '2020-01-15T08:00:00+00:00', 'timeZone': 'Europe/London'},
+ 'end': {'dateTime': '2020-01-15T18:00:00+00:00', 'timeZone': 'Europe/London'},
+ 'subject': 'Event',
+ 'body': {'content': '', 'contentType': 'text'},
+ '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),
+ }
+ ]
+ })
diff --git a/addons/microsoft_calendar/views/microsoft_calendar_templates.xml b/addons/microsoft_calendar/views/microsoft_calendar_templates.xml
index 89d15becb70..bed2c813842 100644
--- a/addons/microsoft_calendar/views/microsoft_calendar_templates.xml
+++ b/addons/microsoft_calendar/views/microsoft_calendar_templates.xml
@@ -2,6 +2,7 @@
+
diff --git a/addons/microsoft_calendar/views/microsoft_calendar_views.xml b/addons/microsoft_calendar/views/microsoft_calendar_views.xml
new file mode 100644
index 00000000000..dd0db461d64
--- /dev/null
+++ b/addons/microsoft_calendar/views/microsoft_calendar_views.xml
@@ -0,0 +1,13 @@
+
+
+
+ microsoft_calendar.event.calendar
+ calendar.event
+
+
+
+
+
+
+
+