From d1ec3306b8a75f8b961192ef5cf39b6f454f3792 Mon Sep 17 00:00:00 2001 From: eco-odoo Date: Mon, 28 May 2018 12:35:54 +0200 Subject: [PATCH] [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 --- addons/account/__manifest__.py | 1 + addons/account/controllers/__init__.py | 1 + addons/account/controllers/onboarding.py | 21 +++ addons/account/models/account_invoice.py | 1 + addons/account/models/company.py | 112 ++++++++++++++- .../src/img/account_invoice_onboarding_bg.jpg | Bin 0 -> 23728 bytes addons/account/views/account_invoice_view.xml | 15 +- .../views/account_onboarding_templates.xml | 82 +++++++++++ .../mail/wizard/mail_compose_message_view.xml | 2 +- addons/payment/__init__.py | 3 +- addons/payment/__manifest__.py | 1 + addons/payment/models/__init__.py | 1 + addons/payment/models/res_company.py | 15 ++ .../payment_acquirer_onboarding_templates.xml | 135 ++++++++++++++++++ addons/payment/wizards/__init__.py | 1 + .../payment_acquirer_onboarding_wizard.py | 127 ++++++++++++++++ 16 files changed, 513 insertions(+), 5 deletions(-) create mode 100644 addons/account/controllers/onboarding.py create mode 100644 addons/account/static/src/img/account_invoice_onboarding_bg.jpg create mode 100644 addons/account/views/account_onboarding_templates.xml create mode 100644 addons/payment/models/res_company.py create mode 100644 addons/payment/views/payment_acquirer_onboarding_templates.xml create mode 100644 addons/payment/wizards/__init__.py create mode 100644 addons/payment/wizards/payment_acquirer_onboarding_wizard.py diff --git a/addons/account/__manifest__.py b/addons/account/__manifest__.py index 9654e47f01e..c65eed84e2f 100644 --- a/addons/account/__manifest__.py +++ b/addons/account/__manifest__.py @@ -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', diff --git a/addons/account/controllers/__init__.py b/addons/account/controllers/__init__.py index e5a6112c48f..6d24304de46 100644 --- a/addons/account/controllers/__init__.py +++ b/addons/account/controllers/__init__.py @@ -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 diff --git a/addons/account/controllers/onboarding.py b/addons/account/controllers/onboarding.py new file mode 100644 index 00000000000..ee51395add8 --- /dev/null +++ b/addons/account/controllers/onboarding.py @@ -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, + }) + } diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 190640cb34d..a38e33ebac1 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -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 diff --git a/addons/account/models/company.py b/addons/account/models/company.py index 65162b07490..1206cd9ccd9 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -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 diff --git a/addons/account/static/src/img/account_invoice_onboarding_bg.jpg b/addons/account/static/src/img/account_invoice_onboarding_bg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7b7e1a524d5d4144381576c7ff6f486ee6189848 GIT binary patch literal 23728 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 literal 0 HcmV?d00001 diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index 147e0d56ac7..e6e57606a70 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -167,6 +167,18 @@ + + account.invoice.tree.with.onboarding + account.invoice + + primary + + + /account/account_invoice_onboarding + + + + account.invoice.kanban account.invoice @@ -727,11 +739,10 @@ - tree - + diff --git a/addons/account/views/account_onboarding_templates.xml b/addons/account/views/account_onboarding_templates.xml new file mode 100644 index 00000000000..f094a8a5bb9 --- /dev/null +++ b/addons/account/views/account_onboarding_templates.xml @@ -0,0 +1,82 @@ + + + + + + + + + + Configure your document template + ir.actions.act_window + res.company + form + form + + new + + + + mail.compose.message.form.account_invoice_onboarding_sample_invoice + + mail.compose.message + + 1000 + primary + + + 1 + + + 1 + + + 1 + + + + + Send a sample invoice + ir.actions.act_window + mail.compose.message + form + form + + new + + diff --git a/addons/mail/wizard/mail_compose_message_view.xml b/addons/mail/wizard/mail_compose_message_view.xml index 5d3a7a910dd..9c349c36bc2 100644 --- a/addons/mail/wizard/mail_compose_message_view.xml +++ b/addons/mail/wizard/mail_compose_message_view.xml @@ -52,7 +52,7 @@ the selected records the current search filter. - Followers of the document and + Followers of the document and diff --git a/addons/payment/__init__.py b/addons/payment/__init__.py index b75bfd8cfe4..4a05b41804c 100644 --- a/addons/payment/__init__.py +++ b/addons/payment/__init__.py @@ -2,4 +2,5 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import models -from . import controllers \ No newline at end of file +from . import controllers +from . import wizards diff --git a/addons/payment/__manifest__.py b/addons/payment/__manifest__.py index 182e3667fc7..2df0e234ab5 100644 --- a/addons/payment/__manifest__.py +++ b/addons/payment/__manifest__.py @@ -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', diff --git a/addons/payment/models/__init__.py b/addons/payment/models/__init__.py index 9758f10d9c9..93f5083aca8 100644 --- a/addons/payment/models/__init__.py +++ b/addons/payment/models/__init__.py @@ -5,3 +5,4 @@ from . import account_invoice from . import res_partner from . import account_payment from . import chart_template +from . import res_company diff --git a/addons/payment/models/res_company.py b/addons/payment/models/res_company.py new file mode 100644 index 00000000000..c2b52486563 --- /dev/null +++ b/addons/payment/models/res_company.py @@ -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 diff --git a/addons/payment/views/payment_acquirer_onboarding_templates.xml b/addons/payment/views/payment_acquirer_onboarding_templates.xml new file mode 100644 index 00000000000..f2d0274a11c --- /dev/null +++ b/addons/payment/views/payment_acquirer_onboarding_templates.xml @@ -0,0 +1,135 @@ + + + + + + + + + payment.acquirer.onboarding.wizard.form + payment.acquirer.onboarding.wizard + +
+
+ Configure our recommended payment methods or + + see all other available methods. + +
+
+
+ +
+
+
+
+
+
+ PayPal + is the easiest way to accept payments. It's easy to configure and it offers + + a transparent fee structure. + + + + + + + +
+
+
+
+ +
+
+
+
+
+
+ Stripe + is a payment provider with + + a simple fee structure and no hidden costs. + + No other gateway or merchant account is required. We recommend this solution + to manage credit card payments. + + + + + Get my Stripe keys + + + +
+
+
+
+ +
+
+
+
+
+
+ Provide customers with instructions to pay outside of your platform (wire + transfer, cash on delivery, etc.). + + + + +
+
+
+
+ You can skip this step if invoices are pre-paid. +
+
+
+
+
+ + Choose a payment method + ir.actions.act_window + payment.acquirer.onboarding.wizard + form + form + + new + +
diff --git a/addons/payment/wizards/__init__.py b/addons/payment/wizards/__init__.py new file mode 100644 index 00000000000..84b24dc0131 --- /dev/null +++ b/addons/payment/wizards/__init__.py @@ -0,0 +1 @@ +from . import payment_acquirer_onboarding_wizard diff --git a/addons/payment/wizards/payment_acquirer_onboarding_wizard.py b/addons/payment/wizards/payment_acquirer_onboarding_wizard.py new file mode 100644 index 00000000000..679facc45a9 --- /dev/null +++ b/addons/payment/wizards/payment_acquirer_onboarding_wizard.py @@ -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})