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 - + diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index 21d0399b2d7..a1f80508f2e 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -447,7 +447,7 @@ primary - /account/account_invoice_onboarding + /onboarding/account_invoice Invoice Currency diff --git a/addons/account/views/account_onboarding_templates.xml b/addons/account/views/account_onboarding_templates.xml deleted file mode 100644 index 9d3d2efcd05..00000000000 --- a/addons/account/views/account_onboarding_templates.xml +++ /dev/null @@ -1,156 +0,0 @@ - - - - - - Invoice Layout - Customize the look of your invoices. - - - - Customize - Looks great! - - - - - - - - Create Invoice - - Create your first invoice. - - - - Create - First invoice sent! - - - - - - - - Bank Account - - Connect your financial accounts in seconds. - - - - Add a bank account - Step Completed! - - - - - - - - Accounting Periods - Define your fiscal years & tax returns periodicity. - - - Configure - Step Completed! - - - - - - - - Chart of Accounts - Setup your chart of accounts and record initial balances. - - - Review - Chart of accounts set. - - - - - - - - Taxes - Set default Taxes for sales and purchase transactions. - - - Review - Taxes set. - - - - - - - - - - - Congratulations! You are all set. - - - - - - - - - - - - - - - - - - - - Create first invoice - account.move - form - - {'default_move_type': 'out_invoice'} - - - - - Taxes - Choose a default sales tax for your products. - Set taxes - - - Step Completed! - - - - - - - account.invoice.layout.onboarding.form - res.company - primary - 1000 - - - Choose a default sales tax for your products. - - - - - - - - Sales tax - res.company - form - - new - - diff --git a/addons/account/views/res_company_views.xml b/addons/account/views/res_company_views.xml index 65b5637a91c..b74c7d32d47 100644 --- a/addons/account/views/res_company_views.xml +++ b/addons/account/views/res_company_views.xml @@ -33,4 +33,35 @@ + + + res.company.form.view.onboarding + res.company + + primary + 1000 + + + + company_onboarding_form + + + + + + res.company.form.view.onboarding.sale.tax + res.company + 1000 + + + Choose a default sales tax for your products. + + + + + + diff --git a/addons/account/wizard/base_document_layout.py b/addons/account/wizard/base_document_layout.py index ab76e69aaa6..873f663175c 100644 --- a/addons/account/wizard/base_document_layout.py +++ b/addons/account/wizard/base_document_layout.py @@ -5,7 +5,9 @@ class BaseDocumentLayout(models.TransientModel): _inherit = 'base.document.layout' def document_layout_save(self): + """Save layout and onboarding step progress, return super() result""" res = super(BaseDocumentLayout, self).document_layout_save() - for wizard in self: - wizard.company_id.action_save_onboarding_invoice_layout() + if step := self.env.ref('account.onboarding_onboarding_step_base_document_layout', raise_if_not_found=False): + for company_id in self.company_id: + step.with_company(company_id).action_set_just_done() return res diff --git a/addons/account/wizard/setup_wizards.py b/addons/account/wizard/setup_wizards.py index d4da41cc8a2..3028f131fe7 100644 --- a/addons/account/wizard/setup_wizards.py +++ b/addons/account/wizard/setup_wizards.py @@ -60,7 +60,7 @@ class FinancialYearOpeningWizard(models.TransientModel): return super().write(vals) def action_save_onboarding_fiscal_year(self): - self.env.company.sudo().set_onboarding_step_done('account_setup_fy_data_state') + return self.env['onboarding.onboarding.step'].action_validate_step('account.onboarding_onboarding_step_fiscal_year') class SetupBarBankConfigWizard(models.TransientModel): @@ -146,7 +146,7 @@ class SetupBarBankConfigWizard(models.TransientModel): selected_journal.name = record.new_journal_name def validate(self): - """ Called by the validation button of this wizard. Serves as an + """Called by the validation button of this wizard. Serves as an extension hook in account_bank_statement_import. """ - self.linked_journal_id.mark_bank_setup_as_done_action() + return self.env["onboarding.onboarding.step"].action_validate_step("account.onboarding_onboarding_step_bank_account") diff --git a/addons/account_payment/__manifest__.py b/addons/account_payment/__manifest__.py index dbcec3696bc..eda961eafea 100644 --- a/addons/account_payment/__manifest__.py +++ b/addons/account_payment/__manifest__.py @@ -9,6 +9,7 @@ 'auto_install': ['account'], 'data': [ 'data/ir_config_parameter.xml', + 'data/onboarding_data.xml', 'security/ir.model.access.csv', 'security/ir_rules.xml', @@ -19,7 +20,6 @@ 'views/account_move_views.xml', 'views/account_journal_views.xml', 'views/account_payment_views.xml', - 'views/onboarding_templates.xml', 'views/payment_provider_views.xml', 'views/payment_transaction_views.xml', diff --git a/addons/account_payment/data/onboarding_data.xml b/addons/account_payment/data/onboarding_data.xml new file mode 100644 index 00000000000..20e1503e099 --- /dev/null +++ b/addons/account_payment/data/onboarding_data.xml @@ -0,0 +1,17 @@ + + + + + Online Payments + Enable credit & debit card payments supported by Stripe. + Activate Stripe + action_open_step_payment_provider + + onboarding_bank-account.png + Onboarding Online Payments + + + + diff --git a/addons/account_payment/models/__init__.py b/addons/account_payment/models/__init__.py index b3efe32217f..db619b3b29c 100644 --- a/addons/account_payment/models/__init__.py +++ b/addons/account_payment/models/__init__.py @@ -5,6 +5,6 @@ from . import account_move from . import account_payment from . import account_payment_method from . import account_payment_method_line +from . import onboarding_onboarding_step from . import payment_provider from . import payment_transaction -from . import res_company diff --git a/addons/account_payment/models/onboarding_onboarding_step.py b/addons/account_payment/models/onboarding_onboarding_step.py new file mode 100644 index 00000000000..59bb6d2a668 --- /dev/null +++ b/addons/account_payment/models/onboarding_onboarding_step.py @@ -0,0 +1,20 @@ +# 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' + + @api.model + def action_open_step_payment_provider(self): + self.env.company.payment_onboarding_payment_method = 'stripe' + menu = self.env.ref('account_payment.payment_provider_menu', raise_if_not_found=False) + menu_id = menu.id if menu else None + return self.env.company._run_payment_onboarding_step(menu_id) + + @api.model + def action_validate_step_payment_provider(self): + validation_response = super().action_validate_step_payment_provider() + self.action_validate_step("account_payment.onboarding_onboarding_step_payment_provider") + return validation_response diff --git a/addons/account_payment/models/res_company.py b/addons/account_payment/models/res_company.py deleted file mode 100644 index 2ed14af310c..00000000000 --- a/addons/account_payment/models/res_company.py +++ /dev/null @@ -1,19 +0,0 @@ -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import api, models - - -class ResCompany(models.Model): - _inherit = 'res.company' - - @api.model - def action_open_payment_onboarding(self): - self.env.company.payment_onboarding_payment_method = 'stripe' - menu = self.env.ref('account_payment.payment_provider_menu', raise_if_not_found=False) - menu_id = menu and menu.id - return self._run_payment_onboarding_step(menu_id) - - def get_account_invoice_onboarding_steps_states_names(self): - """ Override of `account` to add the state of the payment onboarding step. """ - steps = super().get_account_invoice_onboarding_steps_states_names() - return steps + ['payment_provider_onboarding_state'] diff --git a/addons/account_payment/views/onboarding_templates.xml b/addons/account_payment/views/onboarding_templates.xml deleted file mode 100644 index c689f85c2df..00000000000 --- a/addons/account_payment/views/onboarding_templates.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - Online Payments - Enable credit & debit card payments supported by Stripe - - - Activate Stripe - Online payments enabled - - - - - - - diff --git a/addons/onboarding/__manifest__.py b/addons/onboarding/__manifest__.py index 759d6af3e0f..65fb4f35d48 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': """ @@ -13,10 +13,21 @@ This module allows to manage onboardings and their progress 'depends': ['base'], 'installable': True, 'data': [ - 'data/onboarding_data.xml', + 'views/onboarding_templates.xml', 'views/onboarding_views.xml', 'views/onboarding_menus.xml', 'security/ir.model.access.csv', ], + 'assets': { + 'web.assets_backend': [ + 'onboarding/static/src/**/*', + ], + "web.dark_mode_variables": [ + ('before', 'onboarding/static/src/scss/onboarding.variables.scss', 'onboarding/static/src/scss/onboarding.variables.dark.scss'), + ], + 'web._assets_primary_variables': [ + 'onboarding/static/src/scss/onboarding.variables.scss', + ] + }, 'license': 'LGPL-3', } diff --git a/addons/onboarding/data/onboarding_data.xml b/addons/onboarding/data/onboarding_data.xml deleted file mode 100644 index 8c18ac54bfa..00000000000 --- a/addons/onboarding/data/onboarding_data.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - onboarding.onboarding.step - - - - - - - - - - - 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 5630a339773..4eac4da3329 100644 --- a/addons/onboarding/models/onboarding_onboarding.py +++ b/addons/onboarding/models/onboarding_onboarding.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import _, api, fields, models from odoo.addons.onboarding.models.onboarding_progress import ONBOARDING_PROGRESS_STATES @@ -13,10 +13,15 @@ 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') + step_ids = fields.Many2many('onboarding.onboarding.step', string='Onboarding steps') - is_per_company = fields.Boolean('Should be done per company?', default=True) + text_completed = fields.Char( + 'Message at completion', default=_('Nice work! Your configuration is done.'), + help='Text shown on onboarding when completed') + is_per_company = fields.Boolean( + 'Should be done per company?', compute='_compute_is_per_company', readonly=True, store=False, + ) panel_close_action_name = fields.Char( 'Closing action', help='Name of the onboarding model action to execute when closing the panel.') @@ -36,15 +41,24 @@ 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( 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 @@ -53,28 +67,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 @@ -83,8 +113,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): @@ -94,6 +128,7 @@ class Onboarding(models.Model): 'close_model': 'onboarding.onboarding', 'steps': self.step_ids, 'state': self.current_progress_id._get_and_update_onboarding_state(), + 'text_completed': self.text_completed, } return values 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 565534f083c..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,27 +41,56 @@ 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: 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: 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) @@ -82,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/odoo/addons/base/static/src/scss/onboarding.scss b/addons/onboarding/static/src/scss/onboarding.scss similarity index 93% rename from odoo/addons/base/static/src/scss/onboarding.scss rename to addons/onboarding/static/src/scss/onboarding.scss index 758391ecc96..88c2e9e6b2b 100644 --- a/odoo/addons/base/static/src/scss/onboarding.scss +++ b/addons/onboarding/static/src/scss/onboarding.scss @@ -11,8 +11,10 @@ $o-onboarding-step-width: map-get($container-max-widths, 'lg') / 4 !default; } .o_onboarding_completed_message { + opacity: 0; box-shadow: 0 -7px 20px -5px rgba(#000, 0.3); - animation: o_onboarding_slideInUp $o-onboarding-base-time ease-out; + animation: o_onboarding_slideInUpDownOut; + animation-duration: 6s; } // = Step Design @@ -114,6 +116,11 @@ $o-onboarding-step-width: map-get($container-max-widths, 'lg') / 4 !default; } } + @keyframes o_onboarding_slideInUpDownOut { + 25%, 85% { transform: translate3d(0, -80%, 0); opacity: 1; } + 0%, 100% { transform: translate3d(0, 0, 0); } + } + @keyframes o_onboarding_zoomIn { from { transform: scale3d(0.8, 0.8, 0.8); diff --git a/odoo/addons/base/static/src/scss/onboarding.variables.dark.scss b/addons/onboarding/static/src/scss/onboarding.variables.dark.scss similarity index 100% rename from odoo/addons/base/static/src/scss/onboarding.variables.dark.scss rename to addons/onboarding/static/src/scss/onboarding.variables.dark.scss diff --git a/odoo/addons/base/static/src/scss/onboarding.variables.scss b/addons/onboarding/static/src/scss/onboarding.variables.scss similarity index 100% rename from odoo/addons/base/static/src/scss/onboarding.variables.scss rename to addons/onboarding/static/src/scss/onboarding.variables.scss 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/__init__.py b/addons/onboarding/tests/__init__.py index f01d0da0642..efa3fd75052 100644 --- a/addons/onboarding/tests/__init__.py +++ b/addons/onboarding/tests/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import case from . import test_onboarding from . import test_onboarding_concurrency diff --git a/addons/onboarding/tests/case.py b/addons/onboarding/tests/case.py new file mode 100644 index 00000000000..3fa1aa8f0d4 --- /dev/null +++ b/addons/onboarding/tests/case.py @@ -0,0 +1,40 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests.common import TransactionCase + + +class TransactionCaseOnboarding(TransactionCase): + def assert_step_is_done(self, step, also_with_company=None): + self.assertIn( + step.current_progress_step_id.step_state, {'done', 'just_done'}, + f'Expected done-like current state for step {step.id} for {step.env.company.name}') + self.assertEqual(step.current_progress_step_id.step_state, step.current_step_state) + if also_with_company: + self.assert_step_is_done(step.with_company(also_with_company)) + + def assert_step_is_not_done(self, step, also_with_company=None): + self.assertIn( + step.current_progress_step_id.step_state, {'not_done', False}, + f'Expected "not_done" current state for step {step.id} for {step.env.company.name}') + self.assertEqual(step.current_step_state, 'not_done') + if also_with_company: + self.assert_step_is_not_done(step.with_company(also_with_company)) + + def assert_onboarding_is_done(self, onboarding, also_with_company=None): + self.assertIn( + onboarding.current_progress_id.onboarding_state, {'done', 'just_done'}, + f'Expected done-like current state for onboarding "{onboarding.name}" ' + f'for "{onboarding.env.company.name}"') + self.assertEqual(onboarding.current_progress_id.onboarding_state, + onboarding.current_onboarding_state) + if also_with_company: + self.assert_onboarding_is_done(onboarding.with_company(also_with_company)) + + def assert_onboarding_is_not_done(self, onboarding, also_with_company=None): + self.assertIn( + onboarding.current_progress_id.onboarding_state, {'not_done', False}, + f'Expected `"not_done"` or `False` current state for onboarding {onboarding.name} ' + f'for {onboarding.env.company.name}') + self.assertEqual(onboarding.current_onboarding_state, 'not_done') + if also_with_company: + self.assert_onboarding_is_not_done(onboarding.with_company(also_with_company)) diff --git a/addons/onboarding/tests/common.py b/addons/onboarding/tests/common.py index 9f4dba3f271..acd18d6ab1c 100644 --- a/addons/onboarding/tests/common.py +++ b/addons/onboarding/tests/common.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.tests.common import TransactionCase +from odoo.addons.onboarding.tests.case import TransactionCaseOnboarding -class TestOnboardingCommon(TransactionCase): +class TestOnboardingCommon(TransactionCaseOnboarding): @classmethod def setUpClass(cls): super().setUpClass() @@ -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,49 +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() - - 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)) + (cls.onboarding_1 + cls.onboarding_2).with_company(cls.company_1)._search_or_create_progress() def activate_company(self, company): self.onboarding_1_step_1 = self.onboarding_1_step_1.with_company(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/odoo/addons/base/views/onboarding_views.xml b/addons/onboarding/views/onboarding_templates.xml similarity index 64% rename from odoo/addons/base/views/onboarding_views.xml rename to addons/onboarding/views/onboarding_templates.xml index 4b27d26b233..6b57a5540c2 100644 --- a/odoo/addons/base/views/onboarding_views.xml +++ b/addons/onboarding/views/onboarding_templates.xml @@ -1,5 +1,27 @@ - + + + + + + + + + + + + + + + onboarding.onboarding.step + + + + + + + @@ -13,8 +35,8 @@ @@ -79,44 +99,4 @@ - - - - - Company Data - - Set your company's data for documents header/footer. - - - - Let's start! - - - - - - - - base.company.onboarding.form - res.company - - primary - 1000 - - - - - - - - - Set your company data - res.company - form - - new - 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 @@ - + + + + + + + diff --git a/addons/payment/__manifest__.py b/addons/payment/__manifest__.py index 5f948030f51..1e3e73d42fb 100644 --- a/addons/payment/__manifest__.py +++ b/addons/payment/__manifest__.py @@ -5,8 +5,9 @@ 'version': '2.0', 'category': 'Hidden', 'summary': "The payment engine used by payment provider modules.", - 'depends': ['portal'], + 'depends': ['onboarding', 'portal'], 'data': [ + 'data/onboarding_data.xml', 'data/payment_method_data.xml', 'data/payment_provider_data.xml', 'data/payment_cron.xml', diff --git a/addons/payment/data/onboarding_data.xml b/addons/payment/data/onboarding_data.xml new file mode 100644 index 00000000000..ffe9c0588a3 --- /dev/null +++ b/addons/payment/data/onboarding_data.xml @@ -0,0 +1,10 @@ + + + + + + Online Payments + 99 + + + diff --git a/addons/payment/models/__init__.py b/addons/payment/models/__init__.py index ffb32ae6268..b91b37f64ae 100644 --- a/addons/payment/models/__init__.py +++ b/addons/payment/models/__init__.py @@ -5,5 +5,6 @@ from . import payment_method from . import payment_provider from . import payment_token from . import payment_transaction +from . import onboarding_onboarding_step from . import res_company from . import res_partner diff --git a/addons/payment/models/onboarding_onboarding_step.py b/addons/payment/models/onboarding_onboarding_step.py new file mode 100644 index 00000000000..242e00d8082 --- /dev/null +++ b/addons/payment/models/onboarding_onboarding_step.py @@ -0,0 +1,12 @@ +# 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' + + @api.model + def action_validate_step_payment_provider(self): + """Override to validate other steps as well.""" + return self.action_validate_step("payment.onboarding_onboarding_step_payment_provider") diff --git a/addons/payment/models/res_company.py b/addons/payment/models/res_company.py index 271e0b62b47..6ca7745ed01 100644 --- a/addons/payment/models/res_company.py +++ b/addons/payment/models/res_company.py @@ -6,10 +6,6 @@ from odoo import api, fields, models class ResCompany(models.Model): _inherit = 'res.company' - payment_provider_onboarding_state = fields.Selection( - string="State of the onboarding payment provider step", - selection=[('not_done', "Not done"), ('just_done', "Just done"), ('done', "Done")], - default='not_done') payment_onboarding_payment_method = fields.Selection( string="Selected onboarding payment method", selection=[ @@ -56,15 +52,3 @@ class ResCompany(models.Model): modules_sudo = self.env['ir.module.module'].sudo().search([('name', 'in', module_names)]) STATES = ['installed', 'to install', 'to upgrade'] modules_sudo.filtered(lambda m: m.state not in STATES).button_immediate_install() - - def _mark_payment_onboarding_step_as_done(self): - """ Mark the payment onboarding step as done. - - :return: None - """ - self.set_onboarding_step_done('payment_provider_onboarding_state') - - def get_account_invoice_onboarding_steps_states_names(self): - """ Override of account. """ - steps = super().get_account_invoice_onboarding_steps_states_names() - return steps + ['payment_provider_onboarding_state'] diff --git a/addons/payment/wizards/payment_onboarding_wizard.py b/addons/payment/wizards/payment_onboarding_wizard.py index 73edbc9abc4..90d539cae4a 100644 --- a/addons/payment/wizards/payment_onboarding_wizard.py +++ b/addons/payment/wizards/payment_onboarding_wizard.py @@ -134,13 +134,10 @@ class PaymentWizard(models.TransientModel): if payment_method == 'stripe': return self._start_stripe_onboarding() - # the user clicked `apply` and not cancel so we can assume this step is done. - self._set_payment_provider_onboarding_step_done() + # the user clicked `apply` and not cancel, so we can assume this step is done. + self.env['onboarding.onboarding.step'].sudo().action_validate_step_payment_provider() return {'type': 'ir.actions.act_window_close'} - def _set_payment_provider_onboarding_step_done(self): - self.env.company.sudo().set_onboarding_step_done('payment_provider_onboarding_state') - def _start_stripe_onboarding(self): """ Start Stripe Connect onboarding. """ menu = self.env.ref('account_payment.payment_provider_menu', False) diff --git a/addons/payment_stripe/controllers/onboarding.py b/addons/payment_stripe/controllers/onboarding.py index d6795924668..8c9dda8091d 100644 --- a/addons/payment_stripe/controllers/onboarding.py +++ b/addons/payment_stripe/controllers/onboarding.py @@ -12,7 +12,7 @@ class OnboardingController(http.Controller): @http.route(_onboarding_return_url, type='http', methods=['GET'], auth='user') def stripe_return_from_onboarding(self, provider_id, menu_id): - """ Redirect the user to the provider form of the onboarded Stripe account. + """Redirect the user to the provider form of the onboarded Stripe account. The user is redirected to this route by Stripe after or during (if the user clicks on a dedicated button) the onboarding. @@ -23,7 +23,9 @@ class OnboardingController(http.Controller): `ir.ui.menu` id """ stripe_provider = request.env['payment.provider'].browse(int(provider_id)) - stripe_provider.company_id._mark_payment_onboarding_step_as_done() + request.env['onboarding.onboarding.step'].with_company( + stripe_provider.company_id + ).action_validate_step_payment_provider() action = request.env.ref('payment_stripe.action_payment_provider_onboarding') get_params_string = url_encode({'action': action.id, 'id': provider_id, 'menu_id': menu_id}) return request.redirect(f'/web?#{get_params_string}') diff --git a/addons/payment_stripe/models/payment_provider.py b/addons/payment_stripe/models/payment_provider.py index 8f179dd2680..0cac7956a71 100644 --- a/addons/payment_stripe/models/payment_provider.py +++ b/addons/payment_stripe/models/payment_provider.py @@ -134,7 +134,7 @@ class PaymentProvider(models.Model): self.ensure_one() if self.state == 'enabled': - self.company_id._mark_payment_onboarding_step_as_done() + self.env["onboarding.onboarding.step"].action_validate_step_payment_provider() action = {'type': 'ir.actions.act_window_close'} else: # Account creation diff --git a/addons/sale/__manifest__.py b/addons/sale/__manifest__.py index 2732d84d9c4..cc198bd4956 100644 --- a/addons/sale/__manifest__.py +++ b/addons/sale/__manifest__.py @@ -31,6 +31,7 @@ This module contains all the common features of Sales Management and eCommerce. 'data/mail_message_subtype_data.xml', 'data/mail_template_data.xml', 'data/ir_config_parameter.xml', # Needs mail_template_data + 'data/onboarding_data.xml', 'wizard/account_accrued_orders_wizard_views.xml', 'wizard/mass_cancel_orders_views.xml', @@ -51,7 +52,6 @@ This module contains all the common features of Sales Management and eCommerce. 'views/product_product_views.xml', 'views/product_views.xml', 'views/res_partner_views.xml', - 'views/sale_onboarding_views.xml', 'views/sale_order_line_views.xml', 'views/sale_portal_templates.xml', 'views/utm_campaign_views.xml', diff --git a/addons/sale/controllers/__init__.py b/addons/sale/controllers/__init__.py index 85653bbd9df..400dd86ae7f 100644 --- a/addons/sale/controllers/__init__.py +++ b/addons/sale/controllers/__init__.py @@ -2,5 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import catalog -from . import onboarding from . import portal diff --git a/addons/sale/controllers/onboarding.py b/addons/sale/controllers/onboarding.py deleted file mode 100644 index 2d90f083a4a..00000000000 --- a/addons/sale/controllers/onboarding.py +++ /dev/null @@ -1,31 +0,0 @@ -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo.http import request, route, Controller - - -class OnboardingController(Controller): - - @route('/sales/sale_quotation_onboarding_panel', auth='user', type='json') - def sale_quotation_onboarding(self, context=None): - """ Returns the `banner` for the sale 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.sale_quotation_onboarding_state == 'closed': - return {} - - return { - 'html': request.env['ir.qweb']._render( - 'sale.sale_quotation_onboarding_panel', - { - 'company': company, - 'state': company._get_and_update_sale_quotation_onboarding_state() - } - ) - } diff --git a/addons/sale/data/onboarding_data.xml b/addons/sale/data/onboarding_data.xml new file mode 100644 index 00000000000..53f51924e7e --- /dev/null +++ b/addons/sale/data/onboarding_data.xml @@ -0,0 +1,41 @@ + + + + + + Order Confirmation + Choose between electronic signatures or online payments. + Set payments + action_open_step_sale_order_confirmation + + onboarding_default.png + Onboarding Order Confirmation + 6 + + + + Sample Quotation + Send a quotation to test the customer portal. + Send sample + action_open_step_sample_quotation + + onboarding_sample-quotation.png + Onboarding Sample Quotation + 7 + + + + + Sale Quotation Onboarding + + sale_quotation + action_close_panel_sale_quotation + + + + diff --git a/addons/sale/models/__init__.py b/addons/sale/models/__init__.py index a55d995593a..6a8faf82e87 100644 --- a/addons/sale/models/__init__.py +++ b/addons/sale/models/__init__.py @@ -5,6 +5,8 @@ from . import analytic from . import account_move from . import account_move_line from . import crm_team +from . import onboarding_onboarding +from . import onboarding_onboarding_step from . import payment_provider from . import payment_transaction from . import product_product diff --git a/addons/sale/models/onboarding_onboarding.py b/addons/sale/models/onboarding_onboarding.py new file mode 100644 index 00000000000..bae39078945 --- /dev/null +++ b/addons/sale/models/onboarding_onboarding.py @@ -0,0 +1,12 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models + + +class Onboarding(models.Model): + _inherit = 'onboarding.onboarding' + + # Sale Quotation Onboarding + @api.model + def action_close_panel_sale_quotation(self): + self.action_close_panel('sale.onboarding_onboarding_sale_quotation') diff --git a/addons/sale/models/onboarding_onboarding_step.py b/addons/sale/models/onboarding_onboarding_step.py new file mode 100644 index 00000000000..68d34759929 --- /dev/null +++ b/addons/sale/models/onboarding_onboarding_step.py @@ -0,0 +1,100 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 + +from odoo import _, api, Command, models +from odoo.modules.module import get_resource_path +from odoo.tools import file_open + + +class OnboardingStep(models.Model): + _inherit = 'onboarding.onboarding.step' + + @api.model + def action_open_step_sale_order_confirmation(self): + self.env.company.get_chart_of_accounts_or_fail() + action = { + 'type': 'ir.actions.act_window', + 'name': _('Choose how to confirm quotations'), + 'res_model': 'sale.payment.provider.onboarding.wizard', + 'view_mode': 'form', + 'views': [(self.env.ref('payment.payment_provider_onboarding_wizard_form').id, 'form')], + 'target': 'new', + } + return action + + @api.model + def _get_sample_sales_order(self): + """ Get a sample quotation or create one if it does not exist. """ + # use current user as partner + partner = self.env.user.partner_id + company_id = self.env.company.id + # is there already one? + sample_sales_order = self.env['sale.order'].search([ + ('company_id', '=', company_id), + ('partner_id', '=', partner.id), + ('state', '=', 'draft'), + ], limit=1) + if not sample_sales_order: + # take any existing product or create one + product = self.env['product.product'].search([], limit=1) + if not product: + default_image_path = get_resource_path('product', 'static/img', 'product_product_13-image.jpg') + with file_open(default_image_path, 'rb') as default_image_stream: + product = self.env['product.product'].create({ + 'name': _('Sample Product'), + 'active': False, + 'image_1920': base64.b64encode(default_image_stream.read()), + }) + product.product_tmpl_id.active = False + sample_sales_order = self.env['sale.order'].create({ + 'partner_id': partner.id, + 'order_line': [ + Command.create({ + 'name': _('Sample Order Line'), + 'product_id': product.id, + 'product_uom_qty': 10, + 'price_unit': 123, + }) + ] + }) + return sample_sales_order + + @api.model + def action_open_step_sample_quotation(self): + """ Onboarding step for sending a sample quotation. Open a window to compose an email, + with the edi_invoice_template message loaded by default. """ + sample_sales_order = self._get_sample_sales_order() + template = self.env.ref('sale.email_template_edi_sale', False) + + self.env['mail.compose.message'].with_context( + mark_so_as_sent=True, + default_email_layout_xmlid='mail.mail_notification_layout_with_responsible_signature', + proforma=self.env.context.get('proforma', False), + ).create({ + 'res_ids': sample_sales_order.ids, + 'template_id': template.id if template else False, + 'model': sample_sales_order._name, + 'composition_mode': 'comment', + })._action_send_mail() + + self.action_validate_step('sale.onboarding_onboarding_step_sample_quotation') + sale_quotation_onboarding = self.env.ref('sale.onboarding_onboarding_sale_quotation', raise_if_not_found=False) + if sale_quotation_onboarding: + sale_quotation_onboarding.action_close() + + view_id = self.env.ref('sale.view_order_form').id + action = self.env['ir.actions.actions']._for_xml_id('sale.action_orders') + action.update({ + 'view_mode': 'form', + 'views': [[view_id, 'form']], + 'target': 'main', + }) + return action + + @api.model + def action_validate_step_payment_provider(self): + validation_response = super().action_validate_step_payment_provider() + if self.env.company.sale_onboarding_payment_method: # Set if the flow is/was done from the Sales panel + return self.action_validate_step('sale.onboarding_onboarding_step_sale_order_confirmation') + return validation_response diff --git a/addons/sale/models/res_company.py b/addons/sale/models/res_company.py index c22bd3768ff..d2bec4cf369 100644 --- a/addons/sale/models/res_company.py +++ b/addons/sale/models/res_company.py @@ -1,11 +1,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. -import base64 -from odoo import api, fields, models, _ -from odoo.fields import Command -from odoo.modules.module import get_module_resource - -from odoo.addons.account.models.company import DASHBOARD_ONBOARDING_STATES, ONBOARDING_STEP_STATES +from odoo import fields, models class ResCompany(models.Model): @@ -39,18 +34,6 @@ class ResCompany(models.Model): ) # sale onboarding - sale_quotation_onboarding_state = fields.Selection( - selection=DASHBOARD_ONBOARDING_STATES, - string="State of the sale onboarding panel", - default='not_done') - sale_onboarding_order_confirmation_state = fields.Selection( - selection=ONBOARDING_STEP_STATES, - string="State of the onboarding confirmation order step", - default='not_done') - sale_onboarding_sample_quotation_state = fields.Selection( - selection=ONBOARDING_STEP_STATES, - string="State of the onboarding sample quotation step", - default='not_done') sale_onboarding_payment_method = fields.Selection( selection=[ ('digital_signature', "Sign online"), @@ -60,104 +43,3 @@ class ResCompany(models.Model): ('manual', "Manual Payment"), ], string="Sale onboarding selected payment method") - - @api.model - def action_close_sale_quotation_onboarding(self): - """ Mark the onboarding panel as closed. """ - self.env.company.sale_quotation_onboarding_state = 'closed' - - @api.model - def action_open_sale_onboarding_payment_provider(self): - """ Called by onboarding panel above the quotation list.""" - self.env.company.get_chart_of_accounts_or_fail() - return self.env['ir.actions.actions']._for_xml_id('sale.action_open_sale_payment_provider_onboarding_wizard') - - def _mark_payment_onboarding_step_as_done(self): - """ Override of payment to mark the sale onboarding step as done. - - The payment onboarding step of Sales is only marked as done if it was started from Sales. - This prevents incorrectly marking the step as done if another module's payment onboarding - step was marked as done. - - :return: None - """ - super()._mark_payment_onboarding_step_as_done() - if self.sale_onboarding_payment_method: # The onboarding step was started from Sales - self.set_onboarding_step_done('sale_onboarding_order_confirmation_state') - - def _get_sample_sales_order(self): - """ Get a sample quotation or create one if it does not exist. """ - # use current user as partner - partner = self.env.user.partner_id - company_id = self.env.company.id - # is there already one? - sample_sales_order = self.env['sale.order'].search([ - ('company_id', '=', company_id), - ('partner_id', '=', partner.id), - ('state', '=', 'draft'), - ], limit=1) - if not sample_sales_order: - # take any existing product or create one - product = self.env['product.product'].search([], limit=1) - if not product: - default_image_path = get_module_resource('product', 'static/img', 'product_product_13-image.jpg') - product = self.env['product.product'].create({ - 'name': _("Sample Product"), - 'active': False, - 'image_1920': base64.b64encode(open(default_image_path, 'rb').read()) - }) - product.product_tmpl_id.write({'active': False}) - sample_sales_order = self.env['sale.order'].create({ - 'partner_id': partner.id, - 'order_line': [ - Command.create({ - 'name': _("Sample Order Line"), - 'product_id': product.id, - 'product_uom_qty': 10, - 'price_unit': 123, - }) - ] - }) - return sample_sales_order - - @api.model - def action_open_sale_onboarding_sample_quotation(self): - """ Onboarding step for sending a sample quotation. Open a window to compose an email, - with the edi_invoice_template message loaded by default. """ - sample_sales_order = self._get_sample_sales_order() - template = self.env.ref('sale.email_template_edi_sale', False) - - self.env['mail.compose.message'].with_context( - mark_so_as_sent=True, - default_email_layout_xmlid='mail.mail_notification_layout_with_responsible_signature', - proforma=self.env.context.get('proforma', False), - force_email=True, - ).create({ - 'res_ids': sample_sales_order.ids, - 'template_id': template.id if template else False, - 'model': sample_sales_order._name, - 'composition_mode': 'comment', - })._action_send_mail() - - self.set_onboarding_step_done('sale_onboarding_sample_quotation_state') - - self.action_close_sale_quotation_onboarding() - - action = self.env['ir.actions.actions']._for_xml_id('sale.action_orders') - action.update({ - 'views': [[self.env.ref('sale.view_order_form').id, 'form']], - 'view_mode': 'form', - 'target': 'main', - }) - return action - - def _get_and_update_sale_quotation_onboarding_state(self): - """ This method is called on the controller rendering method and ensures that the animations - are displayed only one time. """ - steps = [ - 'base_onboarding_company_state', - 'account_onboarding_invoice_layout_state', - 'sale_onboarding_order_confirmation_state', - 'sale_onboarding_sample_quotation_state', - ] - return self._get_and_update_onboarding_state('sale_quotation_onboarding_state', steps) diff --git a/addons/sale/static/src/img/sale_quotation_onboarding_bg.jpg b/addons/sale/static/src/img/sale_quotation_onboarding_bg.jpg deleted file mode 100644 index 3c0f7a340c7..00000000000 Binary files a/addons/sale/static/src/img/sale_quotation_onboarding_bg.jpg and /dev/null differ diff --git a/addons/sale/static/src/js/tours/sale.js b/addons/sale/static/src/js/tours/sale.js index 6390a8b89b2..f170c7d1109 100644 --- a/addons/sale/static/src/js/tours/sale.js +++ b/addons/sale/static/src/js/tours/sale.js @@ -23,58 +23,58 @@ registry.category("web_tour.tours").add("sale_tour", { position: "bottom", edition: "enterprise" }, { - trigger: 'a.o_onboarding_step_action.btn[data-method=action_open_base_onboarding_company]', + trigger: 'a.o_onboarding_step_action.btn[data-method=action_open_step_company_data]', extra_trigger: ".o_sale_order", 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: 'input[id=street_0]', content: _t("Complete 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: ".modal-content button[name='action_save_onboarding_company_step']", + trigger: ".modal-content button.o_form_button_save", content: _t("Looks good. Let's continue."), position: "left", - 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', }, stepUtils.showAppsMenuItem(), { trigger: ".o_app[data-menu-xmlid='sale.sale_menu_root']", - 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', edition: "community", auto: true, }, { trigger: ".o_app[data-menu-xmlid='sale.sale_menu_root']", - 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', edition: "enterprise", auto: true, }, { - trigger: 'a.o_onboarding_step_action.btn[data-method=action_open_base_document_layout]', + trigger: 'a.o_onboarding_step_action.btn[data-method=action_open_step_base_document_layout]', extra_trigger: ".o_sale_order", content: _t("Customize your quotes and orders."), 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: ".o_sale_order", content: _t("Good job, let's continue."), position: "top", // dot NOT move to bottom, it would cause a resize flicker - 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.btn[data-method=action_open_sale_onboarding_payment_provider]', + trigger: 'a.o_onboarding_step_action.btn[data-method=action_open_step_sale_order_confirmation]', extra_trigger: ".o_sale_order", content: _t("To speed up order confirmation, we can activate electronic signatures or payments."), position: "bottom", - skip_trigger: 'a[data-method=action_open_sale_onboarding_payment_provider].o_onboarding_step_action__done', + skip_trigger: 'a[data-method=action_open_step_sale_order_confirmation].o_onboarding_step_action__done', }, { trigger: "button[name='add_payment_methods']", extra_trigger: ".o_sale_order", content: _t("Lets keep electronic signature for now."), position: "bottom", - skip_trigger: 'a[data-method=action_open_sale_onboarding_payment_provider].o_onboarding_step_action__done', + skip_trigger: 'a[data-method=action_open_step_sale_order_confirmation].o_onboarding_step_action__done', }, { - trigger: 'a.o_onboarding_step_action.btn[data-method=action_open_sale_onboarding_sample_quotation]', + trigger: 'a.o_onboarding_step_action.btn[data-method=action_open_step_sample_quotation]', extra_trigger: ".o_sale_order", content: _t("Now, we'll create a sample quote."), position: "bottom", diff --git a/addons/sale/tests/__init__.py b/addons/sale/tests/__init__.py index 2a21d3536a3..1c2958aa0b9 100644 --- a/addons/sale/tests/__init__.py +++ b/addons/sale/tests/__init__.py @@ -5,6 +5,7 @@ from . import test_access_rights from . import test_accrued_sale_orders from . import test_common from . import test_controllers +from . import test_onboarding from . import test_onchange from . import test_payment_flow from . import test_reinvoice diff --git a/addons/sale/tests/test_onboarding.py b/addons/sale/tests/test_onboarding.py new file mode 100644 index 00000000000..7b52c90ea45 --- /dev/null +++ b/addons/sale/tests/test_onboarding.py @@ -0,0 +1,30 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.onboarding.tests.case import TransactionCaseOnboarding + + +class TestOnboarding(TransactionCaseOnboarding): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.account_payment_provider_step = cls.env.ref( + "account_payment.onboarding_onboarding_step_payment_provider" + ) + cls.sale_quotation_order_confirmation_step = cls.env.ref( + "sale.onboarding_onboarding_step_sale_order_confirmation" + ) + + def test_payment_provider_account_doesnt_validate_sales(self): + self.assert_step_is_not_done(self.sale_quotation_order_confirmation_step) + self.env["onboarding.onboarding.step"].action_validate_step_payment_provider() + self.assert_step_is_not_done(self.sale_quotation_order_confirmation_step) + + # Set field as in payment_provider_onboarding_wizard's add_payment_method override + self.env.company.sale_onboarding_payment_method = "stripe" + self.env["onboarding.onboarding.step"].action_validate_step_payment_provider() + self.assert_step_is_done(self.sale_quotation_order_confirmation_step) + + def test_payment_provider_sales_validates_account(self): + self.assert_step_is_not_done(self.account_payment_provider_step) + self.env["onboarding.onboarding.step"].action_validate_step_payment_provider() + self.assert_step_is_done(self.account_payment_provider_step) diff --git a/addons/sale/tests/test_sale_onboarding.py b/addons/sale/tests/test_sale_onboarding.py index 148a52b669d..08fb153e097 100644 --- a/addons/sale/tests/test_sale_onboarding.py +++ b/addons/sale/tests/test_sale_onboarding.py @@ -15,4 +15,4 @@ class TestOnboarding(HttpCase): ('state', '=', 'draft') ]).state = 'cancel' self.env['product.product'].search([]).active = False - self.env.company._get_sample_sales_order() + self.env['onboarding.onboarding.step']._get_sample_sales_order() diff --git a/addons/sale/views/sale_onboarding_views.xml b/addons/sale/views/sale_onboarding_views.xml deleted file mode 100644 index de3cac7ac12..00000000000 --- a/addons/sale/views/sale_onboarding_views.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - Quotation Layout - Customize the look of your quotations. - - - - Looks great! - Customize - - - - - - - - Order Confirmation - Choose between electronic signatures or online payments. - - - Set payments - - - - - - - - Sample Quotation - Send a quotation to test the customer portal. - - - Send sample - - - - - - - - - - - - - - - - - - - - Choose how to confirm quotations - sale.payment.provider.onboarding.wizard - form - - new - - diff --git a/addons/sale/views/sale_order_views.xml b/addons/sale/views/sale_order_views.xml index 7f539755604..794fe169cf9 100644 --- a/addons/sale/views/sale_order_views.xml +++ b/addons/sale/views/sale_order_views.xml @@ -215,7 +215,7 @@ primary - /sales/sale_quotation_onboarding_panel + /onboarding/sale_quotation diff --git a/addons/sale/wizard/payment_provider_onboarding_wizard.py b/addons/sale/wizard/payment_provider_onboarding_wizard.py index 9ecab2fdf66..d3bc79f187d 100644 --- a/addons/sale/wizard/payment_provider_onboarding_wizard.py +++ b/addons/sale/wizard/payment_provider_onboarding_wizard.py @@ -20,11 +20,6 @@ class PaymentWizard(models.TransientModel): ('paypal', "PayPal"), ('manual', "Custom payment instructions"), ], default=_get_default_payment_method) - # - - def _set_payment_provider_onboarding_step_done(self): - """ Override. """ - self.env.company.sudo().set_onboarding_step_done('sale_onboarding_order_confirmation_state') def add_payment_methods(self): self.env.company.sale_onboarding_payment_method = self.payment_method diff --git a/addons/sale_management/tests/test_sale_ui.py b/addons/sale_management/tests/test_sale_ui.py index 9d73a4400c3..96a9b420d26 100644 --- a/addons/sale_management/tests/test_sale_ui.py +++ b/addons/sale_management/tests/test_sale_ui.py @@ -11,8 +11,8 @@ class TestUi(HttpCase): self.start_tour("/web", 'sale_tour', login="admin", step_delay=100) def test_02_sale_tour_company_onboarding_done(self): - self.env.company.set_onboarding_step_done('base_onboarding_company_state') - self.start_tour("/web", 'sale_tour', login="admin", step_delay=100) + self.env["onboarding.onboarding.step"].action_validate_step("account.onboarding_onboarding_step_company_data") + self.start_tour("/web", "sale_tour", login="admin", step_delay=100) def test_03_sale_quote_tour(self): self.env['res.partner'].create({'name': 'Agrolait', 'email': 'agro@lait.be'}) diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py index 209168aea8c..48e07bab124 100644 --- a/addons/web/__manifest__.py +++ b/addons/web/__manifest__.py @@ -205,7 +205,6 @@ This module provides the core of the Odoo Web Client. 'web/static/src/legacy/scss/ace.scss', 'web/static/src/legacy/scss/fields.scss', 'web/static/src/legacy/scss/views.scss', - 'base/static/src/scss/onboarding.scss', 'web/static/src/legacy/scss/attachment_preview.scss', 'web/static/src/legacy/scss/base_document_layout.scss', 'web/static/src/legacy/scss/fields_extra.scss', @@ -505,9 +504,6 @@ This module provides the core of the Odoo Web Client. ('include', 'web.assets_backend'), 'web/static/src/**/*.dark.scss', ], - "web.dark_mode_variables": [ - ('before', 'base/static/src/scss/onboarding.variables.scss', 'base/static/src/scss/onboarding.variables.dark.scss'), - ], # --------------------------------------------------------------------- # SUB BUNDLES @@ -525,7 +521,6 @@ This module provides the core of the Odoo Web Client. 'web._assets_primary_variables': [ 'web/static/src/scss/primary_variables.scss', 'web/static/src/**/*.variables.scss', - 'base/static/src/scss/onboarding.variables.scss', ], 'web._assets_secondary_variables': [ 'web/static/src/scss/secondary_variables.scss', diff --git a/addons/web/models/base_document_layout.py b/addons/web/models/base_document_layout.py index 47260ba25be..4921124cbbf 100644 --- a/addons/web/models/base_document_layout.py +++ b/addons/web/models/base_document_layout.py @@ -246,14 +246,6 @@ class BaseDocumentLayout(models.TransientModel): return tools.rgb_to_hex(primary), tools.rgb_to_hex(secondary) - @api.model - def action_open_base_document_layout(self, action_ref=None): - if not action_ref: - action_ref = 'web.action_base_document_layout_configurator' - res = self.env["ir.actions.actions"]._for_xml_id(action_ref) - self.env[res["res_model"]].check_access_rights('write') - return res - def document_layout_save(self): # meant to be overridden return self.env.context.get('report_action') or {'type': 'ir.actions.act_window_close'} diff --git a/addons/website_sale/models/res_company.py b/addons/website_sale/models/res_company.py index cc9df3497d6..8e4b6998a0a 100644 --- a/addons/website_sale/models/res_company.py +++ b/addons/website_sale/models/res_company.py @@ -1,21 +1,12 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import models class ResCompany(models.Model): _inherit = 'res.company' - website_sale_onboarding_payment_provider_state = fields.Selection([('not_done', "Not done"), ('just_done', "Just done"), ('done', "Done")], string="State of the website sale onboarding payment provider step", default='not_done') - - @api.model - def action_open_website_sale_onboarding_payment_provider(self): - """ Called by onboarding panel above the quotation list.""" - self.env.company.payment_onboarding_payment_method = 'stripe' - menu_id = self.env.ref('website.menu_website_dashboard').id - return self._run_payment_onboarding_step(menu_id) - def _get_default_pricelist_vals(self): """ Override of product. Called at company creation or activation of the pricelist setting. diff --git a/odoo/addons/base/__manifest__.py b/odoo/addons/base/__manifest__.py index 366f62fe37c..bd4488558c6 100644 --- a/odoo/addons/base/__manifest__.py +++ b/odoo/addons/base/__manifest__.py @@ -72,7 +72,6 @@ The kernel of Odoo, needed for all installation. 'views/ir_property_views.xml', 'views/res_config_settings_views.xml', 'views/report_paperformat_views.xml', - 'views/onboarding_views.xml', 'security/ir.model.access.csv', ], 'demo': [ diff --git a/odoo/addons/base/models/res_company.py b/odoo/addons/base/models/res_company.py index 8c62bd1f974..b91e16035f1 100644 --- a/odoo/addons/base/models/res_company.py +++ b/odoo/addons/base/models/res_company.py @@ -68,8 +68,6 @@ class Company(models.Model): company_registry = fields.Char(related='partner_id.company_registry', string="Company ID", readonly=False) paperformat_id = fields.Many2one('report.paperformat', 'Paper format', default=lambda self: self.env.ref('base.paperformat_euro', raise_if_not_found=False)) external_report_layout_id = fields.Many2one('ir.ui.view', 'Document Template') - base_onboarding_company_state = fields.Selection([ - ('not_done', "Not done"), ('just_done', "Just done"), ('done', "Done")], string="State of the onboarding company step", default='not_done') font = fields.Selection([("Lato", "Lato"), ("Roboto", "Roboto"), ("Open_Sans", "Open Sans"), ("Montserrat", "Montserrat"), ("Oswald", "Oswald"), ("Raleway", "Raleway"), ('Tajawal', 'Tajawal')], default="Lato") primary_color = fields.Char() secondary_color = fields.Char() @@ -284,40 +282,6 @@ class Company(models.Model): return (self.env['ir.actions.report'].search([('report_name', '=', report_name)], limit=1) .report_action(docids)) - @api.model - def action_open_base_onboarding_company(self): - """ Onboarding step for company basic information. """ - action = self.env["ir.actions.actions"]._for_xml_id("base.action_open_base_onboarding_company") - action['res_id'] = self.env.company.id - return action - - def set_onboarding_step_done(self, step_name): - if self[step_name] == 'not_done': - self[step_name] = 'just_done' - - def _get_and_update_onboarding_state(self, onboarding_state, steps_states): - """ Needed to display onboarding animations only one time. """ - old_values = {} - all_done = True - for step_state in steps_states: - old_values[step_state] = self[step_state] - if self[step_state] == 'just_done': - self[step_state] = 'done' - all_done = all_done and self[step_state] == 'done' - - if all_done: - if self[onboarding_state] == 'not_done': - # string `onboarding_state` instead of variable name is not an error - old_values['onboarding_state'] = 'just_done' - else: - old_values['onboarding_state'] = 'done' - self[onboarding_state] = 'done' - return old_values - - def action_save_onboarding_company_step(self): - if bool(self.street): - self.set_onboarding_step_done('base_onboarding_company_state') - @api.model def _get_main_company(self): try: diff --git a/odoo/addons/base/populate/res_company.py b/odoo/addons/base/populate/res_company.py index b4671dc6f19..23e6dc3ccbd 100644 --- a/odoo/addons/base/populate/res_company.py +++ b/odoo/addons/base/populate/res_company.py @@ -29,8 +29,6 @@ class Partner(models.Model): ('name', populate.constant('company_{counter}')), ('sequence', populate.randint(0, 100)), ('company_registry', populate.iterate([False, 'company_registry_{counter}'])), - ('base_onboarding_company_state', populate.iterate( - [False] + [e[0] for e in type(self).base_onboarding_company_state.selection])), ('primary_color', populate.iterate([False, '', '#ff7755'])), ('secondary_color', populate.iterate([False, '', '#ffff55'], seed='primary_color')), ('currency_id', populate.iterate(active_currencies)),