From d6d5f142c3206ddab27b0a154705a91c60f25047 Mon Sep 17 00:00:00 2001 From: "Antoine Vandevenne (anv)" Date: Mon, 15 Apr 2024 15:30:24 +0200 Subject: [PATCH] [FIX] payment(_stripe): adapt validation currency to payment method When payment details are tokenized through a validation operation, the currency to use was usually (except overrides) chosen as that of the payment provider's company. This sometimes caused compatibility issues if the selected payment method did not support the company's main currency. For example, the SEPA Direct Debit payment method only supports the EUR currency. This commit allows passing a payment method when getting the validation currency so that only supported currencies can be returned. Part-of: odoo/odoo#161883 --- addons/payment/controllers/portal.py | 5 ++- addons/payment/models/payment_provider.py | 26 ++++++++++++--- addons/payment/tests/test_payment_provider.py | 32 +++++++++++++++++++ .../payment_stripe/models/payment_provider.py | 10 ++++-- .../models/payment_transaction.py | 4 ++- .../views/payment_stripe_templates.xml | 1 + 6 files changed, 70 insertions(+), 8 deletions(-) diff --git a/addons/payment/controllers/portal.py b/addons/payment/controllers/portal.py index 88511ff037c..59e57f773f3 100644 --- a/addons/payment/controllers/portal.py +++ b/addons/payment/controllers/portal.py @@ -333,7 +333,10 @@ class PaymentPortal(portal.CustomerPortal): ) if is_validation: # Providers determine the amount and currency in validation operations amount = provider_sudo._get_validation_amount() - currency_id = provider_sudo._get_validation_currency().id + payment_method = request.env['payment.method'].browse(payment_method_id) + currency_id = provider_sudo.with_context( + validation_pm=payment_method # Will be converted to a kwarg in master. + )._get_validation_currency().id # Create the transaction tx_sudo = request.env['payment.transaction'].sudo().create({ diff --git a/addons/payment/models/payment_provider.py b/addons/payment/models/payment_provider.py index 7ae402526ab..4ae5fc1bfcf 100644 --- a/addons/payment/models/payment_provider.py +++ b/addons/payment/models/payment_provider.py @@ -611,9 +611,12 @@ class PaymentProvider(models.Model): def _get_validation_currency(self): """ Return the currency to use for validation operations. - For a provider to support tokenization, it must override this method and return the - validation currency. If the validation amount is `0`, it is not necessary to create the - override. + The validation currency must be supported by both the provider and the payment method. If + the payment method is not passed, only the provider's supported currencies are considered. + If no suitable currency is found, the provider's company's currency is returned instead. + + For a provider to support tokenization and specify a different validation currency, it must + override this method and return the appropriate validation currency. Note: `self.ensure_one()` @@ -621,7 +624,22 @@ class PaymentProvider(models.Model): :rtype: recordset of `res.currency` """ self.ensure_one() - return self.company_id.currency_id + + # Find the validation currency at the intersection of the provider's and payment method's + # supported currencies. An empty recordset means that all currencies are supported. + provider_currencies = self.available_currency_ids + pm = self.env.context.get('validation_pm') + pm_currencies = self.env['res.currency'] if not pm else pm.supported_currency_ids + validation_currency = None + if provider_currencies and pm_currencies: + validation_currency = (provider_currencies & pm_currencies)[:1] + elif provider_currencies and not pm_currencies: + validation_currency = provider_currencies[:1] + elif not provider_currencies and pm_currencies: + validation_currency = pm_currencies[:1] + if not validation_currency: # All currencies are supported, or no suitable one was found. + validation_currency = self.company_id.currency_id + return validation_currency def _get_redirect_form_view(self, is_validation=False): """ Return the view of the template used to render the redirect form. diff --git a/addons/payment/tests/test_payment_provider.py b/addons/payment/tests/test_payment_provider.py index 7ab08417bf5..85b16dfaf4f 100644 --- a/addons/payment/tests/test_payment_provider.py +++ b/addons/payment/tests/test_payment_provider.py @@ -226,3 +226,35 @@ class TestPaymentProvider(PaymentCommon): self.company.id, self.partner.id, self.amount, is_express_checkout=True ) self.assertNotIn(self.provider, compatible_providers) + + def test_validation_currency_is_supported(self): + """ Test that only currencies supported by both the provider and the payment method can be + used in validation operations. """ + self.provider.available_currency_ids = [Command.clear()] # Supports all currencies. + self.payment_method.supported_currency_ids = [Command.clear()] # Supports all currencies. + validation_currency = self.provider.with_context( + validation_pm=self.payment_method + )._get_validation_currency() + self.assertEqual(validation_currency, self.provider.company_id.currency_id) + + self.provider.available_currency_ids = [Command.set(self.currency_usd.ids)] + self.payment_method.supported_currency_ids = [Command.clear()] # Supports all currencies. + validation_currency = self.provider.with_context( + validation_pm=self.payment_method + )._get_validation_currency() + self.assertIn(validation_currency, self.provider.available_currency_ids) + + self.provider.available_currency_ids = [Command.clear()] # Supports all currencies. + self.payment_method.supported_currency_ids = [Command.set(self.currency_usd.ids)] + validation_currency = self.provider.with_context( + validation_pm=self.payment_method + )._get_validation_currency() + self.assertIn(validation_currency, self.payment_method.supported_currency_ids) + + self.provider.available_currency_ids = [Command.set(self.currency_usd.ids)] + self.payment_method.supported_currency_ids = [Command.set(self.currency_usd.ids)] + validation_currency = self.provider.with_context( + validation_pm=self.payment_method + )._get_validation_currency() + self.assertIn(validation_currency, self.provider.available_currency_ids) + self.assertIn(validation_currency, self.payment_method.supported_currency_ids) diff --git a/addons/payment_stripe/models/payment_provider.py b/addons/payment_stripe/models/payment_provider.py index 8146b9b6628..2bb59564599 100644 --- a/addons/payment_stripe/models/payment_provider.py +++ b/addons/payment_stripe/models/payment_provider.py @@ -462,7 +462,9 @@ class PaymentProvider(models.Model): return stripe_utils.get_publishable_key(self.sudo()) - def _stripe_get_inline_form_values(self, amount, currency, partner_id, is_validation, **kwargs): + def _stripe_get_inline_form_values( + self, amount, currency, partner_id, is_validation, payment_method_sudo=None, **kwargs + ): """ Return a serialized JSON of the required values to render the inline form. Note: `self.ensure_one()` @@ -471,6 +473,8 @@ class PaymentProvider(models.Model): :param res.currency currency: The currency of the transaction. :param int partner_id: The partner of the transaction, as a `res.partner` id. :param bool is_validation: Whether the operation is a validation. + :param payment.method payment_method_sudo: The sudoed payment method record to which the + inline form belongs. :return: The JSON serial of the required values to render the inline form. :rtype: str """ @@ -479,7 +483,9 @@ class PaymentProvider(models.Model): if not is_validation: currency_name = currency and currency.name.lower() else: - currency_name = self._get_validation_currency().name.lower() + currency_name = self.with_context( + validation_pm=payment_method_sudo # Will be converted to a kwarg in master. + )._get_validation_currency().name.lower() partner = self.env['res.partner'].with_context(show_address=1).browse(partner_id).exists() inline_form_values = { 'publishable_key': self._stripe_get_publishable_key(), diff --git a/addons/payment_stripe/models/payment_transaction.py b/addons/payment_stripe/models/payment_transaction.py index 62d7f235c18..4eb42009a8d 100644 --- a/addons/payment_stripe/models/payment_transaction.py +++ b/addons/payment_stripe/models/payment_transaction.py @@ -227,7 +227,9 @@ class PaymentTransaction(models.Model): f'{OPTION_PATH_PREFIX}[interval_count]': mandate_values['recurrence_duration'], }) if self.operation == 'validation': - currency_name = self.provider_id._get_validation_currency().name.lower() + currency_name = self.provider_id.with_context( + validation_pm=self.payment_method_id # Will be converted to a kwarg in master. + )._get_validation_currency().name.lower() mandate_options[f'{OPTION_PATH_PREFIX}[currency]'] = currency_name return mandate_options diff --git a/addons/payment_stripe/views/payment_stripe_templates.xml b/addons/payment_stripe/views/payment_stripe_templates.xml index e32709093b8..007fe2355ef 100644 --- a/addons/payment_stripe/views/payment_stripe_templates.xml +++ b/addons/payment_stripe/views/payment_stripe_templates.xml @@ -8,6 +8,7 @@ currency, partner_id, mode == 'validation', + payment_method_sudo=pm_sudo, sale_order_id=sale_order_id, )" />