diff --git a/addons/payment/controllers/portal.py b/addons/payment/controllers/portal.py index f1e0e5d642b..bf0312bd613 100644 --- a/addons/payment/controllers/portal.py +++ b/addons/payment/controllers/portal.py @@ -5,6 +5,7 @@ import werkzeug from odoo import _, http from odoo.exceptions import UserError, ValidationError +from odoo.fields import Command from odoo.http import request from odoo.addons.payment import utils as payment_utils @@ -41,7 +42,7 @@ class PaymentPortal(portal.CustomerPortal): ) def payment_pay( self, reference=None, amount=None, currency_id=None, partner_id=None, company_id=None, - acquirer_id=None, access_token=None, **kwargs + acquirer_id=None, access_token=None, invoice_id=None, **kwargs ): """ Display the payment form with optional filtering of payment options. @@ -62,14 +63,15 @@ class PaymentPortal(portal.CustomerPortal): :param str company_id: The related company, as a `res.company` id :param str acquirer_id: The desired acquirer, as a `payment.acquirer` id :param str access_token: The access token used to authenticate the partner + :param str invoice_id: The account move for which a payment id made, as a `account.move` id :param dict kwargs: Optional data. This parameter is not used here :return: The rendered checkout form :rtype: str :raise: werkzeug.exceptions.NotFound if the access token is invalid """ # Cast numeric parameters as int or float and void them if their str value is malformed - currency_id, acquirer_id, partner_id, company_id = tuple(map( - self.cast_as_int, (currency_id, acquirer_id, partner_id, company_id) + currency_id, acquirer_id, partner_id, company_id, invoice_id = tuple(map( + self.cast_as_int, (currency_id, acquirer_id, partner_id, company_id, invoice_id) )) amount = self.cast_as_float(amount) @@ -140,6 +142,7 @@ class PaymentPortal(portal.CustomerPortal): 'transaction_route': '/payment/transaction', 'landing_route': '/payment/confirmation', 'partner_is_different': partner_is_different, + 'invoice_id': invoice_id, **self._get_custom_rendering_context_values(**kwargs), } return request.render('payment.pay', rendering_context) @@ -222,7 +225,8 @@ class PaymentPortal(portal.CustomerPortal): def _create_transaction( self, payment_option_id, reference_prefix, amount, currency_id, partner_id, flow, - tokenization_requested, validation_route, landing_route, custom_create_values=None, **kwargs + tokenization_requested, validation_route, landing_route, invoice_id=None, + custom_create_values=None, **kwargs ): """ Create a draft transaction based on the payment context and return it. @@ -239,6 +243,7 @@ class PaymentPortal(portal.CustomerPortal): :param str validation_route: The route the user is redirected to in order to refund a validation transaction :param str landing_route: The route the user is redirected to after the transaction + :param int|None invoice_id: The account move for which a payment id made, as a `account.move` id :param dict custom_create_values: Additional create values overwriting the default ones :param dict kwargs: Locally unused data passed to `_is_tokenization_required` and `_compute_reference` @@ -270,6 +275,12 @@ class PaymentPortal(portal.CustomerPortal): raise UserError( _("The payment should either be direct, with redirection, or made by a token.") ) + + if invoice_id: + if custom_create_values is None: + custom_create_values = {} + custom_create_values['invoice_ids'] = [Command.set([int(invoice_id)])] + reference = request.env['payment.transaction']._compute_reference( acquirer_sudo.provider, prefix=reference_prefix, diff --git a/addons/payment/static/src/js/payment_form_mixin.js b/addons/payment/static/src/js/payment_form_mixin.js index c9bfba7d44e..49ab222914a 100644 --- a/addons/payment/static/src/js/payment_form_mixin.js +++ b/addons/payment/static/src/js/payment_form_mixin.js @@ -297,6 +297,8 @@ odoo.define('payment.payment_form_mixin', require => { 'currency_id': this.txContext.currencyId ? parseInt(this.txContext.currencyId) : null, 'partner_id': parseInt(this.txContext.partnerId), + 'invoice_id': this.txContext.invoiceId + ? parseInt(this.txContext.invoiceId) : null, 'flow': flow, 'tokenization_requested': this.txContext.tokenizationRequested, 'validation_route': this.txContext.validationRoute diff --git a/addons/payment/tests/test_flows.py b/addons/payment/tests/test_flows.py index fba00222ac7..3363cf6e05a 100644 --- a/addons/payment/tests/test_flows.py +++ b/addons/payment/tests/test_flows.py @@ -279,6 +279,65 @@ class TestFlows(PaymentCommon, PaymentHttpCommon): response = self.portal_pay(**route_values) self.assertEqual(response.status_code, 404) + def test_invoice_payment_flow(self): + """Test the payment of an invoice through the payment/pay route""" + # Create a dummy invoice + account = self.env['account.account'].search([('company_id', '=', self.env.company.id)], limit=1) + invoice = self.env['account.move'].create({ + 'move_type': 'entry', + 'date': '2019-01-01', + 'line_ids': [ + (0, 0, { + 'account_id': account.id, + 'currency_id': self.currency_euro.id, + 'debit': 100.0, + 'credit': 0.0, + 'amount_currency': 200.0, + }), + (0, 0, { + 'account_id': account.id, + 'currency_id': self.currency_euro.id, + 'debit': 0.0, + 'credit': 100.0, + 'amount_currency': -200.0, + }), + ], + }) + + # Pay for this invoice (no impact even if amounts do not match) + route_values = self._prepare_pay_values() + route_values['invoice_id'] = invoice.id + tx_context = self.get_tx_checkout_context(**route_values) + self.assertEqual(tx_context['invoice_id'], invoice.id) + + # payment/transaction + route_values = { + k: tx_context[k] + for k in [ + 'amount', + 'currency_id', + 'reference_prefix', + 'partner_id', + 'access_token', + 'landing_route', + 'invoice_id', + ] + } + route_values.update({ + 'flow': 'direct', + 'payment_option_id': self.acquirer.id, + 'tokenization_requested': False, + 'validation_route': False, + }) + with mute_logger('odoo.addons.payment.models.payment_transaction'): + processing_values = self.get_processing_values(**route_values) + tx_sudo = self._get_tx(processing_values['reference']) + # Note: strangely, the check + # self.assertEqual(tx_sudo.invoice_ids, invoice) + # doesn't work, and cache invalidation doesn't work either. + invoice.invalidate_cache(['transaction_ids']) + self.assertEqual(invoice.transaction_ids, tx_sudo) + def test_transaction_wrong_flow(self): transaction_values = self._prepare_pay_values() transaction_values.update({ diff --git a/addons/payment/views/payment_templates.xml b/addons/payment/views/payment_templates.xml index 369cedb45e0..4d4f3e90a17 100644 --- a/addons/payment/views/payment_templates.xml +++ b/addons/payment/views/payment_templates.xml @@ -17,6 +17,7 @@ - 'transaction_route' - The route used to create a transaction when the user clicks Pay - 'landing_route' - The route the user is redirected to after the transaction - 'footer_template_id' - The template id for the submit button. Optional + - 'invoice_id' - The id of the account move being paid. Optional -->