[MERGE] google/microsoft_calendar: Allow a user to stop the synchro
Description of the issue/feature this PR addresses: Give more flexibility to the users by allowing them to stop the synchronization with Google/Outlook calendar. Current behavior before PR: Only way to stop the active synchronization with Google/Outlook calendar is to uninstall both modules. Desired behavior after PR is merged: Add a "toggle" button that allow to turn on/off the synchronization with Google/ Outlook calendar independently (i.e. possible to turn off Google sync. while leaving Outlook sync. active). (PS for the review: the source codes for Outlook and Google are almost identical, except for the Calendar Popovers and the tests for the new stop/restart feature). Task ID: 2330246 -- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr closes odoo/odoo#60825 Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
@@ -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': [],
|
||||
|
||||
@@ -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": ''
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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("<i class='fa mr-2 fa-times'/>".concat(_t("Stop the Synchronization")));
|
||||
}, () => {
|
||||
switchBadgeClass(this.$googleStopButton);
|
||||
this.$googleStopButton.html("<i class='fa mr-2 fa-check'/>".concat(_t("Synched with Google")));
|
||||
});
|
||||
},
|
||||
|
||||
_getGoogleButton: function () {
|
||||
return $('<button/>', {
|
||||
type: 'button',
|
||||
html: _t("Sync with <b>Google</b>"),
|
||||
class: 'o_google_sync_button w-100 m-auto btn btn-secondary'
|
||||
});
|
||||
},
|
||||
|
||||
_getGoogleStopButton: function () {
|
||||
return $('<span/>', {
|
||||
html: _t("Synched with Google"),
|
||||
class: 'w-100 badge badge-pill badge-success border-0 o_stop_google_sync_button'
|
||||
})
|
||||
.prepend($('<i/>', {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 = $('<span/>', {
|
||||
html: _t("Synched with Google"),
|
||||
class: 'w-100 badge badge-pill badge-success border-0'
|
||||
})
|
||||
.prepend($('<i/>', {class: "fa mr-2 fa-check"}))
|
||||
.appendTo(self.$sidebar);
|
||||
this.$googleStopButton = this._getGoogleStopButton().appendTo(self.$sidebar);
|
||||
this._initGooglePillButton();
|
||||
} else {
|
||||
this.$googleButton = $('<button/>', {
|
||||
type: 'button',
|
||||
html: _t("Sync with <b>Google</b>"),
|
||||
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 {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-extend="Calendar.attendee.status.popover">
|
||||
<t t-jquery=".o_cw_popover_edit" t-operation="after">
|
||||
<a t-if="widget.isGEventSyncedAndArchivable() and widget.isEventDetailsVisible()" href="#" class="btn btn-secondary o_cw_popover_archive_g">Archive</a>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -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)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<odoo>
|
||||
<template id="assets_backend" name="google_calendar assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/google_calendar/static/src/js/google_calendar_popover.js"></script>
|
||||
<script type="text/javascript" src="/google_calendar/static/src/js/google_calendar.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<record id="view_google_calendar_event" model="ir.ui.view">
|
||||
<field name="name">google_calendar.event.calendar</field>
|
||||
<field name="model">calendar.event</field>
|
||||
<field name="inherit_id" ref="calendar.view_calendar_event_calendar"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="attendee_status" position="after">
|
||||
<field name="google_id" invisible="1"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -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': [],
|
||||
|
||||
@@ -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": ''
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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)]
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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("<i class='fa mr-2 fa-times'/>".concat(_t("Stop the Synchronization")));
|
||||
}, () => {
|
||||
switchBadgeClass(this.$microsoftStopButton);
|
||||
this.$microsoftStopButton.html("<i class='fa mr-2 fa-check'/>".concat(_t("Synched with Outlook")));
|
||||
});
|
||||
},
|
||||
|
||||
_getMicrosoftButton: function () {
|
||||
return $('<button/>', {
|
||||
type: 'button',
|
||||
html: _t("Sync with <b>Outlook</b>"),
|
||||
class: 'o_microsoft_sync_button w-100 m-auto btn btn-secondary'
|
||||
});
|
||||
},
|
||||
|
||||
_getMicrosoftStopButton: function () {
|
||||
return $('<span/>', {
|
||||
html: _t("Synched with Outlook"),
|
||||
class: 'w-100 badge badge-pill badge-success border-0 o_stop_microsoft_sync_button'
|
||||
})
|
||||
.prepend($('<i/>', {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 = $('<span/>', {
|
||||
html: _t("Synched with Outlook"),
|
||||
class: 'w-100 badge badge-pill badge-success border-0'
|
||||
})
|
||||
.prepend($('<i/>', {class: "fa mr-2 fa-check"}))
|
||||
.appendTo(self.$sidebar);
|
||||
this.$microsoftStopButton = this._getMicrosoftStopButton().appendTo(self.$sidebar);
|
||||
this._initMicrosoftPillButton();
|
||||
} else {
|
||||
this.$microsoftButton = $('<button/>', {
|
||||
type: 'button',
|
||||
html: _t("Sync with <b>Outlook</b>"),
|
||||
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 {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-extend="Calendar.attendee.status.popover">
|
||||
<t t-jquery=".o_cw_popover_edit" t-operation="after">
|
||||
<a t-if="widget.isMEventSyncedAndArchivable() and widget.isEventDetailsVisible()" href="#" class="btn btn-secondary o_cw_popover_archive_m">Archive</a>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -2,6 +2,7 @@
|
||||
<odoo>
|
||||
<template id="assets_backend" name="microsoft_calendar assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/microsoft_calendar/static/src/js/microsoft_calendar_popover.js"></script>
|
||||
<script type="text/javascript" src="/microsoft_calendar/static/src/js/microsoft_calendar.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<record id="view_microsoft_calendar_event" model="ir.ui.view">
|
||||
<field name="name">microsoft_calendar.event.calendar</field>
|
||||
<field name="model">calendar.event</field>
|
||||
<field name="inherit_id" ref="calendar.view_calendar_event_calendar"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="attendee_status" position="after">
|
||||
<field name="microsoft_id" invisible="1"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user