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, )" />