[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
This commit is contained in:
Antoine Vandevenne (anv)
2024-04-17 22:47:13 +00:00
parent 95f9902d11
commit d6d5f142c3
6 changed files with 70 additions and 8 deletions
+4 -1
View File
@@ -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({
+22 -4
View File
@@ -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.
@@ -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)
@@ -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(),
@@ -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
@@ -8,6 +8,7 @@
currency,
partner_id,
mode == 'validation',
payment_method_sudo=pm_sudo,
sale_order_id=sale_order_id,
)"
/>