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