[ADD] onboarding, *: enable appointment onboarding

* = base, calendar, google_calendar, microsoft_calendar

Purpose: Enable efficient Appointment Onboarding, not performed by company but at a global db level.

This PR enables:
* Track onboarding progress on a database scope (vs company) by introducing the `onboarding` module
* Easier configuration of external calendars with a modal instead of redirecting to "Settings"
  \+ on-the-fly connector modules install
* Basic support for the onboarding panel on the calendar view

See included commits for details.

Task-2852375
Part of Task-2900763

See odoo/enterprise#29139

closes odoo/odoo#97105

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Odoo's Mergebot
2022-09-07 23:10:19 +02:00
committed by GitHub
38 changed files with 995 additions and 44 deletions
+2 -2
View File
@@ -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()
)
+1
View File
@@ -4,3 +4,4 @@
from . import controllers
from . import models
from . import populate
from . import wizard
+2
View File
@@ -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/**/*',
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
11 access_calendar_filters_all access_calendar_contacts_all model_calendar_filters base.group_user 1 1 1 1
12 access_calendar_filters access_calendar_contacts model_calendar_filters base.group_system 1 1 1 1
13 access_calendar_recurrence access_calendar_recurrence model_calendar_recurrence base.group_user 1 1 1 1
14 access_calendar_provider_config_all calendar.provider.config.all model_calendar_provider_config 0 0 0 0
15 access_calendar_provider_config_manager calendar.provider.config.manager model_calendar_provider_config base.group_system 1 1 1 1
Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -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 {
@@ -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();
}
}
@@ -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);
@@ -3,7 +3,21 @@
<t t-name="calendar.CalendarView" t-extend="CalendarView">
<t t-jquery="div.o_calendar_sidebar" t-operation="append">
<div id="calendar_sync" class="o_calendar_sync container inline btn-group"/>
<div class="o_calendar_sync text-center" groups="base.group_system">
<span class="text-primary fs-6">Synchronize with:</span>
</div>
<div id="calendar_sync" class="container inline btn-group justify-content-evenly align-items-center" groups="base.group_system">
<div id="google_calendar_sync" class="o_calendar_sync">
<button type="button" id="google_sync_activate" class="btn btn-muted">
<b><i class='fa fa-plug'/> Google</b>
</button>
</div>
<div id="microsoft_calendar_sync" class="o_calendar_sync">
<button type="button" id="microsoft_sync_activate" class="btn btn-muted">
<b><i class='fa fa-plug'/> Outlook</b>
</button>
</div>
</div>
</t>
</t>
+5
View File
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import calendar_provider_config
@@ -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)
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="calendar_provider_config_view_form" model="ir.ui.view">
<field name="name">calendar.provider.config.view.form</field>
<field name="model">calendar.provider.config</field>
<field name="arch" type="xml">
<form js_class="calendar_provider_config_form">
<field name="external_calendar_provider" widget="radio" options="{'horizontal': true}"/>
<div attrs="{'invisible': [('external_calendar_provider', '!=', 'google')]}">
<img alt="Google Calendar icon" src="/calendar/static/src/img/google_calendar_40.png" style="height: 40px; margin-right: 5px"/>
<span class="me-1 o_form_label">Google Calendar</span>
<a href="https://www.odoo.com/documentation/master/applications/general/calendars/google/google_calendar_credentials.html" title="Read More" class="o_doc_link" target="_blank"></a>
<div class="text-muted mt-2">
Synchronize your calendar with Google Calendar
</div>
<group>
<field name="cal_client_id" attrs="{'required': [('external_calendar_provider', '=', 'google')]}"/>
<field name="cal_client_secret" password="True" attrs="{'required': [('external_calendar_provider', '=', 'google')]}"/>
</group>
</div>
<div attrs="{'invisible': [('external_calendar_provider', '!=', 'microsoft')]}">
<img alt="Microsoft Outlook icon" src="/calendar/static/src/img/microsoft_calendar_40.png" style="height: 40px; margin-right: 5px"/>
<span class="me-1 o_form_label">Outlook Calendar</span>
<a href="https://www.odoo.com/documentation/master/applications/general/calendars/outlook/outlook_calendar.html" title="Read More" class="o_doc_link" target="_blank"></a>
<div class="text-muted mt-2">
Synchronize your calendar with Outlook
</div>
<group>
<field name="microsoft_outlook_client_identifier" attrs="{'required': [('external_calendar_provider', '=', 'microsoft')]}"/>
<field name="microsoft_outlook_client_secret" password="True" attrs="{'required': [('external_calendar_provider', '=', 'microsoft')]}"/>
</group>
</div>
<footer>
<a role="button" title="Connect" class="o_calendar_activate_external_cal btn btn-primary" t-on-click="onConnect">Connect</a>
<button string="Cancel" class="btn btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>
@@ -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?"), {
@@ -1,16 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-inherit="calendar.CalendarView" t-inherit-mode="extension">
<xpath expr="//div[@id='calendar_sync']" position="inside">
<button type="button" id="google_sync_pending" class="o_google_sync_button o_google_sync_pending btn btn-secondary btn">
<b><i class='fa fa-refresh'/> Google</b>
</button>
<button type="button" id="google_sync_configured" class="me-1 o_google_sync_button o_google_sync_button_configured btn text-bg-primary">
<b>
<i id="google_check" class='fa fa-check'/>
<i id="google_stop" class='fa fa-times' style="display: none"/>&amp;nbsp;Google
</b>
</button>
<xpath expr="//div[@id='google_calendar_sync']" position="replace">
<div id="google_calendar_sync" class="o_calendar_sync">
<button type="button" id="google_sync_pending" class="o_google_sync_button o_google_sync_pending btn btn-secondary btn">
<b><i class='fa fa-refresh'/> Google</b>
</button>
<button type="button" id="google_sync_configured" class="me-1 o_google_sync_button o_google_sync_button_configured btn text-bg-primary">
<b>
<i id="google_check" class='fa fa-check'/>
<i id="google_stop" class='fa fa-times' style="display: none"/>&amp;nbsp;Google
</b>
</button>
</div>
</xpath>
</t>
</templates>
@@ -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?"), {
@@ -1,16 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-inherit="calendar.CalendarView" t-inherit-mode="extension">
<xpath expr="//div[@id='calendar_sync']" position="inside">
<button type="button" id="microsoft_sync_pending" class="o_microsoft_sync_button o_microsoft_sync_pending btn btn-secondary btn">
<b><i class='fa fa-refresh'/> Outlook</b>
</button>
<button type="button" id="microsoft_sync_configured" class="me-1 o_microsoft_sync_button o_microsoft_sync_button_configured btn text-bg-primary">
<b>
<i id="microsoft_check" class='fa fa-check'/>
<i id="microsoft_stop" class='fa fa-times' style="display: none"/>&amp;nbsp;Outlook
</b>
</button>
<xpath expr="//div[@id='microsoft_calendar_sync']" position="replace">
<div id="microsoft_calendar_sync" class="o_calendar_sync">
<button type="button" id="microsoft_sync_pending" class="o_microsoft_sync_button o_microsoft_sync_pending btn btn-secondary btn">
<b><i class='fa fa-refresh'/> Outlook</b>
</button>
<button type="button" id="microsoft_sync_configured" class="me-1 o_microsoft_sync_button o_microsoft_sync_button_configured btn text-bg-primary">
<b>
<i id="microsoft_check" class='fa fa-check'/>
<i id="microsoft_stop" class='fa fa-times' style="display: none"/>&amp;nbsp;Outlook
</b>
</button>
</div>
</xpath>
</t>
</templates>
+5
View File
@@ -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
+22
View File
@@ -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',
}
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import onboarding
@@ -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/<string:route_name>', 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 {}
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="onboarding_panel">
<t t-call="onboarding.onboarding_container">
<t t-foreach="steps" t-as="step">
<t t-call="base.onboarding_step">
<t t-set="title" t-value="step.title"/>
<t t-set="description" t-value="step.description"/>
<t t-set="done_icon" t-value="step.done_icon"/>
<t t-set="btn_text" t-value="step.button_text"/>
<t t-set="done_text" t-value="step.done_text"/>
<!-- Model/method used by JS implementation of banners for each step-->
<t t-set="method" t-value="step.panel_step_open_action_name"/>
<t t-set="model">onboarding.onboarding.step</t>
<!-- to mimic first implementation of onboarding, the 'state' queried holds
a rendering state for all steps. See _get_and_update_onboarding_state -->
<t t-set="state" t-value="state.get(step.id)"/>
</t>
</t>
</t>
</template>
<template id="onboarding_container" inherit_id="base.onboarding_container">
<xpath expr="//div[hasclass('o_onboarding_completed_message')]" position="replace"/>
</template>
</odoo>
+7
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_onboarding_all onboarding.onboarding.all model_onboarding_onboarding 0 0 0 0
3 access_onboarding_user onboarding.onboarding.user model_onboarding_onboarding base.group_user 0 0 0 0
4 access_onboarding_manager onboarding.onboarding.manager model_onboarding_onboarding base.group_system 1 1 1 1
5 access_onboarding_step_all onboarding.onboarding.step.all model_onboarding_onboarding_step 0 0 0 0
6 access_onboarding_step_user onboarding.onboarding.step.user model_onboarding_onboarding_step base.group_user 0 0 0 0
7 access_onboarding_step_manager onboarding.onboarding.step.manager model_onboarding_onboarding_step base.group_system 1 1 1 1
8 access_onboarding_progress_all onboarding.progress.all model_onboarding_progress 0 0 0 0
9 access_onboarding_progress_user onboarding.progress.user model_onboarding_progress base.group_user 0 0 0 0
10 access_onboarding_progress_manager onboarding.progress.manager model_onboarding_progress base.group_system 1 1 1 1
11 access_onboarding_progress_step_all onboarding.progress.step.all model_onboarding_progress_step 0 0 0 0
12 access_onboarding_progress_step_user onboarding.progress.step.user model_onboarding_progress_step base.group_user 0 0 0 0
13 access_onboarding_progress_step_manager onboarding.progress.step.manager model_onboarding_progress_step base.group_system 1 1 1 1
Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_onboarding
+86
View File
@@ -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)
+132
View File
@@ -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)
@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<odoo><data>
<!-- Under User Interface -->
<menuitem name="Onboardings"
id="menu_onboarding"
parent="base.next_id_2"
action="action_view_onboarding_onboarding"
sequence="1"/>
<menuitem name="Onboardings Steps"
id="menu_onboarding_step"
parent="base.next_id_2"
action="action_view_onboarding_step"
sequence="1"/>
</data></odoo>
@@ -0,0 +1,100 @@
<?xml version="1.0"?>
<odoo>
<record id="onboarding_onboarding_view_tree" model="ir.ui.view">
<field name="name">onboarding.onboarding.view.tree</field>
<field name="model">onboarding.onboarding</field>
<field name="arch" type="xml">
<tree string="Onboardings">
<header>
<button name="action_toggle_visibility" type="object" string="Toggle visibility"/>
</header>
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="current_onboarding_state"/>
<field name="is_onboarding_closed"/>
<field name="is_per_company" optional="hide"/>
</tree>
</field>
</record>
<record id="onboarding_onboarding_view_form" model="ir.ui.view">
<field name="name">onboarding.onboarding.view.form</field>
<field name="model">onboarding.onboarding</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_toggle_visibility" type="object" string="Toggle visibility"/>
</header>
<sheet>
<group col="2">
<field name="name"/>
<field name="route_name"/>
<field name="is_per_company"/>
<field name="is_onboarding_closed"/>
</group>
<notebook>
<page name="Onboarding steps">
<field name="step_ids"/>
</page>
<page name="Panel Rendering">
<group>
<field name="panel_background_color" string="Background color"/>
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="onboarding_onboarding_step_view_tree" model="ir.ui.view">
<field name="name">onboarding.onboarding.step.view.tree</field>
<field name="model">onboarding.onboarding.step</field>
<field name="arch" type="xml">
<tree string="Onboarding Steps">
<field name="sequence" widget="handle"/>
<field name="title"/>
<field name="onboarding_id" optional="hide"/>
<field name="current_step_state"/>
</tree>
</field>
</record>
<record id="onboarding_onboarding_step_view_form" model="ir.ui.view">
<field name="name">onboarding.onboarding.step.view.form</field>
<field name="model">onboarding.onboarding.step</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="title"/>
<field name="onboarding_id"/>
<field name="panel_step_open_action_name"/>
</group>
<notebook>
<page name="Step rendering">
<group>
<field name="description"/>
<field name="button_text"/>
<field name="done_text"/>
<field name="done_icon"/>
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_view_onboarding_onboarding" model="ir.actions.act_window">
<field name="name">Onboardings</field>
<field name="res_model">onboarding.onboarding</field>
<field name="view_mode">tree,form</field>
</record>
<record id="action_view_onboarding_step" model="ir.actions.act_window">
<field name="name">Onboarding Steps</field>
<field name="res_model">onboarding.onboarding.step</field>
<field name="view_mode">tree,form</field>
</record>
</odoo>
+1 -1
View File
@@ -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.')]
+1 -1
View File
@@ -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
+1
View File
@@ -26,6 +26,7 @@
<rng:optional><rng:attribute name="create"/></rng:optional>
<rng:optional><rng:attribute name="delete"/></rng:optional>
<rng:optional><rng:attribute name="scales"/></rng:optional>
<rng:optional><rng:attribute name="banner_route"/></rng:optional>
<rng:optional>
<rng:attribute name="mode">
<rng:choice>
+5 -5
View File
@@ -5,19 +5,19 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Remove Configuration Tips</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<h5 class="modal-title">Onboarding Tips</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"/>
</div>
<div class="modal-body">
<p>Do you want to remove this configuration panel?</p>
<p>Are you sure you want to hide these onboarding tips?</p>
</div>
<div class="modal-footer">
<div class="modal-footer justify-content-start">
<a type="action" class="btn btn-primary" data-bs-dismiss="modal"
data-o-hide-banner="true"
t-att-data-model="close_model" t-att-data-method="close_method">
Remove
</a>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Discard</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</div>
</div>
</div>