[REF] *: use onboarding module
* = account{_payment}, base, onboarding, payment{_stripe},
sale{_management}, web, website_sale
Use the dedicated onboarding module introduced in 16.0 instead of
the res.company model to store onboarding progress.
It allows
* onboarding steps to be reused across panels
* to support steps that should be completed per-database or per-company
* to clean the res.company model from many fields and methods,
* to remove many views, controllers, actions
Module-specific notes:
* account: We also clean the remaining two steps that are not
part of an accounting panel but make the most sense to be kept here.
* account_payment: Following 8e4e8eb8, the payment provider step is
added to the invoicing onboarding panel. We apply this change here too.
Also impacts the website_sale_dashboard panel (see related ENT PR).
(The "sale tax" one is currently used for to the website sale dashboard).
* payment: Note that the step was already not part of an onboarding
panel within this module.
* website_sale: We clean
* a field not used (The website_sale dashboard onboarding panel used
the payment_provider_onboarding_state field).
* a method that was only called from website_sale_dashboard, so it is
moved there. See related ENT PR.
Includes a few tests.
Moving views/templates/styling, as well as cleaning residual onboarding-related fields and methods in base, including populate.
This also includes restoring the "onboarding_complete" overlay panel
animating it to disappear after a few seconds so that it doesn't hide
text and block buttons to re-open steps.
Task-3025136
Part-of: odoo/odoo#104223
This commit is contained in:
@@ -13,9 +13,8 @@ This module allows to manage onboardings and their progress
|
||||
'depends': ['base'],
|
||||
'installable': True,
|
||||
'data': [
|
||||
'data/onboarding_data.xml',
|
||||
'views/onboarding_views.xml',
|
||||
'views/onboarding_templates.xml',
|
||||
'views/onboarding_views.xml',
|
||||
'views/onboarding_menus.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
@@ -23,6 +22,12 @@ This module allows to manage onboardings and their progress
|
||||
'web.assets_backend': [
|
||||
'onboarding/static/src/**/*',
|
||||
],
|
||||
"web.dark_mode_variables": [
|
||||
('before', 'onboarding/static/src/scss/onboarding.variables.scss', 'onboarding/static/src/scss/onboarding.variables.dark.scss'),
|
||||
],
|
||||
'web._assets_primary_variables': [
|
||||
'onboarding/static/src/scss/onboarding.variables.scss',
|
||||
]
|
||||
},
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<?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"/>
|
||||
<t t-set="image" t-value="'/web/image/onboarding.onboarding.step/'+str(step.id)+'/step_image'"/>
|
||||
<t t-set="alt" t-value="step.step_image_alt"/>
|
||||
<!-- 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>
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.addons.onboarding.models.onboarding_progress import ONBOARDING_PROGRESS_STATES
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ class Onboarding(models.Model):
|
||||
route_name = fields.Char('One word name', required=True)
|
||||
step_ids = fields.Many2many('onboarding.onboarding.step', string='Onboarding steps')
|
||||
|
||||
text_completed = fields.Char(
|
||||
'Message at completion', default=_('Nice work! Your configuration is done.'),
|
||||
help='Text shown on onboarding when completed')
|
||||
|
||||
is_per_company = fields.Boolean(
|
||||
'Should be done per company?', compute='_compute_is_per_company', readonly=True, store=False,
|
||||
)
|
||||
@@ -124,6 +128,7 @@ class Onboarding(models.Model):
|
||||
'close_model': 'onboarding.onboarding',
|
||||
'steps': self.step_ids,
|
||||
'state': self.current_progress_id._get_and_update_onboarding_state(),
|
||||
'text_completed': self.text_completed,
|
||||
}
|
||||
|
||||
return values
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// = Onboarding Panel
|
||||
// ============================================================================
|
||||
$o-onboarding-step-width: map-get($container-max-widths, 'lg') / 4 !default;
|
||||
|
||||
|
||||
.o_onboarding_main {
|
||||
background-color: $o-onboarding-main-bg;
|
||||
|
||||
.o_onboarding_wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.o_onboarding_completed_message {
|
||||
opacity: 0;
|
||||
box-shadow: 0 -7px 20px -5px rgba(#000, 0.3);
|
||||
animation: o_onboarding_slideInUpDownOut;
|
||||
animation-duration: 6s;
|
||||
}
|
||||
|
||||
// = Step Design
|
||||
// ------------------------------------------------------------------------
|
||||
.o_onboarding_step {
|
||||
flex: 1 1 0;
|
||||
|
||||
@for $i from 1 through 5 {
|
||||
&.o_onboarding_step__todo:nth-child(#{$i}) {
|
||||
@include print-variable('o-onboarding-animation-delay', $i * .15s);
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child, &:last-child {
|
||||
.o_onboarding_line {
|
||||
width: $o-onboarding-line-width / 2 ;
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
.o_onboarding_line {
|
||||
margin-left: $o-onboarding-line-width / 2;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
.o_onboarding_line {
|
||||
margin-right: $o-onboarding-line-width / 2;
|
||||
}
|
||||
}
|
||||
|
||||
.o_onboarding_line {
|
||||
background: linear-gradient(to right, $o-onboarding-color-orange 0%, $o-onboarding-color-blue 100%);
|
||||
height: $o-onboarding-line-height;
|
||||
width: $o-onboarding-line-width;
|
||||
top: $o-onboarding-image-size / 2;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
.o_onboarding_step_content {
|
||||
width: $o-onboarding-step-width;
|
||||
}
|
||||
|
||||
.o_onboarding_step_side img {
|
||||
width: $o-onboarding-image-size;
|
||||
height: $o-onboarding-image-size;
|
||||
}
|
||||
|
||||
.o_onboarding_step_action {
|
||||
min-width: $o-onboarding-step-width * .5;
|
||||
border: $border-width solid #{$o-onboarding-color-blue};
|
||||
color: $o-onboarding-color-blue;
|
||||
}
|
||||
|
||||
.o_onboarding_step_action__done {
|
||||
i {
|
||||
background-color: $o-onboarding-color-orange;
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
|
||||
// = "To do" Step Design
|
||||
// --------------------------------------------------------------------
|
||||
&[data-step-state="not_done"] {
|
||||
.o_onboarding_step_content_info {
|
||||
animation: o_onboarding_zoomIn ($o-onboarding-base-time * 1.5) both;
|
||||
animation-delay: var(--o-onboarding-animation-delay, 0);
|
||||
}
|
||||
|
||||
.o_onboarding_step_action {
|
||||
animation: o_onboarding_slideInUp ($o-onboarding-base-time * 2) both;
|
||||
animation-delay: var(--o-onboarding-animation-delay, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// = "Just Done" Step Design
|
||||
// --------------------------------------------------------------------
|
||||
&[data-step-state="just_done"] {
|
||||
.o_onboarding_step_content_info {
|
||||
animation: o_onboarding_fadeIn ($o-onboarding-base-time * 3);
|
||||
}
|
||||
|
||||
.o_onboarding_step_action__done {
|
||||
animation: bounceIn ($o-onboarding-base-time * 2);
|
||||
}
|
||||
|
||||
.o_onboarding_confetti {
|
||||
animation: o_onboarding_fadeInOut ($o-onboarding-base-time * 3) ease 0s 1 forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// = Animations
|
||||
// ------------------------------------------------------------------------
|
||||
@keyframes o_onboarding_slideInUp {
|
||||
from {
|
||||
transform: translate3d(0, 20%, 0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes o_onboarding_slideInUpDownOut {
|
||||
25%, 85% { transform: translate3d(0, -80%, 0); opacity: 1; }
|
||||
0%, 100% { transform: translate3d(0, 0, 0); }
|
||||
}
|
||||
|
||||
@keyframes o_onboarding_zoomIn {
|
||||
from {
|
||||
transform: scale3d(0.8, 0.8, 0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes o_onboarding_fadeIn {
|
||||
10%, 66% { opacity: 0; }
|
||||
0%, 100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes o_onboarding_fadeInOut {
|
||||
10%, 66% { opacity: 1; }
|
||||
0%, 100% { opacity: 0; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// = Onboarding Colors
|
||||
// ============================================================================
|
||||
// No CSS hacks, variables overrides only
|
||||
|
||||
$o-onboarding-main-bg: $o-gray-100 !default;
|
||||
$o-onboarding-color-blue: #516db3 !default;
|
||||
$o-onboarding-color-orange: #c67c7a !default;
|
||||
@@ -0,0 +1,7 @@
|
||||
$o-onboarding-base-time: 0.5s !default;
|
||||
$o-onboarding-image-size: 64px !default;
|
||||
$o-onboarding-line-height: 2px !default;
|
||||
$o-onboarding-line-width: 100% !default;
|
||||
$o-onboarding-main-bg: $o-gray-200 !default;
|
||||
$o-onboarding-color-blue: #374874 !default;
|
||||
$o-onboarding-color-orange: #F39D9B !default;
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import case
|
||||
from . import test_onboarding
|
||||
from . import test_onboarding_concurrency
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TransactionCaseOnboarding(TransactionCase):
|
||||
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.name}')
|
||||
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.name}')
|
||||
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.name}"')
|
||||
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.assertIn(
|
||||
onboarding.current_progress_id.onboarding_state, {'not_done', False},
|
||||
f'Expected `"not_done"` or `False` current state for onboarding {onboarding.name} '
|
||||
f'for {onboarding.env.company.name}')
|
||||
self.assertEqual(onboarding.current_onboarding_state, 'not_done')
|
||||
if also_with_company:
|
||||
self.assert_onboarding_is_not_done(onboarding.with_company(also_with_company))
|
||||
@@ -1,10 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.addons.onboarding.tests.case import TransactionCaseOnboarding
|
||||
|
||||
|
||||
class TestOnboardingCommon(TransactionCase):
|
||||
class TestOnboardingCommon(TransactionCaseOnboarding):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
@@ -53,41 +53,6 @@ class TestOnboardingCommon(TransactionCase):
|
||||
# Create progress records as would happen through the controller
|
||||
(cls.onboarding_1 + cls.onboarding_2).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.assertIn(
|
||||
onboarding.current_progress_id.onboarding_state, {'not_done', False},
|
||||
f'Expected `"not_done"` or `False` 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)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<?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="onboarding.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"/>
|
||||
<t t-set="image" t-value="'/web/image/onboarding.onboarding.step/'+str(step.id)+'/step_image'"/>
|
||||
<t t-set="alt" t-value="step.step_image_alt"/>
|
||||
<!-- 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">
|
||||
<div class="modal o_onboarding_modal o_technical_modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<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>Are you sure you want to hide these onboarding tips?</p>
|
||||
</div>
|
||||
<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">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="o_onboarding_container collapse show">
|
||||
<div class="o_onboarding_main position-relative border-bottom overflow-hidden">
|
||||
<div class="o_onboarding_wrap py-3">
|
||||
<a href="#" data-bs-toggle="modal" data-bs-target=".o_onboarding_modal" class="o_onboarding_btn_close position-absolute top-0 end-0 py-2 px-3 h2 z-index-1" title="Close the onboarding panel"><i class="oi oi-close"/></a>
|
||||
<div class="o_onboarding_steps d-flex" t-out="0"/>
|
||||
|
||||
<div t-if="state.get('onboarding_state') == 'just_done'"
|
||||
t-att-state="state.get('onboarding_state')"
|
||||
class="o_onboarding_completed_message position-absolute end-0 start-0 border-bottom py-4 bg-white d-flex align-items-center justify-content-center">
|
||||
<span class="h3 m-0">
|
||||
<i class="fa fa-check text-success me-3" />
|
||||
<t t-if="text_completed" t-out="text_completed" />
|
||||
</span>
|
||||
<a type="action" class="btn btn-primary ms-4" data-bs-toggle="collapse" href=".o_onboarding_container" t-att-data-model="close_model" t-att-data-method="close_method">
|
||||
Close Panel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="onboarding_step">
|
||||
<div class="o_onboarding_step position-relative d-flex flex-column align-items-center justify-content-start text-center" t-att-data-step-state="state">
|
||||
<img t-if="state == 'just_done'" class="o_onboarding_confetti position-absolute w-100" src="/base/static/img/onboarding_confetti.svg" alt="o_onboarding_confetti"/>
|
||||
|
||||
<div class="o_onboarding_line position-absolute"/>
|
||||
<div class="o_onboarding_step_side d-flex">
|
||||
<img class="z-index-1" t-attf-src="#{image}" t-attf-alt="#{alt}"/>
|
||||
</div>
|
||||
|
||||
<div class="o_onboarding_step_content position-relative pt-2 flex-grow-1 d-flex flex-column align-items-center justify-content-around">
|
||||
<div class="o_onboarding_step_content_info flex-grow-1 mb-2">
|
||||
<a type="action" data-reload-on-close="true" role="button" t-att-data-method="method" t-att-data-model="model">
|
||||
<h5 class="o_onboarding_step_title mb-1" t-out="title"/>
|
||||
</a>
|
||||
<p class="m-0 small" t-out="description"/>
|
||||
</div>
|
||||
<a t-if="state == 'not_done'" class="o_onboarding_step_action btn px-4" type="action" data-reload-on-close="true" role="button" t-att-data-method="method" t-att-data-model="model">
|
||||
<span>
|
||||
<t t-if="btn_text" t-out="btn_text" />
|
||||
<t t-else="">Let's do it</t>
|
||||
</span>
|
||||
</a>
|
||||
<a t-else="" class="o_onboarding_step_action__done btn" type="action" data-reload-on-close="true" role="button" t-att-data-method="method" t-att-data-model="model">
|
||||
<span>
|
||||
<i t-attf-class="p-1 me-1 fa #{done_icon if done_icon else 'fa-check'} rounded-circle" />
|
||||
<t t-if="done_text" t-out="done_text" />
|
||||
<t t-else="">All done!</t>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user