[MERGE] account,(website_)sale: Add onboarding configuration panels

Purpose
=======

Ease the configuration of the main applications by adding configuration panels on the main views,
i.e. on the customer invoices, quotations and ecommerce dashboard. This panels are composed by steps
to configure the company data, the report layout and templates, the payment acquirers,...

Specification
=============

Invoicing Application
---------------------

The gist of the task is to add a config bar for invoicing app. After going through the config step, the system should be all set to send real invoices

Mockup: https://app.moqups.com/fgi@openerp.com/qhrp1ZJB1i/view

1. Configuration bar
- The current onboarding modal should not pop up when the user clicks on "Print Invoice" anymore: https://drive.google.com/a/odoo.com/file/d/1xMin8lBB3guDOlIQaGCnLrBKKl5nbA-9/view?usp=drivesdk
- Add the following configuration bar between the top menu and the list of Customer Invoices
- This bar can only be viewed with "administration - settings" access rights.
- Each part of the bar opens a corresponding modal (defined below).
- Once information is saved on the modal, a green checksign appears next to it on the configuration bar. The user can still open the modal and modify information afterwards.
- Add a grey cross on the top right side of the bar. Clicking on it removes definitely the bar.
- The bar has to stay as long as the user has not clicked on the grey cross.

config bar design branch: https://www.odoo.com/web#id=1835748&view_type=form&model=project.task&action=333&active_id=133&menu_id=4720

2. Company Data
Do the following changes to the current company form:
- hide company logo in the modal (will be shown next step). But keep it visible in the company full form
- remove "Report Footer", "Company tagline", "Verify VAT  Number" https://drive.google.com/a/odoo.com/file/d/1CrGoY_sH4wsTHuu6Zggilo6qOzWb0oCW/view?usp=drivesdk
- remove tabs: https://drive.google.com/a/odoo.com/file/d/1X7EglwY5zHmJtR4Xafnkm9uBX6TlcJsB/view?usp=drivesdk
- mark this step as done if street 1 is filled

3. Invoice Template
new screen to choose a report template, set a logo, etc.
- Use the default logo used in demo data as new default image for any new company. See for reference : http://nimb.ws/1TFn8O
- add "company tagline" and "document footer" (removed from company form)
- open this screen in general settings: https://drive.google.com/a/odoo.com/file/d/1hP5CW7-MPXre8r8Ex-KKSomVaef932qV/view?usp=drivesdk
- mark as done if logo is set.

4. Payment Method
- Activate the payment acquirer selected, and set in production mode (if paypal or stripe)
- For manual payment, rename "Wire Transfer" payment acquirer into the name provider by the user
- if any payment method selected, activate "Online Payment" in invoicing settings: https://drive.google.com/a/odoo.com/file/d/1Vy6os1eDznumOyR80Q2nFctzQ7S2PvYN/view?usp=drivesdk
- otherwise the customer will not be prompted to pay online.
- mark as done if any method is selected or if "Skip this step" is clicked.

5. Sample Invoice - Send a sample draft invoice to user email address
- Open the email composer modal with invoice's email template (like clicking "Send by email" in the invoice), do the following changes:
- Hide the template field and the "save as new template" button.
- Generate & delete direclty the sample draft invoice to be sent when clicking the config bar step. The user should never see this invoice appear in the invoice list
- Generate the invoice with following information:
    - put the admin as customer.
    - for the Invoice Lines : do not enter any Product. Add a 2-3 Descriptions, with a Quantity and a Unit price.
    - the Invoice has to stay in draft and should not appear in the Customer Invoices list.
- Allow to open portal view of this sample invoice from the email sent (as any other invoice) / if complex, to check with AL
- mark as done when the email is sent.

Sale Application
================

help users configure the system in order to send the first quotation to a customer

mockup: https://app.moqups.com/fgi@openerp.com/rP8gOb077Y/view

remove current mechanism of company data completion when clicking Print button in the quotation form (e.g. with company logo not set).
Signature: if Yes, activate the option in sales settings https://drive.google.com/a/odoo.com/file/d/1IoHn_3b4BbevKqdVajBWysn_jamQ1tAA/view?usp=drivesdk
Payment: activate the option in sales settings + activate the selected payment acquirer
background image: https://drive.google.com/a/odoo.com/file/d/1LXRrslRBEo8ma_1dFH8nTTlhiE1uPa-Z/view?usp=drivesdk
see in https://projects.invisionapp.com/share/2MGSJ30H53F#/screens/290568105

Ecommerce Application
=====================

help users go through all the config steps needed to start selling

Add the bar in the ecommerce dashboard: https://drive.google.com/a/odoo.com/file/d/1NmFJXjYZXN2KdZ9Fv19cPqyq1v-1ehiJ/view?usp=drivesdk
steps to implement:
Theme (checked if a theme is installed)
allow the user to change theme. When installing a new theme, uninstall any other theme installed (uninstall theme_common? what about community themes that don't use theme common?)
Company Data
[for eCommerce only] Payment Method (same screen than in invoices)
Domain Name (set a custom domain name, only for saas users) -> display the domain config modal used in https://accounts.odoo.com/my/databases/manage: https://drive.google.com/a/odoo.com/file/d/1K_AYHJGHWBb2g08vq6ICDc2BHhErb5HI/view?usp=drivesdk

Accounting Application
======================

Transform old config bar to new style config bar
This commit is contained in:
Yannick Tivisse
2018-07-20 12:00:35 +02:00
65 changed files with 2081 additions and 169 deletions
+1 -1
View File
@@ -62,7 +62,7 @@ Core mechanisms for the accounting modules. To display the menuitems, install th
'report/account_report_payment_receipt_templates.xml',
'data/payment_receipt_data.xml',
'wizard/setup_wizards_view.xml',
'views/account_dashboard_setup_bar.xml',
'views/account_onboarding_templates.xml',
'wizard/account_report_tax_view.xml',
'views/report_tax.xml',
'views/res_users_views.xml',
+1
View File
@@ -2,4 +2,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mail
from . import onboarding
from . import portal
+37
View File
@@ -0,0 +1,37 @@
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):
""" 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. """
if not request.env.user._is_admin() or \
request.env.user.company_id.account_invoice_onboarding_closed:
return {}
return {
'html': request.env.ref('account.account_invoice_onboarding_panel').render({
'company': request.env.user.company_id,
})
}
@http.route('/account/account_dashboard_onboarding', auth='user', type='json')
def account_dashboard_onboarding(self):
""" 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. """
if not request.env.user._is_admin() or \
request.env.user.company_id.account_dashboard_onboarding_closed:
return {}
return {
'html': request.env.ref('account.account_dashboard_onboarding_panel').render({
'company': request.env.user.company_id,
})
}
+1
View File
@@ -621,6 +621,7 @@ class AccountInvoice(models.Model):
def message_post(self, **kwargs):
if self.env.context.get('mark_invoice_as_sent'):
self.filtered(lambda inv: not inv.sent).write({'sent': True})
self.env.user.company_id.account_onboarding_sample_invoice_sent = True
return super(AccountInvoice, self.with_context(mail_post_autofollow=True)).message_post(**kwargs)
@api.model
+130 -1
View File
@@ -6,7 +6,7 @@ import time
from dateutil.relativedelta import relativedelta
from odoo import fields, models, api, _
from odoo.exceptions import ValidationError, UserError
from odoo.exceptions import ValidationError, UserError, RedirectWarning
from odoo.tools.misc import DEFAULT_SERVER_DATE_FORMAT
from odoo.tools.float_utils import float_round, float_is_zero
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, date_utils
@@ -67,6 +67,27 @@ Best Regards,'''))
account_setup_coa_done = fields.Boolean(string='Chart of Account Checked', help="Technical field holding the status of the chart of account setup step.")
account_setup_bar_closed = fields.Boolean(string='Setup Bar Closed', help="Technical field set to True when setup bar has been closed by the user.")
# account invoice onboarding
account_invoice_onboarding_closed = fields.Boolean(
string="Account invoice onboarding panel closed",
help="Refers to the account invoice onboarding panel closed state.")
account_invoice_onboarding_folded = fields.Boolean(
string="Account invoice onboarding panel folded",
help="Refers to the account invoice onboarding panel folded state.")
account_onboarding_invoice_layout_done = fields.Boolean("Onboarding invoice layout step done",
compute="_compute_account_onboarding_invoice_layout_done")
account_onboarding_sample_invoice_sent = fields.Boolean(
"Onboarding sample invoice step completed", default=False)
# account dashboard onboarding
account_dashboard_onboarding_closed = fields.Boolean(
string="Account dashboard onboarding panel closed",
help="Refers to the account dashboard onboarding panel closed state.")
account_dashboard_onboarding_folded = fields.Boolean(
string="Account dashboard onboarding panel folded",
help="Refers to the account dashboard onboarding panel folded state.")
@api.multi
def _check_lock_dates(self, vals):
'''Check the lock dates for the current companies. This can't be done in a api.constrains because we need
@@ -398,3 +419,111 @@ Best Regards,'''))
'debit': credit_diff,
'credit': debit_diff,
})
@api.depends('logo', 'account_invoice_onboarding_closed')
def _compute_account_onboarding_invoice_layout_done(self):
""" The invoice onboarding step is marked as done if logo is filled
and different from the default one. """
for record in self:
record.account_onboarding_invoice_layout_done = \
record.account_invoice_onboarding_closed or (
bool(record.logo) and record.logo != record._get_logo())
@api.model
def action_toggle_fold_account_invoice_onboarding(self):
""" Toggle the onboarding panel `folded` state. """
self.env.user.company_id.account_invoice_onboarding_folded =\
not self.env.user.company_id.account_invoice_onboarding_folded
@api.model
def action_close_account_invoice_onboarding(self):
""" Mark the onboarding panel as closed. """
self.env.user.company_id.account_invoice_onboarding_closed = True
@api.model
def action_toggle_fold_account_dashboard_onboarding(self):
""" Toggle the dashboard onboarding panel `folded` state. """
self.env.user.company_id.account_dashboard_onboarding_folded =\
not self.env.user.company_id.account_dashboard_onboarding_folded
@api.model
def action_close_account_dashboard_onboarding(self):
""" Mark the dashboard onboarding panel as closed. """
self.env.user.company_id.account_dashboard_onboarding_closed = True
@api.model
def action_open_account_onboarding_invoice_layout(self):
""" Onboarding step for the invoice layout. """
action = self.env.ref('account.action_open_account_onboarding_invoice_layout').read()[0]
action['res_id'] = self.env.user.company_id.id
return action
@api.model
def _get_sample_invoice(self):
""" Get a sample invoice or create one if it does not exist. """
# use current user as partner
partner = self.env.user.partner_id
company_id = self.env.user.company_id.id
# try to find an existing sample invoice
sample_invoice = self.env['account.invoice'].search(
[('company_id', '=', company_id),
('partner_id', '=', partner.id)], limit=1)
if len(sample_invoice) == 0:
# If there are no existing accounts or no journal, fail
account = self.env['account.account'].search([('company_id', '=', company_id)], limit=1)
if len(account) == 0:
action = self.env.ref('account.action_account_config')
msg = _(
"We cannot find a chart of accounts for this company, you should configure it. \n"
"Please go to Account Configuration and select or install a fiscal localization.")
raise RedirectWarning(msg, action.id, _("Go to the configuration panel"))
journal = self.env['account.journal'].search([('company_id', '=', company_id)], limit=1)
if len(journal) == 0:
action = self.env.ref('account.action_account_journal_form')
msg = _("We cannot find any journal for this company. You should create one."
"\nPlease go to Configuration > Journals.")
raise RedirectWarning(msg, action.id, _("Go to the journal configuration"))
sample_invoice = self.env['account.invoice'].create({
'name': _("Sample invoice"),
'journal_id': journal.id,
'partner_id': partner.id,
})
# sample invoice lines
self.env['account.invoice.line'].create({
'name': _("Sample invoice line name"),
'invoice_id': sample_invoice.id,
'account_id': account.id,
'price_unit': 199.99,
'quantity': 2,
})
self.env['account.invoice.line'].create({
'name': _("Sample invoice line name 2"),
'invoice_id': sample_invoice.id,
'account_id': account.id,
'price_unit': 25,
'quantity': 1,
})
return sample_invoice
@api.model
def action_open_account_onboarding_sample_invoice(self):
""" Onboarding step for sending a sample invoice. Open a window to compose an email,
with the edi_invoice_template message loaded by default. """
sample_invoice = self._get_sample_invoice()
template = self.env.ref('account.email_template_edi_invoice', False)
action = self.env.ref('account.action_open_account_onboarding_sample_invoice').read()[0]
action['context'] = {
'default_res_id': sample_invoice.id,
'default_use_template': bool(template),
'default_template_id': template and template.id or False,
'default_model': 'account.invoice',
'default_composition_mode': 'comment',
'mark_invoice_as_sent': True,
'custom_layout': 'mail.mail_notification_borders',
'force_email': True,
}
return action
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

-1
View File
@@ -38,7 +38,6 @@
<xpath expr="//script[contains(@src, '/web/static/tests/views/kanban_tests.js')]" position="after">
<script type="text/javascript" src="/account/static/tests/reconciliation_tests.js"></script>
<script type="text/javascript" src="/account/static/tests/account_payment_field_tests.js"></script>
<script type="text/javascript" src="/account/static/tests/account_dashboard_setup_bar_tests.js"></script>
</xpath>
</template>
</odoo>
+13 -2
View File
@@ -167,6 +167,18 @@
</field>
</record>
<record id="invoice_tree_with_onboarding" model="ir.ui.view">
<field name="name">account.invoice.tree.with.onboarding</field>
<field name="model">account.invoice</field>
<field name="inherit_id" ref="invoice_tree"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//tree" position="attributes">
<attribute name="banner_route">/account/account_invoice_onboarding</attribute>
</xpath>
</field>
</record>
<record model="ir.ui.view" id="invoice_kanban">
<field name="name">account.invoice.kanban</field>
<field name="model">account.invoice</field>
@@ -727,11 +739,10 @@
</field>
</record>
<record id="action_invoice_tree1_view1" model="ir.actions.act_window.view">
<field eval="1" name="sequence"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="invoice_tree"/>
<field name="view_id" ref="invoice_tree_with_onboarding"/>
<field name="act_window_id" ref="action_invoice_tree1"/>
</record>
@@ -3,7 +3,7 @@
<field name="name">account.journal.dashboard.kanban</field>
<field name="model">account.journal</field>
<field name="arch" type="xml">
<kanban create="false" class="oe_background_grey o_kanban_dashboard o_account_kanban">
<kanban create="false" class="oe_background_grey o_kanban_dashboard o_account_kanban" banner_route="/account/account_dashboard_onboarding">
<field name="id"/>
<field name="name"/>
<field name="type"/>
@@ -0,0 +1,118 @@
<?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">Create and send professional looking invoices.</t>
<t t-set="done_icon" t-value="'fa-star'" />
<t t-set="done_text">Looks great!</t>
<t t-set="method" t-value="'action_open_account_onboarding_invoice_layout'" />
<t t-set="model" t-value="'res.company'" />
<t t-set="done" t-value="company.account_onboarding_invoice_layout_done" />
</t>
</template>
<template id="onboarding_sample_invoice_step">
<t t-call="base.onboarding_step">
<t t-set="title">Sample Invoice</t>
<t t-set="description">Quickly preview what your client sees by sending a sample invoice
to yourself.
</t>
<t t-set="btn_text">I'm ready</t>
<t t-set="done_text">Sample invoice sent!</t>
<t t-set="method" t-value="'action_open_account_onboarding_sample_invoice'" />
<t t-set="model" t-value="'res.company'" />
<t t-set="done" t-value="company.account_onboarding_sample_invoice_sent" />
</t>
</template>
<template id="onboarding_fiscal_year_step">
<t t-call="base.onboarding_step">
<t t-set="title">Fiscal Year</t>
<t t-set="description">Set the closing date of your legal reports.</t>
<t t-set="btn_text">I'm ready</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="done" t-value="company.account_setup_fy_data_done" />
</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 enter the initial balances.</t>
<t t-set="btn_text">I'm ready</t>
<t t-set="done_text">Chart of account set.</t>
<t t-set="method" t-value="'setting_chart_of_accounts_action'" />
<t t-set="model" t-value="'res.company'" />
<t t-set="done" t-value="company.account_setup_coa_done" />
</t>
</template>
<!-- ONBOARDING PANELS -->
<template id="account_invoice_onboarding_panel" name="account.invoice.onboarding.panel">
<t t-call="base.onboarding_container">
<t t-set="classes">o_onboarding_blue</t>
<t t-set="bg_image">/account/static/src/img/account_invoice_onboarding_bg.jpg</t>
<t t-set="close_method">action_close_account_invoice_onboarding</t>
<t t-set="close_model">res.company</t>
<t t-set="fold_method">action_toggle_fold_account_invoice_onboarding</t>
<t t-set="fold_model">res.company</t>
<t t-set="folded_by_default" t-value="company.account_invoice_onboarding_folded" />
<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_sample_invoice_step" name="sample_invoice_step" />
</t>
</template>
<template id="account_dashboard_onboarding_panel" name="account.dashboard.onboarding.panel">
<t t-call="base.onboarding_container">
<t t-set="classes">o_onboarding_orange</t>
<t t-set="bg_image">/account/static/src/img/account_dashboard_onboarding_bg.jpg</t>
<t t-set="close_method">action_close_account_dashboard_onboarding</t>
<t t-set="close_model">res.company</t>
<t t-set="fold_method">action_toggle_fold_account_dashboard_onboarding</t>
<t t-set="fold_model">res.company</t>
<t t-set="folded_by_default" t-value="company.account_dashboard_onboarding_folded" />
<t t-call="base.onboarding_company_step" name="company_step" />
<t t-call="account.onboarding_fiscal_year_step" name="fiscal_year_step" />
<t t-call="account.onboarding_chart_of_account_step" name="chart_of_account_step" />
</t>
</template>
<!-- INVOICE FORM -->
<record id="action_open_account_onboarding_invoice_layout" model="ir.actions.act_window">
<field name="name">Configure your document template</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">res.company</field>
<field name="view_mode">form</field>
<field name="view_type">form</field>
<field name="view_id" ref="base.view_company_document_template_form" />
<field name="target">new</field>
</record>
<!-- SAMPLE INVOICE EMAIL -->
<record model="ir.ui.view" id="email_compose_onboarding_sample_invoice">
<field name="name">mail.compose.message.form.account_invoice_onboarding_sample_invoice
</field>
<field name="model">mail.compose.message</field>
<field name="inherit_id" ref="mail.email_compose_message_wizard_form" />
<field name="priority">1000</field>
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//span[@name='document_followers_text']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//button[@name='save_as_template']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='template_id']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
</field>
</record>
<record id="action_open_account_onboarding_sample_invoice" model="ir.actions.act_window">
<field name="name">Send a sample invoice</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">mail.compose.message</field>
<field name="view_mode">form</field>
<field name="view_type">form</field>
<field name="view_id" ref="email_compose_onboarding_sample_invoice" />
<field name="target">new</field>
</record>
</odoo>
+4 -8
View File
@@ -28,14 +28,6 @@ class FinancialYearOpeningWizard(models.TransientModel):
for record in self:
record.opening_move_posted = record.company_id.opening_move_posted()
def mark_as_done(self):
""" Forces fiscal year setup state to 'done'."""
self.company_id.account_setup_fy_data_done = True
def unmark_as_done(self):
""" Forces fiscal year setup state to 'undone'."""
self.company_id.account_setup_fy_data_done = False
@api.multi
def write(self, vals):
if 'fiscalyear_last_day' in vals or 'fiscalyear_last_month' in vals:
@@ -47,6 +39,10 @@ class FinancialYearOpeningWizard(models.TransientModel):
vals.get('fiscalyear_last_month'))
return super(FinancialYearOpeningWizard, self).write(vals)
@api.multi
def action_save_onboarding_fiscal_year(self):
self.env.user.company_id.account_setup_fy_data_done = True
class SetupBarBankConfigWizard(models.TransientModel):
_inherits = {'res.partner.bank': 'res_partner_bank_id'}
+3 -5
View File
@@ -49,11 +49,9 @@
</div>
</div>
<footer>
<field name="account_setup_fy_data_done" invisible="True"/>
<button string="Mark as done" type="object" name="mark_as_done" class="oe_highlight" attrs="{'invisible': [('account_setup_fy_data_done','=',True)]}"/>
<button string="Save" type="action" name="account.setup_wizard_close" attrs="{'invisible': [('account_setup_fy_data_done','=',True)]}"/>
<button string="Save" type="action" name="account.setup_wizard_close" class="oe_highlight" attrs="{'invisible': [('account_setup_fy_data_done','=',False)]}"/>
<button string="Unmark as done" type="object" name="unmark_as_done" attrs="{'invisible': [('account_setup_fy_data_done','=',False)]}"/>
<button name="action_save_onboarding_fiscal_year" string="OK"
class="oe_highlight" type="object" />
<button special="cancel" string="Cancel" />
</footer>
</form>
</field>
@@ -94,7 +94,7 @@ class ResConfigSettings(models.TransientModel):
@api.multi
def change_report_template(self):
self.ensure_one()
template = self.env.ref('base.view_company_report_form')
template = self.env.ref('base.view_company_document_template_form')
return {
'name': _('Choose Your Document Layout'),
'type': 'ir.actions.act_window',
@@ -40,7 +40,7 @@
</div>
<div class="mt16">
<button name="change_report_template" string="Change Document Template" type="object" class="oe_link" icon="fa-arrow-right"/>
<button name="edit_external_header" string="Edit Header" type="object" class="oe_link" groups="base.group_no_one"/>
<button name="edit_external_header" string="Edit Layout" type="object" class="oe_link" groups="base.group_no_one"/>
<button name="%(web.action_report_externalpreview)d" string="Preview Document" type="action" class="oe_link" groups="base.group_no_one"/>
</div>
</div>
@@ -4,7 +4,7 @@ odoo.define('google_calendar.calendar_tests', function (require) {
var CalendarView = require('web.CalendarView');
var testUtils = require('web.test_utils');
var createView = testUtils.createView;
var createAsyncView = testUtils.createAsyncView;
var initialDate = new Date("2016-12-12T08:00:00Z");
@@ -71,8 +71,9 @@ QUnit.module('Google Calendar', {
QUnit.test('sync google calendar', function (assert) {
assert.expect(6);
var done = assert.async();
var calendar = createView({
createAsyncView({
View: CalendarView,
model: 'calendar.event',
data: this.data,
@@ -98,23 +99,24 @@ QUnit.module('Google Calendar', {
}
return this._super.apply(this, arguments);
},
}).then(function (calendar) {
assert.strictEqual(calendar.$('.fc-event').length, 2, "should display 2 events on the month");
var $sidebar = calendar.$('.o_calendar_sidebar');
calendar.$('.o_google_sync_button').click();
assert.verifySteps([
'/web/dataset/call_kw/calendar.event/search_read',
'/google_calendar/sync_data',
'/web/dataset/call_kw/calendar.event/search_read',
], 'should do a search_read before and after the call to sync_data');
assert.strictEqual(calendar.$('.fc-event').length, 3, "should now display 3 events on the month");
calendar.destroy();
done();
});
assert.strictEqual(calendar.$('.fc-event').length, 2, "should display 2 events on the month");
var $sidebar = calendar.$('.o_calendar_sidebar');
calendar.$('.o_google_sync_button').click();
assert.verifySteps([
'/web/dataset/call_kw/calendar.event/search_read',
'/google_calendar/sync_data',
'/web/dataset/call_kw/calendar.event/search_read',
], 'should do a search_read before and after the call to sync_data');
assert.strictEqual(calendar.$('.fc-event').length, 3, "should now display 3 events on the month");
calendar.destroy();
});
});
});
@@ -52,7 +52,7 @@
<span attrs="{'invisible': [('use_active_domain', '=', True)]}">the selected records</span>
<span attrs="{'invisible': [('use_active_domain', '=', False)]}">the current search filter</span>.
</span>
<span attrs="{'invisible':['|', ('model', '=', False), ('composition_mode', '=', 'mass_mail')]}">Followers of the document and</span>
<span name="document_followers_text" attrs="{'invisible':['|', ('model', '=', False), ('composition_mode', '=', 'mass_mail')]}">Followers of the document and</span>
<field name="partner_ids" widget="many2many_tags_email" placeholder="Add contacts to notify..."
context="{'force_email':True, 'show_email':True}"
attrs="{'invisible': [('composition_mode', '!=', 'comment')]}"/>
+2 -1
View File
@@ -2,4 +2,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import controllers
from . import controllers
from . import wizards
+2
View File
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Payment Acquirer',
@@ -14,6 +15,7 @@
'views/payment_views.xml',
'views/account_payment_views.xml',
'views/account_invoice_views.xml',
'views/payment_acquirer_onboarding_templates.xml',
'views/payment_templates.xml',
'views/payment_portal_templates.xml',
'views/assets.xml',
+1
View File
@@ -5,3 +5,4 @@ from . import account_invoice
from . import res_partner
from . import account_payment
from . import chart_template
from . import res_company
+15
View File
@@ -0,0 +1,15 @@
from odoo import _, api, fields, models
class ResCompany(models.Model):
_inherit = 'res.company'
# Step is marked as done if one payment module is installed or
# `custom` payment has been changed or `skip this step` has been clicked
payment_acquirer_onboarding_done = fields.Boolean('Payment acquirer onboarding done', default=False)
@api.model
def action_open_payment_onboarding_payment_acquirer(self):
""" Called by onboarding panel above the customer invoice list."""
action = self.env.ref('payment.action_open_payment_onboarding_payment_acquirer_wizard').read()[0]
return action
@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- onboarding step -->
<template id="onboarding_payment_acquirer_step">
<t t-call="base.onboarding_step">
<t t-set="title">Payment Method</t>
<t t-set="description">Choose the payment methods to collect payments on your invoices.</t>
<t t-set="done_text">Payment method set!</t>
<t t-set="method" t-value="'action_open_payment_onboarding_payment_acquirer'" />
<t t-set="model" t-value="'res.company'" />
<t t-set="id" t-value="company.id" />
<t t-set="done" t-value="company.payment_acquirer_onboarding_done" />
</t>
</template>
<!-- add it in the account invoice onboarding panel -->
<template id="account_invoice_payment_acquirer_onboarding"
name="account invoice payment acquirer onboarding"
inherit_id="account.account_invoice_onboarding_panel">
<xpath expr="//t[@name='sample_invoice_step']" position="before">
<t t-call="payment.onboarding_payment_acquirer_step" name="payment_acquirer_step" />
</xpath>
</template>
<!--PAYMENT ACQUIRER-->
<record model="ir.ui.view" id="payment_acquirer_onboarding_wizard_form">
<field name="name">payment.acquirer.onboarding.wizard.form</field>
<field name="model">payment.acquirer.onboarding.wizard</field>
<field name="arch" type="xml">
<form string="Choose a payment method">
<div class="mb16">
Configure our recommended payment methods or
<a type="action" role="button" name="payment.action_payment_acquirer">
see all other available methods.
</a>
</div>
<div class="row">
<div class="col-xs-1 mb16">
<field name="paypal_checked" nolabel="1" />
</div>
<div>
<label for="paypal_checked" />
</div>
</div>
<div class="row">
<div attrs="{'invisible': [('paypal_checked', '=', False)]}"
class="col-xs-offset-1">
<a href="https://www.paypal.com" target="_blank">PayPal</a>
is the easiest way to accept payments. It's easy to configure and it offers
<a href="https://www.paypal.com/us/webapps/mpp/merchant-fees"
target="_blank">
a transparent fee structure.
</a>
<group>
<field name="paypal_email_account"
attrs="{'required': [('paypal_checked', '=', True)]}" />
<field name="paypal_seller_account" />
<field name="paypal_pdt_token"
attrs="{'required': [('paypal_checked', '=', True)]}" />
</group>
<separator />
</div>
</div>
<div class="row">
<div class="col-xs-1 mb16">
<field name="stripe_checked" nolabel="1" />
</div>
<div>
<label for="stripe_checked" />
</div>
</div>
<div class="row">
<div attrs="{'invisible': [('stripe_checked', '=', False)]}"
class="col-xs-offset-1">
<a href="https://stripe.com" target="_blank">Stripe</a>
is a payment provider with
<a href="https://stripe.com/pricing" target="_blank">
a simple fee structure and no hidden costs.
</a>
No other gateway or merchant account is required. We recommend this solution
to manage credit card payments.
<group>
<field name="stripe_secret_key"
attrs="{'required': [('stripe_checked', '=', True)]}" />
<field name="stripe_publishable_key"
attrs="{'required': [('stripe_checked', '=', True)]}" />
<a href="https://dashboard.stripe.com/account/apikeys" target="_blank">
<span class="fa fa-arrow-right">Get my Stripe keys</span>
</a>
</group>
<separator />
</div>
</div>
<div class="row">
<div class="col-xs-1">
<field name="manual_checked" nolabel="1" />
</div>
<div>
<label for="manual_checked" />
</div>
</div>
<div class="row">
<div attrs="{'invisible': [('manual_checked', '=', False)]}"
class="col-xs-offset-1">
Provide customers with instructions to pay outside of your platform (wire
transfer, cash on delivery, etc.).
<group>
<field name="manual_name"
attrs="{'required': [('manual_checked', '=', True)]}" />
<field name="manual_post_msg"
attrs="{'required': [('manual_checked', '=', True)]}" />
</group>
</div>
</div>
<footer>
<div class="mb16" name="skip_step_text">
You can skip this step if invoices are pre-paid.
</div>
<button name="add_payment_methods" string="OK" class="oe_highlight"
type="object" />
<button name="action_skip_payment_onboarding_payment_acquirer_step" string="Skip this step"
type="object" />
<button special="cancel" string="Cancel" />
</footer>
</form>
</field>
</record>
<record id="action_open_payment_onboarding_payment_acquirer_wizard" model="ir.actions.act_window">
<field name="name">Choose a payment method</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">payment.acquirer.onboarding.wizard</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="view_id" ref="payment.payment_acquirer_onboarding_wizard_form" />
<field name="target">new</field>
</record>
</odoo>
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import payment_acquirer_onboarding_wizard
@@ -0,0 +1,127 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models, fields, _
from odoo.exceptions import UserError
class PaymentWizard(models.TransientModel):
_name = 'payment.acquirer.onboarding.wizard'
paypal_checked = fields.Boolean('Paypal', default=lambda self: self._get_default_payment_acquirer_onboarding_value('paypal_checked'))
paypal_email_account = fields.Char('Paypal Email ID', default=lambda self: self._get_default_payment_acquirer_onboarding_value('paypal_email_account'))
paypal_seller_account = fields.Char('Paypal Merchant ID', default=lambda self: self._get_default_payment_acquirer_onboarding_value('paypal_seller_account'))
paypal_pdt_token = fields.Char('Paypal PDT Token', default=lambda self: self._get_default_payment_acquirer_onboarding_value('paypal_pdt_token'))
stripe_checked = fields.Boolean('Credit Card (via Stripe)', default=lambda self: self._get_default_payment_acquirer_onboarding_value('stripe_checked'))
stripe_secret_key = fields.Char(default=lambda self: self._get_default_payment_acquirer_onboarding_value('stripe_secret_key'))
stripe_publishable_key = fields.Char(default=lambda self: self._get_default_payment_acquirer_onboarding_value('stripe_publishable_key'))
manual_checked = fields.Boolean('Manual Payment', default=lambda self: self._get_default_payment_acquirer_onboarding_value('manual_checked'))
manual_name = fields.Char('Method name', default=lambda self: self._get_default_payment_acquirer_onboarding_value('manual_name'))
manual_post_msg = fields.Html('Payment instructions', default=lambda self: self._get_default_payment_acquirer_onboarding_value('manual_post_msg'))
_payment_acquirer_onboarding_cache = {}
_data_fetched = False
def _get_manual_payment(self, env=None):
if env is None:
env = self.env
return env['payment.acquirer'].search(
[('provider', '=', 'transfer'),
('sequence', '=', 2),
('company_id', '=', self.env.user.company_id.id)], limit=1)
def _get_default_payment_acquirer_onboarding_value(self, key):
if not self.env.user._is_admin():
raise UserError(_('Only administators can access this data.'))
if self._data_fetched:
return self._payment_acquirer_onboarding_cache.get(key, '')
self._data_fetched = True
installed_modules = self.env['ir.module.module'].sudo().search([
('name', 'in', ('payment_paypal', 'payment_stripe')),
('state', '=', 'installed'),
]).mapped('name')
if 'payment_paypal' in installed_modules:
self._payment_acquirer_onboarding_cache['paypal_checked'] = True
acquirer = self.env.ref('payment.payment_acquirer_paypal')
self._payment_acquirer_onboarding_cache['paypal_email_account'] = acquirer['paypal_email_account']
self._payment_acquirer_onboarding_cache['paypal_seller_account'] = acquirer['paypal_seller_account']
self._payment_acquirer_onboarding_cache['paypal_pdt_token'] = acquirer['paypal_pdt_token']
if 'payment_stripe' in installed_modules:
self._payment_acquirer_onboarding_cache['stripe_checked'] = True
acquirer = self.env.ref('payment.payment_acquirer_stripe')
self._payment_acquirer_onboarding_cache['stripe_secret_key'] = acquirer['stripe_secret_key']
self._payment_acquirer_onboarding_cache['stripe_publishable_key'] = acquirer['stripe_publishable_key']
manual_payment = self._get_manual_payment()
# manual payment is checked if the user has renamed it
self._payment_acquirer_onboarding_cache['manual_checked'] = \
(manual_payment['name'] != 'Wire Transfer')
self._payment_acquirer_onboarding_cache['manual_name'] = manual_payment['name']
self._payment_acquirer_onboarding_cache['manual_post_msg'] = manual_payment['post_msg']
return self._payment_acquirer_onboarding_cache.get(key, '')
def _install_module(self, module_name):
module = self.env['ir.module.module'].sudo().search([('name', '=', module_name)])
if module.state not in ('installed', 'to install', 'to upgrade'):
module.button_immediate_install()
def _hook_on_save_onboarding_payment_acquirer(self):
# if any payment method is selected activate "Online Payment" in invoicing settings
self._install_module('account_payment')
@api.multi
def add_payment_methods(self):
if self.paypal_checked is True:
self._install_module('payment_paypal')
if self.stripe_checked is True:
self._install_module('payment_stripe')
if self.paypal_checked is True or self.stripe_checked is True or self.manual_checked is True:
# the wizard is inherited in `sale`
self._hook_on_save_onboarding_payment_acquirer()
# create a new env including the freshly installed module(s)
new_env = api.Environment(self.env.cr, self.env.uid, self.env.context)
if self.paypal_checked is True:
new_env.ref('payment.payment_acquirer_paypal').write({
'paypal_email_account': self.paypal_email_account,
'paypal_seller_account': self.paypal_seller_account,
'paypal_pdt_token': self.paypal_pdt_token,
'website_published': True,
})
if self.stripe_checked is True:
new_env.ref('payment.payment_acquirer_stripe').write({
'stripe_secret_key': self.stripe_secret_key,
'stripe_publishable_key': self.stripe_publishable_key,
'website_published': True,
})
if self.manual_checked is True:
self._get_manual_payment(new_env).write({
'name': self.manual_name,
'post_msg': self.manual_post_msg,
'website_published': True,
})
self._set_payment_acquirer_onboarding_step_done()
# delete wizard data immediately to get rid of residual credentials
self.unlink()
return {'type': 'ir.actions.act_window_close'}
@api.multi
def action_skip_payment_onboarding_payment_acquirer_step(self):
self._set_payment_acquirer_onboarding_step_done()
return {'type': 'ir.actions.act_window_close'}
def _set_payment_acquirer_onboarding_step_done(self):
self.env.user.company_id.write({'payment_acquirer_onboarding_done': True})
+1
View File
@@ -28,6 +28,7 @@ This module contains all the common features of Sales Management and eCommerce.
'views/mail_activity_views.xml',
'views/assets.xml',
'views/sale_portal_templates.xml',
'views/sale_onboarding_views.xml',
'views/res_config_settings_views.xml',
'views/res_users_views.xml',
],
+1
View File
@@ -2,4 +2,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mail
from . import onboarding
from . import portal
+24
View File
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http
from odoo.http import request
class OnboardingController(http.Controller):
@http.route('/sales/onboarding_quotation_panel', auth='user', type='json')
def sale_quotation_onboarding(self):
""" 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. """
if not request.env.user._is_admin() or \
request.env.user.company_id.sale_quotation_onboarding_closed:
return {}
return {
'html': request.env.ref('sale.onboarding_quotation_panel').render({
'company': request.env.user.company_id,
})
}
+59 -1
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo import api, fields, models
class ResCompany(models.Model):
@@ -10,3 +10,61 @@ class ResCompany(models.Model):
sale_note = fields.Text(string='Default Terms and Conditions', translate=True)
portal_confirmation_sign = fields.Boolean(string='Digital Signature')
portal_confirmation_pay = fields.Boolean(string='Electronic Payment')
@api.depends('portal_confirmation_sign')
def _get_sale_onboarding_signature_radio(self):
""" Convert portal_confirmation_sign from boolean to radio. """
for record in self:
record.portal_confirmation_sign_radio = 'yes' if record.portal_confirmation_sign else 'no'
@api.depends('portal_confirmation_sign')
def _set_sale_onboarding_signature_radio(self):
""" Convert portal_confirmation_sign from radio to boolean. """
for record in self:
record.portal_confirmation_sign = record.portal_confirmation_sign_radio == 'yes'
# the target field, portal_confirmation_sign, is a boolean but we would like to render it as a radio button
portal_confirmation_sign_radio = fields.Selection([('yes', "Yes"), ('no', "No")], "Digital Signature (radio)",
compute=_get_sale_onboarding_signature_radio, inverse=_set_sale_onboarding_signature_radio)
# sale quotation onboarding
sale_quotation_onboarding_closed = fields.Boolean(
string="Onboarding sale quotation closed",
help="Refers to the sale quotation onboarding panel closed state.")
sale_quotation_onboarding_folded = fields.Boolean(
string="Onboarding sale quotation folded",
help="Refers to the sale quotation onboarding panel folded state.")
sale_onboarding_quotation_layout_done = fields.Boolean("Onboarding quotation layout step done",
compute="_compute_sale_onboarding_quotation_layout_done")
sale_onboarding_payment_acquirer_done = fields.Boolean("Sale onboarding payment acquirer step done",
default=False)
sale_onboarding_signature_done = fields.Boolean("Sale onboarding signature step done",
default=False)
@api.model
def action_toggle_fold_sale_quotation_onboarding(self):
""" Toggle the onboarding panel `folded` state. """
self.env.user.company_id.sale_quotation_onboarding_folded =\
not self.env.user.company_id.sale_quotation_onboarding_folded
@api.model
def action_close_sale_quotation_onboarding(self):
""" Mark the onboarding panel as closed. """
self.env.user.company_id.sale_quotation_onboarding_closed = True
@api.model
def action_open_sale_onboarding_signature(self):
""" Called by onboarding panel above the quotation list."""
action = self.env.ref('sale.action_open_sale_onboarding_signature').read()[0]
action['res_id'] = self.env.user.company_id.id
return action
@api.model
def action_open_sale_onboarding_payment_acquirer(self):
""" Called by onboarding panel above the quotation list."""
action = self.env.ref('sale.action_open_sale_onboarding_payment_acquirer_wizard').read()[0]
return action
@api.multi
def action_save_onboarding_signature(self):
self.sale_onboarding_signature_done = True
+3 -1
View File
@@ -397,7 +397,9 @@ class SaleOrder(models.Model):
@api.multi
def print_quotation(self):
self.filtered(lambda s: s.state == 'draft').write({'state': 'sent'})
return self.env.ref('sale.action_report_saleorder').report_action(self)
return self.env.ref('sale.action_report_saleorder')\
.with_context({'discard_logo_check': True}).report_action(self)
@api.multi
def action_view_invoice(self):
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+107
View File
@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ONBOARDING STEPS -->
<template id="onboarding_signature_step">
<t t-call="base.onboarding_step">
<t t-set="title">Signature</t>
<t t-set="description">Request a digital signature to confirm orders.</t>
<t t-set="done_icon" t-value="'fa-star'" />
<t t-set="done_text">Looks great!</t>
<t t-set="method" t-value="'action_open_sale_onboarding_signature'" />
<t t-set="model" t-value="'res.company'" />
<t t-set="done" t-value="company.sale_onboarding_signature_done" />
</t>
</template>
<template id="sale_onboarding_payment_acquirer_step" primary="True"
inherit_id="payment.onboarding_payment_acquirer_step">
<xpath expr="//t[@t-set='method']" position="replace">
<t t-set="method" t-value="'action_open_sale_onboarding_payment_acquirer'" />
</xpath>
<xpath expr="//t[@t-set='done']" position="replace">
<t t-set="done" t-value="company.sale_onboarding_payment_acquirer_done" />
</xpath>
</template>
<template id="onboarding_quotation_layout_step" primary="True" inherit_id="account.onboarding_invoice_layout_step">
<xpath expr="//t[@t-set='title']" position="replace">
<t t-set="title">Quotation Layout</t>
</xpath>
<xpath expr="//t[@t-set='description']" position="replace">
<t t-set="description">Create and send professional looking quotations.</t>
</xpath>
</template>
<!-- ONBOARDING PANEL-->
<template id="onboarding_quotation_panel" name="sale.quotation.onboarding.panel">
<t t-call="base.onboarding_container">
<t t-set="classes">o_onboarding_violet</t>
<t t-set="bg_image">/sale/static/src/img/sale_quotation_onboarding_bg.jpg</t>
<t t-set="close_method">action_close_sale_quotation_onboarding</t>
<t t-set="close_model">res.company</t>
<t t-set="fold_method">action_toggle_fold_sale_quotation_onboarding</t>
<t t-set="fold_model">res.company</t>
<t t-set="folded_by_default" t-value="company.sale_quotation_onboarding_folded" />
<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.onboarding_signature_step" name="signature_step" />
<t t-call="sale.sale_onboarding_payment_acquirer_step" name="payment_acquirer_step" />
</t>
</template>
<!-- SIGNATURE FORM -->
<record model="ir.ui.view" id="sale_onboarding_signature_form">
<field name="name">sale.onboarding.signature.form</field>
<field name="model">res.company</field>
<field name="arch" type="xml">
<form string="Request an electronic signature">
<div class="panel-body row">
Quotations can be confirmed automatically based on a customer signature, in
order to automate the delivery process. This functionality is convenient for all
the parties and offers as much legal guarantee as a paper signature.
<br />
<br />
Do you want to activate the digital signature?
</div>
<div class="panel-body row">
<field name="portal_confirmation_sign_radio" widget="radio" />
</div>
<footer>
<button name="action_save_onboarding_signature" string="OK" class="oe_highlight"
type="object" />
<button special="cancel" string="Cancel" />
</footer>
</form>
</field>
</record>
<record id="action_open_sale_onboarding_signature" model="ir.actions.act_window">
<field name="name">Request a digital signature</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">res.company</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="view_id" ref="sale.sale_onboarding_signature_form" />
<field name="target">new</field>
</record>
<!-- PAYMENT ACQUIRER -->
<record id="sale_onboarding_payment_acquirer_form" model="ir.ui.view">
<field name="name">sale.onboarding.payment.acquirer.form</field>
<field name="model">sale.payment.acquirer.onboarding.wizard</field>
<field name="inherit_id" ref="payment.payment_acquirer_onboarding_wizard_form"/>
<field name="mode">primary</field>
<field name="priority">1000</field>
<field name="arch" type="xml">
<xpath expr="//div[@name='skip_step_text']" position="replace">
<div class="mb16">
You can skip this step if you don't manage payments on quotations.
</div>
</xpath>
</field>
</record>
<record id="action_open_sale_onboarding_payment_acquirer_wizard" model="ir.actions.act_window">
<field name="name">Choose a payment method</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">sale.payment.acquirer.onboarding.wizard</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="view_id" ref="sale.sale_onboarding_payment_acquirer_form" />
<field name="target">new</field>
</record>
</odoo>
+29 -3
View File
@@ -194,6 +194,17 @@
</tree>
</field>
</record>
<record id="view_quotation_tree_with_onboarding" model="ir.ui.view">
<field name="name">sale.order.tree</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="view_quotation_tree"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//tree" position="attributes">
<attribute name="banner_route">/sales/onboarding_quotation_panel</attribute>
</xpath>
</field>
</record>
<record id="view_order_form" model="ir.ui.view">
<field name="name">sale.order.form</field>
@@ -706,6 +717,23 @@
sequence="5" active="False"/>
<record id="action_quotations_with_onboarding" model="ir.actions.act_window">
<field name="name">Quotations</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">sale.order</field>
<field name="view_type">form</field>
<field name="view_id" ref="view_quotation_tree_with_onboarding"/>
<field name="view_mode">tree,kanban,form,calendar,pivot,graph</field>
<field name="search_view_id" ref="sale_order_view_search_inherit_quotation"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new quotation, the first step of a new sale!
</p><p>
Once the quotation is confirmed by the customer, it becomes a sales order.<br/> You will be able to create an invoice and collect the payment.
</p>
</field>
</record>
<record id="action_quotations" model="ir.actions.act_window">
<field name="name">Quotations</field>
<field name="type">ir.actions.act_window</field>
@@ -724,7 +752,7 @@
</record>
<menuitem id="menu_sale_quotations"
action="action_quotations"
action="action_quotations_with_onboarding"
parent="sale_order_menu"
sequence="1" groups="sales_team.group_sale_salesman"/>
@@ -985,7 +1013,6 @@
}
</field>
</record>
<record id="action_quotations_salesteams" model="ir.actions.act_window">
<field name="name">Quotations</field>
<field name="type">ir.actions.act_window</field>
@@ -1009,7 +1036,6 @@
</p>
</field>
</record>
<record id="action_invoice_salesteams" model="ir.actions.act_window">
<field name="name">Invoices</field>
<field name="res_model">account.invoice</field>
+1
View File
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import payment_acquirer_onboarding_wizard
from . import sale_make_invoice_advance
@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class PaymentWizard(models.TransientModel):
_inherit = 'payment.acquirer.onboarding.wizard'
_name = 'sale.payment.acquirer.onboarding.wizard'
def _hook_on_save_onboarding_payment_acquirer(self):
""" Override. Activate the option in sales settings. """
self.env.user.company_id.portal_confirmation_pay = True
def _set_payment_acquirer_onboarding_step_done(self):
""" Override. """
self.env.user.company_id.sale_onboarding_payment_acquirer_done = True
+25 -6
View File
@@ -194,15 +194,34 @@ function realSetTimeout (fct, millis) {
setTimeout(wait, millis);
}
function loadCSS(url) {
if (!$('link[href="' + url + '"]').length) {
$('head').append($('<link>', {
/**
* Load css asynchronously: fetch it from the url parameter and add a link tag
* to <head>.
* If the url has already been requested and loaded, the promise will resolve immediately.
*
* @param {String} url of the css to be fetched
* @returns {Deferred} resolved when the css has been loaded.
*/
var loadCSS = (function () {
var urlDefs = Object.create(null);
return function loadCSS(url) {
if (url in urlDefs) {
return urlDefs[url];
}
var $link = $('<link>', {
'href': url,
'rel': 'stylesheet',
'type': 'text/css'
}));
}
}
});
urlDefs[url] = $.Deferred();
$link.on('load', function () {
urlDefs[url].resolve();
});
$('head').append($link);
return urlDefs[url];
};
})();
var loadJS = (function () {
var urls = [];
@@ -12,6 +12,7 @@ odoo.define('web.AbstractController', function (require) {
* reading localstorage, ...) has to go through the controller.
*/
var ajax = require('web.ajax');
var concurrency = require('web.concurrency');
var ControlPanelMixin = require('web.ControlPanelMixin');
var core = require('web.core');
@@ -54,7 +55,7 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
this.activeActions = params.activeActions;
this.controllerID = params.controllerID;
this.initialState = params.initialState;
this.bannerRoute = params.bannerRoute;
// use a DropPrevious to correctly handle concurrent updates
this.dp = new concurrency.DropPrevious();
// those arguments are temporary, they won't be necessary as soon as the
@@ -90,7 +91,7 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
this._super.apply(this, arguments),
this.renderer.appendTo(this.$el)
).then(function () {
self._update(self.initialState);
return self._update(self.initialState);
});
},
/**
@@ -256,7 +257,7 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
return;
}
self.renderer.setLocalState(localState);
self._update(state);
return self._update(state);
});
});
},
@@ -280,6 +281,54 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
state: state || {},
});
},
/**
* Renders the html provided by the route specified by the
* bannerRoute attribute on the controller (banner_route in the template).
* Renders it before the view output and add a css class 'o_has_banner' to it.
* There can be only one banner displayed at a time.
*
* If the banner contains stylesheet links, they are moved to <head>
* (and will only be fetched once).
*
* Route example:
* @http.route('/module/hello', auth='user', type='json')
* def hello(self):
* return {'html': '<h1>hello, world</h1>'}
*
* @private
* @returns {Deferred}
*/
_renderBanner: function () {
if (this.bannerRoute !== undefined) {
var self = this;
return this.dp
.add(this._rpc({route: this.bannerRoute}))
.then(function (response) {
if (!response.html) {
self.$el.removeClass('o_has_banner');
return $.when();
}
self.$el.addClass('o_has_banner');
var $banner = $(response.html);
// we should only display one banner at a time
if (self._$banner && self._$banner.remove) {
self._$banner.remove();
}
// Stylesheets are moved to <head> and we wait for them to be loaded
// to prevent displaying unstyled content.
var defs = [];
$('link[rel="stylesheet"]', $banner).each(function (i, link) {
defs.push(ajax.loadCSS(link.href));
link.remove();
});
return $.when.apply($, defs).then(function () {
$banner.prependTo(self.$el);
self._$banner = $banner;
});
});
}
return $.when();
},
/**
* Renders the control elements (buttons, pager and sidebar) of the current
* view.
@@ -370,7 +419,7 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
});
this._pushState();
return $.when();
return this._renderBanner();
},
//--------------------------------------------------------------------------
@@ -405,12 +454,39 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, {
* When a user clicks on an <a> link with type="action", we need to actually
* do the action. This kind of links is used a lot in no-content helpers.
*
* The <a> may have
* - a data-method and data-model attribute, in that case the corresponding
* rpc will be called. If that rpc returns an action it will be executed.
* - a data-reload-on-close attribute, in that case the view will be
* reloaded after the dialog has been closed.
*
* @private
* @param {OdooEvent} event
*/
_onActionClicked: function (event) {
event.preventDefault();
this.do_action(event.currentTarget.name);
var $target = $(event.currentTarget);
var self = this;
var model = $target.data('model');
var method = $target.data('method');
if (method !== undefined && model !== undefined) {
var options = {};
if ($target.data('reload-on-close')) {
options.on_close = function () {
self.trigger_up('reload');
};
}
this.dp.add(this._rpc({
model: model,
method: method,
})).then(function (action) {
if (action !== undefined) {
self.do_action(action, options);
}
});
} else {
this.do_action($target.attr('name'));
}
},
/**
* Intercepts the 'switch_view' event to add the controllerID into the data,
@@ -121,6 +121,7 @@ var AbstractView = Class.extend({
groupable: this.groupable,
isEmbedded: this.isEmbedded,
controllerID: params.controllerID,
bannerRoute: this.arch.attrs.banner_route,
};
// AAB: these params won't be necessary as soon as the ControlPanel will
// be instantiated by the View
+5
View File
@@ -0,0 +1,5 @@
.o_has_banner {
.o_view_nocontent {
top: 30%;
}
}
+16
View File
@@ -603,6 +603,22 @@
margin-top: 32px !important;
}
}
&.o_company_document_layout {
.image_selection_container {
div {
width: 50%;
img {
margin-left: 0 !important;
}
}
}
td {
padding: 0 45px 0 0;
}
img[name="logo"] {
max-height: 300px;
}
}
}
// Overriden style when form view in modal
@@ -0,0 +1,35 @@
odoo.define('web.ajax_tests', function (require) {
"use strict";
var ajax = require('web.ajax');
QUnit.module('core', function () {
var test_css_url = '/test_assetsbundle/static/src/css/test_cssfile1.css';
var test_link_selector = 'link[href="' + test_css_url + '"]';
QUnit.module('ajax', {
beforeEach: function () {
$(test_link_selector).remove();
},
afterEach: function () {
$(test_link_selector).remove();
}
});
QUnit.test('loadCSS', function (assert) {
var done = assert.async();
assert.expect(2);
ajax.loadCSS(test_css_url).then(function () {
var $links = $(test_link_selector);
assert.strictEqual($links.length, 1, "The css should be added to the dom.");
ajax.loadCSS(test_css_url).then(function () {
var $links = $(test_link_selector);
assert.strictEqual($links.length, 1, "The css should have been added only once.");
done();
});
});
});
});
});
@@ -12,6 +12,7 @@ var session = require('web.session');
var testUtils = require('web.test_utils');
var createView = testUtils.createView;
var createAsyncView = testUtils.createAsyncView;
var DebouncedField = basicFields.DebouncedField;
var _t = core._t;
@@ -1957,7 +1958,7 @@ QUnit.module('basic_fields', {
assert.expect(4);
var graph_key = JSON.parse(this.data.partner.records[0].graph_data)[0].key;
var kanban = createView({
createAsyncView({
View: KanbanView,
model: 'partner',
data: this.data,
@@ -1970,42 +1971,41 @@ QUnit.module('basic_fields', {
'</t>' +
'</templates></kanban>',
domain: [['id', 'in', [1, 2]]],
}).then(function (kanban) {
// nvd3 seems to do a setTimeout(0) each time the addGraph function is
// called, which is done twice in this case as there are 2 records.
// for that reason, we need to do two setTimeout(0) as well here to ensure
// that both graphs are rendered before starting to check if the rendering
// is correct.
concurrency.delay(0).then(function () {
return concurrency.delay(0);
}).then(function () {
assert.strictEqual(kanban.$('.o_kanban_record:first() .o_graph_barchart').length, 1,
"graph of first record should be a barchart");
assert.strictEqual(kanban.$('.o_kanban_record:nth(1) .o_graph_linechart').length, 1,
"graph of second record should be a linechart");
var evt = document.createEvent("MouseEvents"); //taken ref from https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/initMouseEvent
evt.initMouseEvent("mouseover", true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null);
$('.discreteBar')[0].dispatchEvent(evt);
var tooltip = $('.nvtooltip').find('table').find('.key')[0].innerText;
assert.equal(tooltip, graph_key, "graph tooltip should be generated ");
$('.nvtooltip').remove();
// force a re-rendering of the first record (to check if the
// previous rendered graph is correctly removed from the DOM)
var firstRecordState = kanban.model.get(kanban.handle).data[0];
return kanban.renderer.updateRecord(firstRecordState);
}).then(function () {
return concurrency.delay(0); // one graph is re-rendered
}).then(function () {
assert.strictEqual(kanban.$('.o_kanban_record:first() svg').length, 1,
"there should be only one rendered graph by record");
kanban.destroy();
done();
});
});
// nvd3 seems to do a setTimeout(0) each time the addGraph function is
// called, which is done twice in this case as there are 2 records.
// for that reason, we need to do two setTimeout(0) as well here to ensure
// that both graphs are rendered before starting to check if the rendering
// is correct.
concurrency.delay(0).then(function () {
return concurrency.delay(0);
}).then(function () {
assert.strictEqual(kanban.$('.o_kanban_record:first() .o_graph_barchart').length, 1,
"graph of first record should be a barchart");
assert.strictEqual(kanban.$('.o_kanban_record:nth(1) .o_graph_linechart').length, 1,
"graph of second record should be a linechart");
var evt = document.createEvent("MouseEvents"); //taken ref from https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/initMouseEvent
evt.initMouseEvent("mouseover", true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null);
$('.discreteBar')[0].dispatchEvent(evt);
var tooltip = $('.nvtooltip').find('table').find('.key')[0].innerText;
assert.equal(tooltip, graph_key, "graph tooltip should be generated ");
$('.nvtooltip').remove();
// force a re-rendering of the first record (to check if the
// previous rendered graph is correctly removed from the DOM)
var firstRecordState = kanban.model.get(kanban.handle).data[0];
return kanban.renderer.updateRecord(firstRecordState);
}).then(function () {
return concurrency.delay(0); // one graph is re-rendered
}).then(function () {
assert.strictEqual(kanban.$('.o_kanban_record:first() svg').length, 1,
"there should be only one rendered graph by record");
kanban.destroy();
done();
});
});
QUnit.test('graph dashboard widget does not need nv to be destroyed', function (assert) {
@@ -3690,6 +3690,9 @@ QUnit.module('basic_fields', {
'</sheet>' +
'</form>',
res_id: 1,
viewOptions: {
disable_autofocus: true,
},
});
assert.strictEqual(form.$('.o_field_widget.o_selection > a span.o_status.o_status_red').length, 1,
@@ -0,0 +1,82 @@
odoo.define('web.abstract_view_banner_tests', function (require) {
"use strict";
var AbstractRenderer = require('web.AbstractRenderer');
var AbstractView = require('web.AbstractView');
var testUtils = require('web.test_utils');
var createAsyncView = testUtils.createAsyncView;
var TestRenderer = AbstractRenderer.extend({
_renderView: function () {
this.$el.addClass('test_content');
return this._super();
},
});
var TestView = AbstractView.extend({
type: 'test',
config: _.extend({}, AbstractView.prototype.config, {
Renderer: TestRenderer
}),
});
var test_css_url = '/test_assetsbundle/static/src/css/test_cssfile1.css';
QUnit.module('Views', {
beforeEach: function () {
this.data = {
test_model: {
fields: {},
records: [],
},
};
},
afterEach: function () {
$('head link[href$="' + test_css_url + '"]').remove();
}
}, function () {
QUnit.module('BasicRenderer');
QUnit.test("The banner should be fetched from the route", function (assert) {
var done = assert.async();
assert.expect(5);
var banner_html =
'<div>' +
'<link type="text/css" href="' + test_css_url + '" rel="stylesheet">' +
'<div class="hello_banner"/>' +
'</div>';
createAsyncView({
View: TestView,
model: 'test_model',
data: this.data,
arch: '<test banner_route="/module/hello_banner"/>',
mockRPC: function (route, args) {
if (route === '/module/hello_banner') {
assert.step(route);
return $.when({html: banner_html});
}
return this._super(route, args);
},
}).then(function (view) {
var $banner = view.$('.hello_banner');
assert.strictEqual($banner.length, 1,
"The view should contain the response from the controller.");
assert.verifySteps(['/module/hello_banner'], "The banner should be fetched.");
var $head_link = $('head link[href$="' + test_css_url + '"]');
assert.strictEqual($head_link.length, 1,
"The stylesheet should have been added to head.");
var $banner_link = $('link[href$="' + test_css_url + '"]', $banner);
assert.strictEqual($banner_link.length, 0,
"The stylesheet should have been removed from the banner.");
view.destroy();
done();
});
});
}
);
});
@@ -4,7 +4,7 @@ odoo.define('web.calendar_mobile_tests', function (require) {
var CalendarView = require('web.CalendarView');
var testUtils = require('web.test_utils');
var createView = testUtils.createView;
var createAsyncView = testUtils.createAsyncView;
var initialDate = new Date(2016, 11, 12, 8, 0, 0);
initialDate = new Date(initialDate.getTime() - initialDate.getTimezoneOffset()*60*1000);
@@ -35,8 +35,9 @@ QUnit.module('Views', {
QUnit.test('simple calendar rendering in mobile', function (assert) {
assert.expect(3);
var done = assert.async();
var calendar = createView({
createAsyncView({
View: CalendarView,
model: 'event',
data: this.data,
@@ -46,16 +47,18 @@ QUnit.module('Views', {
viewOptions: {
initialDate: initialDate,
},
}).then(function (calendar) {
assert.notOk(calendar.$buttons.find('.o_calendar_button_prev').is(':visible'),
"prev button should be hidden");
assert.notOk(calendar.$buttons.find('.o_calendar_button_next').is(':visible'),
"next button should be hidden");
assert.ok($('.o_control_panel .o_cp_pager .o_calendar_button_today').is(':visible'),
"today button should be visible in the pager area (bottom right corner)");
calendar.destroy();
done();
});
assert.notOk(calendar.$buttons.find('.o_calendar_button_prev').is(':visible'),
"prev button should be hidden");
assert.notOk(calendar.$buttons.find('.o_calendar_button_next').is(':visible'),
"next button should be hidden");
assert.ok($('.o_control_panel .o_cp_pager .o_calendar_button_today').is(':visible'),
"today button should be visible in the pager area (bottom right corner)");
calendar.destroy();
});
});
+62 -58
View File
@@ -23,6 +23,7 @@ CalendarRenderer.include({
var createView = testUtils.createView;
var createAsyncView = testUtils.createAsyncView;
var initialDate = new Date(2016, 11, 12, 8, 0, 0);
initialDate = new Date(initialDate.getTime() - initialDate.getTimezoneOffset()*60*1000);
@@ -125,6 +126,7 @@ QUnit.module('Views', {
QUnit.test('simple calendar rendering', function (assert) {
assert.expect(24);
var done = assert.async();
this.data.event.records.push({
id: 7,
@@ -138,7 +140,7 @@ QUnit.module('Views', {
type: 1
});
var calendar = createView({
createAsyncView({
View: CalendarView,
model: 'event',
data: this.data,
@@ -156,67 +158,69 @@ QUnit.module('Views', {
'<field name="partner_ids" write_model="filter_partner" write_field="partner_id"/>'+
'</calendar>',
archs: archs,
viewOptions: {
initialDate: initialDate,
},
}).then(function (calendar) {
assert.ok(calendar.$('.o_calendar_view').find('.fc-view-container').length, "should instance of fullcalendar");
var $sidebar = calendar.$('.o_calendar_sidebar');
assert.strictEqual($sidebar.find('.ui-state-active').text(), "12", "should highlight the target day");
// test view scales
assert.strictEqual(calendar.$('.fc-event').length, 9, "should display 9 events on the week (4 event + 5 days event)");
assert.strictEqual($sidebar.find('.o_selected_range').length, 7, "week scale should highlight 7 days in mini calendar");
calendar.$buttons.find('.o_calendar_button_day').trigger('click'); // display only one day
assert.strictEqual(calendar.$('.fc-event').length, 2, "should display 2 events on the day");
assert.strictEqual($sidebar.find('.o_selected_range').length, 1, "should highlight the target day in mini calendar");
calendar.$buttons.find('.o_calendar_button_month').trigger('click'); // display all the month
assert.strictEqual(calendar.$('.fc-event').length, 7, "should display 7 events on the month (5 events + 2 week event - 1 'event 6' is filtered + 1 'Undefined event')");
assert.strictEqual($sidebar.find('.o_selected_range').length, 31, "month scale should highlight all days in mini calendar");
// test filters
assert.strictEqual($sidebar.find('.o_calendar_filter').length, 2, "should display 3 filters");
var $typeFilter = $sidebar.find('.o_calendar_filter:has(h3:contains(user))');
assert.ok($typeFilter.length, "should display 'user' filter");
assert.strictEqual($typeFilter.find('.o_calendar_filter_item').length, 3, "should display 3 filter items for 'user'");
// filters which has no value should show with string "Undefined" and should show at the last
assert.strictEqual($typeFilter.find('.o_calendar_filter_item:last').data('value'), false, "filters having false value should be displayed at last in filter items");
assert.strictEqual($typeFilter.find('.o_calendar_filter_item:last > span').text(), "Undefined", "filters having false value should display 'Undefined' string");
var $attendeesFilter = $sidebar.find('.o_calendar_filter:has(h3:contains(attendees))');
assert.ok($attendeesFilter.length, "should display 'attendees' filter");
assert.strictEqual($attendeesFilter.find('.o_calendar_filter_item').length, 3, "should display 3 filter items for 'attendees' who use write_model (2 saved + Everything)");
assert.ok($attendeesFilter.find('.o_field_many2one').length, "should display one2many search bar for 'attendees' filter");
assert.strictEqual(calendar.$('.fc-event').length, 7,
"should display 7 events ('event 5' counts for 2 because it spans two weeks and thus generate two fc-event elements)");
calendar.$('.o_calendar_filter .o_checkbox input').first().click(); // Disable first filter
assert.strictEqual(calendar.$('.fc-event').length, 4, "should now only display 4 event");
calendar.$('.o_calendar_filter .o_checkbox input').eq(1).click(); // Disable second filter
assert.strictEqual(calendar.$('.fc-event').length, 0, "should not display any event anymore");
// test search bar in filter
$sidebar.find('input[type="text"]').trigger('click');
assert.strictEqual($('ul.ui-autocomplete li:not(.o_m2o_dropdown_option)').length, 2, "should display 2 choices in one2many autocomplete"); // TODO: remove :not(.o_m2o_dropdown_option) because can't have "create & edit" choice
$('ul.ui-autocomplete li:first').trigger('click');
assert.strictEqual($sidebar.find('.o_calendar_filter:has(h3:contains(attendees)) .o_calendar_filter_item').length, 4, "should display 4 filter items for 'attendees'");
$sidebar.find('input[type="text"]').trigger('click');
assert.strictEqual($('ul.ui-autocomplete li:not(.o_m2o_dropdown_option)').text(), "partner 4", "should display the last choice in one2many autocomplete"); // TODO: remove :not(.o_m2o_dropdown_option) because can't have "create & edit" choice
$sidebar.find('.o_calendar_filter_item .o_remove').first().trigger('click');
assert.ok($('.modal-footer button.btn:contains(Ok)').length, "should display the confirm message");
$('.modal-footer button.btn:contains(Ok)').trigger('click');
assert.strictEqual($sidebar.find('.o_calendar_filter:has(h3:contains(attendees)) .o_calendar_filter_item').length, 3, "click on remove then should display 3 filter items for 'attendees'");
calendar.destroy();
done();
});
assert.ok(calendar.$('.o_calendar_view').find('.fc-view-container').length, "should instance of fullcalendar");
var $sidebar = calendar.$('.o_calendar_sidebar');
assert.strictEqual($sidebar.find('.ui-state-active').text(), "12", "should highlight the target day");
// test view scales
assert.strictEqual(calendar.$('.fc-event').length, 9, "should display 9 events on the week (4 event + 5 days event)");
assert.strictEqual($sidebar.find('.o_selected_range').length, 7, "week scale should highlight 7 days in mini calendar");
calendar.$buttons.find('.o_calendar_button_day').trigger('click'); // display only one day
assert.strictEqual(calendar.$('.fc-event').length, 2, "should display 2 events on the day");
assert.strictEqual($sidebar.find('.o_selected_range').length, 1, "should highlight the target day in mini calendar");
calendar.$buttons.find('.o_calendar_button_month').trigger('click'); // display all the month
assert.strictEqual(calendar.$('.fc-event').length, 7, "should display 7 events on the month (5 events + 2 week event - 1 'event 6' is filtered + 1 'Undefined event')");
assert.strictEqual($sidebar.find('.o_selected_range').length, 31, "month scale should highlight all days in mini calendar");
// test filters
assert.strictEqual($sidebar.find('.o_calendar_filter').length, 2, "should display 3 filters");
var $typeFilter = $sidebar.find('.o_calendar_filter:has(h3:contains(user))');
assert.ok($typeFilter.length, "should display 'user' filter");
assert.strictEqual($typeFilter.find('.o_calendar_filter_item').length, 3, "should display 3 filter items for 'user'");
// filters which has no value should show with string "Undefined" and should show at the last
assert.strictEqual($typeFilter.find('.o_calendar_filter_item:last').data('value'), false, "filters having false value should be displayed at last in filter items");
assert.strictEqual($typeFilter.find('.o_calendar_filter_item:last > span').text(), "Undefined", "filters having false value should display 'Undefined' string");
var $attendeesFilter = $sidebar.find('.o_calendar_filter:has(h3:contains(attendees))');
assert.ok($attendeesFilter.length, "should display 'attendees' filter");
assert.strictEqual($attendeesFilter.find('.o_calendar_filter_item').length, 3, "should display 3 filter items for 'attendees' who use write_model (2 saved + Everything)");
assert.ok($attendeesFilter.find('.o_field_many2one').length, "should display one2many search bar for 'attendees' filter");
assert.strictEqual(calendar.$('.fc-event').length, 7,
"should display 7 events ('event 5' counts for 2 because it spans two weeks and thus generate two fc-event elements)");
calendar.$('.o_calendar_filter .o_checkbox input').first().click(); // Disable first filter
assert.strictEqual(calendar.$('.fc-event').length, 4, "should now only display 4 event");
calendar.$('.o_calendar_filter .o_checkbox input').eq(1).click(); // Disable second filter
assert.strictEqual(calendar.$('.fc-event').length, 0, "should not display any event anymore");
// test search bar in filter
$sidebar.find('input[type="text"]').trigger('click');
assert.strictEqual($('ul.ui-autocomplete li:not(.o_m2o_dropdown_option)').length, 2, "should display 2 choices in one2many autocomplete"); // TODO: remove :not(.o_m2o_dropdown_option) because can't have "create & edit" choice
$('ul.ui-autocomplete li:first').trigger('click');
assert.strictEqual($sidebar.find('.o_calendar_filter:has(h3:contains(attendees)) .o_calendar_filter_item').length, 4, "should display 4 filter items for 'attendees'");
$sidebar.find('input[type="text"]').trigger('click');
assert.strictEqual($('ul.ui-autocomplete li:not(.o_m2o_dropdown_option)').text(), "partner 4", "should display the last choice in one2many autocomplete"); // TODO: remove :not(.o_m2o_dropdown_option) because can't have "create & edit" choice
$sidebar.find('.o_calendar_filter_item .o_remove').first().trigger('click');
assert.ok($('.modal-footer button.btn:contains(Ok)').length, "should display the confirm message");
$('.modal-footer button.btn:contains(Ok)').trigger('click');
assert.strictEqual($sidebar.find('.o_calendar_filter:has(h3:contains(attendees)) .o_calendar_filter_item').length, 3, "click on remove then should display 3 filter items for 'attendees'");
calendar.destroy();
});
QUnit.test('breadcrumbs are updated with the displayed period', function (assert) {
+3
View File
@@ -52,6 +52,7 @@
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/animation.scss"/>
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/rainbow.scss"/>
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/datepicker.scss"/>
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/banner.scss"/>
<script type="text/javascript" src="/web/static/lib/es5-shim/es5-shim.min.js"></script>
<script type="text/javascript" src="/web/static/lib/underscore/underscore.js"></script>
@@ -528,12 +529,14 @@
<script type="text/javascript" src="/web/static/tests/views/kanban_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/views/calendar_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/views/basic_model_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/views/abstract_view_banner_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/views/kanban_model_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/views/view_dialogs_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/views/search_filters_menu_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/views/search_groupby_menu_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/widgets/dropdown_menu_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/views/search_view_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/core/ajax_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/core/registry_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/core/py_utils_tests.js"></script>
<script type="text/javascript" src="/web/static/tests/core/class_tests.js"></script>
+1
View File
@@ -3,3 +3,4 @@
from . import controllers
from . import models
from . import wizard
+1
View File
@@ -15,6 +15,7 @@
'data/digest_data.xml',
'views/product_views.xml',
'views/account_views.xml',
'views/onboarding_views.xml',
'views/sale_report_views.xml',
'views/sale_order_views.xml',
'views/crm_team_views.xml',
+1
View File
@@ -12,3 +12,4 @@ from . import sale_order
from . import website
from . import res_config_settings
from . import digest
from . import res_company
+17
View File
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class ResCompany(models.Model):
_inherit = 'res.company'
website_sale_onboarding_payment_acquirer_done =\
fields.Boolean("Website sale onboarding payment acquirer step done", default=False)
@api.model
def action_open_website_sale_onboarding_payment_acquirer(self):
""" Called by onboarding panel above the quotation list."""
action = self.env.ref('website_sale.action_open_website_sale_onboarding_payment_acquirer_wizard').read()[0]
return action
@@ -0,0 +1,31 @@
<odoo>
<!-- PAYMENT ACQUIRER -->
<template id="website_sale_onboarding_payment_acquirer_step" primary="True"
inherit_id="payment.onboarding_payment_acquirer_step">
<xpath expr="//t[@t-set='method']" position="replace">
<t t-set="method" t-value="'action_open_website_sale_onboarding_payment_acquirer'" />
</xpath>
<xpath expr="//t[@t-set='done']" position="replace">
<t t-set="done" t-value="company.website_sale_onboarding_payment_acquirer_done" />
</xpath>
</template>
<record id="website_sale_onboarding_payment_acquirer_form" model="ir.ui.view">
<field name="name">website.sale.onboarding.payment.acquirer.form</field>
<field name="model">website.sale.payment.acquirer.onboarding.wizard</field>
<field name="inherit_id" ref="payment.payment_acquirer_onboarding_wizard_form"/>
<field name="mode">primary</field>
<field name="priority">1000</field>
<field name="arch" type="xml">
<xpath expr="//div[@name='skip_step_text']" position="replace"/>
</field>
</record>
<record id="action_open_website_sale_onboarding_payment_acquirer_wizard" model="ir.actions.act_window">
<field name="name">Choose a payment method</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">website.sale.payment.acquirer.onboarding.wizard</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="view_id" ref="website_sale_onboarding_payment_acquirer_form" />
<field name="target">new</field>
</record>
</odoo>
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import payment_acquirer_onboarding_wizard
@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class PaymentWizard(models.TransientModel):
_inherit = 'payment.acquirer.onboarding.wizard'
_name = 'website.sale.payment.acquirer.onboarding.wizard'
def _set_payment_acquirer_onboarding_step_done(self):
""" Override. """
self.env.user.company_id.website_sale_onboarding_payment_acquirer_done = True
+4 -1
View File
@@ -1 +1,4 @@
from . import models
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
@@ -1 +1,5 @@
from . import ir_module_module
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import ir_module_module
from . import res_company
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from ast import literal_eval
class ResCompany(models.Model):
_inherit = 'res.company'
def _compute_website_theme_onboarding_done(self):
""" The step is marked as done if one theme is installed. """
# we need the same domain as the existing action
action = self.env.ref('website_theme_install.theme_install_kanban_action').read()[0]
domain = literal_eval(action['domain'])
domain.append(('state', '=', 'installed'))
installed_themes_count = self.env['ir.module.module'].sudo().search_count(domain)
for record in self:
record.website_theme_onboarding_done = (installed_themes_count > 0)
website_theme_onboarding_done = fields.Boolean("Onboarding website theme step done",
compute='_compute_website_theme_onboarding_done')
@api.model
def action_open_website_theme_selector(self):
action = self.env.ref('website_theme_install.theme_install_kanban_action').read()[0]
action['target'] = 'new'
return action
@@ -95,4 +95,15 @@ action = {
<a href="/web#action=website_theme_install.theme_install_kanban_action">See themes here</a>
</xpath>
</template>
<!-- ONBOARDING -->
<template id="onboarding_website_theme_step">
<t t-call="base.onboarding_step">
<t t-set="title">Website Theme</t>
<t t-set="description">Select a theme for your wonderful website.</t>
<t t-set="btn_text">Let's start!</t>
<t t-set="method" t-value="'action_open_website_theme_selector'" />
<t t-set="model" t-value="'res.company'" />
<t t-set="done" t-value="company.website_theme_onboarding_done" />
</t>
</template>
</odoo>
+39
View File
@@ -44,6 +44,45 @@ otherwise.
``application``
website feature defining togglable views. By default, views are always
applied
``banner_route``
a route address to be fetched and prepended to the view.
If this attribute is set, the
:ref:`controller route url<reference/http/controllers>` will be fetched and
displayed above the view. The json response from the controller should
contain an "html" key.
If the html contains a stylesheet <link> tag, it will be
removed and appended to <head>.
To interact with the backend you can use <a type="action"> tags. Please take
a look at the documentation of the _onActionClicked method of
AbstractController (*addons/web/static/src/js/views/abstract_controller.js*)
for more details.
Only views extending AbstractView and AbstractController can use this
attribute, like :ref:`reference/views/form`, :ref:`reference/views/kanban`,
:ref:`reference/views/list`, ...
Example:
.. code-block:: xml
<tree banner_route="/module_name/hello" />
.. code-block:: python
class MyController(odoo.http.Controller):
@http.route('/module_name/hello', auth='user', type='json')
def hello(self):
return {
'html': """
<div>
<link href="/module_name/static/src/css/banner.css"
rel="stylesheet">
<h1>hello, world</h1>
</div> """
}
.. _reference/views/inheritance:
+1
View File
@@ -68,6 +68,7 @@ The kernel of Odoo, needed for all installation.
'views/ir_property_views.xml',
'views/res_config_settings_views.xml',
'views/report_paperformat_views.xml',
'views/onboarding_views.xml',
'security/ir.model.access.csv',
],
'demo': [
@@ -12,6 +12,7 @@
<field name="phone"></field>
<field name="email">info@yourcompany.com</field>
<field name="website">www.yourcompany.com</field>
<field name="image" type="base64" file="base/static/img/res_company_logo.png"/>
</record>
<record model="res.partner" id="base.partner_root">
-1
View File
@@ -25,7 +25,6 @@
<field name="phone">+1 555 123 8069</field>
<field name="email">info@yourcompany.example.com</field>
<field name="website">www.example.com</field>
<field name="image" type="base64" file="base/static/img/res_company_logo.png"/>
</record>
<record id="user_demo" model="res.users">
+15
View File
@@ -66,6 +66,8 @@ class Company(models.Model):
_sql_constraints = [
('name_uniq', 'unique (name)', 'The company name must be unique !')
]
base_onboarding_company_done = fields.Boolean("Onboarding company step done",
compute="_compute_base_onboarding_company_done")
@api.model_cr
def init(self):
@@ -263,3 +265,16 @@ class Company(models.Model):
.report_action(docids))
else:
return res
@api.depends('street')
def _compute_base_onboarding_company_done(self):
""" The company onboarding step is marked as done if street1 is filled. """
for record in self:
record.base_onboarding_company_done = bool(record.street)
@api.model
def action_open_base_onboarding_company(self):
""" Onboarding step for company basic information. """
action = self.env.ref('base.action_open_base_onboarding_company').read()[0]
action['res_id'] = self.env.user.company_id.id
return action
+1
View File
@@ -30,6 +30,7 @@
<rng:optional><rng:attribute name="decoration-primary"/></rng:optional>
<rng:optional><rng:attribute name="decoration-success"/></rng:optional>
<rng:optional><rng:attribute name="decoration-warning"/></rng:optional>
<rng:optional><rng:attribute name="banner_route"/></rng:optional>
<rng:optional>
<rng:attribute name="limit">
<rng:data type="int"/>
@@ -0,0 +1,456 @@
$o-onboarding-dot-size: 20px;
$o-onboarding-dot-timing: 0.5s;
$o-onboarding-blue-bg-1: #60ABC3;
$o-onboarding-blue-bg-2: #4187AE;
$o-onboarding-blue-ui-1: #F39D9B;
$o-onboarding-blue-ui-2: #FBEEC8;
$o-onboarding-orange-bg-1: #DA9D77;
$o-onboarding-orange-bg-2: #B9684F;
$o-onboarding-orange-ui-1: #599FF1;
$o-onboarding-orange-ui-2: #68B3C9;
$o-onboarding-violet-bg-1: #6070C3;
$o-onboarding-violet-bg-2: #475DA4;
$o-onboarding-violet-ui-1: #F39D9B;
$o-onboarding-violet-ui-2: #FBEEC8;
.o_onboarding {
position: relative;
overflow: hidden;
background: white center;
background-size: cover;
background-position: center top;
border-bottom: 1px solid $gray-lighter-dark;
.o_onboarding_wrap {
padding: $o-horizontal-padding*2 $o-horizontal-padding;
}
.o_onboarding_folded_title {
display: none;
}
.o_onboarding_btn_fold, .o_onboarding_btn_close {
@include o-position-absolute(auto,0,0);
padding: 6px $o-horizontal-padding + 6px;
cursor: pointer;
}
.o_onboarding_btn_close {
top:0
}
.modal-body, .modal-title {
color: black;
}
.o_onboarding_steps, .o_onboarding_step {
@include o-flex-display();
}
.o_onboarding_steps {
@include o-flex-flow(row, nowrap);
@include o-justify-content(initial);
@media (max-width: $screen-xs-max) {
@include o-flex-flow(column, nowrap);
.o_onboarding_folded_title {
border-right: none;
}
}
}
.o_onboarding_step {
@include o-flex-flow(column, nowrap);
@include o-flex(1, 1, auto);
@include o-align-items(center);
text-align: center;
position: relative;
opacity: 0.6;
@media (max-width: $screen-xs-max) {
text-align: left;
margin-bottom: 16px;
@include o-align-items(baseline);
.o_onboarding_step_content {
max-width: none;
}
}
&:first-of-type {
.o_onboarding_progress {
display: none;
}
.o_onboarding_dot {
@include transition-delay(0s);
}
}
.o_onboarding_step_content {
max-width: 230px;
padding: 0 10px;
.o_onboarding_step_action {
border: 1px solid $gray-lighter;
display: none;
}
p {
opacity: 0.8;
}
}
.o_onboarding_progress {
@include o-position-absolute($o-onboarding-dot-size*0.5 - 2px, calc(50% + 10px));
@include size(calc(100% - 20px), 4px);
background-color: $gray-lighter;
.progress-bar {
@include o-linear-gradient($deg: 90deg, $start-color: $o-brand-optional, $end-color: $o-brand-primary);
@include o-transition(opacity, $o-onboarding-dot-timing);
}
}
.o_onboarding_dot {
@include square($o-onboarding-dot-size);
color: transparent;
padding-top: 5px;
font-size: 11px;
display: inline-block;
border-radius: 100%;
background-color: $gray-lighter;
position: relative;
@include o-transition(background, $o-onboarding-dot-timing, ease, $o-onboarding-dot-timing);
}
.o_onboarding_all_done {
display: none;
}
// Steps' Status
&.o_onboarding_current, &.o_onboarding_done {
opacity: 1;
.o_onboarding_progress .progress-bar {
width: 100%;
box-shadow: none;
@include o-transition(width, $o-onboarding-dot-timing);
}
}
&.o_onboarding_current {
.o_onboarding_step_action, .o_onboarding_dot {
@include o-animation(bounceIn, $duration: $o-onboarding-dot-timing*2);
}
.o_onboarding_step_action {
display: inline-block;
}
.o_onboarding_dot {
background: $o-brand-primary;
}
}
&.o_onboarding_done {
.o_onboarding_dot {
background: $o-brand-optional;
color: white;
}
.o_onboarding_all_done {
@include o-animation(bounceIn, $duration: $o-onboarding-dot-timing*2);
display: block;
}
.o_onboarding_step_content_info {
@include o-animation(o_onb_fadeIn, $duration: $o-onboarding-dot-timing*3);
}
+ .o_onboarding_done {
.o_onboarding_dot {
@include transition-delay(0s);
}
.o_onboarding_progress {
background: $o-brand-optional;
}
.progress-bar {
opacity: 0;
}
}
}
}
@keyframes o_onb_fadeIn {
0% {opacity: 0}
33% {opacity: 0}
100% {opacity: 1}
}
// ========= Transition =========
&.collapsing {
@include o-transition(all, .25s);
min-height: 30px;
}
// ========= Folded Design =========
// ======================================
&.collapse:not(.in) {
min-height: 30px;
background-image: none!important;
display: block;
.o_onboarding_hide_folded {
display: none!important;
}
.o_onboarding_btn_fold {
top: 0;
left: 0;
@include o-transform(scaleY(-1));
}
.o_onboarding_wrap {
padding: 0 $o-horizontal-padding;
.o_onboarding_steps {
padding: 0;
@include o-justify-content(baseline);
}
.o_onboarding_folded_title {
padding: 6px 10px 6px 0;
@media (min-width: $screen-sm-min) {
border-right: 1px solid $gray-lighter-dark;
}
}
.o_onboarding_folded_title, .o_onboarding_step {
@include o-inline-flex-display();
}
.o_onboarding_step {
@include o-align-items(flex-start);
@include o-flex(0,1,auto);
padding-right: 12px;
&:not(:last-of-type) {
&:before, &:after {
content: " ";
@include o-position-absolute(0, 0);
border-top: floor($o-statusbar-height/2) solid transparent;
border-bottom: ceil($o-statusbar-height/2) solid transparent;
border-right: none;
border-left: $o-statusbar-arrow-width solid $o-view-background-color;
-moz-transform: scale(0.9999); // Smooth the triangle on firefox
}
&:before {
right: -1px;
border-left-color: $gray-lighter-dark;
}
}
.o_onboarding_step_content {
padding: 0;
@include o-flex-display();
}
.o_onboarding_step_content_info .o_onboarding_step_title {
margin: 7px!important;
font-size: $font-size-base;
}
.o_onboarding_all_done {
margin: 6px 10px 0 -4px!important;
> i:before {
content: "\f00c";
}
}
}
}
}
// ========= Color Variations =========
// ======================================
// ========= Variation Mixin ==========
@mixin o-onboarding-variation($bg1, $bg2, $ui1, $ui2) {
&, .o_onboarding_btn_fold, .o_onboarding_step_title, .o_onboarding_step_action, a.o_onboarding_all_done, .o_onboarding_all_done > .fa, .o_onboarding_btn_close {
color: white;
}
.o_onboarding_wrap {
@include o-vertical-gradient(fade-out($bg1, 0.5), $bg2);
box-shadow: inset 0 -7px 20px -5px fade-out(black, 0.7);
}
.o_onboarding_dot, .o_onboarding_progress {
background-color: fade-out(white, 0.5);
}
.o_onboarding_step_action {
border-color: fade-out(white, 0.5);
}
.progress-bar {
@include o-linear-gradient($deg: 90deg, $start-color: $ui1, $end-color: $ui2);
}
.o_onboarding_step {
&.o_onboarding_done, .o_onboarding_done + .o_onboarding_done {
.o_onboarding_dot, .o_onboarding_progress {
background: $ui1;
}
}
&.o_onboarding_current .o_onboarding_dot {
background: $ui2;
}
}
}
// ========= Variation Templates ==========
&.o_onboarding_blue.collapse.in, &.o_onboarding_blue.collapsing {
@include o-onboarding-variation($o-onboarding-blue-bg-1, $o-onboarding-blue-bg-2, $o-onboarding-blue-ui-1, $o-onboarding-blue-ui-2);
}
&.o_onboarding_orange.collapse.in, &.o_onboarding_orange.collapsing {
@include o-onboarding-variation($o-onboarding-orange-bg-1, $o-onboarding-orange-bg-2, $o-onboarding-orange-ui-1, $o-onboarding-orange-ui-2);
}
&.o_onboarding_violet.collapse.in, &.o_onboarding_violet.collapsing {
@include o-onboarding-variation($o-onboarding-violet-bg-1, $o-onboarding-violet-bg-2, $o-onboarding-violet-ui-1, $o-onboarding-violet-ui-2);
}
// ========= "Cannon Confetti" animation ==========
.o_onboarding_confetti {
@include o-animation(cannon-explosion, $o-onboarding-dot-timing*3, ease-out, 0s, 1, normal, both);
@include o-position-absolute(0, 50%, 30px);
@include o-transform-origin(50%, 100%);
display: none;
width: 1px;
@keyframes cannon-explosion {
0% {
@include o-transform(scale(0));
}
70% {
opacity: 1;
}
100% {
opacity: 0;
}
}
> div {
@include o-position-absolute(auto, 0, 5px,0);
@include o-transform-origin(50%, 100%);
@include o-flex-display();
@include o-flex-flow(column, nowrap);
@include o-justify-content(space-around);
@include o-align-content(center);
padding-bottom: 30%;
&:nth-child(1), &:nth-child(5) {
height: 90%;
@include o-transform(rotate(-20deg));
}
&:nth-child(2), &:nth-child(4) {
height: 95%;
@include o-transform(rotate(-10deg));
}
&:nth-child(3) {
height: 100%;
}
&:nth-child(4) {
@include o-transform(rotate(10deg));
}
&:nth-child(5) {
@include o-transform(rotate(20deg));
}
> div {
@include o-flex(0,1,auto);
@include o-animation(confetti-rotate-l, $o-onboarding-dot-timing*3, ease-out, 0s, 1, normal, both);
&:nth-child(odd) {
@include o-animation(confetti-rotate-r, $o-onboarding-dot-timing*3, ease-out, 0s, 1, normal, both);
}
@keyframes confetti-rotate-l {
0% {
@include o-transform(rotate(0) scaleY(1));
}
50% {
@include o-transform(rotate(2turn) scaleY(1.5));
opacity: 1;
}
100% {
@include o-transform(rotate(2.05turn) scaleY(1));
opacity: 0;
}
}
@keyframes confetti-rotate-r {
0% {
@include o-transform(rotate(0) scaleY(1));
}
50% {
@include o-transform(rotate(-2turn) scaleY(1.5));
opacity: 1;
}
100% {
@include o-transform(rotate(-2.05turn) scaleY(1));
opacity: 0;
}
}
&.ribbon {
@include size(.3em, .7em);
border-radius: .1em;
}
&.flake {
@include square(.3em);
border-radius: 50%;
}
// Confetti Colors
&.color-1 {
background-color: #feb535;
}
&.color-2 {
background-color: #bea4ff;
}
&.color-3 {
background-color: #ff6e83;
}
&.color-4 {
background-color: #58cafe;
}
}
}
}
.o_onboarding_done {
// Activate confetti cannon when step is done
.o_onboarding_confetti {
display: block;
}
}
}
+177
View File
@@ -0,0 +1,177 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="onboarding_container">
<div class="o_onboarding_container collapse in">
<t t-call-assets="base.assets_onboarding" />
<div t-att-class="'o_onboarding ' + classes + (' collapse' if folded_by_default else ' collapse in')"
t-att-style="'background-image:url(' + bg_image + ')'">
<div class="o_onboarding_wrap">
<a t-if="not no_close_btn" href="#" data-toggle="modal" data-target=".o_onboarding_modal"
class="pull-right o_onboarding_btn_close o_onboarding_hide_folded">
<i class="fa fa-times" title="Close the onboarding panel" />
</a>
<!-- Modal -->
<div class="o_onboarding_modal modal fade" tabindex="-1" role="dialog" aria-labelledby="onboardingCloseLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-label="close the modal">&amp;times;</span>
</button>
<h4 class="modal-title" id="onboardingCloseLabel">Closing the onboarding panel</h4>
</div>
<div class="modal-body">
Are you sure you want to close this onboarding panel?
This action is definitive.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">
No
</button>
<a type="action" class="btn btn-primary" data-dismiss="modal"
data-toggle="collapse" href=".o_onboarding_container"
t-att-data-model="close_model"
t-att-data-method="close_method">
Yes
</a>
</div>
</div>
</div>
</div>
<div class="o_onboarding_steps">
<span t-if="not no_fold_btn" class="o_onboarding_folded_title">
Configuration steps:
</span>
<t t-raw="0" />
</div>
<a t-if="not no_fold_btn or folded_by_default" data-toggle="collapse"
href=".o_onboarding" type="action" t-att-data-model="fold_model"
t-att-data-method="fold_method" class="o_onboarding_btn_fold text-right">
<i class="fa fa-chevron-up" title="Fold the onboarding panel" />
</a>
</div>
</div>
</div>
</template>
<template id="onboarding_step">
<div t-att-class="'o_onboarding_step ' + ('o_onboarding_done' if done else 'o_onboarding_current')">
<t t-call="base.onboarding_confetti" />
<div class="o_onboarding_progress o_onboarding_hide_folded hidden-xs progress">
<div class="progress-bar"></div>
</div>
<span class="o_onboarding_dot o_onboarding_hide_folded hidden-xs fa fa-check" />
<div class="o_onboarding_step_content">
<div class="o_onboarding_step_content_info">
<h4 class="o_onboarding_step_title mt16">
<t t-esc="title" />
</h4>
<p class="o_onboarding_hide_folded mb16">
<t t-esc="description" />
</p>
</div>
<a class="o_onboarding_step_action o_onboarding_hide_folded btn" type="action"
data-reload-on-close="true" role="button" t-att-data-method="method"
t-att-data-model="model">
<t t-if="btn_text" t-esc="btn_text" />
<t t-else="">Let's do it</t>
</a>
<a class="o_onboarding_all_done mt16" t-att-name="form_ref" type="action"
data-reload-on-close="true" role="button" t-att-data-method="method"
t-att-data-model="model">
<i t-attf-class="text-success mr6 fa #{done_icon if done_icon else 'fa-check'}" />
<span class="o_onboarding_hide_folded">
<t t-if="done_text" t-esc="done_text" />
<t t-else="">All done!</t>
</span>
</a>
</div>
</div>
</template>
<template id="onboarding_confetti">
<div class="o_onboarding_confetti">
<div>
<div class="flake color-3" />
<div class="ribbon color-1" />
<div class="ribbon color-2" />
<div class="flake color-1" />
<div /> <!-- spacer -->
</div>
<div>
<div class="ribbon color-2" />
<div class="flake color-2" />
<div class="flake color-3" />
<div class="ribbon color-1" />
<div class="flake color-4" />
<div /> <!-- spacer -->
</div>
<div>
<div class="ribbon color-1" />
<div class="ribbon color-4" />
<div class="ribbon color-2" />
<div class="flake color-3" />
<div /> <!-- spacer -->
</div>
<div>
<div class="flake color-3" />
<div class="ribbon color-1" />
<div class="ribbon color-4" />
<div class="flake color-3" />
<div class="ribbon color-2" />
<div /> <!-- spacer -->
</div>
<div>
<div class="ribbon color-2" />
<div class="flake color-3" />
<div class="flake color-4" />
<div class="ribbon color-1" />
<div /> <!-- spacer -->
</div>
</div>
</template>
<!-- ONBOARDING STEPS -->
<template id="onboarding_company_step">
<t t-call="base.onboarding_step">
<t t-set="title">Company Data</t>
<t t-set="description">Your information is displayed on the invoices you send to
clients.
</t>
<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="done" t-value="company.base_onboarding_company_done" />
</t>
</template>
<template id="assets_onboarding">
<t t-call="web._assets_helpers"/>
<link rel="stylesheet" type="text/scss" href="/base/static/src/scss/onboarding.scss"/>
</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">
<field name="logo" position="attributes">
<attribute name="attrs">{'invisible': 1}</attribute>
</field>
<xpath expr="//group[@name='social_media']" position="replace" />
<form position="inside">
<footer position="replace">
<button special="save" string="Save" class="btn btn-primary" type="object" />
<button special="cancel" 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="type">ir.actions.act_window</field>
<field name="res_model">res.company</field>
<field name="view_mode">form</field>
<field name="view_type">form</field>
<field name="view_id" ref="base_onboarding_company_form" />
<field name="target">new</field>
</record>
</odoo>
+50 -3
View File
@@ -28,7 +28,6 @@
<field name="zip" placeholder="ZIP" class="o_address_zip"/>
<field name="country_id" placeholder="Country" class="o_address_country" options='{"no_open": True}'/>
</div>
<field name="report_header" placeholder="e.g. Global Business Solutions"/>
</group>
<group>
<field name="website" widget="url" placeholder="e.g. www.odoo.com"/>
@@ -39,7 +38,6 @@
<field name="currency_id" options="{'no_create': True, 'no_open': True}" id="company_currency" context="{'active_test': False}"/>
<field name="parent_id" groups="base.group_multi_company"/>
<field name="sequence" invisible="1"/>
<field name="report_footer" placeholder="e.g. Your Bank Accounts, one per line"/>
</group>
<group name="social_media"/>
</group>
@@ -182,6 +180,55 @@
</xpath>
</field>
</record>
<record id="view_company_document_template_form" model="ir.ui.view">
<field name="name">res.company.document.template.form</field>
<field name="model">res.company</field>
<field name="mode">primary</field>
<field name="priority">2048</field>
<field name="arch" type="xml">
<form class="o_company_document_layout">
<group col="2">
<group>
<label for="external_report_layout" string="Template" colspan="2" />
<field name="external_report_layout" colspan="2" nolabel="1"
class="image_selection_container"
widget="image_selection" options="{
'background': {
'image_link': '/base/static/img/preview_background.png',
'preview_link': '/base/static/pdf/preview_background.pdf'
},
'boxed': {
'image_link': '/base/static/img/preview_boxed.png',
'preview_link': '/base/static/pdf/preview_boxed.pdf'
},
'clean': {
'image_link': '/base/static/img/preview_clean.png',
'preview_link': '/base/static/pdf/preview_clean.pdf'
},
'standard': {
'image_link': '/base/static/img/preview_standard.png',
'preview_link': '/base/static/pdf/preview_standard.pdf'
}
}" />
</group>
<group>
<label for="logo" colspan="2" string="Company logo" />
<field name="logo" widget="image" nolabel="1" colspan="2" />
<label for="report_header" colspan="2" string="Company Tagline" />
<field name="report_header" nolabel="1" colspan="2"
placeholder="e.g. Global Business Solutions" />
<label for="report_footer" colspan="2"
string="Extra information in the footer" />
<field name="report_footer" nolabel="1" colspan="2"
placeholder="e.g. Opening hours, bank accounts (one per line)" />
</group>
</group>
<footer>
<button special="save" string="Save" class="btn btn-primary" type="object" />
<button special="cancel" string="Cancel" />
</footer>
</form>
</field>
</record>
</data>
</odoo>