[ADD] onboarding, base: manage onboardings
This technical module can be used to configure onboarding panels flows at company or global level (required for new Appointment onboarding, see ENT PR). This implementation, which should be able to support the migration of the other onboardings, separates the closed state from the completion state (allowing to know that steps are not completed even though the panel is closed). Basic tree and form view are included. Adding an onboarding with this module requires to provide records data for the models: - `onboarding.onboarding` + action to close the panel - `onboarding.onboarding.step` + actions for the 'opening' and 'saving' of each step. See related ENT PR for the `appointment` example, in particular `onboarding_data.xml` and `onboarding_onboarding.py` Several python tests are also included. Misc. In base, we slightly refresh the wording/style of the modal closing the onboarding panel. Task-2852375 Part of Task-2900763 Part-of: odoo/odoo#97105
This commit is contained in:
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_onboarding
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user