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 00000000000..7b7e1a524d5 Binary files /dev/null and b/addons/account/static/src/img/account_invoice_onboarding_bg.jpg differ 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 @@ + + + + + + Invoice Layout + Create and send professional looking invoices. + + Looks great! + + + + + + + + Sample Invoice + Quickly preview what your client sees by sending a sample invoice + to yourself. + + I'm ready + Sample invoice sent! + + + + + + + + + o_onboarding_blue + /account/static/src/img/account_invoice_onboarding_bg.jpg + action_close_account_invoice_onboarding + res.company + action_toggle_fold_account_invoice_onboarding + res.company + + + + + + + + + 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 Method + Choose the payment methods to collect payments on your invoices. + Payment method set! + + + + + + + + + + + + + + + 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.). + + + + + + + + + + + + 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})