[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:
@@ -14,7 +14,7 @@ You could use this simplified accounting in case you work with an (external) acc
|
|||||||
""",
|
""",
|
||||||
'category': 'Accounting/Accounting',
|
'category': 'Accounting/Accounting',
|
||||||
'website': 'https://www.odoo.com/app/invoicing',
|
'website': 'https://www.odoo.com/app/invoicing',
|
||||||
'depends' : ['base_setup', 'product', 'analytic', 'portal', 'digest'],
|
'depends': ['base_setup', 'onboarding', 'product', 'analytic', 'portal', 'digest'],
|
||||||
'data': [
|
'data': [
|
||||||
'security/account_security.xml',
|
'security/account_security.xml',
|
||||||
'security/ir.model.access.csv',
|
'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',
|
'data/digest_data.xml',
|
||||||
'views/account_report.xml',
|
'views/account_report.xml',
|
||||||
'data/mail_template_data.xml',
|
'data/mail_template_data.xml',
|
||||||
|
'data/onboarding_data.xml',
|
||||||
'views/account_payment_view.xml',
|
'views/account_payment_view.xml',
|
||||||
'wizard/account_automatic_entry_wizard_views.xml',
|
'wizard/account_automatic_entry_wizard_views.xml',
|
||||||
'wizard/account_unreconcile_view.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_journal_dashboard_view.xml',
|
||||||
'views/account_portal_templates.xml',
|
'views/account_portal_templates.xml',
|
||||||
'views/report_payment_receipt_templates.xml',
|
'views/report_payment_receipt_templates.xml',
|
||||||
'views/account_onboarding_templates.xml',
|
|
||||||
'data/service_cron.xml',
|
'data/service_cron.xml',
|
||||||
'views/account_incoterms_view.xml',
|
'views/account_incoterms_view.xml',
|
||||||
'data/account_incoterms_data.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/components/**/*',
|
||||||
'account/static/src/js/tours/account.js',
|
'account/static/src/js/tours/account.js',
|
||||||
'account/static/src/xml/**/*',
|
'account/static/src/xml/**/*',
|
||||||
|
'account/static/src/views/**/*.js',
|
||||||
],
|
],
|
||||||
'web.assets_frontend': [
|
'web.assets_frontend': [
|
||||||
'account/static/src/js/account_portal_sidebar.js',
|
'account/static/src/js/account_portal_sidebar.js',
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from . import onboarding
|
|
||||||
from . import portal
|
from . import portal
|
||||||
from . import terms
|
from . import terms
|
||||||
|
|||||||
@@ -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()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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 & 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>
|
||||||
@@ -37,5 +37,7 @@ from . import ir_actions_report
|
|||||||
from . import ir_module
|
from . import ir_module
|
||||||
from . import res_currency
|
from . import res_currency
|
||||||
from . import account_report
|
from . import account_report
|
||||||
|
from . import onboarding_onboarding
|
||||||
|
from . import onboarding_onboarding_step
|
||||||
from . import template_generic_coa
|
from . import template_generic_coa
|
||||||
from . import uom_uom
|
from . import uom_uom
|
||||||
|
|||||||
@@ -620,7 +620,7 @@ class account_journal(models.Model):
|
|||||||
""" This function is called by the "Import" button of Vendor Bills,
|
""" This function is called by the "Import" button of Vendor Bills,
|
||||||
visible on dashboard if no bill has been created yet.
|
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({})
|
new_wizard = self.env['account.tour.upload.bill'].create({})
|
||||||
view_id = self.env.ref('account.account_tour_upload_bill').id
|
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):
|
def create_internal_transfer(self):
|
||||||
"""return action to create a internal transfer"""
|
"""return action to create a internal transfer"""
|
||||||
return self.open_payments_action('transfer', mode='form')
|
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'
|
|
||||||
|
|||||||
@@ -26,14 +26,6 @@ MONTH_SELECTION = [
|
|||||||
('12', 'December'),
|
('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):
|
class ResCompany(models.Model):
|
||||||
_name = "res.company"
|
_name = "res.company"
|
||||||
_inherit = ["res.company", "mail.thread"]
|
_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_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.")
|
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)
|
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')],
|
terms_type = fields.Selection([('plain', 'Add a Note'), ('html', 'Add a link to a Web Page')],
|
||||||
string='Terms & Conditions format', default='plain')
|
string='Terms & Conditions format', default='plain')
|
||||||
@@ -125,8 +103,6 @@ class ResCompany(models.Model):
|
|||||||
sanitize_attributes=False,
|
sanitize_attributes=False,
|
||||||
compute='_compute_invoice_terms_html', store=True, readonly=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
|
# Needed in the Point of Sale
|
||||||
account_default_pos_receivable_account_id = fields.Many2one('account.account', string="Default PoS Receivable Account")
|
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)])
|
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
|
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')
|
@api.depends('terms_type')
|
||||||
def _compute_invoice_terms_html(self):
|
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'):
|
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:
|
if html:
|
||||||
company.invoice_terms_html = 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):
|
def get_new_account_code(self, current_code, old_prefix, new_prefix):
|
||||||
digits = len(current_code)
|
digits = len(current_code)
|
||||||
return new_prefix + current_code.replace(old_prefix, '', 1).lstrip('0').rjust(digits-len(new_prefix), '0')
|
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
|
@api.model
|
||||||
def setting_init_bank_account_action(self):
|
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
|
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 {
|
return {
|
||||||
'type': 'ir.actions.act_window',
|
'type': 'ir.actions.act_window',
|
||||||
'name': _('Accounting Periods'),
|
'name': _('Create a Bank Account'),
|
||||||
'view_mode': 'form',
|
'res_model': 'account.setup.bank.manual.config',
|
||||||
'res_model': 'account.financial.year.op',
|
|
||||||
'target': 'new',
|
'target': 'new',
|
||||||
'res_id': new_wizard.id,
|
'view_mode': 'form',
|
||||||
'views': [[view_id, '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
|
@api.model
|
||||||
def create_op_move_if_non_existant(self):
|
def create_op_move_if_non_existant(self):
|
||||||
""" Creates an empty opening move in 'draft' state for the current company
|
""" Creates an empty opening move in 'draft' state for the current company
|
||||||
@@ -559,53 +449,9 @@ class ResCompany(models.Model):
|
|||||||
'credit': debit_diff,
|
'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):
|
def action_save_onboarding_sale_tax(self):
|
||||||
""" Set the onboarding step as done """
|
""" 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):
|
def get_chart_of_accounts_or_fail(self):
|
||||||
account = self.env['account.account'].search([('company_id', '=', self.id)], limit=1)
|
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']],
|
||||||
|
}
|
||||||
@@ -14,29 +14,29 @@ registry.category("web_tour.tours").add('account_tour', {
|
|||||||
steps: [
|
steps: [
|
||||||
...stepUtils.goToAppSteps('account.menu_finance', _t('Send invoices to your customers in no time with the <b>Invoicing app</b>.')),
|
...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."),
|
content: _t("Start by checking your company's data."),
|
||||||
position: "bottom",
|
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]",
|
trigger: "button.o_form_button_save",
|
||||||
extra_trigger: "a.o_onboarding_step_action[data-method=action_open_base_onboarding_company]",
|
extra_trigger: "a.o_onboarding_step_action[data-method=action_open_step_company_data]",
|
||||||
content: _t("Looks good. Let's continue."),
|
content: _t("Looks good. Let's continue."),
|
||||||
position: "bottom",
|
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."),
|
content: _t("Customize your layout."),
|
||||||
position: "bottom",
|
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]",
|
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."),
|
content: _t("Once everything is as you want it, validate."),
|
||||||
position: "top",
|
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."),
|
content: _t("Now, we'll create your first invoice."),
|
||||||
position: "bottom",
|
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="name">account.journal.dashboard.kanban</field>
|
||||||
<field name="model">account.journal</field>
|
<field name="model">account.journal</field>
|
||||||
<field name="arch" type="xml">
|
<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="id"/>
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="type"/>
|
<field name="type"/>
|
||||||
|
|||||||
@@ -447,7 +447,7 @@
|
|||||||
<field name="mode">primary</field>
|
<field name="mode">primary</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//tree" position="attributes">
|
<xpath expr="//tree" position="attributes">
|
||||||
<attribute name="banner_route">/account/account_invoice_onboarding</attribute>
|
<attribute name="banner_route">/onboarding/account_invoice</attribute>
|
||||||
</xpath>
|
</xpath>
|
||||||
<field name="currency_id" position="attributes">
|
<field name="currency_id" position="attributes">
|
||||||
<attribute name="string">Invoice Currency</attribute>
|
<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 & 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>
|
</field>
|
||||||
</record>
|
</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>
|
</odoo>
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ class BaseDocumentLayout(models.TransientModel):
|
|||||||
_inherit = 'base.document.layout'
|
_inherit = 'base.document.layout'
|
||||||
|
|
||||||
def document_layout_save(self):
|
def document_layout_save(self):
|
||||||
|
"""Save layout and onboarding step progress, return super() result"""
|
||||||
res = super(BaseDocumentLayout, self).document_layout_save()
|
res = super(BaseDocumentLayout, self).document_layout_save()
|
||||||
for wizard in self:
|
if step := self.env.ref('account.onboarding_onboarding_step_base_document_layout', raise_if_not_found=False):
|
||||||
wizard.company_id.action_save_onboarding_invoice_layout()
|
for company_id in self.company_id:
|
||||||
|
step.with_company(company_id).action_set_just_done()
|
||||||
return res
|
return res
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class FinancialYearOpeningWizard(models.TransientModel):
|
|||||||
return super().write(vals)
|
return super().write(vals)
|
||||||
|
|
||||||
def action_save_onboarding_fiscal_year(self):
|
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):
|
class SetupBarBankConfigWizard(models.TransientModel):
|
||||||
@@ -146,7 +146,7 @@ class SetupBarBankConfigWizard(models.TransientModel):
|
|||||||
selected_journal.name = record.new_journal_name
|
selected_journal.name = record.new_journal_name
|
||||||
|
|
||||||
def validate(self):
|
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.
|
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")
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
'auto_install': ['account'],
|
'auto_install': ['account'],
|
||||||
'data': [
|
'data': [
|
||||||
'data/ir_config_parameter.xml',
|
'data/ir_config_parameter.xml',
|
||||||
|
'data/onboarding_data.xml',
|
||||||
|
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'security/ir_rules.xml',
|
'security/ir_rules.xml',
|
||||||
@@ -19,7 +20,6 @@
|
|||||||
'views/account_move_views.xml',
|
'views/account_move_views.xml',
|
||||||
'views/account_journal_views.xml',
|
'views/account_journal_views.xml',
|
||||||
'views/account_payment_views.xml',
|
'views/account_payment_views.xml',
|
||||||
'views/onboarding_templates.xml',
|
|
||||||
'views/payment_provider_views.xml',
|
'views/payment_provider_views.xml',
|
||||||
'views/payment_transaction_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 & 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>
|
||||||
@@ -5,6 +5,6 @@ from . import account_move
|
|||||||
from . import account_payment
|
from . import account_payment
|
||||||
from . import account_payment_method
|
from . import account_payment_method
|
||||||
from . import account_payment_method_line
|
from . import account_payment_method_line
|
||||||
|
from . import onboarding_onboarding_step
|
||||||
from . import payment_provider
|
from . import payment_provider
|
||||||
from . import payment_transaction
|
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 & 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>
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Onboarding Toolbox',
|
'name': 'Onboarding Toolbox',
|
||||||
'version': '1.1',
|
'version': '1.2',
|
||||||
'category': 'Hidden',
|
'category': 'Hidden',
|
||||||
'sequence': 9001,
|
'sequence': 9001,
|
||||||
'description': """
|
'description': """
|
||||||
@@ -13,10 +13,21 @@ This module allows to manage onboardings and their progress
|
|||||||
'depends': ['base'],
|
'depends': ['base'],
|
||||||
'installable': True,
|
'installable': True,
|
||||||
'data': [
|
'data': [
|
||||||
'data/onboarding_data.xml',
|
'views/onboarding_templates.xml',
|
||||||
'views/onboarding_views.xml',
|
'views/onboarding_views.xml',
|
||||||
'views/onboarding_menus.xml',
|
'views/onboarding_menus.xml',
|
||||||
'security/ir.model.access.csv',
|
'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',
|
'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>
|
|
||||||
@@ -2,6 +2,6 @@
|
|||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from . import onboarding_onboarding
|
from . import onboarding_onboarding
|
||||||
from . import onboarding_step
|
from . import onboarding_onboarding_step
|
||||||
from . import onboarding_progress
|
from . import onboarding_progress
|
||||||
from . import onboarding_progress_step
|
from . import onboarding_progress_step
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# 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
|
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)
|
name = fields.Char('Name of the onboarding', translate=True)
|
||||||
# One word identifier used to define the onboarding panel's route: `/onboarding/{route_name}`.
|
# One word identifier used to define the onboarding panel's route: `/onboarding/{route_name}`.
|
||||||
route_name = fields.Char('One word name', required=True)
|
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(
|
panel_close_action_name = fields.Char(
|
||||||
'Closing action', help='Name of the onboarding model action to execute when closing the panel.')
|
'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.'),
|
('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_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):
|
def _compute_current_progress(self):
|
||||||
for onboarding in self:
|
for onboarding in self:
|
||||||
current_progress_id = onboarding.progress_ids.filtered(
|
current_progress_id = onboarding.progress_ids.filtered(
|
||||||
lambda progress: progress.company_id.id in {False, self.env.company.id})
|
lambda progress: progress.company_id.id in {False, self.env.company.id})
|
||||||
if current_progress_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_onboarding_state = current_progress_id.onboarding_state
|
||||||
onboarding.current_progress_id = current_progress_id
|
onboarding.current_progress_id = current_progress_id
|
||||||
onboarding.is_onboarding_closed = current_progress_id.is_onboarding_closed
|
onboarding.is_onboarding_closed = current_progress_id.is_onboarding_closed
|
||||||
@@ -53,28 +67,44 @@ class Onboarding(models.Model):
|
|||||||
onboarding.current_progress_id = False
|
onboarding.current_progress_id = False
|
||||||
onboarding.is_onboarding_closed = 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):
|
def action_close(self):
|
||||||
"""Close the onboarding panel."""
|
"""Close the onboarding panel."""
|
||||||
self.current_progress_id.action_close()
|
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):
|
def action_toggle_visibility(self):
|
||||||
self.current_progress_id.action_toggle_visibility()
|
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):
|
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 = self.filtered(lambda onboarding: not onboarding.current_progress_id)
|
||||||
onboardings_without_progress._create_progress()
|
onboardings_without_progress._create_progress()
|
||||||
return self.current_progress_id
|
return self.current_progress_id
|
||||||
@@ -83,8 +113,12 @@ class Onboarding(models.Model):
|
|||||||
return self.env['onboarding.progress'].create([
|
return self.env['onboarding.progress'].create([
|
||||||
{
|
{
|
||||||
'company_id': self.env.company.id if onboarding.is_per_company else False,
|
'company_id': self.env.company.id if onboarding.is_per_company else False,
|
||||||
'onboarding_id': onboarding.id
|
'onboarding_id': onboarding.id,
|
||||||
} for onboarding in self
|
'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):
|
def _prepare_rendering_values(self):
|
||||||
@@ -94,6 +128,7 @@ class Onboarding(models.Model):
|
|||||||
'close_model': 'onboarding.onboarding',
|
'close_model': 'onboarding.onboarding',
|
||||||
'steps': self.step_ids,
|
'steps': self.step_ids,
|
||||||
'state': self.current_progress_id._get_and_update_onboarding_state(),
|
'state': self.current_progress_id._get_and_update_onboarding_state(),
|
||||||
|
'text_completed': self.text_completed,
|
||||||
}
|
}
|
||||||
|
|
||||||
return values
|
return values
|
||||||
|
|||||||
+53
-22
@@ -1,8 +1,9 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# 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.addons.onboarding.models.onboarding_progress import ONBOARDING_PROGRESS_STATES
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
class OnboardingStep(models.Model):
|
class OnboardingStep(models.Model):
|
||||||
@@ -11,8 +12,7 @@ class OnboardingStep(models.Model):
|
|||||||
_order = 'sequence asc, id asc'
|
_order = 'sequence asc, id asc'
|
||||||
_rec_name = 'title'
|
_rec_name = 'title'
|
||||||
|
|
||||||
onboarding_id = fields.Many2one(
|
onboarding_ids = fields.Many2many('onboarding.onboarding', string='Onboardings')
|
||||||
'onboarding.onboarding', string='Onboarding', readonly=True, required=True, ondelete='cascade')
|
|
||||||
|
|
||||||
title = fields.Char('Title', translate=True)
|
title = fields.Char('Title', translate=True)
|
||||||
description = fields.Char('Description', 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")
|
help="Text on the panel's button to start this step")
|
||||||
done_icon = fields.Char('Font Awesome Icon when completed', default='fa-star')
|
done_icon = fields.Char('Font Awesome Icon when completed', default='fa-star')
|
||||||
done_text = fields.Char(
|
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 = fields.Binary("Step Image")
|
||||||
step_image_filename = fields.Char("Step Image Filename")
|
step_image_filename = fields.Char("Step Image Filename")
|
||||||
step_image_alt = fields.Char(
|
step_image_alt = fields.Char(
|
||||||
'Alt Text for the Step Image', default='Onboarding Step Image', translate=True,
|
'Alt Text for the Step Image', default='Onboarding Step Image', translate=True,
|
||||||
help='Show when impossible to load the image')
|
help='Show when impossible to load the image')
|
||||||
panel_step_open_action_name = fields.Char(
|
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, '
|
help='Name of the onboarding step model action to execute when opening the step, '
|
||||||
'e.g. action_open_onboarding_1_step_1')
|
'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,
|
'onboarding.progress.step', 'step_id', string='Onboarding Progress Step Records', readonly=True,
|
||||||
help='All related Onboarding Progress Step Records (across companies)')
|
help='All related Onboarding Progress Step Records (across companies)')
|
||||||
|
|
||||||
|
is_per_company = fields.Boolean('Is per company', default=True)
|
||||||
sequence = fields.Integer(default=10)
|
sequence = fields.Integer(default=10)
|
||||||
|
|
||||||
@api.depends_context('company')
|
@api.depends_context('company')
|
||||||
@api.depends('progress_ids', 'progress_ids.step_state')
|
@api.depends('progress_ids', 'progress_ids.step_state')
|
||||||
def _compute_current_progress(self):
|
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([
|
existing_progress_steps = self.progress_ids.filtered_domain([
|
||||||
('step_id', 'in', self.ids),
|
('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:
|
for step in self:
|
||||||
if step in existing_progress_steps.step_id:
|
if step in existing_progress_steps.step_id:
|
||||||
current_progress_step_id = existing_progress_steps.filtered(
|
current_progress_step_id = existing_progress_steps.filtered(
|
||||||
lambda progress_step: progress_step.step_id == step)
|
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_progress_step_id = current_progress_step_id
|
||||||
step.current_step_state = current_progress_step_id.step_state
|
step.current_step_state = current_progress_step_id.step_state
|
||||||
else:
|
else:
|
||||||
step.current_progress_step_id = False
|
step.current_progress_step_id = False
|
||||||
step.current_step_state = 'not_done'
|
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):
|
def action_set_just_done(self):
|
||||||
# Make sure progress records exist for the current context (company)
|
# Make sure progress records exist for the current context (company)
|
||||||
steps_without_progress = self.filtered(lambda step: not step.current_progress_step_id)
|
steps_without_progress = self.filtered(lambda step: not step.current_progress_step_id)
|
||||||
@@ -82,21 +111,23 @@ class OnboardingStep(models.Model):
|
|||||||
return super()._get_placeholder_filename(field)
|
return super()._get_placeholder_filename(field)
|
||||||
|
|
||||||
def _create_progress_steps(self):
|
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_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])
|
('company_id', 'in', [False, self.env.company.id])
|
||||||
])
|
])
|
||||||
progress_step_values = []
|
progress_step_values = [
|
||||||
for onboarding_progress_record in onboarding_progress_records:
|
{
|
||||||
progress_step_values += [
|
'step_id': step_id.id,
|
||||||
{
|
'progress_ids': [
|
||||||
'onboarding_id': onboarding_progress_record.onboarding_id.id,
|
Command.link(onboarding_progress_record.id)
|
||||||
'progress_id': onboarding_progress_record.id,
|
for onboarding_progress_record
|
||||||
'step_id': step_id.id,
|
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
|
} for step_id in self
|
||||||
in self.filtered(lambda step: self.onboarding_id == onboarding_progress_record.onboarding_id)
|
]
|
||||||
if step_id not in onboarding_progress_record.progress_step_ids.step_id
|
|
||||||
]
|
|
||||||
|
|
||||||
return self.env['onboarding.progress.step'].create(progress_step_values)
|
return self.env['onboarding.progress.step'].create(progress_step_values)
|
||||||
@@ -22,9 +22,10 @@ class OnboardingProgress(models.Model):
|
|||||||
company_id = fields.Many2one('res.company')
|
company_id = fields.Many2one('res.company')
|
||||||
onboarding_id = fields.Many2one(
|
onboarding_id = fields.Many2one(
|
||||||
'onboarding.onboarding', 'Related onboarding tracked', required=True, ondelete='cascade')
|
'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):
|
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
|
# not in _sql_constraint because COALESCE is not supported for PostgreSQL constraint
|
||||||
self.env.cr.execute("""
|
self.env.cr.execute("""
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS onboarding_progress_onboarding_company_uniq
|
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')
|
@api.depends('onboarding_id.step_ids', 'progress_step_ids', 'progress_step_ids.step_state')
|
||||||
def _compute_onboarding_state(self):
|
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:
|
for progress in self:
|
||||||
progress.onboarding_state = (
|
progress.onboarding_state = (
|
||||||
'not_done' if result.get(progress.id, 0) != len(progress.onboarding_id.step_ids)
|
'not_done' if (
|
||||||
else 'done')
|
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):
|
def action_close(self):
|
||||||
self.is_onboarding_closed = True
|
self.is_onboarding_closed = True
|
||||||
@@ -51,9 +56,11 @@ class OnboardingProgress(models.Model):
|
|||||||
progress.is_onboarding_closed = not progress.is_onboarding_closed
|
progress.is_onboarding_closed = not progress.is_onboarding_closed
|
||||||
|
|
||||||
def _get_and_update_onboarding_state(self):
|
def _get_and_update_onboarding_state(self):
|
||||||
"""Used to fetch the progress of an onboarding for rendering its panel and is expected to
|
"""Fetch the progress of an onboarding for rendering its panel.
|
||||||
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.
|
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()
|
self.ensure_one()
|
||||||
onboarding_states_values = {}
|
onboarding_states_values = {}
|
||||||
|
|||||||
@@ -10,18 +10,21 @@ class OnboardingProgressStep(models.Model):
|
|||||||
_description = 'Onboarding Progress Step Tracker'
|
_description = 'Onboarding Progress Step Tracker'
|
||||||
_rec_name = 'step_id'
|
_rec_name = 'step_id'
|
||||||
|
|
||||||
progress_id = fields.Many2one(
|
progress_ids = fields.Many2many('onboarding.progress', string='Related Onboarding Progress Tracker')
|
||||||
'onboarding.progress', 'Related Onboarding Progress Tracker', required=True, ondelete='cascade')
|
|
||||||
step_state = fields.Selection(
|
step_state = fields.Selection(
|
||||||
ONBOARDING_PROGRESS_STATES, string='Onboarding Step Progress', default='not_done')
|
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(
|
step_id = fields.Many2one(
|
||||||
'onboarding.onboarding.step', string='Onboarding Step', required=True, ondelete='cascade')
|
'onboarding.onboarding.step', string='Onboarding Step', required=True, ondelete='cascade')
|
||||||
|
|
||||||
_sql_constraints = [
|
company_id = fields.Many2one('res.company', ondelete='cascade')
|
||||||
('progress_step_uniq', 'unique (progress_id, step_id)',
|
|
||||||
'There cannot be multiple records of the same onboarding step completion for the same Progress record.'),
|
def 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):
|
def action_consolidate_just_done(self):
|
||||||
was_just_done = self.filtered(lambda progress: progress.step_state == 'just_done')
|
was_just_done = self.filtered(lambda progress: progress.step_state == 'just_done')
|
||||||
|
|||||||
+8
-1
@@ -11,8 +11,10 @@ $o-onboarding-step-width: map-get($container-max-widths, 'lg') / 4 !default;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.o_onboarding_completed_message {
|
.o_onboarding_completed_message {
|
||||||
|
opacity: 0;
|
||||||
box-shadow: 0 -7px 20px -5px rgba(#000, 0.3);
|
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
|
// = 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 {
|
@keyframes o_onboarding_zoomIn {
|
||||||
from {
|
from {
|
||||||
transform: scale3d(0.8, 0.8, 0.8);
|
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,5 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import case
|
||||||
from . import test_onboarding
|
from . import test_onboarding
|
||||||
from . import test_onboarding_concurrency
|
from . import test_onboarding_concurrency
|
||||||
|
|||||||
@@ -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))
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# 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
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super().setUpClass()
|
super().setUpClass()
|
||||||
@@ -18,12 +18,12 @@ class TestOnboardingCommon(TransactionCase):
|
|||||||
})
|
})
|
||||||
cls.user_admin.company_ids |= cls.company_2
|
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,
|
'is_per_company': False,
|
||||||
'route_name': 'onboarding1'
|
} for onboarding_id in range(2)
|
||||||
}
|
|
||||||
])
|
])
|
||||||
|
|
||||||
# create a fake action for step opening
|
# 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([
|
cls.onboarding_1_step_1, cls.onboarding_1_step_2 = cls.env['onboarding.onboarding.step'].create([
|
||||||
{
|
{
|
||||||
'title': f'Test Onboarding 1 - Step {step_n}',
|
'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',
|
'panel_step_open_action_name': 'action_fake_open_onboarding_step',
|
||||||
}
|
}
|
||||||
for step_n in range(1, 3)
|
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
|
# Create progress records as would happen through the controller
|
||||||
cls.onboarding_1.with_company(cls.company_1)._search_or_create_progress()
|
(cls.onboarding_1 + cls.onboarding_2).with_company(cls.company_1)._search_or_create_progress()
|
||||||
|
|
||||||
def assert_step_is_done(self, step, also_with_company=None):
|
|
||||||
self.assertIn(
|
|
||||||
step.current_progress_step_id.step_state, {'done', 'just_done'},
|
|
||||||
f'Expected done-like current state for step {step.id} for {step.env.company}')
|
|
||||||
self.assertEqual(step.current_progress_step_id.step_state, step.current_step_state)
|
|
||||||
if also_with_company:
|
|
||||||
self.assert_step_is_done(step.with_company(also_with_company))
|
|
||||||
|
|
||||||
def assert_step_is_not_done(self, step, also_with_company=None):
|
|
||||||
self.assertIn(
|
|
||||||
step.current_progress_step_id.step_state, {'not_done', False},
|
|
||||||
f'Expected "not_done" current state for step {step.id} for {step.env.company}')
|
|
||||||
self.assertEqual(step.current_step_state, 'not_done')
|
|
||||||
if also_with_company:
|
|
||||||
self.assert_step_is_not_done(step.with_company(also_with_company))
|
|
||||||
|
|
||||||
def assert_onboarding_is_done(self, onboarding, also_with_company=None):
|
|
||||||
self.assertIn(
|
|
||||||
onboarding.current_progress_id.onboarding_state, {'done', 'just_done'},
|
|
||||||
f'Expected done-like current state for onboarding {onboarding.name} '
|
|
||||||
f'for {onboarding.env.company}')
|
|
||||||
self.assertEqual(onboarding.current_progress_id.onboarding_state,
|
|
||||||
onboarding.current_onboarding_state)
|
|
||||||
if also_with_company:
|
|
||||||
self.assert_onboarding_is_done(onboarding.with_company(also_with_company))
|
|
||||||
|
|
||||||
def assert_onboarding_is_not_done(self, onboarding, also_with_company=None):
|
|
||||||
self.assertEqual(
|
|
||||||
onboarding.current_progress_id.onboarding_state, 'not_done',
|
|
||||||
f'Expected "not_done" current state for onboarding {onboarding.name} '
|
|
||||||
f'for {onboarding.env.company}')
|
|
||||||
self.assertEqual(onboarding.current_onboarding_state, 'not_done')
|
|
||||||
if also_with_company:
|
|
||||||
self.assert_onboarding_is_not_done(onboarding.with_company(also_with_company))
|
|
||||||
|
|
||||||
def activate_company(self, company):
|
def activate_company(self, company):
|
||||||
self.onboarding_1_step_1 = self.onboarding_1_step_1.with_company(company)
|
self.onboarding_1_step_1 = self.onboarding_1_step_1.with_company(company)
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
|
|
||||||
from psycopg2 import IntegrityError
|
from psycopg2 import IntegrityError
|
||||||
|
|
||||||
|
from odoo import Command
|
||||||
from odoo.addons.onboarding.tests.common import TestOnboardingCommon
|
from odoo.addons.onboarding.tests.common import TestOnboardingCommon
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
from odoo.tools import mute_logger
|
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
|
# Adding new step resets onboarding state to 'not_done' even if closed
|
||||||
onboarding_1_step_3 = self.env['onboarding.onboarding.step'].create({
|
onboarding_1_step_3 = self.env['onboarding.onboarding.step'].create({
|
||||||
'title': 'Test Onboarding 1 - Step 3',
|
'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',
|
'panel_step_open_action_name': 'action_fake_open_onboarding_step',
|
||||||
})
|
})
|
||||||
self.assert_step_is_not_done(onboarding_1_step_3)
|
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'
|
# Adding new step resets onboarding state to 'not_done'
|
||||||
self.env['onboarding.onboarding.step'].create({
|
self.env['onboarding.onboarding.step'].create({
|
||||||
'title': 'Test Onboarding 1 - Step 4',
|
'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',
|
'panel_step_open_action_name': 'action_fake_open_onboarding_step',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -85,14 +89,16 @@ class TestOnboarding(TestOnboardingCommon):
|
|||||||
# Completing onboarding as company_1
|
# Completing onboarding as company_1
|
||||||
self.assertEqual(self.env.company, self.company_1)
|
self.assertEqual(self.env.company, self.company_1)
|
||||||
|
|
||||||
# Updating onboarding to per-company
|
# Updating onboarding (and steps) to per-company
|
||||||
self.onboarding_1.is_per_company = True
|
self.onboarding_1_step_1.is_per_company = True
|
||||||
|
|
||||||
# Required after progress reset (simulate role of controller)
|
# Required after progress reset (simulate role of controller)
|
||||||
self.onboarding_1._search_or_create_progress()
|
self.onboarding_1._search_or_create_progress()
|
||||||
|
|
||||||
self.onboarding_1_step_1.action_set_just_done()
|
self.onboarding_1_step_1.action_set_just_done()
|
||||||
self.assert_step_is_done(self.onboarding_1_step_1)
|
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.onboarding_1_step_2.action_set_just_done()
|
||||||
self.assert_onboarding_is_done(self.onboarding_1)
|
self.assert_onboarding_is_done(self.onboarding_1)
|
||||||
|
|
||||||
@@ -101,19 +107,14 @@ class TestOnboarding(TestOnboardingCommon):
|
|||||||
# First access from company_2
|
# First access from company_2
|
||||||
self.onboarding_1._search_or_create_progress()
|
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)
|
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)
|
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.onboarding_1_step_1.action_set_just_done()
|
||||||
self.assert_step_is_done(self.onboarding_1_step_1)
|
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)
|
self.assert_onboarding_is_done(self.onboarding_1)
|
||||||
|
|
||||||
# is_onboarding_closed status is also company-independent
|
# 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)
|
self.assertFalse(self.onboarding_1.with_company(self.company_1).current_progress_id.is_onboarding_closed)
|
||||||
|
|
||||||
def test_onboarding_to_company_change(self):
|
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
|
# Completing onboarding as company_1
|
||||||
self.assertEqual(self.env.company, self.company_1)
|
self.assertEqual(self.env.company, self.company_1)
|
||||||
self.onboarding_1_step_1.action_set_just_done()
|
self.onboarding_1_step_1.action_set_just_done()
|
||||||
self.onboarding_1_step_2.action_set_just_done()
|
self.onboarding_1_step_2.action_set_just_done()
|
||||||
self.assert_onboarding_is_done(self.onboarding_1)
|
self.assert_onboarding_is_done(self.onboarding_1)
|
||||||
|
|
||||||
# Updating onboarding to per-company
|
# Updating onboarding step 1 to per-company
|
||||||
self.onboarding_1.is_per_company = True
|
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)
|
# Required after progress reset (simulate role of controller)
|
||||||
self.onboarding_1._search_or_create_progress()
|
self.onboarding_1._search_or_create_progress()
|
||||||
|
|
||||||
self.assert_onboarding_is_not_done(self.onboarding_1)
|
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')
|
@mute_logger('odoo.sql_db')
|
||||||
def test_progress_no_company_uniqueness(self):
|
def test_progress_no_company_uniqueness(self):
|
||||||
"""Check that there cannot be two progress records created for
|
"""Check that there cannot be two progress records created for
|
||||||
@@ -159,8 +171,8 @@ class TestOnboarding(TestOnboardingCommon):
|
|||||||
See also ``test_progress_no_company_uniqueness``
|
See also ``test_progress_no_company_uniqueness``
|
||||||
"""
|
"""
|
||||||
# Updating onboarding to per-company
|
# Updating onboarding to per-company
|
||||||
self.onboarding_1.is_per_company = True
|
self.onboarding_1_step_1.is_per_company = True
|
||||||
# Required after progress reset (simulate role of controller)
|
# Create an onboarding_progress (simulate role of controller)
|
||||||
self.onboarding_1._search_or_create_progress()
|
self.onboarding_1._search_or_create_progress()
|
||||||
|
|
||||||
with self.assertRaises(IntegrityError):
|
with self.assertRaises(IntegrityError):
|
||||||
@@ -168,3 +180,38 @@ class TestOnboarding(TestOnboardingCommon):
|
|||||||
'onboarding_id': self.onboarding_1.id,
|
'onboarding_id': self.onboarding_1.id,
|
||||||
'company_id': self.env.company.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))
|
||||||
|
|||||||
+28
-48
@@ -1,5 +1,27 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<odoo>
|
<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">
|
<template id="onboarding_container">
|
||||||
<div class="modal o_onboarding_modal o_technical_modal" tabindex="-1" role="dialog">
|
<div class="modal o_onboarding_modal o_technical_modal" tabindex="-1" role="dialog">
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
@@ -13,8 +35,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-footer justify-content-start">
|
<div class="modal-footer justify-content-start">
|
||||||
<a type="action" class="btn btn-primary" data-bs-dismiss="modal"
|
<a type="action" class="btn btn-primary" data-bs-dismiss="modal"
|
||||||
data-o-hide-banner="true"
|
data-o-hide-banner="true" t-att-data-model="close_model" t-att-data-method="close_method"
|
||||||
t-att-data-model="close_model" t-att-data-method="close_method">
|
>
|
||||||
Remove
|
Remove
|
||||||
</a>
|
</a>
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
<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>
|
<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 class="o_onboarding_steps d-flex" t-out="0"/>
|
||||||
|
|
||||||
|
<div t-if="state.get('onboarding_state') == 'just_done'"
|
||||||
<div t-if="state.get('onboarding_state') in ('done', 'just_done')"
|
|
||||||
t-att-state="state.get('onboarding_state')"
|
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">
|
<span class="h3 m-0">
|
||||||
<i class="fa fa-check text-success me-3" />
|
<i class="fa fa-check text-success me-3" />
|
||||||
<t t-if="text_completed" t-out="text_completed" />
|
<t t-if="text_completed" t-out="text_completed" />
|
||||||
<t t-else="">Nice work! Your configuration is done.</t>
|
|
||||||
</span>
|
</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">
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,44 +99,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
</odoo>
|
||||||
@@ -36,7 +36,9 @@
|
|||||||
</group>
|
</group>
|
||||||
<notebook>
|
<notebook>
|
||||||
<page name="Onboarding steps">
|
<page name="Onboarding steps">
|
||||||
<field name="step_ids"/>
|
<group string="Steps">
|
||||||
|
<field name="step_ids" nolabel="1" colspan="2"/>
|
||||||
|
</group>
|
||||||
</page>
|
</page>
|
||||||
</notebook>
|
</notebook>
|
||||||
</sheet>
|
</sheet>
|
||||||
@@ -51,8 +53,9 @@
|
|||||||
<tree string="Onboarding Steps">
|
<tree string="Onboarding Steps">
|
||||||
<field name="sequence" widget="handle"/>
|
<field name="sequence" widget="handle"/>
|
||||||
<field name="title"/>
|
<field name="title"/>
|
||||||
<field name="onboarding_id" optional="hide"/>
|
<field name="onboarding_ids" optional="hide"/>
|
||||||
<field name="current_step_state"/>
|
<field name="current_step_state"/>
|
||||||
|
<field name="is_per_company" optional="hide"/>
|
||||||
</tree>
|
</tree>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
@@ -65,10 +68,16 @@
|
|||||||
<sheet>
|
<sheet>
|
||||||
<group>
|
<group>
|
||||||
<field name="title"/>
|
<field name="title"/>
|
||||||
<field name="onboarding_id"/>
|
<field name="current_step_state"/>
|
||||||
<field name="panel_step_open_action_name"/>
|
<field name="panel_step_open_action_name"/>
|
||||||
|
<field name="is_per_company"/>
|
||||||
</group>
|
</group>
|
||||||
<notebook>
|
<notebook>
|
||||||
|
<page name="Onboardings using this step">
|
||||||
|
<group string="Onboardings">
|
||||||
|
<field name="onboarding_ids" nolabel="1" colspan="2"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
<page name="Step rendering">
|
<page name="Step rendering">
|
||||||
<group>
|
<group>
|
||||||
<field name="description"/>
|
<field name="description"/>
|
||||||
|
|||||||
@@ -5,8 +5,9 @@
|
|||||||
'version': '2.0',
|
'version': '2.0',
|
||||||
'category': 'Hidden',
|
'category': 'Hidden',
|
||||||
'summary': "The payment engine used by payment provider modules.",
|
'summary': "The payment engine used by payment provider modules.",
|
||||||
'depends': ['portal'],
|
'depends': ['onboarding', 'portal'],
|
||||||
'data': [
|
'data': [
|
||||||
|
'data/onboarding_data.xml',
|
||||||
'data/payment_method_data.xml',
|
'data/payment_method_data.xml',
|
||||||
'data/payment_provider_data.xml',
|
'data/payment_provider_data.xml',
|
||||||
'data/payment_cron.xml',
|
'data/payment_cron.xml',
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -5,5 +5,6 @@ from . import payment_method
|
|||||||
from . import payment_provider
|
from . import payment_provider
|
||||||
from . import payment_token
|
from . import payment_token
|
||||||
from . import payment_transaction
|
from . import payment_transaction
|
||||||
|
from . import onboarding_onboarding_step
|
||||||
from . import res_company
|
from . import res_company
|
||||||
from . import res_partner
|
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")
|
||||||
@@ -6,10 +6,6 @@ from odoo import api, fields, models
|
|||||||
class ResCompany(models.Model):
|
class ResCompany(models.Model):
|
||||||
_inherit = 'res.company'
|
_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(
|
payment_onboarding_payment_method = fields.Selection(
|
||||||
string="Selected onboarding payment method",
|
string="Selected onboarding payment method",
|
||||||
selection=[
|
selection=[
|
||||||
@@ -56,15 +52,3 @@ class ResCompany(models.Model):
|
|||||||
modules_sudo = self.env['ir.module.module'].sudo().search([('name', 'in', module_names)])
|
modules_sudo = self.env['ir.module.module'].sudo().search([('name', 'in', module_names)])
|
||||||
STATES = ['installed', 'to install', 'to upgrade']
|
STATES = ['installed', 'to install', 'to upgrade']
|
||||||
modules_sudo.filtered(lambda m: m.state not in STATES).button_immediate_install()
|
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':
|
if payment_method == 'stripe':
|
||||||
return self._start_stripe_onboarding()
|
return self._start_stripe_onboarding()
|
||||||
|
|
||||||
# the user clicked `apply` and not cancel so we can assume this step is done.
|
# the user clicked `apply` and not cancel, so we can assume this step is done.
|
||||||
self._set_payment_provider_onboarding_step_done()
|
self.env['onboarding.onboarding.step'].sudo().action_validate_step_payment_provider()
|
||||||
return {'type': 'ir.actions.act_window_close'}
|
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):
|
def _start_stripe_onboarding(self):
|
||||||
""" Start Stripe Connect onboarding. """
|
""" Start Stripe Connect onboarding. """
|
||||||
menu = self.env.ref('account_payment.payment_provider_menu', False)
|
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')
|
@http.route(_onboarding_return_url, type='http', methods=['GET'], auth='user')
|
||||||
def stripe_return_from_onboarding(self, provider_id, menu_id):
|
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
|
The user is redirected to this route by Stripe after or during (if the user clicks on a
|
||||||
dedicated button) the onboarding.
|
dedicated button) the onboarding.
|
||||||
@@ -23,7 +23,9 @@ class OnboardingController(http.Controller):
|
|||||||
`ir.ui.menu` id
|
`ir.ui.menu` id
|
||||||
"""
|
"""
|
||||||
stripe_provider = request.env['payment.provider'].browse(int(provider_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')
|
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})
|
get_params_string = url_encode({'action': action.id, 'id': provider_id, 'menu_id': menu_id})
|
||||||
return request.redirect(f'/web?#{get_params_string}')
|
return request.redirect(f'/web?#{get_params_string}')
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ class PaymentProvider(models.Model):
|
|||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|
||||||
if self.state == 'enabled':
|
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'}
|
action = {'type': 'ir.actions.act_window_close'}
|
||||||
else:
|
else:
|
||||||
# Account creation
|
# Account creation
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ This module contains all the common features of Sales Management and eCommerce.
|
|||||||
'data/mail_message_subtype_data.xml',
|
'data/mail_message_subtype_data.xml',
|
||||||
'data/mail_template_data.xml',
|
'data/mail_template_data.xml',
|
||||||
'data/ir_config_parameter.xml', # Needs mail_template_data
|
'data/ir_config_parameter.xml', # Needs mail_template_data
|
||||||
|
'data/onboarding_data.xml',
|
||||||
|
|
||||||
'wizard/account_accrued_orders_wizard_views.xml',
|
'wizard/account_accrued_orders_wizard_views.xml',
|
||||||
'wizard/mass_cancel_orders_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_product_views.xml',
|
||||||
'views/product_views.xml',
|
'views/product_views.xml',
|
||||||
'views/res_partner_views.xml',
|
'views/res_partner_views.xml',
|
||||||
'views/sale_onboarding_views.xml',
|
|
||||||
'views/sale_order_line_views.xml',
|
'views/sale_order_line_views.xml',
|
||||||
'views/sale_portal_templates.xml',
|
'views/sale_portal_templates.xml',
|
||||||
'views/utm_campaign_views.xml',
|
'views/utm_campaign_views.xml',
|
||||||
|
|||||||
@@ -2,5 +2,4 @@
|
|||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from . import catalog
|
from . import catalog
|
||||||
from . import onboarding
|
|
||||||
from . import portal
|
from . import portal
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
@@ -5,6 +5,8 @@ from . import analytic
|
|||||||
from . import account_move
|
from . import account_move
|
||||||
from . import account_move_line
|
from . import account_move_line
|
||||||
from . import crm_team
|
from . import crm_team
|
||||||
|
from . import onboarding_onboarding
|
||||||
|
from . import onboarding_onboarding_step
|
||||||
from . import payment_provider
|
from . import payment_provider
|
||||||
from . import payment_transaction
|
from . import payment_transaction
|
||||||
from . import product_product
|
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,11 +1,6 @@
|
|||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
import base64
|
|
||||||
|
|
||||||
from odoo import api, fields, models, _
|
from odoo import 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
|
|
||||||
|
|
||||||
|
|
||||||
class ResCompany(models.Model):
|
class ResCompany(models.Model):
|
||||||
@@ -39,18 +34,6 @@ class ResCompany(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# sale onboarding
|
# 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(
|
sale_onboarding_payment_method = fields.Selection(
|
||||||
selection=[
|
selection=[
|
||||||
('digital_signature', "Sign online"),
|
('digital_signature', "Sign online"),
|
||||||
@@ -60,104 +43,3 @@ class ResCompany(models.Model):
|
|||||||
('manual', "Manual Payment"),
|
('manual', "Manual Payment"),
|
||||||
],
|
],
|
||||||
string="Sale onboarding selected payment method")
|
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 |
@@ -23,58 +23,58 @@ registry.category("web_tour.tours").add("sale_tour", {
|
|||||||
position: "bottom",
|
position: "bottom",
|
||||||
edition: "enterprise"
|
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",
|
extra_trigger: ".o_sale_order",
|
||||||
content: _t("Start by checking your company's data."),
|
content: _t("Start by checking your company's data."),
|
||||||
position: "bottom",
|
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]',
|
trigger: 'input[id=street_0]',
|
||||||
content: _t("Complete your company's data"),
|
content: _t("Complete your company's data"),
|
||||||
position: "bottom",
|
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."),
|
content: _t("Looks good. Let's continue."),
|
||||||
position: "left",
|
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(),
|
}, stepUtils.showAppsMenuItem(),
|
||||||
{
|
{
|
||||||
trigger: ".o_app[data-menu-xmlid='sale.sale_menu_root']",
|
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",
|
edition: "community",
|
||||||
auto: true,
|
auto: true,
|
||||||
}, {
|
}, {
|
||||||
trigger: ".o_app[data-menu-xmlid='sale.sale_menu_root']",
|
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",
|
edition: "enterprise",
|
||||||
auto: true,
|
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",
|
extra_trigger: ".o_sale_order",
|
||||||
content: _t("Customize your quotes and orders."),
|
content: _t("Customize your quotes and orders."),
|
||||||
position: "bottom",
|
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']",
|
trigger: "button[name='document_layout_save']",
|
||||||
extra_trigger: ".o_sale_order",
|
extra_trigger: ".o_sale_order",
|
||||||
content: _t("Good job, let's continue."),
|
content: _t("Good job, let's continue."),
|
||||||
position: "top", // dot NOT move to bottom, it would cause a resize flicker
|
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",
|
extra_trigger: ".o_sale_order",
|
||||||
content: _t("To speed up order confirmation, we can activate electronic signatures or payments."),
|
content: _t("To speed up order confirmation, we can activate electronic signatures or payments."),
|
||||||
position: "bottom",
|
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']",
|
trigger: "button[name='add_payment_methods']",
|
||||||
extra_trigger: ".o_sale_order",
|
extra_trigger: ".o_sale_order",
|
||||||
content: _t("Lets keep electronic signature for now."),
|
content: _t("Lets keep electronic signature for now."),
|
||||||
position: "bottom",
|
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",
|
extra_trigger: ".o_sale_order",
|
||||||
content: _t("Now, we'll create a sample quote."),
|
content: _t("Now, we'll create a sample quote."),
|
||||||
position: "bottom",
|
position: "bottom",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from . import test_access_rights
|
|||||||
from . import test_accrued_sale_orders
|
from . import test_accrued_sale_orders
|
||||||
from . import test_common
|
from . import test_common
|
||||||
from . import test_controllers
|
from . import test_controllers
|
||||||
|
from . import test_onboarding
|
||||||
from . import test_onchange
|
from . import test_onchange
|
||||||
from . import test_payment_flow
|
from . import test_payment_flow
|
||||||
from . import test_reinvoice
|
from . import test_reinvoice
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -15,4 +15,4 @@ class TestOnboarding(HttpCase):
|
|||||||
('state', '=', 'draft')
|
('state', '=', 'draft')
|
||||||
]).state = 'cancel'
|
]).state = 'cancel'
|
||||||
self.env['product.product'].search([]).active = False
|
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>
|
|
||||||
@@ -215,7 +215,7 @@
|
|||||||
<field name="mode">primary</field>
|
<field name="mode">primary</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//tree" position="attributes">
|
<xpath expr="//tree" position="attributes">
|
||||||
<attribute name="banner_route">/sales/sale_quotation_onboarding_panel</attribute>
|
<attribute name="banner_route">/onboarding/sale_quotation</attribute>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|||||||
@@ -20,11 +20,6 @@ class PaymentWizard(models.TransientModel):
|
|||||||
('paypal', "PayPal"),
|
('paypal', "PayPal"),
|
||||||
('manual', "Custom payment instructions"),
|
('manual', "Custom payment instructions"),
|
||||||
], default=_get_default_payment_method)
|
], 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):
|
def add_payment_methods(self):
|
||||||
self.env.company.sale_onboarding_payment_method = self.payment_method
|
self.env.company.sale_onboarding_payment_method = self.payment_method
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ class TestUi(HttpCase):
|
|||||||
self.start_tour("/web", 'sale_tour', login="admin", step_delay=100)
|
self.start_tour("/web", 'sale_tour', login="admin", step_delay=100)
|
||||||
|
|
||||||
def test_02_sale_tour_company_onboarding_done(self):
|
def test_02_sale_tour_company_onboarding_done(self):
|
||||||
self.env.company.set_onboarding_step_done('base_onboarding_company_state')
|
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)
|
self.start_tour("/web", "sale_tour", login="admin", step_delay=100)
|
||||||
|
|
||||||
def test_03_sale_quote_tour(self):
|
def test_03_sale_quote_tour(self):
|
||||||
self.env['res.partner'].create({'name': 'Agrolait', 'email': 'agro@lait.be'})
|
self.env['res.partner'].create({'name': 'Agrolait', 'email': 'agro@lait.be'})
|
||||||
|
|||||||
@@ -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/ace.scss',
|
||||||
'web/static/src/legacy/scss/fields.scss',
|
'web/static/src/legacy/scss/fields.scss',
|
||||||
'web/static/src/legacy/scss/views.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/attachment_preview.scss',
|
||||||
'web/static/src/legacy/scss/base_document_layout.scss',
|
'web/static/src/legacy/scss/base_document_layout.scss',
|
||||||
'web/static/src/legacy/scss/fields_extra.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'),
|
('include', 'web.assets_backend'),
|
||||||
'web/static/src/**/*.dark.scss',
|
'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
|
# SUB BUNDLES
|
||||||
@@ -525,7 +521,6 @@ This module provides the core of the Odoo Web Client.
|
|||||||
'web._assets_primary_variables': [
|
'web._assets_primary_variables': [
|
||||||
'web/static/src/scss/primary_variables.scss',
|
'web/static/src/scss/primary_variables.scss',
|
||||||
'web/static/src/**/*.variables.scss',
|
'web/static/src/**/*.variables.scss',
|
||||||
'base/static/src/scss/onboarding.variables.scss',
|
|
||||||
],
|
],
|
||||||
'web._assets_secondary_variables': [
|
'web._assets_secondary_variables': [
|
||||||
'web/static/src/scss/secondary_variables.scss',
|
'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)
|
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):
|
def document_layout_save(self):
|
||||||
# meant to be overridden
|
# meant to be overridden
|
||||||
return self.env.context.get('report_action') or {'type': 'ir.actions.act_window_close'}
|
return self.env.context.get('report_action') or {'type': 'ir.actions.act_window_close'}
|
||||||
|
|||||||
@@ -1,21 +1,12 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# 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):
|
class ResCompany(models.Model):
|
||||||
_inherit = 'res.company'
|
_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):
|
def _get_default_pricelist_vals(self):
|
||||||
""" Override of product. Called at company creation or activation of the pricelist setting.
|
""" Override of product. Called at company creation or activation of the pricelist setting.
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ The kernel of Odoo, needed for all installation.
|
|||||||
'views/ir_property_views.xml',
|
'views/ir_property_views.xml',
|
||||||
'views/res_config_settings_views.xml',
|
'views/res_config_settings_views.xml',
|
||||||
'views/report_paperformat_views.xml',
|
'views/report_paperformat_views.xml',
|
||||||
'views/onboarding_views.xml',
|
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
],
|
],
|
||||||
'demo': [
|
'demo': [
|
||||||
|
|||||||
@@ -68,8 +68,6 @@ class Company(models.Model):
|
|||||||
company_registry = fields.Char(related='partner_id.company_registry', string="Company ID", readonly=False)
|
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))
|
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')
|
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")
|
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()
|
primary_color = fields.Char()
|
||||||
secondary_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)
|
return (self.env['ir.actions.report'].search([('report_name', '=', report_name)], limit=1)
|
||||||
.report_action(docids))
|
.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
|
@api.model
|
||||||
def _get_main_company(self):
|
def _get_main_company(self):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ class Partner(models.Model):
|
|||||||
('name', populate.constant('company_{counter}')),
|
('name', populate.constant('company_{counter}')),
|
||||||
('sequence', populate.randint(0, 100)),
|
('sequence', populate.randint(0, 100)),
|
||||||
('company_registry', populate.iterate([False, 'company_registry_{counter}'])),
|
('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'])),
|
('primary_color', populate.iterate([False, '', '#ff7755'])),
|
||||||
('secondary_color', populate.iterate([False, '', '#ffff55'], seed='primary_color')),
|
('secondary_color', populate.iterate([False, '', '#ffff55'], seed='primary_color')),
|
||||||
('currency_id', populate.iterate(active_currencies)),
|
('currency_id', populate.iterate(active_currencies)),
|
||||||
|
|||||||
Reference in New Issue
Block a user