diff --git a/addons/calendar/__init__.py b/addons/calendar/__init__.py
index ca9d16e5409..48dadfaf2b2 100644
--- a/addons/calendar/__init__.py
+++ b/addons/calendar/__init__.py
@@ -4,3 +4,4 @@
from . import controllers
from . import models
from . import populate
+from . import wizard
diff --git a/addons/calendar/__manifest__.py b/addons/calendar/__manifest__.py
index f8aec351693..636b7f9e4fa 100644
--- a/addons/calendar/__manifest__.py
+++ b/addons/calendar/__manifest__.py
@@ -34,6 +34,7 @@ If you need to manage your meetings, you should install the CRM module.
'views/calendar_templates.xml',
'views/calendar_views.xml',
'views/res_partner_views.xml',
+ 'wizard/calendar_provider_config.xml'
],
'installable': True,
'application': True,
@@ -49,6 +50,7 @@ If you need to manage your meetings, you should install the CRM module.
'calendar/static/src/js/calendar_model.js',
'calendar/static/src/js/calendar_view.js',
'calendar/static/src/js/services/calendar_notification_service.js',
+ 'calendar/static/src/views/*',
],
'web.qunit_suite_tests': [
'calendar/static/tests/**/*',
diff --git a/addons/calendar/security/ir.model.access.csv b/addons/calendar/security/ir.model.access.csv
index 5a3b8812f1d..da97d846e0a 100644
--- a/addons/calendar/security/ir.model.access.csv
+++ b/addons/calendar/security/ir.model.access.csv
@@ -11,3 +11,5 @@ access_calendar_alarm_manager,access_calendar_alarm_manager,model_calendar_alarm
access_calendar_filters_all,access_calendar_contacts_all,model_calendar_filters,base.group_user,1,1,1,1
access_calendar_filters,access_calendar_contacts,model_calendar_filters,base.group_system,1,1,1,1
access_calendar_recurrence,access_calendar_recurrence,model_calendar_recurrence,base.group_user,1,1,1,1
+access_calendar_provider_config_all,calendar.provider.config.all,model_calendar_provider_config,,0,0,0,0
+access_calendar_provider_config_manager,calendar.provider.config.manager,model_calendar_provider_config,base.group_system,1,1,1,1
diff --git a/addons/calendar/static/src/img/google_calendar_40.png b/addons/calendar/static/src/img/google_calendar_40.png
new file mode 100644
index 00000000000..46eacd17924
Binary files /dev/null and b/addons/calendar/static/src/img/google_calendar_40.png differ
diff --git a/addons/calendar/static/src/img/microsoft_calendar_40.png b/addons/calendar/static/src/img/microsoft_calendar_40.png
new file mode 100644
index 00000000000..baddba785c7
Binary files /dev/null and b/addons/calendar/static/src/img/microsoft_calendar_40.png differ
diff --git a/addons/calendar/static/src/js/calendar_renderer.js b/addons/calendar/static/src/js/calendar_renderer.js
index 516720d33ad..a71de015dc8 100644
--- a/addons/calendar/static/src/js/calendar_renderer.js
+++ b/addons/calendar/static/src/js/calendar_renderer.js
@@ -1,5 +1,6 @@
/** @odoo-module alias=calendar.CalendarRenderer **/
+import {_t} from 'web.core';
import CalendarRenderer from 'web.CalendarRenderer';
import CalendarPopover from 'web.CalendarPopover';
import session from 'web.session';
@@ -110,6 +111,20 @@ const AttendeeCalendarRenderer = CalendarRenderer.extend({
CalendarPopover: AttendeeCalendarPopover,
eventTemplate: 'Calendar.calendar-box',
}),
+
+ events: _.extend({}, CalendarRenderer.prototype.events, {
+ 'click #google_sync_activate': '_onConfigureExternalCalendarGoogle',
+ 'click #microsoft_sync_activate': '_onConfigureExternalCalendarMicrosoft',
+ }),
+
+ _onConfigureExternalCalendarGoogle: function (e) {
+ e.preventDefault();
+ this._configureCalendarProviderSync('google');
+ },
+ _onConfigureExternalCalendarMicrosoft: function (e) {
+ e.preventDefault();
+ this._configureCalendarProviderSync('microsoft');
+ },
/**
* Add the attendee-id attribute in order to distinct the events when there are
* several attendees in the event.
@@ -131,6 +146,20 @@ const AttendeeCalendarRenderer = CalendarRenderer.extend({
}
return selector;
},
+ _configureCalendarProviderSync: function (ProviderName) {
+ this.do_action({
+ name: _t('Connect your Calendar'),
+ type: 'ir.actions.act_window',
+ res_model: 'calendar.provider.config',
+ views: [[false, "form"]],
+ view_mode: "form",
+ target: 'new',
+ context: {
+ 'default_external_calendar_provider': ProviderName,
+ 'dialog_size': 'medium',
+ }
+ });
+ },
});
export default {
diff --git a/addons/calendar/static/src/views/calendar_provider_config_form_renderer.js b/addons/calendar/static/src/views/calendar_provider_config_form_renderer.js
new file mode 100644
index 00000000000..97e25a03231
--- /dev/null
+++ b/addons/calendar/static/src/views/calendar_provider_config_form_renderer.js
@@ -0,0 +1,71 @@
+/** @odoo-module **/
+
+import { FormRenderer } from "@web/views/form/form_renderer";
+import { session } from "@web/session";
+import { useService } from "@web/core/utils/hooks";
+
+const providerData = {
+ 'google': {
+ 'restart_sync_method': 'restart_google_synchronization',
+ 'sync_route': '/google_calendar/sync_data'
+ },
+ 'microsoft': {
+ 'restart_sync_method': 'restart_microsoft_synchronization',
+ 'sync_route': '/microsoft_calendar/sync_data'
+ }
+}
+
+
+export class CalendarProviderConfigFormRenderer extends FormRenderer {
+ setup() {
+ super.setup();
+ this.orm = useService('orm');
+ this.rpc = useService('rpc');
+ }
+
+ /**
+ * Activate the external sync for the first time, after installing the
+ * relevant submodule if necessary.
+ *
+ * @private
+ */
+ async onConnect(ev) {
+ ev.preventDefault();
+ ev.stopImmediatePropagation();
+ if (!await this.props.record.save({stayInEdition: true})) {
+ return; // handled by view
+ }
+ await this.orm.call(
+ this.props.record.resModel,
+ 'action_calendar_prepare_external_provider_sync',
+ [this.props.record.resId]
+ )
+ // See google/microsoft_calendar for the origin of this shortened version
+ const { restart_sync_method, sync_route } = providerData[this.props.record.data.external_calendar_provider];
+ await this.orm.call(
+ 'res.users',
+ restart_sync_method,
+ [[session.uid]]
+ );
+ const response = await this.rpc(
+ sync_route, {
+ model: 'calendar.event',
+ fromurl: window.location.href,
+ }
+ );
+ await this._beforeLeaveContext();
+ if (response.status === "need_auth") {
+ window.location.assign(response.url);
+ } else if (response.status === "need_refresh") {
+ window.location.reload();
+ }
+ }
+ /**
+ * Hook to perform additional work before redirecting to external url or reloading.
+ *
+ * @private
+ */
+ async _beforeLeaveContext() {
+ return Promise.resolve();
+ }
+}
diff --git a/addons/calendar/static/src/views/calendar_provider_config_form_view.js b/addons/calendar/static/src/views/calendar_provider_config_form_view.js
new file mode 100644
index 00000000000..bca8bfbf241
--- /dev/null
+++ b/addons/calendar/static/src/views/calendar_provider_config_form_view.js
@@ -0,0 +1,13 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+import { formView } from "@web/views/form/form_view";
+import { CalendarProviderConfigFormRenderer } from "./calendar_provider_config_form_renderer";
+
+
+export const CalendarProviderConfigFormView = {
+ ...formView,
+ Renderer: CalendarProviderConfigFormRenderer,
+};
+
+registry.category("views").add("calendar_provider_config_form", CalendarProviderConfigFormView);
diff --git a/addons/calendar/static/src/xml/base_calendar.xml b/addons/calendar/static/src/xml/base_calendar.xml
index 8919bb7fc70..9ca5ec3ef8b 100644
--- a/addons/calendar/static/src/xml/base_calendar.xml
+++ b/addons/calendar/static/src/xml/base_calendar.xml
@@ -3,7 +3,21 @@
-
+
+ Synchronize with:
+
+
+
+
+
+
+
+
+
diff --git a/addons/calendar/wizard/__init__.py b/addons/calendar/wizard/__init__.py
new file mode 100644
index 00000000000..0d1fe747cc7
--- /dev/null
+++ b/addons/calendar/wizard/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+
+from . import calendar_provider_config
diff --git a/addons/calendar/wizard/calendar_provider_config.py b/addons/calendar/wizard/calendar_provider_config.py
new file mode 100644
index 00000000000..8755303da5a
--- /dev/null
+++ b/addons/calendar/wizard/calendar_provider_config.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+from odoo.addons.base.models.ir_module import assert_log_admin_access
+
+
+class CalendarProviderConfig(models.TransientModel):
+ _name = 'calendar.provider.config'
+ _description = 'Calendar Provider Configuration Wizard'
+
+ external_calendar_provider = fields.Selection([
+ ('google', 'Google'), ('microsoft', 'Outlook')],
+ "Choose an external calendar to configure", default='google')
+
+ # Allow to sync with eventually existing ICP keys without creating them if respective module is not installed
+ # Using same field names and strings as their respective res.config.settings
+ cal_client_id = fields.Char(
+ "Google Client_id",
+ default=lambda self: self.env['ir.config_parameter'].get_param('google_calendar_client_id'))
+ cal_client_secret = fields.Char(
+ "Google Client_key",
+ default=lambda self: self.env['ir.config_parameter'].get_param('google_calendar_client_secret'))
+ microsoft_outlook_client_identifier = fields.Char(
+ "Outlook Client Id",
+ default=lambda self: self.env['ir.config_parameter'].get_param('microsoft_calendar_client_id'))
+ microsoft_outlook_client_secret = fields.Char(
+ "Outlook Client Secret",
+ default=lambda self: self.env['ir.config_parameter'].get_param('microsoft_calendar_client_secret'))
+
+ @assert_log_admin_access
+ def action_calendar_prepare_external_provider_sync(self):
+ """ Called by the wizard to configure an external calendar provider without requiring users
+ to access the general settings page.
+ Make sure that the provider calendar module is installed or install it. Then, set
+ the API keys into the applicable config parameters.
+ """
+ self.ensure_one()
+ calendar_module = self.env['ir.module.module'].search([
+ ('name', '=', f'{self.external_calendar_provider}_calendar')])
+
+ if calendar_module.state != 'installed':
+ calendar_module.button_immediate_install()
+
+ if self.external_calendar_provider == 'google':
+ self.env['ir.config_parameter'].set_param('google_calendar_client_id', self.cal_client_id)
+ self.env['ir.config_parameter'].set_param('google_calendar_client_secret', self.cal_client_secret)
+ elif self.external_calendar_provider == 'microsoft':
+ self.env['ir.config_parameter'].set_param('microsoft_calendar_client_id', self.microsoft_outlook_client_identifier)
+ self.env['ir.config_parameter'].set_param('microsoft_calendar_client_secret', self.microsoft_outlook_client_secret)
diff --git a/addons/calendar/wizard/calendar_provider_config.xml b/addons/calendar/wizard/calendar_provider_config.xml
new file mode 100644
index 00000000000..be4bf019cde
--- /dev/null
+++ b/addons/calendar/wizard/calendar_provider_config.xml
@@ -0,0 +1,40 @@
+
+
+
+ calendar.provider.config.view.form
+ calendar.provider.config
+
+
+
+
+
diff --git a/addons/google_calendar/static/src/js/google_calendar.js b/addons/google_calendar/static/src/js/google_calendar.js
index ccd47ba3a29..312c9485cbc 100644
--- a/addons/google_calendar/static/src/js/google_calendar.js
+++ b/addons/google_calendar/static/src/js/google_calendar.js
@@ -3,7 +3,6 @@ odoo.define('google_calendar.CalendarView', function (require) {
var core = require('web.core');
var Dialog = require('web.Dialog');
-var framework = require('web.framework');
const CalendarRenderer = require('@calendar/js/calendar_renderer')[Symbol.for("default")].AttendeeCalendarRenderer;
const CalendarController = require('@calendar/js/calendar_controller')[Symbol.for("default")];
const CalendarModel = require('@calendar/js/calendar_model')[Symbol.for("default")];
@@ -112,12 +111,8 @@ const GoogleCalendarController = CalendarController.include({
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() {
- framework.redirect(o.url);
- },
- title: _t('Redirection'),
- });
+ // allows to configure API keys in modal and will then redirect.
+ self.renderer._configureCalendarProviderSync('google');
} else if (o.status === "need_config_from_admin") {
if (!_.isUndefined(o.action) && parseInt(o.action)) {
Dialog.confirm(self, _t("The Google Synchronization needs to be configured before you can use it, do you want to do it now?"), {
diff --git a/addons/google_calendar/static/src/xml/base_calendar.xml b/addons/google_calendar/static/src/xml/base_calendar.xml
index 667d8c5188d..40265369aca 100644
--- a/addons/google_calendar/static/src/xml/base_calendar.xml
+++ b/addons/google_calendar/static/src/xml/base_calendar.xml
@@ -1,16 +1,18 @@
-
-
-
+
+
+
+
+
diff --git a/addons/microsoft_calendar/static/src/js/microsoft_calendar.js b/addons/microsoft_calendar/static/src/js/microsoft_calendar.js
index 1b74b341eb2..d9a62b88947 100644
--- a/addons/microsoft_calendar/static/src/js/microsoft_calendar.js
+++ b/addons/microsoft_calendar/static/src/js/microsoft_calendar.js
@@ -3,7 +3,6 @@ odoo.define('microsoft_calendar.CalendarView', function (require) {
var core = require('web.core');
var Dialog = require('web.Dialog');
-var framework = require('web.framework');
const CalendarRenderer = require('@calendar/js/calendar_renderer')[Symbol.for("default")].AttendeeCalendarRenderer;
const CalendarController = require('@calendar/js/calendar_controller')[Symbol.for("default")];
const CalendarModel = require('@calendar/js/calendar_model')[Symbol.for("default")];
@@ -111,12 +110,8 @@ const MicrosoftCalendarController = CalendarController.include({
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() {
- framework.redirect(o.url);
- },
- title: _t('Redirection'),
- });
+ // allows to configure API keys in modal and will then redirect.
+ self.renderer._configureCalendarProviderSync('microsoft');
} else if (o.status === "need_config_from_admin") {
if (!_.isUndefined(o.action) && parseInt(o.action)) {
Dialog.confirm(self, _t("The Outlook Synchronization needs to be configured before you can use it, do you want to do it now?"), {
diff --git a/addons/microsoft_calendar/static/src/xml/base_calendar.xml b/addons/microsoft_calendar/static/src/xml/base_calendar.xml
index 812e2b4d4d7..f6fb06aa72f 100644
--- a/addons/microsoft_calendar/static/src/xml/base_calendar.xml
+++ b/addons/microsoft_calendar/static/src/xml/base_calendar.xml
@@ -1,16 +1,18 @@
-
-
-
+
+