From afe71fbd3cd1cc228fb849b79d23d6aaa1fc60f0 Mon Sep 17 00:00:00 2001 From: Florian Charlier Date: Thu, 9 Feb 2023 15:30:02 +0000 Subject: [PATCH 1/3] [CLN] onboarding: remove multiple progress records precaution As we prevented creating multiple records in beaae001 and cleaned the databases for existing records in odoo/upgrade#4142, we can now safely remove these checks. Follow-up of Task-3101666 Task-3025136 Part-of: odoo/odoo#104223 --- addons/onboarding/models/onboarding_onboarding.py | 2 -- addons/onboarding/models/onboarding_step.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/addons/onboarding/models/onboarding_onboarding.py b/addons/onboarding/models/onboarding_onboarding.py index 5630a339773..8eef10a1ac6 100644 --- a/addons/onboarding/models/onboarding_onboarding.py +++ b/addons/onboarding/models/onboarding_onboarding.py @@ -43,8 +43,6 @@ class Onboarding(models.Model): current_progress_id = onboarding.progress_ids.filtered( lambda progress: progress.company_id.id in {False, self.env.company.id}) if current_progress_id: - if len(current_progress_id) > 1: - current_progress_id = current_progress_id.sorted('create_date', reverse=True)[0] 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 diff --git a/addons/onboarding/models/onboarding_step.py b/addons/onboarding/models/onboarding_step.py index 565534f083c..7f2d83c26e4 100644 --- a/addons/onboarding/models/onboarding_step.py +++ b/addons/onboarding/models/onboarding_step.py @@ -54,8 +54,6 @@ class OnboardingStep(models.Model): if step in existing_progress_steps.step_id: current_progress_step_id = existing_progress_steps.filtered( lambda progress_step: progress_step.step_id == step) - if len(current_progress_step_id) > 1: - current_progress_step_id = current_progress_step_id.sorted('create_date', reverse=True)[0] step.current_progress_step_id = current_progress_step_id step.current_step_state = current_progress_step_id.step_state else: From 518a4e4c4308985e6beaed22521853cce87f8850 Mon Sep 17 00:00:00 2001 From: Florian Charlier Date: Tue, 24 Jan 2023 10:48:01 +0000 Subject: [PATCH 2/3] [IMP] onboarding: prepare all onboardings migration ### enable onboarding step sharing Purpose: Allow to use the same step (and progress) for multiple onboardings. This is required for the migration of some onboardings such as the `account` invoicing and sale `quotation` onboardings which both use common steps (s.g., "Set up company data"). This is done by mutating the onboarding - onboarding step relation and progress - progress step models relationships to m2m: * Progress step records are now linked to several progress records if the step they track is used in several onboardings. * There will still be multiple progress step records for per-company tracking, if applicable. ### Allow steps w/o onboarding This commit enables creating and tracking the completion of onboarding steps that are not included in an onboarding panel. Before this PR, such steps could already be defined and used. This is the case of the `payment_provider` step from the payment module, that could be included in an onboarding panel defined in website_sale_dashboard (both are migrated in later commits of this PR and related ENT). ### Support changing is_per_company Having onboarding (steps) being immutably per-company or not per-company is not convenient. We are here taking advantage of the migration of onboardings (next commits) to have a working framework for these changes. The alternative solution of having to change all onboardings and linked steps together and ensuring all or none are per-company was in fact more complex to support and fragile as depending on python constraints on the many2many relationship. We also considered it wasn't worth the complexity to identify which onboarding_progress record could safely be removed when its last 'per-company' step is updated to not per-company. In that case, all companies will have to close the panel independently, but the completion of each remaining step will not have to be done for each company. ### Miscellaneous #### add safe close action This eases the handling closing an onboarding panel that may have been deleted. The code for each panel can therefore be simplified. #### add onboarding step form controller Extracted from `appointment` to be reused in other modules' onboardings. We are also cleaning the closing behavior which is no longer required as dialogs now do close on save/discard. Tests are added and updated. Task-3025136 Part-of: odoo/odoo#104223 --- addons/onboarding/__manifest__.py | 8 +- addons/onboarding/models/__init__.py | 2 +- .../models/onboarding_onboarding.py | 72 +++++++++++----- ..._step.py => onboarding_onboarding_step.py} | 73 +++++++++++----- .../onboarding/models/onboarding_progress.py | 29 ++++--- .../models/onboarding_progress_step.py | 17 ++-- .../form/onboarding_step_form_controller.js | 49 +++++++++++ addons/onboarding/tests/common.py | 28 ++++--- addons/onboarding/tests/test_onboarding.py | 83 +++++++++++++++---- addons/onboarding/views/onboarding_views.xml | 15 +++- 10 files changed, 285 insertions(+), 91 deletions(-) rename addons/onboarding/models/{onboarding_step.py => onboarding_onboarding_step.py} (60%) create mode 100644 addons/onboarding/static/src/views/form/onboarding_step_form_controller.js diff --git a/addons/onboarding/__manifest__.py b/addons/onboarding/__manifest__.py index 759d6af3e0f..6ec2a67c99a 100644 --- a/addons/onboarding/__manifest__.py +++ b/addons/onboarding/__manifest__.py @@ -3,7 +3,7 @@ { 'name': 'Onboarding Toolbox', - 'version': '1.1', + 'version': '1.2', 'category': 'Hidden', 'sequence': 9001, 'description': """ @@ -15,8 +15,14 @@ This module allows to manage onboardings and their progress 'data': [ 'data/onboarding_data.xml', 'views/onboarding_views.xml', + 'views/onboarding_templates.xml', 'views/onboarding_menus.xml', 'security/ir.model.access.csv', ], + 'assets': { + 'web.assets_backend': [ + 'onboarding/static/src/**/*', + ], + }, 'license': 'LGPL-3', } diff --git a/addons/onboarding/models/__init__.py b/addons/onboarding/models/__init__.py index 8fd1fc0e1d6..0e27595dad9 100644 --- a/addons/onboarding/models/__init__.py +++ b/addons/onboarding/models/__init__.py @@ -2,6 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import onboarding_onboarding -from . import onboarding_step +from . import onboarding_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 index 8eef10a1ac6..7acba62d45e 100644 --- a/addons/onboarding/models/onboarding_onboarding.py +++ b/addons/onboarding/models/onboarding_onboarding.py @@ -13,10 +13,11 @@ class Onboarding(models.Model): 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) + step_ids = fields.Many2many('onboarding.onboarding.step', string='Onboarding steps') + is_per_company = fields.Boolean( + 'Should be done per company?', compute='_compute_is_per_company', readonly=True, store=False, + ) panel_close_action_name = fields.Char( 'Closing action', help='Name of the onboarding model action to execute when closing the panel.') @@ -36,8 +37,19 @@ class Onboarding(models.Model): ('route_name_uniq', 'UNIQUE (route_name)', 'Onboarding alias must be unique.'), ] + @api.depends('progress_ids', 'progress_ids.company_id', 'step_ids', 'step_ids.is_per_company') + def _compute_is_per_company(self): + # Once an onboarding is made "per-company", there is no drawback to simply still consider + # it per-company even when if its last per-company step is unlinked. This allows to avoid + # handling the merging of existing progress (step) records. + + onboardings_with_per_company_steps_or_progress = self.filtered( + lambda o: o.progress_ids.company_id or (True in o.step_ids.mapped('is_per_company'))) + onboardings_with_per_company_steps_or_progress.is_per_company = True + (self - onboardings_with_per_company_steps_or_progress).is_per_company = False + @api.depends_context('company') - @api.depends('progress_ids', 'progress_ids.is_onboarding_closed', 'progress_ids.onboarding_state') + @api.depends('progress_ids', 'progress_ids.is_onboarding_closed', 'progress_ids.onboarding_state', 'progress_ids.company_id') def _compute_current_progress(self): for onboarding in self: current_progress_id = onboarding.progress_ids.filtered( @@ -51,28 +63,44 @@ class Onboarding(models.Model): onboarding.current_progress_id = False onboarding.is_onboarding_closed = False + def write(self, vals): + """Recompute progress step ids if new steps are added/removed.""" + already_linked_steps = self.step_ids + res = super().write(vals) + if self.step_ids != already_linked_steps: + self.progress_ids._recompute_progress_step_ids() + return res + def action_close(self): """Close the onboarding panel.""" self.current_progress_id.action_close() + @api.model + def action_close_panel(self, xmlid): + """Close the onboarding panel identified by its `xmlid`. + + If not found, quietly do nothing. + """ + if onboarding := self.env.ref(xmlid, raise_if_not_found=False): + onboarding.action_close() + + def action_refresh_progress_ids(self): + """Re-initialize onboarding progress records (after step is_per_company change). + + Meant to be called when `is_per_company` of linked steps is modified (or per-company + steps are added to an onboarding). + """ + onboardings_to_refresh_progress = self.filtered( + lambda o: o.is_per_company and o.progress_ids and not o.progress_ids.company_id + ) + onboardings_to_refresh_progress.progress_ids.unlink() + onboardings_to_refresh_progress._create_progress() + 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. - """ + """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 @@ -81,8 +109,12 @@ class Onboarding(models.Model): 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 + 'onboarding_id': onboarding.id, + 'progress_step_ids': onboarding.step_ids.progress_ids.filtered( + lambda p: p.company_id.id in [False, self.env.company.id] + ), + } + for onboarding in self ]) def _prepare_rendering_values(self): diff --git a/addons/onboarding/models/onboarding_step.py b/addons/onboarding/models/onboarding_onboarding_step.py similarity index 60% rename from addons/onboarding/models/onboarding_step.py rename to addons/onboarding/models/onboarding_onboarding_step.py index 7f2d83c26e4..52b5eb56770 100644 --- a/addons/onboarding/models/onboarding_step.py +++ b/addons/onboarding/models/onboarding_onboarding_step.py @@ -1,8 +1,9 @@ # -*- 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, Command, fields, models from odoo.addons.onboarding.models.onboarding_progress import ONBOARDING_PROGRESS_STATES +from odoo.exceptions import ValidationError class OnboardingStep(models.Model): @@ -11,8 +12,7 @@ class OnboardingStep(models.Model): _order = 'sequence asc, id asc' _rec_name = 'title' - onboarding_id = fields.Many2one( - 'onboarding.onboarding', string='Onboarding', readonly=True, required=True, ondelete='cascade') + onboarding_ids = fields.Many2many('onboarding.onboarding', string='Onboardings') title = fields.Char('Title', translate=True) description = fields.Char('Description', translate=True) @@ -21,14 +21,14 @@ class OnboardingStep(models.Model): 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) + 'Text to show when step is completed', default=_('Step Completed!'), translate=True) step_image = fields.Binary("Step Image") step_image_filename = fields.Char("Step Image Filename") step_image_alt = fields.Char( 'Alt Text for the Step Image', default='Onboarding Step Image', translate=True, help='Show when impossible to load the image') panel_step_open_action_name = fields.Char( - string='Opening action', required=True, + string='Opening action', required=False, help='Name of the onboarding step model action to execute when opening the step, ' 'e.g. action_open_onboarding_1_step_1') @@ -41,14 +41,16 @@ class OnboardingStep(models.Model): 'onboarding.progress.step', 'step_id', string='Onboarding Progress Step Records', readonly=True, help='All related Onboarding Progress Step Records (across companies)') + is_per_company = fields.Boolean('Is per company', default=True) sequence = fields.Integer(default=10) @api.depends_context('company') @api.depends('progress_ids', 'progress_ids.step_state') def _compute_current_progress(self): + # When `is_per_company` is changed, `progress_ids` is updated (see `write`) which triggers this `_compute`. existing_progress_steps = self.progress_ids.filtered_domain([ ('step_id', 'in', self.ids), - ('progress_id.company_id', 'in', [False, self.env.company.id]), + ('company_id', 'in', [False, self.env.company.id]), ]) for step in self: if step in existing_progress_steps.step_id: @@ -60,6 +62,35 @@ class OnboardingStep(models.Model): step.current_progress_step_id = False step.current_step_state = 'not_done' + @api.constrains('onboarding_ids') + def check_step_on_onboarding_has_action(self): + if steps_without_action := self.filtered(lambda step: step.onboarding_ids and not step.panel_step_open_action_name): + raise ValidationError(_( + 'An "Opening Action" is required for the following steps to be ' + 'linked to an onboarding panel: %(step_titles)s', + step_titles=steps_without_action.mapped('title'), + )) + + def write(self, vals): + new_is_per_company = vals.get('is_per_company') + steps_changing_is_per_company = ( + self.browse() if new_is_per_company is None + else self.filtered(lambda step: step.is_per_company != new_is_per_company) + ) + already_linked_onboardings = self.onboarding_ids + + res = super().write(vals) + + # Progress is reset (to be done per-company or, for steps, to have a single record) + if steps_changing_is_per_company: + steps_changing_is_per_company.progress_ids.unlink() + self.onboarding_ids.action_refresh_progress_ids() + + if self.onboarding_ids - already_linked_onboardings: + self.onboarding_ids.progress_ids._recompute_progress_step_ids() + + return res + 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) @@ -80,21 +111,23 @@ class OnboardingStep(models.Model): return super()._get_placeholder_filename(field) def _create_progress_steps(self): + """Create progress step records as necessary to validate steps. + + Only considers existing `onboarding.progress` records for the current + company or without company (depending on `is_per_company`). + """ onboarding_progress_records = self.env['onboarding.progress'].search([ - ('onboarding_id', 'in', self.onboarding_id.ids), + ('onboarding_id', 'in', self.onboarding_ids.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 - ] - + progress_step_values = [ + { + 'step_id': step_id.id, + 'progress_ids': [ + Command.link(onboarding_progress_record.id) + for onboarding_progress_record + in onboarding_progress_records.filtered(lambda p: step_id in p.onboarding_id.step_ids)], + 'company_id': self.env.company.id if step_id.is_per_company else False, + } for step_id in self + ] return self.env['onboarding.progress.step'].create(progress_step_values) diff --git a/addons/onboarding/models/onboarding_progress.py b/addons/onboarding/models/onboarding_progress.py index 11539d2d143..3bb15560b7e 100644 --- a/addons/onboarding/models/onboarding_progress.py +++ b/addons/onboarding/models/onboarding_progress.py @@ -22,9 +22,10 @@ class OnboardingProgress(models.Model): 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') + progress_step_ids = fields.Many2many('onboarding.progress.step', string='Progress Steps Trackers') def init(self): + """Make sure there aren't multiple records for the same onboarding and company.""" # not in _sql_constraint because COALESCE is not supported for PostgreSQL constraint self.env.cr.execute(""" CREATE UNIQUE INDEX IF NOT EXISTS onboarding_progress_onboarding_company_uniq @@ -33,15 +34,19 @@ class OnboardingProgress(models.Model): @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'], ['__count'] - ) - result = {progress.id: count for progress, count 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') + 'not_done' if ( + len(progress.progress_step_ids.filtered(lambda p: p.step_state in {'just_done', 'done'})) + != len(progress.onboarding_id.step_ids) + ) + else 'done' + ) + + def _recompute_progress_step_ids(self): + """Update progress steps when a step (with existing progress) is added to an onboarding.""" + for progress in self: + progress.progress_step_ids = progress.onboarding_id.step_ids.current_progress_step_id def action_close(self): self.is_onboarding_closed = True @@ -51,9 +56,11 @@ class OnboardingProgress(models.Model): 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. + """Fetch the progress of an onboarding for rendering its panel. + + This method is expected to only be called by the onboarding controller. + It also has the responsibility of updating the 'just_done' state into + 'done' so that the 'just_done' states are only rendered once. """ self.ensure_one() onboarding_states_values = {} diff --git a/addons/onboarding/models/onboarding_progress_step.py b/addons/onboarding/models/onboarding_progress_step.py index 04c3538ff44..87e5e5b30f2 100644 --- a/addons/onboarding/models/onboarding_progress_step.py +++ b/addons/onboarding/models/onboarding_progress_step.py @@ -10,18 +10,21 @@ class OnboardingProgressStep(models.Model): _description = 'Onboarding Progress Step Tracker' _rec_name = 'step_id' - progress_id = fields.Many2one( - 'onboarding.progress', 'Related Onboarding Progress Tracker', required=True, ondelete='cascade') + progress_ids = fields.Many2many('onboarding.progress', string='Related Onboarding Progress Tracker') 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.'), - ] + company_id = fields.Many2one('res.company', ondelete='cascade') + + def init(self): + """Make sure there aren't multiple records for the same onboarding step and company.""" + # not in _sql_constraint because COALESCE is not supported for PostgreSQL constraint + self.env.cr.execute(""" + CREATE UNIQUE INDEX IF NOT EXISTS onboarding_progress_step_company_uniq + ON onboarding_progress_step (step_id, COALESCE(company_id, 0)) + """) def action_consolidate_just_done(self): was_just_done = self.filtered(lambda progress: progress.step_state == 'just_done') diff --git a/addons/onboarding/static/src/views/form/onboarding_step_form_controller.js b/addons/onboarding/static/src/views/form/onboarding_step_form_controller.js new file mode 100644 index 00000000000..5d276e6b915 --- /dev/null +++ b/addons/onboarding/static/src/views/form/onboarding_step_form_controller.js @@ -0,0 +1,49 @@ +/** @odoo-module **/ + +import { FormController } from "@web/views/form/form_controller"; +import { useService } from "@web/core/utils/hooks"; + +/** + * Controller to use for an onboarding step dialog, not the + * onboarding.onboarding.step form view itself. + */ +export default class OnboardingStepFormController extends FormController { + setup() { + super.setup(); + this.action = useService('action'); + this.orm = useService('orm'); + } + /** + * If necessary, mark the step as done and reload the main view. + * @override + */ + async onRecordSaved(record) { + const { reloadOnFirstValidation, reloadAlways } = this.stepConfig; + const validationResponse = await this.orm.call( + 'onboarding.onboarding.step', + 'action_validate_step', + [this.stepName], + ); + if (reloadAlways || (reloadOnFirstValidation && validationResponse === "JUST_DONE")) { + this.action.restore(this.action.currentController.jsId); + } + } + /** + * Returns the name of the onboarding step to validate after the dialog + * record is saved + * + * @return {string} + */ + get stepName() { + return '' + } + /** + * Returns whether to reload the page (useful if the current + * view needs to be updated). + * + * @returns {{reloadAlways: boolean, reloadOnFirstValidation: boolean}} + */ + get stepConfig() { + return { reloadAlways: false, reloadOnFirstValidation: false }; + } +} diff --git a/addons/onboarding/tests/common.py b/addons/onboarding/tests/common.py index 9f4dba3f271..50a08c2a81a 100644 --- a/addons/onboarding/tests/common.py +++ b/addons/onboarding/tests/common.py @@ -18,12 +18,12 @@ class TestOnboardingCommon(TransactionCase): }) cls.user_admin.company_ids |= cls.company_2 - cls.onboarding_1 = cls.env['onboarding.onboarding'].create([ + cls.onboarding_1, cls.onboarding_2 = cls.env['onboarding.onboarding'].create([ { - 'name': 'Test Onboarding 1', + 'name': f'Test Onboarding {onboarding_id}', + 'route_name': f'onboarding{onboarding_id}', 'is_per_company': False, - 'route_name': 'onboarding1' - } + } for onboarding_id in range(2) ]) # create a fake action for step opening @@ -36,14 +36,22 @@ class TestOnboardingCommon(TransactionCase): 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, + 'onboarding_ids': [cls.onboarding_1.id], + 'is_per_company': False, 'panel_step_open_action_name': 'action_fake_open_onboarding_step', } for step_n in range(1, 3) ]) - + # Add one of these in onboarding_2, and an "original" one + cls.onboarding_2.step_ids = [cls.onboarding_1_step_1.id] + cls.onboarding_2_step_2 = cls.env['onboarding.onboarding.step'].create([{ + 'title': 'Test Onboarding 2 - Step 2', + 'onboarding_ids': [cls.onboarding_2.id], + 'is_per_company': False, + 'panel_step_open_action_name': 'action_fake_open_onboarding_step', + }]) # Create progress records as would happen through the controller - cls.onboarding_1.with_company(cls.company_1)._search_or_create_progress() + (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( @@ -72,9 +80,9 @@ class TestOnboardingCommon(TransactionCase): 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} ' + 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: diff --git a/addons/onboarding/tests/test_onboarding.py b/addons/onboarding/tests/test_onboarding.py index f3c08ea76a3..f5dab985eca 100644 --- a/addons/onboarding/tests/test_onboarding.py +++ b/addons/onboarding/tests/test_onboarding.py @@ -3,7 +3,9 @@ from psycopg2 import IntegrityError +from odoo import Command from odoo.addons.onboarding.tests.common import TestOnboardingCommon +from odoo.exceptions import ValidationError from odoo.tools import mute_logger @@ -48,7 +50,8 @@ class TestOnboarding(TestOnboardingCommon): # 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, + 'onboarding_ids': [self.onboarding_1.id], + 'is_per_company': False, 'panel_step_open_action_name': 'action_fake_open_onboarding_step', }) self.assert_step_is_not_done(onboarding_1_step_3) @@ -68,7 +71,8 @@ class TestOnboarding(TestOnboardingCommon): # 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, + 'onboarding_ids': [self.onboarding_1.id], + 'is_per_company': False, 'panel_step_open_action_name': 'action_fake_open_onboarding_step', }) @@ -85,14 +89,16 @@ class TestOnboarding(TestOnboardingCommon): # 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 + # Updating onboarding (and steps) to per-company + self.onboarding_1_step_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.assertFalse(self.onboarding_1_step_2.is_per_company) self.onboarding_1_step_2.action_set_just_done() self.assert_onboarding_is_done(self.onboarding_1) @@ -101,19 +107,14 @@ class TestOnboarding(TestOnboardingCommon): # First access from company_2 self.onboarding_1._search_or_create_progress() - # Blank state for company 2 + # Blank state for company 2 for step 1 self.assert_step_is_not_done(self.onboarding_1_step_1) + # But step 2 is done + self.assert_step_is_done(self.onboarding_1_step_2) 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 @@ -122,21 +123,32 @@ class TestOnboarding(TestOnboardingCommon): 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. - """ + """ Checks that changing an onboarding step to per-company resets + completion 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 + # Updating onboarding step 1 to per-company + self.onboarding_1_step_1.is_per_company = True + self.assertTrue(self.onboarding_1.is_per_company) # Required after progress reset (simulate role of controller) self.onboarding_1._search_or_create_progress() self.assert_onboarding_is_not_done(self.onboarding_1) + def test_onboarding_shared_steps(self): + self.onboarding_2_step_2.action_set_just_done() + self.assert_step_is_done(self.onboarding_2_step_2) + # Completing common step is also required to be "done" + self.assert_onboarding_is_not_done(self.onboarding_2) + + self.onboarding_1_step_1.action_set_just_done() + self.assert_onboarding_is_not_done(self.onboarding_1) + self.assert_onboarding_is_done(self.onboarding_2) + @mute_logger('odoo.sql_db') def test_progress_no_company_uniqueness(self): """Check that there cannot be two progress records created for @@ -159,8 +171,8 @@ class TestOnboarding(TestOnboardingCommon): See also ``test_progress_no_company_uniqueness`` """ # Updating onboarding to per-company - self.onboarding_1.is_per_company = True - # Required after progress reset (simulate role of controller) + self.onboarding_1_step_1.is_per_company = True + # Create an onboarding_progress (simulate role of controller) self.onboarding_1._search_or_create_progress() with self.assertRaises(IntegrityError): @@ -168,3 +180,38 @@ class TestOnboarding(TestOnboardingCommon): 'onboarding_id': self.onboarding_1.id, 'company_id': self.env.company.id }) + + def test_onboarding_step_without_onboarding(self): + self.step_initially_w_o_onboarding = self.env['onboarding.onboarding.step'].create({ + 'title': 'Step Initially Without Onboarding', + }) + self.assertEqual(self.step_initially_w_o_onboarding.current_step_state, 'not_done') + self.step_initially_w_o_onboarding.action_set_just_done() + + self.assert_step_is_done(self.step_initially_w_o_onboarding) + + self.onboarding_3 = self.env['onboarding.onboarding'].create({ + 'name': 'Test Onboarding 3', + 'route_name': 'onboarding3', + }) + self.onboarding_3._search_or_create_progress() + + with self.assertRaises(ValidationError): + self.step_initially_w_o_onboarding.onboarding_ids = [Command.link(self.onboarding_3.id)] + + self.step_initially_w_o_onboarding.write({ + 'panel_step_open_action_name': 'action_fake_open_onboarding_step' + }) + self.step_initially_w_o_onboarding.onboarding_ids = [Command.link(self.onboarding_3.id)] + + with self.subTest('Progress records are recreated for companies with completed steps'): + # Onboarding is done as only step was already done by company 1 + self.assert_onboarding_is_done(self.onboarding_3) + + # Not by company 2 + self.onboarding_3.with_company(self.company_2)._search_or_create_progress() + self.assert_onboarding_is_not_done(self.onboarding_3.with_company(self.company_2)) + + # But it can + self.step_initially_w_o_onboarding.with_company(self.company_2).action_set_just_done() + self.assert_onboarding_is_done(self.onboarding_3.with_company(self.company_2)) diff --git a/addons/onboarding/views/onboarding_views.xml b/addons/onboarding/views/onboarding_views.xml index 146546769d3..e0da601fbb5 100644 --- a/addons/onboarding/views/onboarding_views.xml +++ b/addons/onboarding/views/onboarding_views.xml @@ -36,7 +36,9 @@ - + + + @@ -51,8 +53,9 @@ - + + @@ -65,10 +68,16 @@ - + + + + + + + From 77f9ff50db3cdb88397d0b1cc7042c772d0d417b Mon Sep 17 00:00:00 2001 From: Florian Charlier Date: Mon, 17 Oct 2022 09:48:48 +0000 Subject: [PATCH 3/3] [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 --- addons/account/__manifest__.py | 5 +- addons/account/controllers/__init__.py | 1 - addons/account/controllers/onboarding.py | 49 ------ addons/account/data/onboarding_data.xml | 134 ++++++++++++++ addons/account/models/__init__.py | 2 + .../models/account_journal_dashboard.py | 13 +- addons/account/models/company.py | 164 +----------------- .../account/models/onboarding_onboarding.py | 29 ++++ .../models/onboarding_onboarding_step.py | 133 ++++++++++++++ addons/account/static/src/js/tours/account.js | 20 +-- .../res_company_onboarding_form_controller.js | 12 ++ .../views/res_company_onboarding_form_view.js | 15 ++ .../views/account_journal_dashboard_view.xml | 2 +- addons/account/views/account_move_views.xml | 2 +- .../views/account_onboarding_templates.xml | 156 ----------------- addons/account/views/res_company_views.xml | 31 ++++ addons/account/wizard/base_document_layout.py | 6 +- addons/account/wizard/setup_wizards.py | 6 +- addons/account_payment/__manifest__.py | 2 +- .../account_payment/data/onboarding_data.xml | 17 ++ addons/account_payment/models/__init__.py | 2 +- .../models/onboarding_onboarding_step.py | 20 +++ addons/account_payment/models/res_company.py | 19 -- .../views/onboarding_templates.xml | 24 --- addons/onboarding/__manifest__.py | 9 +- addons/onboarding/data/onboarding_data.xml | 28 --- .../models/onboarding_onboarding.py | 7 +- .../static/src/scss/onboarding.scss | 9 +- .../src/scss/onboarding.variables.dark.scss | 0 .../static/src/scss/onboarding.variables.scss | 0 addons/onboarding/tests/__init__.py | 1 + addons/onboarding/tests/case.py | 40 +++++ addons/onboarding/tests/common.py | 39 +---- .../onboarding/views/onboarding_templates.xml | 76 +++----- addons/payment/__manifest__.py | 3 +- addons/payment/data/onboarding_data.xml | 10 ++ addons/payment/models/__init__.py | 1 + .../models/onboarding_onboarding_step.py | 12 ++ addons/payment/models/res_company.py | 16 -- .../wizards/payment_onboarding_wizard.py | 7 +- .../payment_stripe/controllers/onboarding.py | 6 +- .../payment_stripe/models/payment_provider.py | 2 +- addons/sale/__manifest__.py | 2 +- addons/sale/controllers/__init__.py | 1 - addons/sale/controllers/onboarding.py | 31 ---- addons/sale/data/onboarding_data.xml | 41 +++++ addons/sale/models/__init__.py | 2 + addons/sale/models/onboarding_onboarding.py | 12 ++ .../sale/models/onboarding_onboarding_step.py | 100 +++++++++++ addons/sale/models/res_company.py | 120 +------------ .../src/img/sale_quotation_onboarding_bg.jpg | Bin 35910 -> 0 bytes addons/sale/static/src/js/tours/sale.js | 28 +-- addons/sale/tests/__init__.py | 1 + addons/sale/tests/test_onboarding.py | 30 ++++ addons/sale/tests/test_sale_onboarding.py | 2 +- addons/sale/views/sale_onboarding_views.xml | 62 ------- addons/sale/views/sale_order_views.xml | 2 +- .../payment_provider_onboarding_wizard.py | 5 - addons/sale_management/tests/test_sale_ui.py | 4 +- addons/web/__manifest__.py | 5 - addons/web/models/base_document_layout.py | 8 - addons/website_sale/models/res_company.py | 11 +- odoo/addons/base/__manifest__.py | 1 - odoo/addons/base/models/res_company.py | 36 ---- odoo/addons/base/populate/res_company.py | 2 - 65 files changed, 754 insertions(+), 882 deletions(-) delete mode 100644 addons/account/controllers/onboarding.py create mode 100644 addons/account/data/onboarding_data.xml create mode 100644 addons/account/models/onboarding_onboarding.py create mode 100644 addons/account/models/onboarding_onboarding_step.py create mode 100644 addons/account/static/src/views/res_company_onboarding_form_controller.js create mode 100644 addons/account/static/src/views/res_company_onboarding_form_view.js delete mode 100644 addons/account/views/account_onboarding_templates.xml create mode 100644 addons/account_payment/data/onboarding_data.xml create mode 100644 addons/account_payment/models/onboarding_onboarding_step.py delete mode 100644 addons/account_payment/models/res_company.py delete mode 100644 addons/account_payment/views/onboarding_templates.xml delete mode 100644 addons/onboarding/data/onboarding_data.xml rename {odoo/addons/base => addons/onboarding}/static/src/scss/onboarding.scss (93%) rename {odoo/addons/base => addons/onboarding}/static/src/scss/onboarding.variables.dark.scss (100%) rename {odoo/addons/base => addons/onboarding}/static/src/scss/onboarding.variables.scss (100%) create mode 100644 addons/onboarding/tests/case.py rename odoo/addons/base/views/onboarding_views.xml => addons/onboarding/views/onboarding_templates.xml (64%) create mode 100644 addons/payment/data/onboarding_data.xml create mode 100644 addons/payment/models/onboarding_onboarding_step.py delete mode 100644 addons/sale/controllers/onboarding.py create mode 100644 addons/sale/data/onboarding_data.xml create mode 100644 addons/sale/models/onboarding_onboarding.py create mode 100644 addons/sale/models/onboarding_onboarding_step.py delete mode 100644 addons/sale/static/src/img/sale_quotation_onboarding_bg.jpg create mode 100644 addons/sale/tests/test_onboarding.py delete mode 100644 addons/sale/views/sale_onboarding_views.xml diff --git a/addons/account/__manifest__.py b/addons/account/__manifest__.py index 02d00494814..e78ffc3a7b2 100644 --- a/addons/account/__manifest__.py +++ b/addons/account/__manifest__.py @@ -14,7 +14,7 @@ You could use this simplified accounting in case you work with an (external) acc """, 'category': 'Accounting/Accounting', 'website': 'https://www.odoo.com/app/invoicing', - 'depends' : ['base_setup', 'product', 'analytic', 'portal', 'digest'], + 'depends': ['base_setup', 'onboarding', 'product', 'analytic', 'portal', 'digest'], 'data': [ 'security/account_security.xml', 'security/ir.model.access.csv', @@ -22,6 +22,7 @@ You could use this simplified accounting in case you work with an (external) acc 'data/digest_data.xml', 'views/account_report.xml', 'data/mail_template_data.xml', + 'data/onboarding_data.xml', 'views/account_payment_view.xml', 'wizard/account_automatic_entry_wizard_views.xml', 'wizard/account_unreconcile_view.xml', @@ -59,7 +60,6 @@ You could use this simplified accounting in case you work with an (external) acc 'views/account_journal_dashboard_view.xml', 'views/account_portal_templates.xml', 'views/report_payment_receipt_templates.xml', - 'views/account_onboarding_templates.xml', 'data/service_cron.xml', 'views/account_incoterms_view.xml', 'data/account_incoterms_data.xml', @@ -94,6 +94,7 @@ You could use this simplified accounting in case you work with an (external) acc 'account/static/src/components/**/*', 'account/static/src/js/tours/account.js', 'account/static/src/xml/**/*', + 'account/static/src/views/**/*.js', ], 'web.assets_frontend': [ 'account/static/src/js/account_portal_sidebar.js', diff --git a/addons/account/controllers/__init__.py b/addons/account/controllers/__init__.py index 4cb0be3e577..44eea6b8e43 100644 --- a/addons/account/controllers/__init__.py +++ b/addons/account/controllers/__init__.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import onboarding from . import portal from . import terms diff --git a/addons/account/controllers/onboarding.py b/addons/account/controllers/onboarding.py deleted file mode 100644 index 240d7aef2f6..00000000000 --- a/addons/account/controllers/onboarding.py +++ /dev/null @@ -1,49 +0,0 @@ -from odoo import http -from odoo.http import request - - -class OnboardingController(http.Controller): - - @http.route('/account/account_invoice_onboarding', auth='user', type='json') - def account_invoice_onboarding(self, context=None): - """ Returns the `banner` for the account invoice onboarding panel. - It can be empty if the user has closed it or if he doesn't have - the permission to see it. """ - - # Forward `allowed_company_ids` in the context - # so `request.env.company` returns the company selected in the dropdown - if context: - request.update_context(**context) - company = request.env.company - if not request.env.is_admin() or \ - company.account_invoice_onboarding_state == 'closed': - return {} - - return { - 'html': request.env['ir.qweb']._render('account.account_invoice_onboarding_panel', { - 'company': company, - 'state': company.get_and_update_account_invoice_onboarding_state() - }) - } - - @http.route('/account/account_dashboard_onboarding', auth='user', type='json') - def account_dashboard_onboarding(self, context=None): - """ Returns the `banner` for the account dashboard onboarding panel. - It can be empty if the user has closed it or if he doesn't have - the permission to see it. """ - # Forward `allowed_company_ids` in the context - # so `request.env.company` returns the company selected in the dropdown - if context: - request.update_context(**context) - company = request.env.company - - if not request.env.is_admin() or \ - company.account_dashboard_onboarding_state == 'closed': - return {} - - return { - 'html': request.env['ir.qweb']._render('account.account_dashboard_onboarding_panel', { - 'company': company, - 'state': company.get_and_update_account_dashboard_onboarding_state() - }) - } diff --git a/addons/account/data/onboarding_data.xml b/addons/account/data/onboarding_data.xml new file mode 100644 index 00000000000..ca89a63521c --- /dev/null +++ b/addons/account/data/onboarding_data.xml @@ -0,0 +1,134 @@ + + + + + + + Company Data + Set your company's data for documents header/footer. + Let's start! + Looks great! + action_open_step_company_data + + onboarding_company-data.png + Onboarding Company Data + 1 + + + + Documents Layout + Customize the look of your documents. + Customize + Looks great! + action_open_step_base_document_layout + + onboarding_quotation-layout.png + Onboarding Documents Layout + 3 + + + + Create Invoice + Create your first invoice. + Create + First invoice sent! + action_open_step_create_invoice + + onboarding_default.png + Onboarding Create Invoice + 4 + + + + + Accounting Periods + Define your fiscal years & tax returns periodicity. + Configure + Step completed! + action_open_step_fiscal_year + + onboarding_accounting-periods.png + Onboarding Accounting Periods + 1 + + + + Bank Account + Connect your financial accounts in seconds. + Add a bank account + Step Completed! + action_open_step_bank_account + + onboarding_puzzle.png + Onboarding Bank Account + 2 + + + + Taxes + Set default Taxes for sales and purchase transactions. + Review + Taxes set! + action_open_step_default_taxes + + onboarding_taxes.png + Onboarding Taxes + 3 + + + + Chart of Accounts + Set up your chart of accounts and record initial balances. + Review + Chart of accounts set! + action_open_step_chart_of_accounts + + onboarding_chart-of-accounts.png + Onboarding Bank Account + 4 + + + + + Import Vendor Bills + 1000 + + + + Taxes + + Choose a default sales tax for your products. + Set taxes + Step Completed! + action_open_step_sales_tax + + onboarding_puzzle.png + Onboarding Bank Account + 100 + + + + + Invoicing Onboarding + + account_invoice + action_close_panel_account_invoice + + + + Account Dashboard Onboarding + + account_dashboard + action_close_panel_account_dashboard + + + diff --git a/addons/account/models/__init__.py b/addons/account/models/__init__.py index dedb26f5e69..76f6bb8cb6e 100644 --- a/addons/account/models/__init__.py +++ b/addons/account/models/__init__.py @@ -37,5 +37,7 @@ from . import ir_actions_report from . import ir_module from . import res_currency from . import account_report +from . import onboarding_onboarding +from . import onboarding_onboarding_step from . import template_generic_coa from . import uom_uom diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py index 792655f8e9d..919e5128476 100644 --- a/addons/account/models/account_journal_dashboard.py +++ b/addons/account/models/account_journal_dashboard.py @@ -620,7 +620,7 @@ class account_journal(models.Model): """ This function is called by the "Import" button of Vendor Bills, visible on dashboard if no bill has been created yet. """ - self.env.company.sudo().set_onboarding_step_done('account_setup_bill_state') + self.env['onboarding.onboarding.step'].action_validate_step('account.onboarding_onboarding_step_setup_bill') new_wizard = self.env['account.tour.upload.bill'].create({}) view_id = self.env.ref('account.account_tour_upload_bill').id @@ -777,14 +777,3 @@ class account_journal(models.Model): def create_internal_transfer(self): """return action to create a internal transfer""" return self.open_payments_action('transfer', mode='form') - - ##################### - # Setup Steps Stuff # - ##################### - def mark_bank_setup_as_done_action(self): - """ Marks the 'bank setup' step as done in the setup bar and in the company.""" - self.company_id.sudo().set_onboarding_step_done('account_setup_bank_data_state') - - def unmark_bank_setup_as_done_action(self): - """ Marks the 'bank setup' step as not done in the setup bar and in the company.""" - self.company_id.account_setup_bank_data_state = 'not_done' diff --git a/addons/account/models/company.py b/addons/account/models/company.py index 6ea588e4e20..c705106109c 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -26,14 +26,6 @@ MONTH_SELECTION = [ ('12', 'December'), ] -ONBOARDING_STEP_STATES = [ - ('not_done', "Not done"), - ('just_done', "Just done"), - ('done', "Done"), -] -DASHBOARD_ONBOARDING_STATES = ONBOARDING_STEP_STATES + [('closed', 'Closed')] - - class ResCompany(models.Model): _name = "res.company" _inherit = ["res.company", "mail.thread"] @@ -104,20 +96,6 @@ class ResCompany(models.Model): account_opening_journal_id = fields.Many2one(string='Opening Journal', comodel_name='account.journal', related='account_opening_move_id.journal_id', help="Journal where the opening entry of this company's accounting has been posted.", readonly=False) account_opening_date = fields.Date(string='Opening Entry', default=lambda self: fields.Date.context_today(self).replace(month=1, day=1), required=True, help="That is the date of the opening entry.") - # Fields marking the completion of a setup step - account_setup_bank_data_state = fields.Selection(ONBOARDING_STEP_STATES, string="State of the onboarding bank data step", default='not_done') - account_setup_fy_data_state = fields.Selection(ONBOARDING_STEP_STATES, string="State of the onboarding fiscal year step", default='not_done') - account_setup_coa_state = fields.Selection(ONBOARDING_STEP_STATES, string="State of the onboarding charts of account step", default='not_done') - account_setup_taxes_state = fields.Selection(ONBOARDING_STEP_STATES, string="State of the onboarding Taxes step", default='not_done') - account_onboarding_invoice_layout_state = fields.Selection(ONBOARDING_STEP_STATES, string="State of the onboarding invoice layout step", default='not_done') - account_onboarding_create_invoice_state = fields.Selection(ONBOARDING_STEP_STATES, string="State of the onboarding create invoice step", compute='_compute_account_onboarding_create_invoice_state') - #this field must be there to ensure that the create_invoice_state stay complete and because we can't use a dependencies on account move - account_onboarding_create_invoice_state_flag = fields.Boolean(default=False, store=True) - account_onboarding_sale_tax_state = fields.Selection(ONBOARDING_STEP_STATES, string="State of the onboarding sale tax step", default='not_done') - - # account dashboard onboarding - account_invoice_onboarding_state = fields.Selection(DASHBOARD_ONBOARDING_STATES, string="State of the account invoice onboarding panel", default='not_done') - account_dashboard_onboarding_state = fields.Selection(DASHBOARD_ONBOARDING_STATES, string="State of the account dashboard onboarding panel", default='not_done') invoice_terms = fields.Html(string='Default Terms and Conditions', translate=True) terms_type = fields.Selection([('plain', 'Add a Note'), ('html', 'Add a link to a Web Page')], string='Terms & Conditions format', default='plain') @@ -125,8 +103,6 @@ class ResCompany(models.Model): sanitize_attributes=False, compute='_compute_invoice_terms_html', store=True, readonly=False) - account_setup_bill_state = fields.Selection(ONBOARDING_STEP_STATES, string="State of the onboarding bill step", default='not_done') - # Needed in the Point of Sale account_default_pos_receivable_account_id = fields.Many2one('account.account', string="Default PoS Receivable Account") @@ -232,17 +208,6 @@ class ResCompany(models.Model): foreign_vat_fpos = self.env['account.fiscal.position'].search([('company_id', '=', record.id), ('foreign_vat', '!=', False)]) record.account_enabled_tax_country_ids = foreign_vat_fpos.country_id + record.account_fiscal_country_id - @api.depends('account_onboarding_create_invoice_state_flag') - def _compute_account_onboarding_create_invoice_state(self): - for record in self: - if record.account_onboarding_create_invoice_state_flag: - record.account_onboarding_create_invoice_state = 'done' - elif self.env['account.move'].search([('company_id', '=', record.id), ('move_type', '=', 'out_invoice')], limit=1): - record.account_onboarding_create_invoice_state = 'just_done' - record.account_onboarding_create_invoice_state_flag = True - else: - record.account_onboarding_create_invoice_state = 'not_done' - @api.depends('terms_type') def _compute_invoice_terms_html(self): for company in self.filtered(lambda company: is_html_empty(company.invoice_terms_html) and company.terms_type == 'html'): @@ -252,40 +217,6 @@ class ResCompany(models.Model): if html: company.invoice_terms_html = html - def get_and_update_account_invoice_onboarding_state(self): - """ This method is called on the controller rendering method and ensures that the animations - are displayed only one time. """ - return self._get_and_update_onboarding_state( - 'account_invoice_onboarding_state', - self.get_account_invoice_onboarding_steps_states_names() - ) - - # YTI FIXME: Define only one method that returns {'account': [], 'sale': [], ...} - def get_account_invoice_onboarding_steps_states_names(self): - """ Necessary to add/edit steps from other modules (payment provider in this case). """ - return [ - 'base_onboarding_company_state', - 'account_onboarding_invoice_layout_state', - 'account_onboarding_create_invoice_state', - ] - - def get_and_update_account_dashboard_onboarding_state(self): - """ This method is called on the controller rendering method and ensures that the animations - are displayed only one time. """ - return self._get_and_update_onboarding_state( - 'account_dashboard_onboarding_state', - self.get_account_dashboard_onboarding_steps_states_names() - ) - - def get_account_dashboard_onboarding_steps_states_names(self): - """ Necessary to add/edit steps from other modules (account_winbooks_import in this case). """ - return [ - 'account_setup_bank_data_state', - 'account_setup_fy_data_state', - 'account_setup_coa_state', - 'account_setup_taxes_state', - ] - def get_new_account_code(self, current_code, old_prefix, new_prefix): digits = len(current_code) return new_prefix + current_code.replace(old_prefix, '', 1).lstrip('0').rjust(digits-len(new_prefix), '0') @@ -409,58 +340,17 @@ class ResCompany(models.Model): @api.model def setting_init_bank_account_action(self): - """ Called by the 'Bank Accounts' button of the setup bar.""" + """ Called by the 'Bank Accounts' button of the setup bar or from the Financial configuration menu.""" view_id = self.env.ref('account.setup_bank_account_wizard').id - return {'type': 'ir.actions.act_window', - 'name': _('Create a Bank Account'), - 'res_model': 'account.setup.bank.manual.config', - 'target': 'new', - 'view_mode': 'form', - 'views': [[view_id, 'form']], - } - - @api.model - def setting_init_fiscal_year_action(self): - """ Called by the 'Fiscal Year Opening' button of the setup bar.""" - company = self.env.company - new_wizard = self.env['account.financial.year.op'].create({'company_id': company.id}) - view_id = self.env.ref('account.setup_financial_year_opening_form').id - return { 'type': 'ir.actions.act_window', - 'name': _('Accounting Periods'), - 'view_mode': 'form', - 'res_model': 'account.financial.year.op', + 'name': _('Create a Bank Account'), + 'res_model': 'account.setup.bank.manual.config', 'target': 'new', - 'res_id': new_wizard.id, + 'view_mode': 'form', 'views': [[view_id, 'form']], } - @api.model - def setting_chart_of_accounts_action(self): - """ Called by the 'Chart of Accounts' button of the setup bar.""" - company = self.env.company - company.sudo().set_onboarding_step_done('account_setup_coa_state') - - # If an opening move has already been posted, we open the tree view showing all the accounts - if company.opening_move_posted(): - return 'account.action_account_form' - - # Then, we open will open a custom tree view allowing to edit opening balances of the account - view_id = self.env.ref('account.init_accounts_tree').id - # Hide the current year earnings account as it is automatically computed - domain = [('account_type', '!=', 'equity_unaffected'), ('company_id', '=', company.id)] - return { - 'type': 'ir.actions.act_window', - 'name': _('Chart of Accounts'), - 'res_model': 'account.account', - 'view_mode': 'tree', - 'limit': 99999999, - 'search_view_id': [self.env.ref('account.view_account_search').id], - 'views': [[view_id, 'list']], - 'domain': domain, - } - @api.model def create_op_move_if_non_existant(self): """ Creates an empty opening move in 'draft' state for the current company @@ -559,53 +449,9 @@ class ResCompany(models.Model): 'credit': debit_diff, }) - @api.model - def action_close_account_invoice_onboarding(self): - """ Mark the invoice onboarding panel as closed. """ - self.env.company.account_invoice_onboarding_state = 'closed' - - @api.model - def action_close_account_dashboard_onboarding(self): - """ Mark the dashboard onboarding panel as closed. """ - self.env.company.account_dashboard_onboarding_state = 'closed' - - @api.model - def action_open_account_onboarding_sale_tax(self): - """ Onboarding step for the invoice layout. """ - action = self.env["ir.actions.actions"]._for_xml_id("account.action_open_account_onboarding_sale_tax") - action['res_id'] = self.env.company.id - return action - - @api.model - def action_open_account_onboarding_create_invoice(self): - return self.env["ir.actions.actions"]._for_xml_id("account.action_open_account_onboarding_create_invoice") - - @api.model - def action_open_taxes_onboarding(self): - """ Called by the 'Taxes' button of the setup bar.""" - - company = self.env.company - company.sudo().set_onboarding_step_done('account_setup_taxes_state') - view_id_list = self.env.ref('account.view_onboarding_tax_tree').id - view_id_form = self.env.ref('account.view_tax_form').id - - return { - 'type': 'ir.actions.act_window', - 'name': _('Taxes'), - 'res_model': 'account.tax', - 'target': 'current', - 'views': [[view_id_list, 'list'], [view_id_form, 'form']], - 'context': {'search_default_sale': True, 'search_default_purchase': True, 'active_test': False}, - } - - def action_save_onboarding_invoice_layout(self): - """ Set the onboarding step as done """ - if bool(self.external_report_layout_id): - self.set_onboarding_step_done('account_onboarding_invoice_layout_state') - def action_save_onboarding_sale_tax(self): """ Set the onboarding step as done """ - self.set_onboarding_step_done('account_onboarding_sale_tax_state') + self.env['onboarding.onboarding.step'].action_validate_step('account.onboarding_onboarding_step_sales_tax') def get_chart_of_accounts_or_fail(self): account = self.env['account.account'].search([('company_id', '=', self.id)], limit=1) diff --git a/addons/account/models/onboarding_onboarding.py b/addons/account/models/onboarding_onboarding.py new file mode 100644 index 00000000000..9b51e26e572 --- /dev/null +++ b/addons/account/models/onboarding_onboarding.py @@ -0,0 +1,29 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models + + +class Onboarding(models.Model): + _inherit = 'onboarding.onboarding' + + # Invoice Onboarding + @api.model + def action_close_panel_account_invoice(self): + self.action_close_panel('account.onboarding_onboarding_account_invoice') + + def _prepare_rendering_values(self): + """Compute existence of invoices for company.""" + self.ensure_one() + if self == self.env.ref('account.onboarding_onboarding_account_invoice', raise_if_not_found=False): + step = self.env.ref('account.onboarding_onboarding_step_create_invoice', raise_if_not_found=False) + if step and step.current_step_state == 'not_done': + if self.env['account.move'].search( + [('company_id', '=', self.env.company.id), ('move_type', '=', 'out_invoice')], limit=1 + ): + step.action_set_just_done() + return super()._prepare_rendering_values() + + # Dashboard Onboarding + @api.model + def action_close_panel_account_dashboard(self): + self.action_close_panel('account.onboarding_onboarding_account_dashboard') diff --git a/addons/account/models/onboarding_onboarding_step.py b/addons/account/models/onboarding_onboarding_step.py new file mode 100644 index 00000000000..df74c659eb0 --- /dev/null +++ b/addons/account/models/onboarding_onboarding_step.py @@ -0,0 +1,133 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import _, api, models + + +class OnboardingStep(models.Model): + _inherit = 'onboarding.onboarding.step' + + # COMMON STEPS + @api.model + def action_open_step_company_data(self): + """Set company's basic information.""" + action = { + 'type': 'ir.actions.act_window', + 'name': _('Set your company data'), + 'res_model': 'res.company', + 'res_id': self.env.company.id, + 'views': [(self.env.ref('account.res_company_form_view_onboarding').id, "form")], + 'target': 'new', + } + return action + + @api.model + def action_open_step_base_document_layout(self): + view_id = self.env.ref('web.view_base_document_layout').id + return { + 'name': _('Configure your document layout'), + 'type': 'ir.actions.act_window', + 'res_model': 'base.document.layout', + 'target': 'new', + 'views': [(view_id, 'form')], + } + + @api.model + def action_validate_step_base_document_layout(self): + """Set the onboarding(s) step as done only if layout is set.""" + step = self.env.ref('account.onboarding_onboarding_step_base_document_layout', raise_if_not_found=False) + if not step or not self.env.company.external_report_layout_id: + return False + return self.action_validate_step('account.onboarding_onboarding_step_base_document_layout') + + # INVOICE ONBOARDING + @api.model + def action_open_step_bank_account(self): + return self.env.company.setting_init_bank_account_action() + + @api.model + def action_open_step_create_invoice(self): + return { + 'type': 'ir.actions.act_window', + 'name': _('Create first invoice'), + 'views': [(self.env.ref("account.view_move_form").id, 'form')], + 'res_model': 'account.move', + 'context': {'default_move_type': 'out_invoice'}, + } + + # DASHBOARD ONBOARDING + @api.model + def action_open_step_fiscal_year(self): + company = self.env.company + company.create_op_move_if_non_existant() + new_wizard = self.env['account.financial.year.op'].create({'company_id': company.id}) + view_id = self.env.ref('account.setup_financial_year_opening_form').id + + return { + 'type': 'ir.actions.act_window', + 'name': _('Accounting Periods'), + 'view_mode': 'form', + 'res_model': 'account.financial.year.op', + 'target': 'new', + 'res_id': new_wizard.id, + 'views': [[view_id, 'form']], + } + + @api.model + def action_open_step_default_taxes(self): + """ Called by the 'Taxes' button of the setup bar.""" + self.action_validate_step('account.onboarding_onboarding_step_default_taxes') + + view_id_list = self.env.ref('account.view_onboarding_tax_tree').id + view_id_form = self.env.ref('account.view_tax_form').id + + return { + 'type': 'ir.actions.act_window', + 'name': _('Taxes'), + 'res_model': 'account.tax', + 'target': 'current', + 'views': [[view_id_list, 'list'], [view_id_form, 'form']], + 'context': {'search_default_sale': True, 'search_default_purchase': True, 'active_test': False}, + } + + @api.model + def action_open_step_chart_of_accounts(self): + """ Called by the 'Chart of Accounts' button of the dashboard onboarding panel.""" + company = self.env.company + self.sudo().action_validate_step('account.onboarding_onboarding_step_chart_of_accounts') + + # If an opening move has already been posted, we open the tree view showing all the accounts + if company.opening_move_posted(): + return 'account.action_account_form' + + # Otherwise, we create the opening move + company.create_op_move_if_non_existant() + + # Then, we open will open a custom tree view allowing to edit opening balances of the account + view_id = self.env.ref('account.init_accounts_tree').id + # Hide the current year earnings account as it is automatically computed + domain = [('account_type', '!=', 'equity_unaffected'), ('company_id', '=', company.id)] + return { + 'type': 'ir.actions.act_window', + 'name': _('Chart of Accounts'), + 'res_model': 'account.account', + 'view_mode': 'tree', + 'limit': 99999999, + 'search_view_id': [self.env.ref('account.view_account_search').id], + 'views': [[view_id, 'list']], + 'domain': domain, + } + + # STEPS WITHOUT PANEL + @api.model + def action_open_step_sales_tax(self): + view_id = self.env.ref('account.res_company_form_view_onboarding_sale_tax').id + + return { + 'type': 'ir.actions.act_window', + 'name': _('Sales tax'), + 'res_id': self.env.company.id, + 'res_model': 'res.company', + 'target': 'new', + 'view_mode': 'form', + 'views': [[view_id, 'form']], + } diff --git a/addons/account/static/src/js/tours/account.js b/addons/account/static/src/js/tours/account.js index 31b31678aa1..671a13b5e5d 100644 --- a/addons/account/static/src/js/tours/account.js +++ b/addons/account/static/src/js/tours/account.js @@ -14,29 +14,29 @@ registry.category("web_tour.tours").add('account_tour', { steps: [ ...stepUtils.goToAppSteps('account.menu_finance', _t('Send invoices to your customers in no time with the Invoicing app.')), { - trigger: "a.o_onboarding_step_action[data-method=action_open_base_onboarding_company]", + trigger: "a.o_onboarding_step_action[data-method=action_open_step_company_data]", content: _t("Start by checking your company's data."), position: "bottom", - skip_trigger: 'a[data-method=action_open_base_onboarding_company].o_onboarding_step_action__done', + skip_trigger: 'a[data-method=action_open_step_company_data].o_onboarding_step_action__done', }, { - trigger: "button[name=action_save_onboarding_company_step]", - extra_trigger: "a.o_onboarding_step_action[data-method=action_open_base_onboarding_company]", + trigger: "button.o_form_button_save", + extra_trigger: "a.o_onboarding_step_action[data-method=action_open_step_company_data]", content: _t("Looks good. Let's continue."), position: "bottom", - skip_trigger: 'a[data-method=action_open_base_onboarding_company].o_onboarding_step_action__done', + skip_trigger: 'a[data-method=action_open_step_company_data].o_onboarding_step_action__done', }, { - trigger: "a.o_onboarding_step_action[data-method=action_open_base_document_layout]", + trigger: "a.o_onboarding_step_action[data-method=action_open_step_base_document_layout]", content: _t("Customize your layout."), position: "bottom", - skip_trigger: 'a[data-method=action_open_base_document_layout].o_onboarding_step_action__done', + skip_trigger: 'a[data-method=action_open_step_base_document_layout].o_onboarding_step_action__done', }, { trigger: "button[name=document_layout_save]", - extra_trigger: "a.o_onboarding_step_action[data-method=action_open_base_document_layout]", + extra_trigger: "a.o_onboarding_step_action[data-method=action_open_step_base_document_layout]", content: _t("Once everything is as you want it, validate."), position: "top", - skip_trigger: 'a[data-method=action_open_base_document_layout].o_onboarding_step_action__done', + skip_trigger: 'a[data-method=action_open_step_base_document_layout].o_onboarding_step_action__done', }, { - trigger: "a.o_onboarding_step_action[data-method=action_open_account_onboarding_create_invoice]", + trigger: "a[data-method=action_open_step_create_invoice]", content: _t("Now, we'll create your first invoice."), position: "bottom", }, { diff --git a/addons/account/static/src/views/res_company_onboarding_form_controller.js b/addons/account/static/src/views/res_company_onboarding_form_controller.js new file mode 100644 index 00000000000..d6212a98e4a --- /dev/null +++ b/addons/account/static/src/views/res_company_onboarding_form_controller.js @@ -0,0 +1,12 @@ +/** @odoo-module **/ + +import OnboardingStepFormController from "@onboarding/views/form/onboarding_step_form_controller"; + +/** + * Validate the onboarding step on saving a record of another model (here `res.company`). + **/ +export default class CompanyOnboardingFormController extends OnboardingStepFormController { + get stepName() { + return "account.onboarding_onboarding_step_company_data"; + } +} diff --git a/addons/account/static/src/views/res_company_onboarding_form_view.js b/addons/account/static/src/views/res_company_onboarding_form_view.js new file mode 100644 index 00000000000..c40a897862d --- /dev/null +++ b/addons/account/static/src/views/res_company_onboarding_form_view.js @@ -0,0 +1,15 @@ +/** @odoo-module **/ + +import { formView } from "@web/views/form/form_view"; +import { registry } from "@web/core/registry"; + +import CompanyOnboardingFormController from "./res_company_onboarding_form_controller.js"; + + +const CompanyOnboardingFormView = { + ...formView, + Controller: CompanyOnboardingFormController, +}; + + +registry.category("views").add("company_onboarding_form", CompanyOnboardingFormView); diff --git a/addons/account/views/account_journal_dashboard_view.xml b/addons/account/views/account_journal_dashboard_view.xml index 924eba2b989..7bfc4e1810a 100644 --- a/addons/account/views/account_journal_dashboard_view.xml +++ b/addons/account/views/account_journal_dashboard_view.xml @@ -3,7 +3,7 @@ account.journal.dashboard.kanban account.journal -