[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:
Florian Charlier
2023-06-30 23:37:50 +02:00
parent 518a4e4c43
commit 77f9ff50db
65 changed files with 754 additions and 882 deletions
+7 -2
View File
@@ -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
View File
@@ -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
+40
View File
@@ -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))
+2 -37
View File
@@ -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>