[ADD] account: onboarding panel for invoicing

Add an onboarding panel for invoicing, it appears above the invoice
list.
After going through the steps the system should be all set to send real invoices.

In the payment module, add a payment acquirer step and add it to the invoice onboarding panel.

task: 60668
This commit is contained in:
eco-odoo
2018-07-20 11:59:12 +02:00
committed by Yannick Tivisse
parent 9744fecd05
commit d1ec3306b8
16 changed files with 513 additions and 5 deletions
+1
View File
@@ -63,6 +63,7 @@ Core mechanisms for the accounting modules. To display the menuitems, install th
'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
+21
View File
@@ -0,0 +1,21 @@
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,
})
}
+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
+111 -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,19 @@ 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)
@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 +411,100 @@ 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_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: 23 KiB

+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>
@@ -0,0 +1,82 @@
<?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>
<!-- ONBOARDING PANEL-->
<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>
<!-- 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>
@@ -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
+1
View File
@@ -14,6 +14,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>
+1
View File
@@ -0,0 +1 @@
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})