diff --git a/addons/account/models/company.py b/addons/account/models/company.py
index 5785b096074..61bdb338293 100644
--- a/addons/account/models/company.py
+++ b/addons/account/models/company.py
@@ -217,7 +217,7 @@ class ResCompany(models.Model):
def get_and_update_account_invoice_onboarding_state(self):
""" This method is called on the controller rendering method and ensures that the animations
are displayed only one time. """
- return self.get_and_update_onbarding_state(
+ return self._get_and_update_onboarding_state(
'account_invoice_onboarding_state',
self.get_account_invoice_onboarding_steps_states_names()
)
@@ -234,7 +234,7 @@ class ResCompany(models.Model):
def get_and_update_account_dashboard_onboarding_state(self):
""" This method is called on the controller rendering method and ensures that the animations
are displayed only one time. """
- return self.get_and_update_onbarding_state(
+ return self._get_and_update_onboarding_state(
'account_dashboard_onboarding_state',
self.get_account_dashboard_onboarding_steps_states_names()
)
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 @@
-
-
-
+
+
+
+
+
diff --git a/addons/onboarding/__init__.py b/addons/onboarding/__init__.py
new file mode 100644
index 00000000000..7d34c7c054a
--- /dev/null
+++ b/addons/onboarding/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import controllers
+from . import models
diff --git a/addons/onboarding/__manifest__.py b/addons/onboarding/__manifest__.py
new file mode 100644
index 00000000000..05402a416de
--- /dev/null
+++ b/addons/onboarding/__manifest__.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+{
+ 'name': 'Onboarding toolbox',
+ 'version': '1.0',
+ 'category': 'Hidden',
+ 'sequence': 9001,
+ 'description': """
+This module allows to manage onboardings and their progress
+================================================================================
+ """,
+ 'depends': ['base'],
+ 'installable': True,
+ 'data': [
+ 'data/onboarding_data.xml',
+ 'views/onboarding_views.xml',
+ 'views/onboarding_menus.xml',
+ 'security/ir.model.access.csv',
+ ],
+ 'license': 'LGPL-3',
+}
diff --git a/addons/onboarding/controllers/__init__.py b/addons/onboarding/controllers/__init__.py
new file mode 100644
index 00000000000..fd2842f8696
--- /dev/null
+++ b/addons/onboarding/controllers/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import onboarding
diff --git a/addons/onboarding/controllers/onboarding.py b/addons/onboarding/controllers/onboarding.py
new file mode 100644
index 00000000000..30c0f189459
--- /dev/null
+++ b/addons/onboarding/controllers/onboarding.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import http
+from odoo.http import request
+
+
+class OnboardingController(http.Controller):
+ @http.route('/onboarding/', auth='user', type='json')
+ def get_onboarding_data(self, route_name=None):
+ if not request.env.is_admin():
+ return {}
+
+ onboarding = request.env['onboarding.onboarding'].search([('route_name', '=', route_name)])
+ if onboarding and not onboarding._search_or_create_progress().is_onboarding_closed:
+ # JS implementation of the onboarding panel expects this data structure
+ return {
+ 'html': request.env['ir.qweb']._render(
+ 'onboarding.onboarding_panel', onboarding._prepare_rendering_values())
+ }
+
+ return {}
diff --git a/addons/onboarding/data/onboarding_data.xml b/addons/onboarding/data/onboarding_data.xml
new file mode 100644
index 00000000000..5e30c744697
--- /dev/null
+++ b/addons/onboarding/data/onboarding_data.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ onboarding.onboarding.step
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/onboarding/models/__init__.py b/addons/onboarding/models/__init__.py
new file mode 100644
index 00000000000..8fd1fc0e1d6
--- /dev/null
+++ b/addons/onboarding/models/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import onboarding_onboarding
+from . import onboarding_step
+from . import onboarding_progress
+from . import onboarding_progress_step
diff --git a/addons/onboarding/models/onboarding_onboarding.py b/addons/onboarding/models/onboarding_onboarding.py
new file mode 100644
index 00000000000..c6e786d70f1
--- /dev/null
+++ b/addons/onboarding/models/onboarding_onboarding.py
@@ -0,0 +1,105 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+from odoo.addons.onboarding.models.onboarding_progress import ONBOARDING_PROGRESS_STATES
+
+
+class Onboarding(models.Model):
+ _name = 'onboarding.onboarding'
+ _description = 'Onboarding'
+ _order = 'sequence asc, id desc'
+
+ name = fields.Char('Name of the onboarding', translate=True)
+ # One word identifier used to define the onboarding panel's route: `/onboarding/{route_name}`.
+ route_name = fields.Char('One word name', required=True)
+ step_ids = fields.One2many('onboarding.onboarding.step', 'onboarding_id', 'Onboarding steps')
+
+ is_per_company = fields.Boolean('Should be done per company?', default=True)
+
+ panel_background_color = fields.Selection(
+ [('orange', 'Orange'), ('blue', 'Blue'), ('violet', 'Violet'), ('none', 'None')],
+ string="Panel's Background color", default='orange',
+ help="Color gradient added to the panel's background.")
+ panel_background_image = fields.Binary("Panel's background image")
+ panel_close_action_name = fields.Char(
+ 'Closing action', help='Name of the onboarding model action to execute when closing the panel.')
+
+ current_progress_id = fields.Many2one(
+ 'onboarding.progress', 'Onboarding Progress', compute='_compute_current_progress',
+ help='Onboarding Progress for the current context (company).')
+ current_onboarding_state = fields.Selection(
+ ONBOARDING_PROGRESS_STATES, string='Completion State', compute='_compute_current_progress', readonly=True)
+ is_onboarding_closed = fields.Boolean(string='Was panel closed?', compute='_compute_current_progress')
+
+ progress_ids = fields.One2many(
+ 'onboarding.progress', 'onboarding_id', string='Onboarding Progress Records', readonly=True,
+ help='All Onboarding Progress Records (across companies).')
+
+ sequence = fields.Integer(default=10)
+ _sql_constraints = [
+ ('route_name_uniq', 'UNIQUE (route_name)', 'Onboarding alias must be unique.'),
+ ]
+
+ @api.depends_context('company')
+ @api.depends('progress_ids', 'progress_ids.is_onboarding_closed', 'progress_ids.onboarding_state')
+ def _compute_current_progress(self):
+ for onboarding in self:
+ current_progress_id = onboarding.progress_ids.filtered(
+ lambda progress: progress.company_id.id in {False, self.env.company.id})
+ if current_progress_id:
+ onboarding.current_onboarding_state = current_progress_id.onboarding_state
+ onboarding.current_progress_id = current_progress_id
+ onboarding.is_onboarding_closed = current_progress_id.is_onboarding_closed
+ else:
+ onboarding.current_onboarding_state = 'not_done'
+ onboarding.current_progress_id = False
+ onboarding.is_onboarding_closed = False
+
+ def action_close(self):
+ """Close the onboarding panel."""
+ self.current_progress_id.action_close()
+
+ def action_toggle_visibility(self):
+ self.current_progress_id.action_toggle_visibility()
+
+ def write(self, values):
+ if 'is_per_company' in values:
+ onboardings_per_company_update = self.filtered(
+ lambda onboarding: onboarding.is_per_company != values['is_per_company'])
+
+ res = super().write(values)
+
+ if 'is_per_company' in values:
+ # When changing this parameter, all progress (onboarding and steps) is reset.
+ onboardings_per_company_update.progress_ids.unlink()
+ return res
+
+ def _search_or_create_progress(self):
+ """Create Progress record(s) as necessary for the context.
+ """
+ onboardings_without_progress = self.filtered(lambda onboarding: not onboarding.current_progress_id)
+ onboardings_without_progress._create_progress()
+ return self.current_progress_id
+
+ def _create_progress(self):
+ return self.env['onboarding.progress'].create([
+ {
+ 'company_id': self.env.company.id if onboarding.is_per_company else False,
+ 'onboarding_id': onboarding.id
+ } for onboarding in self
+ ])
+
+ def _prepare_rendering_values(self):
+ self.ensure_one()
+ values = {
+ 'bg_image': f'/web/image/onboarding.onboarding/{self.id}/panel_background_image',
+ 'close_method': self.panel_close_action_name,
+ 'close_model': 'onboarding.onboarding',
+ 'steps': self.step_ids,
+ 'state': self.current_progress_id._get_and_update_onboarding_state(),
+ }
+ if self.panel_background_color != 'none':
+ values.update(classes=f'o_onboarding_{self.panel_background_color}')
+
+ return values
diff --git a/addons/onboarding/models/onboarding_progress.py b/addons/onboarding/models/onboarding_progress.py
new file mode 100644
index 00000000000..c5a5dcc7da8
--- /dev/null
+++ b/addons/onboarding/models/onboarding_progress.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+ONBOARDING_PROGRESS_STATES = [
+ ('not_done', 'Not done'),
+ ('just_done', 'Just done'),
+ ('done', 'Done'),
+]
+
+
+class OnboardingProgress(models.Model):
+ _name = 'onboarding.progress'
+ _description = 'Onboarding Progress Tracker'
+ _rec_name = 'onboarding_id'
+
+ onboarding_state = fields.Selection(
+ ONBOARDING_PROGRESS_STATES, string='Onboarding progress', compute='_compute_onboarding_state', store=True)
+ is_onboarding_closed = fields.Boolean('Was panel closed?')
+ company_id = fields.Many2one('res.company')
+ onboarding_id = fields.Many2one(
+ 'onboarding.onboarding', 'Related onboarding tracked', required=True, ondelete='cascade')
+ progress_step_ids = fields.One2many('onboarding.progress.step', 'progress_id', 'Progress Steps Trackers')
+ _sql_constraints = [
+ ('onboarding_company_uniq', 'unique (onboarding_id,company_id)',
+ 'There cannot be multiple records of the same onboarding completion for the same company.'),
+ ]
+
+ @api.depends('onboarding_id.step_ids', 'progress_step_ids', 'progress_step_ids.step_state')
+ def _compute_onboarding_state(self):
+ progress_steps_data = self.env['onboarding.progress.step'].read_group(
+ [('progress_id', 'in', self.ids), ('step_state', 'in', ['just_done', 'done'])],
+ ['progress_id'], ['progress_id']
+ )
+ result = dict((data['progress_id'][0], data['progress_id_count']) for data in progress_steps_data)
+ for progress in self:
+ progress.onboarding_state = (
+ 'not_done' if result.get(progress.id, 0) != len(progress.onboarding_id.step_ids)
+ else 'done')
+
+ def action_close(self):
+ self.is_onboarding_closed = True
+
+ def action_toggle_visibility(self):
+ for progress in self:
+ progress.is_onboarding_closed = not progress.is_onboarding_closed
+
+ def _get_and_update_onboarding_state(self):
+ """Used to fetch the progress of an onboarding for rendering its panel and is expected to
+ be called by the onboarding controller. It also has the responsibility of updating the
+ 'just_done' states into 'done' so that the 'just_done' states are only rendered once.
+ """
+ self.ensure_one()
+ onboarding_states_values = {}
+ progress_steps_to_consolidate = self.env['onboarding.progress.step']
+
+ # Iterate over onboarding step_ids and not self.progress_step_ids because 'not_done' steps
+ # may not have a progress_step record.
+ for step in self.onboarding_id.step_ids:
+ step_state = step.current_step_state
+ if step_state == 'just_done':
+ progress_steps_to_consolidate |= step.current_progress_step_id
+ onboarding_states_values[step.id] = step_state
+
+ progress_steps_to_consolidate.action_consolidate_just_done()
+
+ if self.is_onboarding_closed:
+ onboarding_states_values['onboarding_state'] = 'closed'
+ elif self.onboarding_state == 'done':
+ onboarding_states_values['onboarding_state'] = 'just_done' if progress_steps_to_consolidate else 'done'
+ return onboarding_states_values
diff --git a/addons/onboarding/models/onboarding_progress_step.py b/addons/onboarding/models/onboarding_progress_step.py
new file mode 100644
index 00000000000..04c3538ff44
--- /dev/null
+++ b/addons/onboarding/models/onboarding_progress_step.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+from odoo.addons.onboarding.models.onboarding_progress import ONBOARDING_PROGRESS_STATES
+
+
+class OnboardingProgressStep(models.Model):
+ _name = 'onboarding.progress.step'
+ _description = 'Onboarding Progress Step Tracker'
+ _rec_name = 'step_id'
+
+ progress_id = fields.Many2one(
+ 'onboarding.progress', 'Related Onboarding Progress Tracker', required=True, ondelete='cascade')
+ step_state = fields.Selection(
+ ONBOARDING_PROGRESS_STATES, string='Onboarding Step Progress', default='not_done')
+ onboarding_id = fields.Many2one(related='progress_id.onboarding_id', string='Onboarding')
+ step_id = fields.Many2one(
+ 'onboarding.onboarding.step', string='Onboarding Step', required=True, ondelete='cascade')
+
+ _sql_constraints = [
+ ('progress_step_uniq', 'unique (progress_id, step_id)',
+ 'There cannot be multiple records of the same onboarding step completion for the same Progress record.'),
+ ]
+
+ def action_consolidate_just_done(self):
+ was_just_done = self.filtered(lambda progress: progress.step_state == 'just_done')
+ was_just_done.step_state = 'done'
+ return was_just_done
+
+ def action_set_just_done(self):
+ not_done = self.filtered_domain([('step_state', '=', 'not_done')])
+ not_done.step_state = 'just_done'
+ return not_done
diff --git a/addons/onboarding/models/onboarding_step.py b/addons/onboarding/models/onboarding_step.py
new file mode 100644
index 00000000000..d98d771cf0a
--- /dev/null
+++ b/addons/onboarding/models/onboarding_step.py
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import _, api, fields, models
+from odoo.addons.onboarding.models.onboarding_progress import ONBOARDING_PROGRESS_STATES
+
+
+class OnboardingStep(models.Model):
+ _name = 'onboarding.onboarding.step'
+ _description = 'Onboarding Step'
+ _order = 'sequence asc, id asc'
+ _rec_name = 'title'
+
+ onboarding_id = fields.Many2one(
+ 'onboarding.onboarding', string='Onboarding', readonly=True, required=True, ondelete='cascade')
+
+ title = fields.Char('Title', translate=True)
+ description = fields.Char('Description', translate=True)
+ button_text = fields.Char(
+ 'Button text', required=True, default=_("Let's do it"), translate=True,
+ help="Text on the panel's button to start this step")
+ done_icon = fields.Char('Font Awesome Icon when completed', default='fa-star')
+ done_text = fields.Char(
+ 'Text to show when step is completed', default=_('Step Completed! - Click to review'), translate=True)
+ panel_step_open_action_name = fields.Char(
+ string='Opening action', required=True,
+ help='Name of the onboarding step model action to execute when opening the step, '
+ 'e.g. action_open_onboarding_1_step_1')
+
+ current_progress_step_id = fields.Many2one(
+ 'onboarding.progress.step', string='Step Progress',
+ compute='_compute_current_progress', help='Onboarding Progress Step for the current context (company).')
+ current_step_state = fields.Selection(
+ ONBOARDING_PROGRESS_STATES, string='Completion State', compute='_compute_current_progress')
+ progress_ids = fields.One2many(
+ 'onboarding.progress.step', 'step_id', string='Onboarding Progress Step Records', readonly=True,
+ help='All related Onboarding Progress Step Records (across companies)')
+
+ sequence = fields.Integer(default=10)
+
+ @api.depends_context('company')
+ @api.depends('progress_ids', 'progress_ids.step_state')
+ def _compute_current_progress(self):
+ existing_progress_steps = self.progress_ids.filtered_domain([
+ ('step_id', 'in', self.ids),
+ ('progress_id.company_id', 'in', [False, self.env.company.id]),
+ ])
+ for step in self:
+ if step in existing_progress_steps.step_id:
+ current_progress_step_id = existing_progress_steps.filtered(
+ lambda progress_step: progress_step.step_id == step)
+ step.current_progress_step_id = current_progress_step_id
+ step.current_step_state = current_progress_step_id.step_state
+ else:
+ step.current_progress_step_id = False
+ step.current_step_state = 'not_done'
+
+ def action_set_just_done(self):
+ # Make sure progress records exist for the current context (company)
+ steps_without_progress = self.filtered(lambda step: not step.current_progress_step_id)
+ steps_without_progress._create_progress_steps()
+ return self.current_progress_step_id.action_set_just_done()
+
+ def _create_progress_steps(self):
+ onboarding_progress_records = self.env['onboarding.progress'].search([
+ ('onboarding_id', 'in', self.onboarding_id.ids),
+ ('company_id', 'in', [False, self.env.company.id])
+ ])
+ progress_step_values = []
+ for onboarding_progress_record in onboarding_progress_records:
+ progress_step_values += [
+ {
+ 'onboarding_id': onboarding_progress_record.onboarding_id.id,
+ 'progress_id': onboarding_progress_record.id,
+ 'step_id': step_id.id,
+ }
+ for step_id
+ in self.filtered(lambda step: self.onboarding_id == onboarding_progress_record.onboarding_id)
+ if step_id not in onboarding_progress_record.progress_step_ids.step_id
+ ]
+
+ return self.env['onboarding.progress.step'].create(progress_step_values)
diff --git a/addons/onboarding/security/ir.model.access.csv b/addons/onboarding/security/ir.model.access.csv
new file mode 100644
index 00000000000..f99bf429ceb
--- /dev/null
+++ b/addons/onboarding/security/ir.model.access.csv
@@ -0,0 +1,13 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_onboarding_all,onboarding.onboarding.all,model_onboarding_onboarding,,0,0,0,0
+access_onboarding_user,onboarding.onboarding.user,model_onboarding_onboarding,base.group_user,0,0,0,0
+access_onboarding_manager,onboarding.onboarding.manager,model_onboarding_onboarding,base.group_system,1,1,1,1
+access_onboarding_step_all,onboarding.onboarding.step.all,model_onboarding_onboarding_step,,0,0,0,0
+access_onboarding_step_user,onboarding.onboarding.step.user,model_onboarding_onboarding_step,base.group_user,0,0,0,0
+access_onboarding_step_manager,onboarding.onboarding.step.manager,model_onboarding_onboarding_step,base.group_system,1,1,1,1
+access_onboarding_progress_all,onboarding.progress.all,model_onboarding_progress,,0,0,0,0
+access_onboarding_progress_user,onboarding.progress.user,model_onboarding_progress,base.group_user,0,0,0,0
+access_onboarding_progress_manager,onboarding.progress.manager,model_onboarding_progress,base.group_system,1,1,1,1
+access_onboarding_progress_step_all,onboarding.progress.step.all,model_onboarding_progress_step,,0,0,0,0
+access_onboarding_progress_step_user,onboarding.progress.step.user,model_onboarding_progress_step,base.group_user,0,0,0,0
+access_onboarding_progress_step_manager,onboarding.progress.step.manager,model_onboarding_progress_step,base.group_system,1,1,1,1
diff --git a/addons/onboarding/static/src/img/onboarding_bg.png b/addons/onboarding/static/src/img/onboarding_bg.png
new file mode 100644
index 00000000000..de7cf0fec97
Binary files /dev/null and b/addons/onboarding/static/src/img/onboarding_bg.png differ
diff --git a/addons/onboarding/tests/__init__.py b/addons/onboarding/tests/__init__.py
new file mode 100644
index 00000000000..cd240e870a0
--- /dev/null
+++ b/addons/onboarding/tests/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import test_onboarding
diff --git a/addons/onboarding/tests/common.py b/addons/onboarding/tests/common.py
new file mode 100644
index 00000000000..9f4dba3f271
--- /dev/null
+++ b/addons/onboarding/tests/common.py
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests.common import TransactionCase
+
+
+class TestOnboardingCommon(TransactionCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ # Create two companies including Mitchell
+ cls.user_admin = cls.env.ref('base.user_admin')
+
+ cls.company_1 = cls.user_admin.company_id
+ cls.company_2 = cls.env['res.company'].create({
+ 'currency_id': cls.env.ref('base.AUD').id,
+ 'name': 'New Test Company',
+ })
+ cls.user_admin.company_ids |= cls.company_2
+
+ cls.onboarding_1 = cls.env['onboarding.onboarding'].create([
+ {
+ 'name': 'Test Onboarding 1',
+ 'is_per_company': False,
+ 'route_name': 'onboarding1'
+ }
+ ])
+
+ # create a fake action for step opening
+ cls.action_fake_open_onboarding_step = cls.env['ir.actions.act_window'].create({
+ 'name': 'action_fake_open_onboarding_step',
+ 'res_model': 'onboarding.onboarding',
+ })
+
+ # Add two steps
+ cls.onboarding_1_step_1, cls.onboarding_1_step_2 = cls.env['onboarding.onboarding.step'].create([
+ {
+ 'title': f'Test Onboarding 1 - Step {step_n}',
+ 'onboarding_id': cls.onboarding_1.id,
+ 'panel_step_open_action_name': 'action_fake_open_onboarding_step',
+ }
+ for step_n in range(1, 3)
+ ])
+
+ # Create progress records as would happen through the controller
+ cls.onboarding_1.with_company(cls.company_1)._search_or_create_progress()
+
+ def assert_step_is_done(self, step, also_with_company=None):
+ self.assertIn(
+ step.current_progress_step_id.step_state, {'done', 'just_done'},
+ f'Expected done-like current state for step {step.id} for {step.env.company}')
+ self.assertEqual(step.current_progress_step_id.step_state, step.current_step_state)
+ if also_with_company:
+ self.assert_step_is_done(step.with_company(also_with_company))
+
+ def assert_step_is_not_done(self, step, also_with_company=None):
+ self.assertIn(
+ step.current_progress_step_id.step_state, {'not_done', False},
+ f'Expected "not_done" current state for step {step.id} for {step.env.company}')
+ self.assertEqual(step.current_step_state, 'not_done')
+ if also_with_company:
+ self.assert_step_is_not_done(step.with_company(also_with_company))
+
+ def assert_onboarding_is_done(self, onboarding, also_with_company=None):
+ self.assertIn(
+ onboarding.current_progress_id.onboarding_state, {'done', 'just_done'},
+ f'Expected done-like current state for onboarding {onboarding.name} '
+ f'for {onboarding.env.company}')
+ self.assertEqual(onboarding.current_progress_id.onboarding_state,
+ onboarding.current_onboarding_state)
+ if also_with_company:
+ self.assert_onboarding_is_done(onboarding.with_company(also_with_company))
+
+ def assert_onboarding_is_not_done(self, onboarding, also_with_company=None):
+ self.assertEqual(
+ onboarding.current_progress_id.onboarding_state, 'not_done',
+ f'Expected "not_done" current state for onboarding {onboarding.name} '
+ f'for {onboarding.env.company}')
+ self.assertEqual(onboarding.current_onboarding_state, 'not_done')
+ if also_with_company:
+ self.assert_onboarding_is_not_done(onboarding.with_company(also_with_company))
+
+ def activate_company(self, company):
+ self.onboarding_1_step_1 = self.onboarding_1_step_1.with_company(company)
+ self.onboarding_1_step_2 = self.onboarding_1_step_2.with_company(company)
+ self.onboarding_1 = self.onboarding_1.with_company(company)
diff --git a/addons/onboarding/tests/test_onboarding.py b/addons/onboarding/tests/test_onboarding.py
new file mode 100644
index 00000000000..92e82a17918
--- /dev/null
+++ b/addons/onboarding/tests/test_onboarding.py
@@ -0,0 +1,132 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.onboarding.tests.common import TestOnboardingCommon
+
+
+class TestOnboarding(TestOnboardingCommon):
+ def test_onboarding_completion_global(self):
+ # Completing onboarding as company_1
+ self.assertEqual(self.env.company, self.company_1)
+ self.assertDictEqual(
+ self.onboarding_1.current_progress_id._get_and_update_onboarding_state(),
+ {self.onboarding_1_step_1.id: 'not_done', self.onboarding_1_step_2.id: 'not_done'})
+
+ self.onboarding_1_step_1.action_set_just_done()
+ # Test completed step state consolidation from `just_done` to `done`
+ self.assertDictEqual(
+ self.onboarding_1.current_progress_id._get_and_update_onboarding_state(),
+ {self.onboarding_1_step_1.id: 'just_done', self.onboarding_1_step_2.id: 'not_done'})
+ self.assertDictEqual(
+ self.onboarding_1.current_progress_id._get_and_update_onboarding_state(),
+ {self.onboarding_1_step_1.id: 'done', self.onboarding_1_step_2.id: 'not_done'})
+ self.assert_step_is_done(self.onboarding_1_step_1, self.company_2)
+ self.assert_onboarding_is_not_done(self.onboarding_1, self.company_2)
+
+ self.onboarding_1_step_2.action_set_just_done()
+ self.assert_step_is_done(self.onboarding_1_step_2, self.company_2)
+ self.assert_onboarding_is_done(self.onboarding_1, self.company_2)
+
+ # Once onboarding is done, a key 'onboarding_state' is added to the rendering values
+ self.assertDictEqual(
+ self.onboarding_1.current_progress_id._get_and_update_onboarding_state(),
+ {self.onboarding_1_step_1.id: 'done', self.onboarding_1_step_2.id: 'just_done', 'onboarding_state': 'just_done'})
+ # Consolidate values
+ self.assertDictEqual(
+ self.onboarding_1.current_progress_id._get_and_update_onboarding_state(),
+ {self.onboarding_1_step_1.id: 'done', self.onboarding_1_step_2.id: 'done', 'onboarding_state': 'done'})
+
+ self.onboarding_1.current_progress_id.action_close()
+ self.assertTrue(self.onboarding_1.current_progress_id.is_onboarding_closed)
+
+ # Adding new step resets onboarding state to 'not_done' even if closed
+ onboarding_1_step_3 = self.env['onboarding.onboarding.step'].create({
+ 'title': 'Test Onboarding 1 - Step 3',
+ 'onboarding_id': self.onboarding_1.id,
+ 'panel_step_open_action_name': 'action_fake_open_onboarding_step',
+ })
+ self.assert_step_is_not_done(onboarding_1_step_3)
+ self.assert_onboarding_is_not_done(self.onboarding_1)
+
+ # Completing it sets onboarding state to done again
+ onboarding_1_step_3.action_set_just_done()
+ self.assert_onboarding_is_done(self.onboarding_1)
+
+ # If a company is added, onboarding is 'done'
+ company_3 = self.env.company.create({
+ 'currency_id': self.env.ref('base.EUR').id,
+ 'name': 'Another Test Company',
+ })
+ self.assert_onboarding_is_done(self.onboarding_1.with_company(company_3))
+
+ # Adding new step resets onboarding state to 'not_done'
+ self.env['onboarding.onboarding.step'].create({
+ 'title': 'Test Onboarding 1 - Step 4',
+ 'onboarding_id': self.onboarding_1.id,
+ 'panel_step_open_action_name': 'action_fake_open_onboarding_step',
+ })
+
+ # Closing the panel still allows to track if all steps are completed
+ self.onboarding_1.action_close()
+ self.assertTrue(self.onboarding_1.current_progress_id.is_onboarding_closed)
+ self.assert_onboarding_is_not_done(self.onboarding_1)
+
+ def test_onboarding_completion_per_company(self):
+ """Checks the behavior of onboarding and step states in multi-company setting:
+ the onboarding state has to track the completion of each of its steps, global and
+ per-company, to determine if whether it is completed.
+ """
+ # Completing onboarding as company_1
+ self.assertEqual(self.env.company, self.company_1)
+
+ # Updating onboarding to per-company
+ self.onboarding_1.is_per_company = True
+ # Required after progress reset (simulate role of controller)
+ self.onboarding_1._search_or_create_progress()
+
+ self.onboarding_1_step_1.action_set_just_done()
+ self.assert_step_is_done(self.onboarding_1_step_1)
+
+ self.onboarding_1_step_2.action_set_just_done()
+ self.assert_onboarding_is_done(self.onboarding_1)
+
+ # Completing onboarding as existing company_2
+ self.activate_company(self.company_2)
+ # First access from company_2
+ self.onboarding_1._search_or_create_progress()
+
+ # Blank state for company 2
+ self.assert_step_is_not_done(self.onboarding_1_step_1)
+ self.assert_onboarding_is_not_done(self.onboarding_1)
+
+ # But no change for company 1
+ self.assert_step_is_done(self.onboarding_1_step_1.with_company(self.company_1))
+ self.assert_onboarding_is_done(self.onboarding_1.with_company(self.company_1))
+
+ self.onboarding_1_step_1.action_set_just_done()
+ self.assert_step_is_done(self.onboarding_1_step_1)
+ self.assert_onboarding_is_not_done(self.onboarding_1)
+ self.onboarding_1_step_2.with_company(self.company_2).action_set_just_done()
+ self.assert_step_is_done(self.onboarding_1_step_2)
+ self.assert_onboarding_is_done(self.onboarding_1)
+
+ # is_onboarding_closed status is also company-independent
+ self.onboarding_1.action_close()
+ self.assertTrue(self.onboarding_1.current_progress_id.is_onboarding_closed)
+ self.assertFalse(self.onboarding_1.with_company(self.company_1).current_progress_id.is_onboarding_closed)
+
+ def test_onboarding_to_company_change(self):
+ """Checks that changing onboarding to per-company resets completions states.
+ """
+ # Completing onboarding as company_1
+ self.assertEqual(self.env.company, self.company_1)
+ self.onboarding_1_step_1.action_set_just_done()
+ self.onboarding_1_step_2.action_set_just_done()
+ self.assert_onboarding_is_done(self.onboarding_1)
+
+ # Updating onboarding to per-company
+ self.onboarding_1.is_per_company = True
+ # Required after progress reset (simulate role of controller)
+ self.onboarding_1._search_or_create_progress()
+
+ self.assert_onboarding_is_not_done(self.onboarding_1)
diff --git a/addons/onboarding/views/onboarding_menus.xml b/addons/onboarding/views/onboarding_menus.xml
new file mode 100644
index 00000000000..f036c70072c
--- /dev/null
+++ b/addons/onboarding/views/onboarding_menus.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/addons/onboarding/views/onboarding_views.xml b/addons/onboarding/views/onboarding_views.xml
new file mode 100644
index 00000000000..87595825a37
--- /dev/null
+++ b/addons/onboarding/views/onboarding_views.xml
@@ -0,0 +1,100 @@
+
+
+
+ onboarding.onboarding.view.tree
+ onboarding.onboarding
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ onboarding.onboarding.view.form
+ onboarding.onboarding
+
+
+
+
+
+
+ onboarding.onboarding.step.view.tree
+ onboarding.onboarding.step
+
+
+
+
+
+
+
+
+
+
+
+ onboarding.onboarding.step.view.form
+ onboarding.onboarding.step
+
+
+
+
+
+
+ Onboardings
+ onboarding.onboarding
+ tree,form
+
+
+
+ Onboarding Steps
+ onboarding.onboarding.step
+ tree,form
+
+
diff --git a/addons/sale/models/res_company.py b/addons/sale/models/res_company.py
index 90ce7d3acd5..257a06f43d8 100644
--- a/addons/sale/models/res_company.py
+++ b/addons/sale/models/res_company.py
@@ -130,6 +130,6 @@ class ResCompany(models.Model):
'sale_onboarding_order_confirmation_state',
'sale_onboarding_sample_quotation_state',
]
- return self.get_and_update_onbarding_state('sale_quotation_onboarding_state', steps)
+ return self._get_and_update_onboarding_state('sale_quotation_onboarding_state', steps)
_sql_constraints = [('check_quotation_validity_days', 'CHECK(quotation_validity_days > 0)', 'Quotation Validity is required and must be greater than 0.')]
diff --git a/odoo/addons/base/models/res_company.py b/odoo/addons/base/models/res_company.py
index 86b822eca8e..83b792c4edc 100644
--- a/odoo/addons/base/models/res_company.py
+++ b/odoo/addons/base/models/res_company.py
@@ -287,7 +287,7 @@ class Company(models.Model):
if self[step_name] == 'not_done':
self[step_name] = 'just_done'
- def get_and_update_onbarding_state(self, onboarding_state, steps_states):
+ def _get_and_update_onboarding_state(self, onboarding_state, steps_states):
""" Needed to display onboarding animations only one time. """
old_values = {}
all_done = True
diff --git a/odoo/addons/base/rng/calendar_view.rng b/odoo/addons/base/rng/calendar_view.rng
index fc1c8d9eedd..72ef55ded27 100644
--- a/odoo/addons/base/rng/calendar_view.rng
+++ b/odoo/addons/base/rng/calendar_view.rng
@@ -26,6 +26,7 @@
+
diff --git a/odoo/addons/base/views/onboarding_views.xml b/odoo/addons/base/views/onboarding_views.xml
index b47b0282246..837199872a2 100644
--- a/odoo/addons/base/views/onboarding_views.xml
+++ b/odoo/addons/base/views/onboarding_views.xml
@@ -5,19 +5,19 @@
-
Remove Configuration Tips
-
+
Onboarding Tips
+
-
Do you want to remove this configuration panel?
+
Are you sure you want to hide these onboarding tips?