[REF] *: use onboarding module

Purpose:
Use the onboarding module to manage onboardings, steps and their progress for the different companies

This PR (and related ENT one) moves these:

* `account`:" invoicing" and "dashboard" + "vendor bill" and "sale_tax" steps
* `sale`: "quotation"
* `account_consolidation`: "dashboard"
* `(account_)payment`: payment provider setup step, used in different modules.
* `website_sale_dashboard`

The onboarding models themselves changed a little to support sharing steps between onboardings or having steps
defined in one module, only to be shown on a panel (`onboarding.onboarding`) in another module, mainly by changing the `onboarding.onboarding` <> `onboarding.onboarding.step` and `onboarding.progress` <> `onboarding.progress.step` relations to m2m.

Task-3025136
See https://github.com/odoo/enterprise/pull/33818
See https://github.com/odoo/upgrade/pull/4021

closes odoo/odoo#104223

Related: odoo/enterprise#33818
Related: odoo/upgrade#4021
Signed-off-by: Florian Charlier (flch) <flch@odoo.com>
This commit is contained in:
Odoo's Mergebot
2023-06-30 23:37:51 +02:00
committed by GitHub
72 changed files with 1034 additions and 972 deletions
+3 -2
View File
@@ -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',
-1
View File
@@ -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
-49
View File
@@ -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()
})
}
+134
View File
@@ -0,0 +1,134 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- ONBOARDING STEPS -->
<!-- INVOICING -->
<record id="onboarding_onboarding_step_company_data" model="onboarding.onboarding.step">
<field name="title">Company Data</field>
<field name="description">Set your company's data for documents header/footer.</field>
<field name="button_text">Let's start!</field>
<field name="done_text">Looks great!</field>
<field name="panel_step_open_action_name">action_open_step_company_data</field>
<field name="step_image" type="base64" file="base/static/img/onboarding_company-data.png"></field>
<field name="step_image_filename">onboarding_company-data.png</field>
<field name="step_image_alt">Onboarding Company Data</field>
<field name="sequence">1</field>
</record>
<record id="onboarding_onboarding_step_base_document_layout" model="onboarding.onboarding.step">
<field name="title">Documents Layout</field>
<field name="description">Customize the look of your documents.</field>
<field name="button_text">Customize</field>
<field name="done_text">Looks great!</field>
<field name="panel_step_open_action_name">action_open_step_base_document_layout</field>
<field name="step_image" type="base64" file="base/static/img/onboarding_quotation-layout.png"></field>
<field name="step_image_filename">onboarding_quotation-layout.png</field>
<field name="step_image_alt">Onboarding Documents Layout</field>
<field name="sequence">3</field>
</record>
<record id="onboarding_onboarding_step_create_invoice" model="onboarding.onboarding.step">
<field name="title">Create Invoice</field>
<field name="description">Create your first invoice.</field>
<field name="button_text">Create</field>
<field name="done_text">First invoice sent!</field>
<field name="panel_step_open_action_name">action_open_step_create_invoice</field>
<field name="step_image" type="base64" file="base/static/img/onboarding_default.png"></field>
<field name="step_image_filename">onboarding_default.png</field>
<field name="step_image_alt">Onboarding Create Invoice</field>
<field name="sequence">4</field>
</record>
<!-- DASHBOARD -->
<record id="onboarding_onboarding_step_fiscal_year" model="onboarding.onboarding.step">
<field name="title">Accounting Periods</field>
<field name="description">Define your fiscal years &amp; tax returns periodicity.</field>
<field name="button_text">Configure</field>
<field name="done_text">Step completed!</field>
<field name="panel_step_open_action_name">action_open_step_fiscal_year</field>
<field name="step_image" type="base64" file="base/static/img/onboarding_accounting-periods.png"></field>
<field name="step_image_filename">onboarding_accounting-periods.png</field>
<field name="step_image_alt">Onboarding Accounting Periods</field>
<field name="sequence">1</field>
</record>
<record id="onboarding_onboarding_step_bank_account" model="onboarding.onboarding.step">
<field name="title">Bank Account</field>
<field name="description">Connect your financial accounts in seconds.</field>
<field name="button_text">Add a bank account</field>
<field name="done_text">Step Completed!</field>
<field name="panel_step_open_action_name">action_open_step_bank_account</field>
<field name="step_image" type="base64" file="base/static/img/onboarding_puzzle.png"></field>
<field name="step_image_filename">onboarding_puzzle.png</field>
<field name="step_image_alt">Onboarding Bank Account</field>
<field name="sequence">2</field>
</record>
<record id="onboarding_onboarding_step_default_taxes" model="onboarding.onboarding.step">
<field name="title">Taxes</field>
<field name="description">Set default Taxes for sales and purchase transactions.</field>
<field name="button_text">Review</field>
<field name="done_text">Taxes set!</field>
<field name="panel_step_open_action_name">action_open_step_default_taxes</field>
<field name="step_image" type="base64" file="base/static/img/onboarding_taxes.png"></field>
<field name="step_image_filename">onboarding_taxes.png</field>
<field name="step_image_alt">Onboarding Taxes</field>
<field name="sequence">3</field>
</record>
<record id="onboarding_onboarding_step_chart_of_accounts" model="onboarding.onboarding.step">
<field name="title">Chart of Accounts</field>
<field name="description">Set up your chart of accounts and record initial balances.</field>
<field name="button_text">Review</field>
<field name="done_text">Chart of accounts set!</field>
<field name="panel_step_open_action_name">action_open_step_chart_of_accounts</field>
<field name="step_image" type="base64" file="base/static/img/onboarding_chart-of-accounts.png"></field>
<field name="step_image_filename">onboarding_chart-of-accounts.png</field>
<field name="step_image_alt">Onboarding Bank Account</field>
<field name="sequence">4</field>
</record>
<!-- WITHOUT PANEL -->
<record id="onboarding_onboarding_step_setup_bill" model="onboarding.onboarding.step">
<field name="title">Import Vendor Bills</field>
<field name="sequence">1000</field>
</record>
<record id="onboarding_onboarding_step_sales_tax" model="onboarding.onboarding.step">
<field name="title">Taxes</field>
<!-- Fields values used if/when added in a panel within other modules -->
<field name="description">Choose a default sales tax for your products.</field>
<field name="button_text">Set taxes</field>
<field name="done_text">Step Completed!</field>
<field name="panel_step_open_action_name">action_open_step_sales_tax</field>
<field name="step_image" type="base64" file="base/static/img/onboarding_puzzle.png"></field>
<field name="step_image_filename">onboarding_puzzle.png</field>
<field name="step_image_alt">Onboarding Bank Account</field>
<field name="sequence">100</field> <!-- after "Online Payment" -->
</record>
<!-- ONBOARDING PANELS -->
<record id="onboarding_onboarding_account_invoice" model="onboarding.onboarding">
<field name="name">Invoicing Onboarding</field>
<field name="step_ids" eval="[
Command.link(ref('account.onboarding_onboarding_step_company_data')),
Command.link(ref('account.onboarding_onboarding_step_base_document_layout')),
Command.link(ref('account.onboarding_onboarding_step_create_invoice'))
]"/>
<field name="route_name">account_invoice</field>
<field name="panel_close_action_name">action_close_panel_account_invoice</field>
</record>
<record id="onboarding_onboarding_account_dashboard" model="onboarding.onboarding">
<field name="name">Account Dashboard Onboarding</field>
<field name="step_ids" eval="[
Command.link(ref('account.onboarding_onboarding_step_fiscal_year')),
Command.link(ref('account.onboarding_onboarding_step_bank_account')),
Command.link(ref('account.onboarding_onboarding_step_default_taxes')),
Command.link(ref('account.onboarding_onboarding_step_chart_of_accounts'))
]"/>
<field name="route_name">account_dashboard</field>
<field name="panel_close_action_name">action_close_panel_account_dashboard</field>
</record>
</data>
</odoo>
+2
View File
@@ -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
@@ -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'
+5 -159
View File
@@ -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)
@@ -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')
@@ -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']],
}
+10 -10
View File
@@ -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 <b>Invoicing app</b>.')),
{
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",
}, {
@@ -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";
}
}
@@ -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);
@@ -3,7 +3,7 @@
<field name="name">account.journal.dashboard.kanban</field>
<field name="model">account.journal</field>
<field name="arch" type="xml">
<kanban create="false" class="o_kanban_dashboard o_account_kanban" banner_route="/account/account_dashboard_onboarding" js_class="account_dashboard_kanban">
<kanban create="false" class="o_kanban_dashboard o_account_kanban" banner_route="/onboarding/account_dashboard" js_class="account_dashboard_kanban">
<field name="id"/>
<field name="name"/>
<field name="type"/>
+1 -1
View File
@@ -447,7 +447,7 @@
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//tree" position="attributes">
<attribute name="banner_route">/account/account_invoice_onboarding</attribute>
<attribute name="banner_route">/onboarding/account_invoice</attribute>
</xpath>
<field name="currency_id" position="attributes">
<attribute name="string">Invoice Currency</attribute>
@@ -1,156 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ONBOARDING STEPS -->
<template id="onboarding_invoice_layout_step">
<t t-call="base.onboarding_step">
<t t-set="title">Invoice Layout</t>
<t t-set="description">Customize the look of your invoices.</t>
<t t-set="image" t-value="'/base/static/img/onboarding_quotation-layout.png'"/>
<t t-set="alt" t-value="'Invoice Layout'"/>
<t t-set="done_icon" t-value="'fa-star'" />
<t t-set="btn_text">Customize</t>
<t t-set="done_text">Looks great!</t>
<t t-set="method" t-value="'action_open_base_document_layout'" />
<t t-set="model" t-value="'base.document.layout'" />
<t t-set="state" t-value="state.get('account_onboarding_invoice_layout_state')" />
</t>
</template>
<template id="onboarding_create_invoice_step">
<t t-call="base.onboarding_step">
<t t-set="title">Create Invoice</t>
<t t-set="description">
Create your first invoice.
</t>
<t t-set="image" t-value="'/base/static/img/onboarding_default.png'"/>
<t t-set="alt" t-value="'Onboarding Create Invoice'"/>
<t t-set="btn_text">Create</t>
<t t-set="done_text">First invoice sent!</t>
<t t-set="method" t-value="'action_open_account_onboarding_create_invoice'" />
<t t-set="model" t-value="'res.company'" />
<t t-set="state" t-value="state.get('account_onboarding_create_invoice_state')" />
</t>
</template>
<template id="onboarding_bank_account_step">
<t t-call="base.onboarding_step">
<t t-set="title">Bank Account</t>
<t t-set="description">
Connect your financial accounts in seconds.
</t>
<t t-set="image" t-value="'/base/static/img/onboarding_puzzle.png'"/>
<t t-set="alt" t-value="'Onboarding Bank Account'"/>
<t t-set="btn_text">Add a bank account</t>
<t t-set="done_text">Step Completed!</t>
<t t-set="method" t-value="'setting_init_bank_account_action'" />
<t t-set="model" t-value="'res.company'" />
<t t-set="state" t-value="state.get('account_setup_bank_data_state')" />
</t>
</template>
<template id="onboarding_fiscal_year_step">
<t t-call="base.onboarding_step">
<t t-set="title">Accounting Periods</t>
<t t-set="description">Define your fiscal years &amp; tax returns periodicity.</t>
<t t-set="image" t-value="'/base/static/img/onboarding_accounting-periods.png'"/>
<t t-set="alt" t-value="'Onboarding Accounting Periods'"/>
<t t-set="btn_text">Configure</t>
<t t-set="done_text">Step Completed!</t>
<t t-set="method" t-value="'setting_init_fiscal_year_action'" />
<t t-set="model" t-value="'res.company'" />
<t t-set="state" t-value="state.get('account_setup_fy_data_state')" />
</t>
</template>
<template id="onboarding_chart_of_account_step">
<t t-call="base.onboarding_step">
<t t-set="title">Chart of Accounts</t>
<t t-set="description">Setup your chart of accounts and record initial balances.</t>
<t t-set="image" t-value="'/base/static/img/onboarding_chart-of-accounts.png'"/>
<t t-set="alt" t-value="'Onboarding Chart of Accounts'"/>
<t t-set="btn_text">Review</t>
<t t-set="done_text">Chart of accounts set.</t>
<t t-set="method" t-value="'setting_chart_of_accounts_action'" />
<t t-set="model" t-value="'res.company'" />
<t t-set="state" t-value="state.get('account_setup_coa_state')" />
</t>
</template>
<template id="onboarding_taxes_step">
<t t-call="base.onboarding_step">
<t t-set="title">Taxes</t>
<t t-set="description">Set default Taxes for sales and purchase transactions.</t>
<t t-set="image" t-value="'/base/static/img/onboarding_taxes.png'"/>
<t t-set="alt" t-value="'Onboarding Taxes'"/>
<t t-set="btn_text">Review</t>
<t t-set="done_text">Taxes set.</t>
<t t-set="method" t-value="'action_open_taxes_onboarding'" />
<t t-set="model" t-value="'res.company'" />
<t t-set="state" t-value="state.get('account_setup_taxes_state')" />
</t>
</template>
<!-- ONBOARDING PANELS -->
<template id="account_invoice_onboarding_panel" name="account.invoice.onboarding.panel">
<t t-call="base.onboarding_container">
<t t-set="close_method" t-value="'action_close_account_invoice_onboarding'"/>
<t t-set="close_model" t-value="'res.company'"/>
<t t-set="text_completed">Congratulations! You are all set.</t>
<t t-call="base.onboarding_company_step" name="company_step" />
<t t-call="account.onboarding_invoice_layout_step" name="invoice_layout_step" />
<t t-call="account.onboarding_create_invoice_step" name="create_invoice_step" />
</t>
</template>
<template id="account_dashboard_onboarding_panel" name="account.dashboard.onboarding.panel">
<t t-call="base.onboarding_container">
<t t-set="close_method" t-value="'action_close_account_dashboard_onboarding'"/>
<t t-set="close_model" t-value="'res.company'"/>
<t t-call="account.onboarding_fiscal_year_step" name="fiscal_year_step" />
<t t-call="account.onboarding_bank_account_step" name="bank_account_step" />
<t t-call="account.onboarding_taxes_step" name="taxes_step" />
<t t-call="account.onboarding_chart_of_account_step" name="chart_of_account_step" />
</t>
</template>
<!-- SAMPLE INVOICE EMAIL -->
<record id="action_open_account_onboarding_create_invoice" model="ir.actions.act_window">
<field name="name">Create first invoice</field>
<field name="res_model">account.move</field>
<field name="view_mode">form</field>
<field name="view_id" ref="account.view_move_form" />
<field name="context">{'default_move_type': 'out_invoice'}</field>
</record>
<!-- SALE TAX -->
<template id="onboarding_sale_tax_step">
<t t-call="base.onboarding_step">
<t t-set="title">Taxes</t>
<t t-set="description">Choose a default sales tax for your products.</t>
<t t-set="btn_text">Set taxes</t>
<t t-set="image" t-value="'/base/static/img/onboarding_taxes.png'"/>
<t t-set="alt" t-value="'Onboarding Taxes'"/>
<t t-set="done_text">Step Completed!</t>
<t t-set="method" t-value="'action_open_account_onboarding_sale_tax'" />
<t t-set="model" t-value="'res.company'" />
<t t-set="state" t-value="state.get('account_onboarding_sale_tax_state')" />
</t>
</template>
<record id="account_invoice_onboarding_sale_tax_form" model="ir.ui.view">
<field name="name">account.invoice.layout.onboarding.form</field>
<field name="model">res.company</field>
<field name="mode">primary</field>
<field name="priority">1000</field>
<field name="arch" type="xml">
<form>
<div class="mb16">Choose a default sales tax for your products.</div>
<label for="account_sale_tax_id" string="Sales Tax"/>
<field name="account_sale_tax_id" />
<footer>
<button string="Apply" class="btn btn-primary" type="object" name="action_save_onboarding_sale_tax" data-hotkey="q" />
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z" />
</footer>
</form>
</field>
</record>
<record id="action_open_account_onboarding_sale_tax" model="ir.actions.act_window">
<field name="name">Sales tax</field>
<field name="res_model">res.company</field>
<field name="view_mode">form</field>
<field name="view_id" ref="account_invoice_onboarding_sale_tax_form" />
<field name="target">new</field>
</record>
</odoo>
@@ -33,4 +33,35 @@
</field>
</record>
<!-- Onboarding -->
<record id="res_company_form_view_onboarding" model="ir.ui.view">
<field name="name">res.company.form.view.onboarding</field>
<field name="model">res.company</field>
<field name="inherit_id" ref="base.view_company_form"/>
<field name="mode">primary</field>
<field name="priority">1000</field>
<field name="arch" type="xml">
<xpath expr="//group[@name='social_media']" position="replace"/>
<form position="attributes">
<attribute name="js_class">company_onboarding_form</attribute>
</form>
</field>
</record>
<record id="res_company_form_view_onboarding_sale_tax" model="ir.ui.view">
<field name="name">res.company.form.view.onboarding.sale.tax</field>
<field name="model">res.company</field>
<field name="priority">1000</field>
<field name="arch" type="xml">
<form>
<div class="mb16">Choose a default sales tax for your products.</div>
<label for="account_sale_tax_id" string="Sales Tax"/>
<field name="account_sale_tax_id" />
<footer>
<button string="Apply" class="btn btn-primary" type="object" name="action_save_onboarding_sale_tax" data-hotkey="q" />
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z" />
</footer>
</form>
</field>
</record>
</odoo>
@@ -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
+3 -3
View File
@@ -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")
+1 -1
View File
@@ -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',
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="onboarding_onboarding_step_payment_provider" model="onboarding.onboarding.step">
<field name="title">Online Payments</field>
<field name="description">Enable credit &amp; debit card payments supported by Stripe.</field>
<field name="button_text">Activate Stripe</field>
<field name="panel_step_open_action_name">action_open_step_payment_provider</field>
<field name="step_image" type="base64" file="base/static/img/onboarding_bank-account.png"></field>
<field name="step_image_filename">onboarding_bank-account.png</field>
<field name="step_image_alt">Onboarding Online Payments</field>
<field name="onboarding_ids" eval="[
Command.link(ref('account.onboarding_onboarding_account_invoice'))
]"></field>
</record>
</data>
</odoo>
+1 -1
View File
@@ -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
@@ -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
@@ -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']
@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="account_payment_onboarding_panel" inherit_id="account.account_invoice_onboarding_panel" priority="30">
<xpath expr="//t[@name='create_invoice_step']" position="after">
<t t-call="account_payment.payment_onboarding_step" name="payment_step"/>
</xpath>
</template>
<template id="payment_onboarding_step" name="Payment Onboarding Step">
<t t-call="base.onboarding_step">
<t t-set="title">Online Payments</t>
<t t-set="description">Enable credit &amp; debit card payments supported by Stripe</t>
<t t-set="image" t-value="'/base/static/img/onboarding_bank-account.png'"/>
<t t-set="alt" t-value="'Online Payments'"/>
<t t-set="btn_text">Activate Stripe</t>
<t t-set="done_text">Online payments enabled</t>
<t t-set="model" t-value="'res.company'"/>
<t t-set="method" t-value="'action_open_payment_onboarding'"/>
<t t-set="state" t-value="state.get('payment_provider_onboarding_state')"/>
</t>
</template>
</odoo>
+13 -2
View File
@@ -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',
}
@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="onboarding_panel">
<t t-call="onboarding.onboarding_container">
<t t-foreach="steps" t-as="step">
<t t-call="base.onboarding_step">
<t t-set="title" t-value="step.title"/>
<t t-set="description" t-value="step.description"/>
<t t-set="done_icon" t-value="step.done_icon"/>
<t t-set="btn_text" t-value="step.button_text"/>
<t t-set="done_text" t-value="step.done_text"/>
<t t-set="image" t-value="'/web/image/onboarding.onboarding.step/'+str(step.id)+'/step_image'"/>
<t t-set="alt" t-value="step.step_image_alt"/>
<!-- Model/method used by JS implementation of banners for each step-->
<t t-set="method" t-value="step.panel_step_open_action_name"/>
<t t-set="model">onboarding.onboarding.step</t>
<!-- to mimic first implementation of onboarding, the 'state' queried holds
a rendering state for all steps. See _get_and_update_onboarding_state -->
<t t-set="state" t-value="state.get(step.id)"/>
</t>
</t>
</t>
</template>
<template id="onboarding_container" inherit_id="base.onboarding_container">
<xpath expr="//div[hasclass('o_onboarding_completed_message')]" position="replace"/>
</template>
</odoo>
+1 -1
View File
@@ -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
@@ -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
@@ -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)
+18 -11
View File
@@ -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 = {}
@@ -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')
@@ -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);
@@ -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 };
}
}
+1
View File
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import case
from . import test_onboarding
from . import test_onboarding_concurrency
+40
View File
@@ -0,0 +1,40 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase
class TransactionCaseOnboarding(TransactionCase):
def assert_step_is_done(self, step, also_with_company=None):
self.assertIn(
step.current_progress_step_id.step_state, {'done', 'just_done'},
f'Expected done-like current state for step {step.id} for {step.env.company.name}')
self.assertEqual(step.current_progress_step_id.step_state, step.current_step_state)
if also_with_company:
self.assert_step_is_done(step.with_company(also_with_company))
def assert_step_is_not_done(self, step, also_with_company=None):
self.assertIn(
step.current_progress_step_id.step_state, {'not_done', False},
f'Expected "not_done" current state for step {step.id} for {step.env.company.name}')
self.assertEqual(step.current_step_state, 'not_done')
if also_with_company:
self.assert_step_is_not_done(step.with_company(also_with_company))
def assert_onboarding_is_done(self, onboarding, also_with_company=None):
self.assertIn(
onboarding.current_progress_id.onboarding_state, {'done', 'just_done'},
f'Expected done-like current state for onboarding "{onboarding.name}" '
f'for "{onboarding.env.company.name}"')
self.assertEqual(onboarding.current_progress_id.onboarding_state,
onboarding.current_onboarding_state)
if also_with_company:
self.assert_onboarding_is_done(onboarding.with_company(also_with_company))
def assert_onboarding_is_not_done(self, onboarding, also_with_company=None):
self.assertIn(
onboarding.current_progress_id.onboarding_state, {'not_done', False},
f'Expected `"not_done"` or `False` current state for onboarding {onboarding.name} '
f'for {onboarding.env.company.name}')
self.assertEqual(onboarding.current_onboarding_state, 'not_done')
if also_with_company:
self.assert_onboarding_is_not_done(onboarding.with_company(also_with_company))
+17 -44
View File
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase
from odoo.addons.onboarding.tests.case import TransactionCaseOnboarding
class TestOnboardingCommon(TransactionCase):
class TestOnboardingCommon(TransactionCaseOnboarding):
@classmethod
def setUpClass(cls):
super().setUpClass()
@@ -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)
+65 -18
View File
@@ -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))
@@ -1,5 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<template id="onboarding_panel">
<t t-call="onboarding.onboarding_container">
<t t-foreach="steps" t-as="step">
<t t-call="onboarding.onboarding_step">
<t t-set="title" t-value="step.title"/>
<t t-set="description" t-value="step.description"/>
<t t-set="done_icon" t-value="step.done_icon"/>
<t t-set="btn_text" t-value="step.button_text"/>
<t t-set="done_text" t-value="step.done_text"/>
<t t-set="image" t-value="'/web/image/onboarding.onboarding.step/'+str(step.id)+'/step_image'"/>
<t t-set="alt" t-value="step.step_image_alt"/>
<!-- Model/method used by JS implementation of banners for each step-->
<t t-set="method" t-value="step.panel_step_open_action_name"/>
<t t-set="model">onboarding.onboarding.step</t>
<!-- to mimic first implementation of onboarding, the 'state' queried holds
a rendering state for all steps. See _get_and_update_onboarding_state -->
<t t-set="state" t-value="state.get(step.id)"/>
</t>
</t>
</t>
</template>
<template id="onboarding_container">
<div class="modal o_onboarding_modal o_technical_modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
@@ -13,8 +35,8 @@
</div>
<div class="modal-footer justify-content-start">
<a type="action" class="btn btn-primary" data-bs-dismiss="modal"
data-o-hide-banner="true"
t-att-data-model="close_model" t-att-data-method="close_method">
data-o-hide-banner="true" t-att-data-model="close_model" t-att-data-method="close_method"
>
Remove
</a>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
@@ -29,17 +51,15 @@
<a href="#" data-bs-toggle="modal" data-bs-target=".o_onboarding_modal" class="o_onboarding_btn_close position-absolute top-0 end-0 py-2 px-3 h2 z-index-1" title="Close the onboarding panel"><i class="oi oi-close"/></a>
<div class="o_onboarding_steps d-flex" t-out="0"/>
<div t-if="state.get('onboarding_state') in ('done', 'just_done')"
<div t-if="state.get('onboarding_state') == 'just_done'"
t-att-state="state.get('onboarding_state')"
class="o_onboarding_completed_message position-absolute end-0 bottom-0 start-0 border-bottom py-4 bg-white d-flex align-items-center justify-content-center">
class="o_onboarding_completed_message position-absolute end-0 start-0 border-bottom py-4 bg-white d-flex align-items-center justify-content-center">
<span class="h3 m-0">
<i class="fa fa-check text-success me-3" />
<t t-if="text_completed" t-out="text_completed" />
<t t-else="">Nice work! Your configuration is done.</t>
</span>
<a type="action" class="btn btn-primary ms-4" data-bs-toggle="collapse" href=".o_onboarding_container" t-att-data-model="close_model" t-att-data-method="close_method">
Close
Close Panel
</a>
</div>
</div>
@@ -79,44 +99,4 @@
</div>
</div>
</template>
<!-- ONBOARDING STEPS -->
<template id="onboarding_company_step">
<t t-call="base.onboarding_step">
<t t-set="title">Company Data</t>
<t t-set="description">
Set your company's data for documents header/footer.
</t>
<t t-set="image" t-value="'/base/static/img/onboarding_company-data.png'"/>
<t t-set="alt" t-value="'Onboarding Company Data'"/>
<t t-set="btn_text">Let's start!</t>
<t t-set="method" t-value="'action_open_base_onboarding_company'" />
<t t-set="model" t-value="'res.company'" />
<t t-set="state" t-value="state.get('base_onboarding_company_state')" />
</t>
</template>
<!-- COMPANY FORM -->
<record id="base_onboarding_company_form" model="ir.ui.view">
<field name="name">base.company.onboarding.form</field>
<field name="model">res.company</field>
<field name="inherit_id" ref="base.view_company_form" />
<field name="mode">primary</field>
<field name="priority">1000</field>
<field name="arch" type="xml">
<xpath expr="//group[@name='social_media']" position="replace" />
<form position="inside">
<footer position="replace">
<button name="action_save_onboarding_company_step" class="btn btn-primary" type="object" string="Apply" data-hotkey="q"/>
<button special="cancel" data-hotkey="z" string="Cancel" />
</footer>
</form>
</field>
</record>
<record id="action_open_base_onboarding_company" model="ir.actions.act_window">
<field name="name">Set your company data</field>
<field name="res_model">res.company</field>
<field name="view_mode">form</field>
<field name="view_id" ref="base_onboarding_company_form" />
<field name="target">new</field>
</record>
</odoo>
+12 -3
View File
@@ -36,7 +36,9 @@
</group>
<notebook>
<page name="Onboarding steps">
<field name="step_ids"/>
<group string="Steps">
<field name="step_ids" nolabel="1" colspan="2"/>
</group>
</page>
</notebook>
</sheet>
@@ -51,8 +53,9 @@
<tree string="Onboarding Steps">
<field name="sequence" widget="handle"/>
<field name="title"/>
<field name="onboarding_id" optional="hide"/>
<field name="onboarding_ids" optional="hide"/>
<field name="current_step_state"/>
<field name="is_per_company" optional="hide"/>
</tree>
</field>
</record>
@@ -65,10 +68,16 @@
<sheet>
<group>
<field name="title"/>
<field name="onboarding_id"/>
<field name="current_step_state"/>
<field name="panel_step_open_action_name"/>
<field name="is_per_company"/>
</group>
<notebook>
<page name="Onboardings using this step">
<group string="Onboardings">
<field name="onboarding_ids" nolabel="1" colspan="2"/>
</group>
</page>
<page name="Step rendering">
<group>
<field name="description"/>
+2 -1
View File
@@ -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',
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- ONBOARDING STEPS (WITHOUT PANEL)-->
<record id="onboarding_onboarding_step_payment_provider" model="onboarding.onboarding.step">
<field name="title">Online Payments</field>
<field name="sequence">99</field>
</record>
</data>
</odoo>
+1
View File
@@ -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
@@ -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")
-16
View File
@@ -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']
@@ -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)
@@ -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}')
@@ -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
+1 -1
View File
@@ -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',
-1
View File
@@ -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
-31
View File
@@ -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()
}
)
}
+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Sale Quotation - ONBOARDING STEPS -->
<record id="onboarding_onboarding_step_sale_order_confirmation" model="onboarding.onboarding.step">
<field name="title">Order Confirmation</field>
<field name="description">Choose between electronic signatures or online payments.</field>
<field name="button_text">Set payments</field>
<field name="panel_step_open_action_name">action_open_step_sale_order_confirmation</field>
<field name="step_image" type="base64" file="base/static/img/onboarding_default.png"></field>
<field name="step_image_filename">onboarding_default.png</field>
<field name="step_image_alt">Onboarding Order Confirmation</field>
<field name="sequence">6</field>
</record>
<record id="onboarding_onboarding_step_sample_quotation" model="onboarding.onboarding.step">
<field name="title">Sample Quotation</field>
<field name="description">Send a quotation to test the customer portal.</field>
<field name="button_text">Send sample</field>
<field name="panel_step_open_action_name">action_open_step_sample_quotation</field>
<field name="step_image" type="base64" file="base/static/img/onboarding_sample-quotation.png"></field>
<field name="step_image_filename">onboarding_sample-quotation.png</field>
<field name="step_image_alt">Onboarding Sample Quotation</field>
<field name="sequence">7</field>
</record>
<!-- Sale Quotation - ONBOARDING PANEL -->
<record id="onboarding_onboarding_sale_quotation" model="onboarding.onboarding">
<field name="name">Sale Quotation Onboarding</field>
<field name="step_ids" eval="[
Command.link(ref('sale.onboarding_onboarding_step_sale_order_confirmation')),
Command.link(ref('sale.onboarding_onboarding_step_sample_quotation')),
Command.link(ref('account.onboarding_onboarding_step_company_data')),
Command.link(ref('account.onboarding_onboarding_step_base_document_layout'))
]"/>
<field name="route_name">sale_quotation</field>
<field name="panel_close_action_name">action_close_panel_sale_quotation</field>
</record>
</data>
</odoo>
+2
View File
@@ -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
@@ -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')
@@ -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
+1 -119
View File
@@ -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)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

+14 -14
View File
@@ -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",
+1
View File
@@ -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
+30
View File
@@ -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)
+1 -1
View File
@@ -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()
@@ -1,62 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ONBOARDING STEPS -->
<template id="onboarding_quotation_layout_step">
<t t-call="base.onboarding_step">
<t t-set="title">Quotation Layout</t>
<t t-set="description">Customize the look of your quotations.</t>
<t t-set="image" t-value="'/base/static/img/onboarding_quotation-layout.png'"/>
<t t-set="alt" t-value="'Onboarding Quotation Layout'"/>
<t t-set="done_icon" t-value="'fa-star'" />
<t t-set="done_text">Looks great!</t>
<t t-set="btn_text">Customize</t>
<t t-set="model" t-value="'base.document.layout'" />
<t t-set="method" t-value="'action_open_base_document_layout'" />
<t t-set="state" t-value="state.get('account_onboarding_invoice_layout_state')" />
</t>
</template>
<template id="sale_onboarding_order_confirmation_step">
<t t-call="base.onboarding_step">
<t t-set="title">Order Confirmation</t>
<t t-set="description">Choose between electronic signatures or online payments.</t>
<t t-set="image" t-value="'/base/static/img/onboarding_default.png'"/>
<t t-set="alt" t-value="'Onboarding Quotation Layout'"/>
<t t-set="btn_text">Set payments</t>
<t t-set="method" t-value="'action_open_sale_onboarding_payment_provider'" />
<t t-set="model" t-value="'res.company'" />
<t t-set="state" t-value="state.get('sale_onboarding_order_confirmation_state')" />
</t>
</template>
<template id="sale_onboarding_sample_quotation_step">
<t t-call="base.onboarding_step">
<t t-set="title">Sample Quotation</t>
<t t-set="description">Send a quotation to test the customer portal.</t>
<t t-set="image" t-value="'/base/static/img/onboarding_sample-quotation.png'"/>
<t t-set="alt" t-value="'Onboarding Quotation Layout'"/>
<t t-set="btn_text">Send sample</t>
<t t-set="method" t-value="'action_open_sale_onboarding_sample_quotation'" />
<t t-set="model" t-value="'res.company'" />
<t t-set="state" t-value="state.get('sale_onboarding_sample_quotation_state')" />
</t>
</template>
<!-- ONBOARDING PANEL-->
<template id="sale_quotation_onboarding_panel" name="sale.quotation.onboarding.panel">
<t t-call="base.onboarding_container">
<t t-set="close_method" t-value="'action_close_sale_quotation_onboarding'" />
<t t-set="close_model" t-value="'res.company'" />
<t t-call="base.onboarding_company_step" name="company_step" />
<t t-call="sale.onboarding_quotation_layout_step" name="quotation_layout_step" />
<t t-call="sale.sale_onboarding_order_confirmation_step" name="payment_provider_step" />
<t t-call="sale.sale_onboarding_sample_quotation_step" name="sample_quotation_step" />
</t>
</template>
<!-- ORDER CONFIRMATION -->
<record id="action_open_sale_payment_provider_onboarding_wizard" model="ir.actions.act_window">
<field name="name">Choose how to confirm quotations</field>
<field name="res_model">sale.payment.provider.onboarding.wizard</field>
<field name="view_mode">form</field>
<field name="view_id" ref="payment.payment_provider_onboarding_wizard_form" />
<field name="target">new</field>
</record>
</odoo>
+1 -1
View File
@@ -215,7 +215,7 @@
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//tree" position="attributes">
<attribute name="banner_route">/sales/sale_quotation_onboarding_panel</attribute>
<attribute name="banner_route">/onboarding/sale_quotation</attribute>
</xpath>
</field>
</record>
@@ -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
+2 -2
View File
@@ -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'})
-5
View File
@@ -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',
@@ -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'}
+1 -10
View File
@@ -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.
-1
View File
@@ -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': [
-36
View File
@@ -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:
-2
View File
@@ -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)),