[IMP] payment(_*): show available currencies for payment providers

Before this commit, the lists of supported currencies by payment
provider were hard-coded in the Python scripts, which made them
unavailable to the users.

With this commit, the implemented initial lists of supported currencies
are displayed on the form view and are editable, because Odoo lists may
not be up-to-date. Empty lists do not trigger any filtering on the
payment providers to access payment methods.

For Authorize.net and Asiapay payment providers, the specific
`(authorize,asiapay)_currency_id` are removed and the generic payment
provider field `available_currency_ids` is restricted to a single-item
list when one of those providers is enabled.

task-2926016

closes odoo/odoo#101018

Related: odoo/enterprise#34158
Related: odoo/documentation#2788
Related: odoo/upgrade#4069
Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
This commit is contained in:
Valentin Vallaeys (vava)
2023-01-05 16:51:56 +01:00
parent 8726fcf822
commit 90af85c2e4
35 changed files with 385 additions and 197 deletions
@@ -3,7 +3,7 @@
from odoo import api, fields, models from odoo import api, fields, models
class Paymentprovider(models.Model): class PaymentProvider(models.Model):
_inherit = 'payment.provider' _inherit = 'payment.provider'
journal_id = fields.Many2one( journal_id = fields.Many2one(
+57 -2
View File
@@ -102,6 +102,19 @@ class PaymentProvider(models.Model):
column1='payment_id', column1='payment_id',
column2='country_id', column2='country_id',
) )
available_currency_ids = fields.Many2many(
string="Currencies",
help="The currencies available with this payment provider. Leave empty not to restrict "
"any.",
comodel_name='res.currency',
relation='payment_currency_rel',
column1="payment_provider_id",
column2="currency_id",
compute='_compute_available_currency_ids',
store=True,
readonly=False,
context={'active_test': False},
)
maximum_amount = fields.Monetary( maximum_amount = fields.Monetary(
string="Maximum Amount", string="Maximum Amount",
help="The maximum payment amount that this payment provider is available for. Leave blank " help="The maximum payment amount that this payment provider is available for. Leave blank "
@@ -190,6 +203,22 @@ class PaymentProvider(models.Model):
#=== COMPUTE METHODS ===# #=== COMPUTE METHODS ===#
@api.depends('code')
def _compute_available_currency_ids(self):
""" Compute the available currencies based on their support by the providers.
If the provider does not filter out any currency, the field is left empty for UX reasons.
:return: None
"""
all_currencies = self.env['res.currency'].with_context(active_test=False).search([])
for provider in self:
supported_currencies = provider._get_supported_currencies()
if supported_currencies < all_currencies: # Some currencies have been filtered out.
provider.available_currency_ids = supported_currencies
else:
provider.available_currency_ids = None
@api.depends('state', 'module_state') @api.depends('state', 'module_state')
def _compute_color(self): def _compute_color(self):
""" Update the color of the kanban card based on the state of the provider. """ Update the color of the kanban card based on the state of the provider.
@@ -242,6 +271,7 @@ class PaymentProvider(models.Model):
'show_cancel_msg': True, 'show_cancel_msg': True,
}) })
@api.depends('code')
def _compute_feature_support_fields(self): def _compute_feature_support_fields(self):
""" Compute the feature support fields based on the provider. """ Compute the feature support fields based on the provider.
@@ -415,8 +445,8 @@ class PaymentProvider(models.Model):
""" Select and return the providers matching the criteria. """ Select and return the providers matching the criteria.
The criteria are that providers must not be disabled, be in the company that is provided, The criteria are that providers must not be disabled, be in the company that is provided,
and support the country of the partner if it exists. The criteria can be further refined support the country of the partner if it exists, and be compatible with the currency if
by providing the keyword arguments. provided. The criteria can be further refined by providing the keyword arguments.
:param int company_id: The company to which providers must belong, as a `res.company` id. :param int company_id: The company to which providers must belong, as a `res.company` id.
:param int partner_id: The partner making the payment, as a `res.partner` id. :param int partner_id: The partner making the payment, as a `res.partner` id.
@@ -462,6 +492,16 @@ class PaymentProvider(models.Model):
] ]
]) ])
# Handle the available currencies (only if supported currencies list is not empty).
if currency:
domain = expression.AND([
domain, [
'|',
('available_currency_ids', '=', False),
('available_currency_ids', 'in', [currency.id]),
]
])
# Handle tokenization support requirements. # Handle tokenization support requirements.
if force_tokenization or self._is_tokenization_required(**kwargs): if force_tokenization or self._is_tokenization_required(**kwargs):
domain = expression.AND([domain, [('allow_tokenization', '=', True)]]) domain = expression.AND([domain, [('allow_tokenization', '=', True)]])
@@ -473,6 +513,21 @@ class PaymentProvider(models.Model):
compatible_providers = self.env['payment.provider'].search(domain) compatible_providers = self.env['payment.provider'].search(domain)
return compatible_providers return compatible_providers
def _get_supported_currencies(self):
""" Return the supported currencies for the payment provider.
By default, all currencies are considered supported, including the inactive ones. For a
provider to filter out specific currencies, it must override this method and return the
subset of supported currencies.
Note: `self.ensure_one()`
:return: The supported currencies.
:rtype: res.currency
"""
self.ensure_one()
return self.env['res.currency'].with_context(active_test=False).search([])
def _is_tokenization_required(self, **kwargs): def _is_tokenization_required(self, **kwargs):
""" Return whether tokenizing the transaction is required given its context. """ Return whether tokenizing the transaction is required given its context.
+1
View File
@@ -79,6 +79,7 @@ class PaymentCommon(BaseCommon):
'is_published': True, 'is_published': True,
'allow_tokenization': True, 'allow_tokenization': True,
'redirect_form_view_id': redirect_form.id, 'redirect_form_view_id': redirect_form.id,
'available_currency_ids': [Command.set((cls.currency_euro + cls.currency_usd).ids)],
}) })
cls.provider = cls.dummy_provider cls.provider = cls.dummy_provider
@@ -2,6 +2,7 @@
from unittest.mock import patch from unittest.mock import patch
from odoo import Command
from odoo.tests import tagged from odoo.tests import tagged
from odoo.addons.payment.tests.common import PaymentCommon from odoo.addons.payment.tests.common import PaymentCommon
@@ -70,6 +71,30 @@ class TestPaymentProvider(PaymentCommon):
) )
self.assertNotIn(self.provider, compatible_providers) self.assertNotIn(self.provider, compatible_providers)
def test_provider_compatible_with_available_currencies(self):
""" Test that the provider is compatible with its available currencies. """
compatible_providers = self.provider._get_compatible_providers(
self.company.id, self.partner.id, self.amount, currency_id=self.currency_euro.id
)
self.assertIn(self.provider, compatible_providers)
def test_provider_not_compatible_with_unavailable_currencies(self):
""" Test that the provider is not compatible with a currency that is not available. """
# Make sure the list of available currencies is not empty.
self.provider.available_currency_ids = [Command.unlink(self.currency_usd.id)]
compatible_providers = self.provider._get_compatible_providers(
self.company.id, self.partner.id, self.amount, currency_id=self.currency_usd.id
)
self.assertNotIn(self.provider, compatible_providers)
def test_provider_compatible_when_no_available_currency_set(self):
""" Test that the provider is always compatible when no available currency is set. """
self.provider.available_currency_ids = [Command.clear()]
compatible_providers = self.provider._get_compatible_providers(
self.company.id, self.partner.id, self.amount, currency_id=self.currency_euro.id
)
self.assertIn(self.provider, compatible_providers)
def test_fees_null_when_deactivated(self): def test_fees_null_when_deactivated(self):
""" Test that extra fees of a provider are null if these fees are deactivated. """ """ Test that extra fees of a provider are null if these fees are deactivated. """
self.provider.write({ self.provider.write({
@@ -84,10 +84,19 @@
</group> </group>
<group string="Availability" name="availability"> <group string="Availability" name="availability">
<field name="maximum_amount"/> <field name="maximum_amount"/>
<label for="available_currency_ids"/>
<!-- Use `o_row` to allow placing a button next to the field in overrides. -->
<div name="available_currencies" class="o_row">
<field name="available_currency_ids"
widget="many2many_tags"
placeholder="Select currencies. Leave empty not to restrict any."
context="{'active_test': False}"
options="{'no_create': True}"/>
</div>
<field name="available_country_ids" <field name="available_country_ids"
widget="many2many_tags" widget="many2many_tags"
placeholder="Select countries. Leave empty to make available everywhere." placeholder="Select countries. Leave empty to make available everywhere."
options="{'no_open': True, 'no_create': True}"/> options="{'no_create': True}"/>
</group> </group>
<group string="Payment Followup" name="payment_followup" invisible="1"/> <group string="Payment Followup" name="payment_followup" invisible="1"/>
</group> </group>
@@ -41,7 +41,8 @@ class PaymentProvider(models.Model):
@api.model @api.model
def _get_compatible_providers(self, *args, currency_id=None, **kwargs): def _get_compatible_providers(self, *args, currency_id=None, **kwargs):
""" Override of payment to unlist Alipay providers for unsupported currencies. """ """ Override of payment to unlist Alipay providers when the currency is not CNY in case of
express checkout. """
providers = super()._get_compatible_providers(*args, currency_id=currency_id, **kwargs) providers = super()._get_compatible_providers(*args, currency_id=currency_id, **kwargs)
currency = self.env['res.currency'].browse(currency_id).exists() currency = self.env['res.currency'].browse(currency_id).exists()
@@ -1,6 +1,5 @@
-- disable asiapay payment provider -- disable asiapay payment provider
UPDATE payment_provider UPDATE payment_provider
SET asiapay_merchant_id = NULL, SET asiapay_merchant_id = NULL,
asiapay_currency_id = NULL,
asiapay_secure_hash_secret = NULL, asiapay_secure_hash_secret = NULL,
asiapay_secure_hash_function = NULL; asiapay_secure_hash_function = NULL;
@@ -2,7 +2,8 @@
from hashlib import new as hashnew from hashlib import new as hashnew
from odoo import api, fields, models from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.addons.payment_asiapay import const from odoo.addons.payment_asiapay import const
@@ -10,10 +11,6 @@ from odoo.addons.payment_asiapay import const
class PaymentProvider(models.Model): class PaymentProvider(models.Model):
_inherit = 'payment.provider' _inherit = 'payment.provider'
def _domain_asiapay_currency_id(self):
currency_xmlids = [f'base.{key}' for key in const.CURRENCY_MAPPING]
return [('id', 'in', [self.env.ref(xmlid).id for xmlid in currency_xmlids])]
code = fields.Selection( code = fields.Selection(
selection_add=[('asiapay', "AsiaPay")], ondelete={'asiapay': 'set default'} selection_add=[('asiapay', "AsiaPay")], ondelete={'asiapay': 'set default'}
) )
@@ -22,13 +19,6 @@ class PaymentProvider(models.Model):
help="The Merchant ID solely used to identify your AsiaPay account.", help="The Merchant ID solely used to identify your AsiaPay account.",
required_if_provider='asiapay', required_if_provider='asiapay',
) )
asiapay_currency_id = fields.Many2one(
string="AsiaPay Currency",
help="The currency associated to your AsiaPay account.",
comodel_name='res.currency',
domain=_domain_asiapay_currency_id,
required_if_provider='asiapay',
)
asiapay_secure_hash_secret = fields.Char( asiapay_secure_hash_secret = fields.Char(
string="AsiaPay Secure Hash Secret", string="AsiaPay Secure Hash Secret",
required_if_provider='asiapay', required_if_provider='asiapay',
@@ -42,21 +32,16 @@ class PaymentProvider(models.Model):
required_if_provider='asiapay', required_if_provider='asiapay',
) )
# ==== CONSTRAINT METHODS ===#
@api.constrains('available_currency_ids', 'state')
def _limit_available_currency_ids(self):
for provider in self.filtered(lambda p: p.code == 'asiapay'):
if len(provider.available_currency_ids) > 1 and provider.state != 'disabled':
raise ValidationError(_("Only one currency can be selected by AsiaPay account."))
# === BUSINESS METHODS ===# # === BUSINESS METHODS ===#
@api.model
def _get_compatible_providers(self, *args, currency_id=None, **kwargs):
""" Override of `payment` to filter out AsiaPay providers for unsupported currencies. """
providers = super()._get_compatible_providers(*args, currency_id=currency_id, **kwargs)
currency = self.env['res.currency'].browse(currency_id).exists()
if currency:
providers = providers.filtered(
lambda p: p.code != 'asiapay' or currency == p.asiapay_currency_id
)
return providers
def _asiapay_get_api_url(self): def _asiapay_get_api_url(self):
""" Return the URL of the API corresponding to the provider's state. """ Return the URL of the API corresponding to the provider's state.
@@ -88,7 +88,7 @@ class PaymentTransaction(models.Model):
'merchant_id': self.provider_id.asiapay_merchant_id, 'merchant_id': self.provider_id.asiapay_merchant_id,
'amount': self.amount, 'amount': self.amount,
'reference': self.reference, 'reference': self.reference,
'currency_code': const.CURRENCY_MAPPING[self.provider_id.asiapay_currency_id.name], 'currency_code': const.CURRENCY_MAPPING[self.provider_id.available_currency_ids[0].name],
'mps_mode': 'SCP', 'mps_mode': 'SCP',
'return_url': urls.url_join(base_url, AsiaPayController._return_url), 'return_url': urls.url_join(base_url, AsiaPayController._return_url),
'payment_type': 'N', 'payment_type': 'N',
+3 -1
View File
@@ -1,5 +1,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.addons.payment.tests.common import PaymentCommon from odoo.addons.payment.tests.common import PaymentCommon
@@ -11,9 +13,9 @@ class AsiaPayCommon(PaymentCommon):
cls.asiapay = cls._prepare_provider('asiapay', update_values={ cls.asiapay = cls._prepare_provider('asiapay', update_values={
'asiapay_merchant_id': '123456789', 'asiapay_merchant_id': '123456789',
'asiapay_currency_id': cls.currency_euro,
'asiapay_secure_hash_secret': 'coincoin_motherducker', 'asiapay_secure_hash_secret': 'coincoin_motherducker',
'asiapay_secure_hash_function': 'sha1', 'asiapay_secure_hash_function': 'sha1',
'available_currency_ids': [Command.set(cls.currency_euro.ids)],
}) })
cls.provider = cls.asiapay cls.provider = cls.asiapay
@@ -11,9 +11,6 @@
<field name="asiapay_merchant_id" <field name="asiapay_merchant_id"
string="Merchant ID" string="Merchant ID"
attrs="{'required': [('code', '=', 'asiapay'), ('state', '!=', 'disabled')]}"/> attrs="{'required': [('code', '=', 'asiapay'), ('state', '!=', 'disabled')]}"/>
<field name="asiapay_currency_id"
string="Currency"
attrs="{'required': [('code', '=', 'asiapay'), ('state', '!=', 'disabled')]}"/>
<field name="asiapay_secure_hash_secret" <field name="asiapay_secure_hash_secret"
string="Secure Hash Secret" string="Secure Hash Secret"
attrs="{'required': [('code', '=', 'asiapay'), ('state', '!=', 'disabled')]}" attrs="{'required': [('code', '=', 'asiapay'), ('state', '!=', 'disabled')]}"
@@ -24,6 +21,9 @@
groups="base.group_no_one"/> groups="base.group_no_one"/>
</group> </group>
</group> </group>
<field name="available_currency_ids" position="attributes">
<attribute name="attrs">{'required': [('code', '=', 'asiapay'), ('state', '!=', 'disabled')]}</attribute>
</field>
</field> </field>
</record> </record>
@@ -27,10 +27,6 @@ class PaymentProvider(models.Model):
authorize_client_key = fields.Char( authorize_client_key = fields.Char(
string="API Client Key", string="API Client Key",
help="The public client key. To generate directly from Odoo or from Authorize.Net backend.") help="The public client key. To generate directly from Odoo or from Authorize.Net backend.")
# Authorize.Net supports only one currency: "One gateway account is required for each currency"
# See https://community.developer.authorize.net/t5/The-Authorize-Net-Developer-Blog/Authorize-Net-UK-Europe-Update/ba-p/35957
authorize_currency_id = fields.Many2one(
string="Authorize Currency", comodel_name='res.currency')
authorize_payment_method_type = fields.Selection( authorize_payment_method_type = fields.Selection(
string="Allow Payments From", string="Allow Payments From",
help="Determines with what payment method the customer can pay.", help="Determines with what payment method the customer can pay.",
@@ -51,6 +47,16 @@ class PaymentProvider(models.Model):
"method type on the duplicated provider." "method type on the duplicated provider."
)) ))
# Authorize.Net supports only one currency: "One gateway account is required for each currency"
# See https://community.developer.authorize.net/t5/The-Authorize-Net-Developer-Blog/Authorize-Net-UK-Europe-Update/ba-p/35957
@api.constrains('available_currency_ids', 'state')
def _limit_available_currency_ids(self):
for provider in self.filtered(lambda p: p.code == 'authorize'):
if len(provider.available_currency_ids) > 1 and provider.state != 'disabled':
raise ValidationError(
_("Only one currency can be selected by Authorize.Net account.")
)
#=== COMPUTE METHODS ===# #=== COMPUTE METHODS ===#
def _compute_feature_support_fields(self): def _compute_feature_support_fields(self):
@@ -104,24 +110,11 @@ class PaymentProvider(models.Model):
raise UserError(_("Could not fetch merchant details:\n%s", res_content['err_msg'])) raise UserError(_("Could not fetch merchant details:\n%s", res_content['err_msg']))
currency = self.env['res.currency'].search([('name', 'in', res_content.get('currencies'))]) currency = self.env['res.currency'].search([('name', 'in', res_content.get('currencies'))])
self.authorize_currency_id = currency self.available_currency_ids = [Command.set(currency.ids)]
self.authorize_client_key = res_content.get('publicClientKey') self.authorize_client_key = res_content.get('publicClientKey')
# === BUSINESS METHODS ===# # === BUSINESS METHODS ===#
@api.model
def _get_compatible_providers(self, *args, currency_id=None, **kwargs):
""" Override of payment to unlist Authorize providers for unsupported currencies. """
providers = super()._get_compatible_providers(*args, currency_id=currency_id, **kwargs)
currency = self.env['res.currency'].browse(currency_id).exists()
if currency:
providers = providers.filtered(
lambda p: p.code != 'authorize' or currency == p.authorize_currency_id
)
return providers
def _get_validation_amount(self): def _get_validation_amount(self):
""" Override of payment to return the amount for Authorize.Net validation operations. """ Override of payment to return the amount for Authorize.Net validation operations.
@@ -144,4 +137,4 @@ class PaymentProvider(models.Model):
if self.code != 'authorize': if self.code != 'authorize':
return res return res
return self.authorize_currency_id return self.available_currency_ids[0]
+3 -1
View File
@@ -1,5 +1,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.fields import Command
from odoo.addons.payment.tests.common import PaymentCommon from odoo.addons.payment.tests.common import PaymentCommon
@@ -13,7 +15,7 @@ class AuthorizeCommon(PaymentCommon):
'authorize_login': 'dummy', 'authorize_login': 'dummy',
'authorize_transaction_key': 'dummy', 'authorize_transaction_key': 'dummy',
'authorize_signature_key': '00000000', 'authorize_signature_key': '00000000',
'authorize_currency_id': cls.currency_usd.id, 'available_currency_ids': [Command.set(cls.currency_usd.ids)]
}) })
cls.provider = cls.authorize cls.provider = cls.authorize
@@ -14,7 +14,7 @@ from .common import AuthorizeCommon
class AuthorizeTest(AuthorizeCommon): class AuthorizeTest(AuthorizeCommon):
def test_compatible_providers(self): def test_compatible_providers(self):
# Note: in the test common, 'USD' is specified as authorize_currency_id # Note: in the test common, 'USD' is specified as the currency linked to the user account.
unsupported_currency = self._prepare_currency('CHF') unsupported_currency = self._prepare_currency('CHF')
providers = self.env['payment.provider']._get_compatible_providers( providers = self.env['payment.provider']._get_compatible_providers(
self.company.id, self.partner.id, self.amount, currency_id=unsupported_currency.id self.company.id, self.partner.id, self.amount, currency_id=unsupported_currency.id
@@ -43,6 +43,6 @@ class AuthorizeTest(AuthorizeCommon):
)) ))
def test_validation(self): def test_validation(self):
self.assertEqual(self.authorize.authorize_currency_id, self.currency_usd) self.assertEqual(self.authorize.available_currency_ids[0], self.currency_usd)
self.assertEqual(self.authorize._get_validation_amount(), 0.01) self.assertEqual(self.authorize._get_validation_amount(), 0.01)
self.assertEqual(self.authorize._get_validation_currency(), self.currency_usd) self.assertEqual(self.authorize._get_validation_currency(), self.currency_usd)
@@ -27,15 +27,14 @@
<field name="authorize_payment_method_type" <field name="authorize_payment_method_type"
attrs="{'invisible': [('code', '!=', 'authorize')], 'required':[('code', '=', 'authorize'), ('state', '!=', 'disabled')]}"/> attrs="{'invisible': [('code', '!=', 'authorize')], 'required':[('code', '=', 'authorize'), ('state', '!=', 'disabled')]}"/>
</field> </field>
<field name="available_country_ids" position="after"> <div name="available_currencies" position="inside">
<label for="authorize_currency_id" string="Currency" attrs="{'invisible': [('code', '!=', 'authorize')]}"/> <button string="Set Account Currency"
<div class="o_row" col="2" attrs="{'invisible': [('code', '!=', 'authorize')]}"> type="object"
<field name="authorize_currency_id"/> name="action_update_merchant_details"
<button class="oe_link" icon="fa-refresh" type="object" attrs="{'invisible': [('code', '!=', 'authorize')]}"
name="action_update_merchant_details" icon="fa-refresh"
string="Set Account Currency"/> class="oe_link"/>
</div> </div>
</field>
</field> </field>
</record> </record>
+14
View File
@@ -10,3 +10,17 @@ STATUS_CODES_MAPPING = {
'refused': (690,), 'refused': (690,),
'error': (490, 491, 492,), 'error': (490, 491, 492,),
} }
# The currencies supported by Buckaroo, in ISO 4217 format.
# See https://support.buckaroo.eu/frequently-asked-questions
# Last seen online: 7 November 2022.
SUPPORTED_CURRENCIES = [
'EUR',
'GBP',
'PLN',
'DKK',
'NOK',
'SEK',
'CHF',
'USD',
]
@@ -6,6 +6,8 @@ from werkzeug import urls
from odoo import fields, models from odoo import fields, models
from odoo.addons.payment_buckaroo.const import SUPPORTED_CURRENCIES
class PaymentProvider(models.Model): class PaymentProvider(models.Model):
_inherit = 'payment.provider' _inherit = 'payment.provider'
@@ -18,6 +20,15 @@ class PaymentProvider(models.Model):
buckaroo_secret_key = fields.Char( buckaroo_secret_key = fields.Char(
string="Buckaroo Secret Key", required_if_provider='buckaroo', groups='base.group_system') string="Buckaroo Secret Key", required_if_provider='buckaroo', groups='base.group_system')
def _get_supported_currencies(self):
""" Override of `payment` to return the supported currencies. """
supported_currencies = super()._get_supported_currencies()
if self.code == 'buckaroo':
supported_currencies = supported_currencies.filtered(
lambda c: c.name in SUPPORTED_CURRENCIES
)
return supported_currencies
def _buckaroo_get_api_url(self): def _buckaroo_get_api_url(self):
""" Return the API URL according to the state. """ Return the API URL according to the state.
+1 -1
View File
@@ -7,6 +7,7 @@
SUPPORTED_CURRENCIES = [ SUPPORTED_CURRENCIES = [
'GBP', 'GBP',
'CAD', 'CAD',
'XAF',
'CLP', 'CLP',
'COP', 'COP',
'EGP', 'EGP',
@@ -24,7 +25,6 @@ SUPPORTED_CURRENCIES = [
'TZS', 'TZS',
'UGX', 'UGX',
'USD', 'USD',
'XAF',
'XOF', 'XOF',
'ZMW', 'ZMW',
] ]
@@ -49,19 +49,24 @@ class PaymentProvider(models.Model):
# === BUSINESS METHODS ===# # === BUSINESS METHODS ===#
@api.model @api.model
def _get_compatible_providers(self, *args, currency_id=None, is_validation=False, **kwargs): def _get_compatible_providers(self, *args, is_validation=False, **kwargs):
""" Override of payment to filter out Flutterwave providers for unsupported currencies or """ Override of `payment` to filter out Flutterwave providers for validation operations. """
for validation operations. """ providers = super()._get_compatible_providers(*args, is_validation=is_validation, **kwargs)
providers = super()._get_compatible_providers(
*args, currency_id=currency_id, is_validation=is_validation, **kwargs
)
currency = self.env['res.currency'].browse(currency_id).exists() if is_validation:
if (currency and currency.name not in SUPPORTED_CURRENCIES) or is_validation:
providers = providers.filtered(lambda p: p.code != 'flutterwave') providers = providers.filtered(lambda p: p.code != 'flutterwave')
return providers return providers
def _get_supported_currencies(self):
""" Override of `payment` to return the supported currencies. """
supported_currencies = super()._get_supported_currencies()
if self.code == 'flutterwave':
supported_currencies = supported_currencies.filtered(
lambda c: c.name in SUPPORTED_CURRENCIES
)
return supported_currencies
def _flutterwave_make_request(self, endpoint, payload=None, method='POST'): def _flutterwave_make_request(self, endpoint, payload=None, method='POST'):
""" Make a request to Flutterwave API at the specified endpoint. """ Make a request to Flutterwave API at the specified endpoint.
@@ -6,7 +6,7 @@ import pprint
import requests import requests
from werkzeug import urls from werkzeug import urls
from odoo import _, api, fields, models from odoo import _, fields, models
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from odoo.addons.payment_mercado_pago.const import SUPPORTED_CURRENCIES from odoo.addons.payment_mercado_pago.const import SUPPORTED_CURRENCIES
@@ -15,7 +15,7 @@ from odoo.addons.payment_mercado_pago.const import SUPPORTED_CURRENCIES
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class Paymentprovider(models.Model): class PaymentProvider(models.Model):
_inherit = 'payment.provider' _inherit = 'payment.provider'
code = fields.Selection( code = fields.Selection(
@@ -29,16 +29,14 @@ class Paymentprovider(models.Model):
# === BUSINESS METHODS === # # === BUSINESS METHODS === #
@api.model def _get_supported_currencies(self):
def _get_compatible_providers(self, *args, currency_id=None, **kwargs): """ Override of `payment` to return the supported currencies. """
""" Override of `payment` to unlist Mercado Pago providers for unsupported currencies. """ supported_currencies = super()._get_supported_currencies()
providers = super()._get_compatible_providers(*args, currency_id=currency_id, **kwargs) if self.code == 'mercado_pago':
supported_currencies = supported_currencies.filtered(
currency = self.env['res.currency'].browse(currency_id).exists() lambda c: c.name in SUPPORTED_CURRENCIES
if currency and currency.name not in SUPPORTED_CURRENCIES: )
providers = providers.filtered(lambda p: p.code != 'mercado_pago') return supported_currencies
return providers
def _mercado_pago_make_request(self, endpoint, payload=None, method='POST'): def _mercado_pago_make_request(self, endpoint, payload=None, method='POST'):
""" Make a request to Mercado Pago API at the specified endpoint. """ Make a request to Mercado Pago API at the specified endpoint.
@@ -62,7 +62,7 @@ class TestPaymentTransaction(MercadoPagoCommon, PaymentHttpCommon):
successful payment. """ successful payment. """
tx = self._create_transaction(flow='redirect') tx = self._create_transaction(flow='redirect')
with patch( with patch(
'odoo.addons.payment_mercado_pago.models.payment_provider.Paymentprovider' 'odoo.addons.payment_mercado_pago.models.payment_provider.PaymentProvider'
'._mercado_pago_make_request', return_value=self.verification_data '._mercado_pago_make_request', return_value=self.verification_data
): ):
tx._process_notification_data(self.redirect_notification_data) tx._process_notification_data(self.redirect_notification_data)
+32 -6
View File
@@ -12,11 +12,37 @@ SUPPORTED_LOCALES = [
] ]
# Currency codes in ISO 4217 format supported by mollie. # Currency codes in ISO 4217 format supported by mollie.
# See https://docs.mollie.com/payments/multicurrency # Note: support varies per payment method.
# See https://docs.mollie.com/payments/multicurrency. Last seen online: 22 September 2022.
SUPPORTED_CURRENCIES = [ SUPPORTED_CURRENCIES = [
'AED', 'AUD', 'BGN', 'BRL', 'CAD', 'CHF', 'AED',
'CZK', 'DKK', 'EUR', 'GBP', 'HKD', 'HRK', 'AUD',
'HUF', 'ILS', 'ISK', 'JPY', 'MXN', 'MYR', 'BGN',
'NOK', 'NZD', 'PHP', 'PLN', 'RON', 'RUB', 'BRL',
'SEK', 'SGD', 'THB', 'TWD', 'USD', 'ZAR' 'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HRK',
'HUF',
'ILS',
'ISK',
'JPY',
'MXN',
'MYR',
'NOK',
'NZD',
'PHP',
'PLN',
'RON',
'RUB',
'SEK',
'SGD',
'THB',
'TWD',
'USD',
'ZAR'
] ]
@@ -5,7 +5,7 @@ import logging
import requests import requests
from werkzeug import urls from werkzeug import urls
from odoo import _, api, fields, models, service from odoo import _, fields, models, service
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from odoo.addons.payment_mollie.const import SUPPORTED_CURRENCIES from odoo.addons.payment_mollie.const import SUPPORTED_CURRENCIES
@@ -27,16 +27,14 @@ class PaymentProvider(models.Model):
#=== BUSINESS METHODS ===# #=== BUSINESS METHODS ===#
@api.model def _get_supported_currencies(self):
def _get_compatible_providers(self, *args, currency_id=None, **kwargs): """ Override of `payment` to return the supported currencies. """
""" Override of payment to unlist Mollie providers for unsupported currencies. """ supported_currencies = super()._get_supported_currencies()
providers = super()._get_compatible_providers(*args, currency_id=currency_id, **kwargs) if self.code == 'mollie':
supported_currencies = supported_currencies.filtered(
currency = self.env['res.currency'].browse(currency_id).exists() lambda c: c.name in SUPPORTED_CURRENCIES
if currency and currency.name not in SUPPORTED_CURRENCIES: )
providers = providers.filtered(lambda p: p.code != 'mollie') return supported_currencies
return providers
def _mollie_make_request(self, endpoint, data=None, method='POST'): def _mollie_make_request(self, endpoint, data=None, method='POST'):
""" Make a request at mollie endpoint. """ Make a request at mollie endpoint.
+2
View File
@@ -1,6 +1,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
# ISO 4217 codes of currencies supported by PayPal # ISO 4217 codes of currencies supported by PayPal
# See https://developer.paypal.com/docs/reports/reference/paypal-supported-currencies/.
# Last seen on: 22 September 2022.
SUPPORTED_CURRENCIES = ( SUPPORTED_CURRENCIES = (
'AUD', 'AUD',
'BRL', 'BRL',
@@ -2,7 +2,7 @@
import logging import logging
from odoo import _, api, fields, models from odoo import _, fields, models
from odoo.addons.payment_paypal.const import SUPPORTED_CURRENCIES from odoo.addons.payment_paypal.const import SUPPORTED_CURRENCIES
@@ -35,16 +35,14 @@ class PaymentProvider(models.Model):
#=== BUSINESS METHODS ===# #=== BUSINESS METHODS ===#
@api.model def _get_supported_currencies(self):
def _get_compatible_providers(self, *args, currency_id=None, **kwargs): """ Override of `payment` to return the supported currencies. """
""" Override of payment to unlist PayPal providers when the currency is not supported. """ supported_currencies = super()._get_supported_currencies()
providers = super()._get_compatible_providers(*args, currency_id=currency_id, **kwargs) if self.code == 'paypal':
supported_currencies = supported_currencies.filtered(
currency = self.env['res.currency'].browse(currency_id).exists() lambda c: c.name in SUPPORTED_CURRENCIES
if currency and currency.name not in SUPPORTED_CURRENCIES: )
providers = providers.filtered(lambda p: p.code != 'paypal') return supported_currencies
return providers
def _paypal_get_api_url(self): def _paypal_get_api_url(self):
""" Return the API URL according to the provider state. """ Return the API URL according to the provider state.
+14
View File
@@ -0,0 +1,14 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
# Supported currencies of PayuLatam, in ISO 4217 currency codes.
# https://developers.payulatam.com/latam/en/docs/getting-started/response-codes-and-variables.html#accepted-currencies.
# Last seen online: 22 September 2022.
SUPPORTED_CURRENCIES = [
'ARS',
'BRL',
'CLP',
'COP',
'MXN',
'PEN',
'USD'
]
@@ -2,11 +2,10 @@
from hashlib import md5 from hashlib import md5
from odoo import api, fields, models from odoo import fields, models
from odoo.tools.float_utils import float_repr from odoo.tools.float_utils import float_repr
SUPPORTED_CURRENCIES = ('ARS', 'BRL', 'CLP', 'COP', 'MXN', 'PEN', 'USD') from odoo.addons.payment_payulatam.const import SUPPORTED_CURRENCIES
class PaymentProvider(models.Model): class PaymentProvider(models.Model):
_inherit = 'payment.provider' _inherit = 'payment.provider'
@@ -25,16 +24,14 @@ class PaymentProvider(models.Model):
string="PayU Latam API Key", required_if_provider='payulatam', string="PayU Latam API Key", required_if_provider='payulatam',
groups='base.group_system') groups='base.group_system')
@api.model def _get_supported_currencies(self):
def _get_compatible_providers(self, *args, currency_id=None, **kwargs): """ Override of `payment` to return the supported currencies. """
""" Override of payment to unlist PayU Latam providers for unsupported currencies. """ supported_currencies = super()._get_supported_currencies()
providers = super()._get_compatible_providers(*args, currency_id=currency_id, **kwargs) if self.code == 'payulatam':
supported_currencies = supported_currencies.filtered(
currency = self.env['res.currency'].browse(currency_id).exists() lambda c: c.name in SUPPORTED_CURRENCIES
if currency and currency.name not in SUPPORTED_CURRENCIES: )
providers = providers.filtered(lambda p: p.code != 'payulatam') return supported_currencies
return providers
def _payulatam_generate_sign(self, values, incoming=True): def _payulatam_generate_sign(self, values, incoming=True):
""" Generate the signature for incoming or outgoing communications. """ Generate the signature for incoming or outgoing communications.
@@ -12,22 +12,12 @@ from odoo.tools import mute_logger
from odoo.addons.payment.tests.http_common import PaymentHttpCommon from odoo.addons.payment.tests.http_common import PaymentHttpCommon
from odoo.addons.payment_payulatam.controllers.main import PayuLatamController from odoo.addons.payment_payulatam.controllers.main import PayuLatamController
from odoo.addons.payment_payulatam.models.payment_provider import SUPPORTED_CURRENCIES
from odoo.addons.payment_payulatam.tests.common import PayULatamCommon from odoo.addons.payment_payulatam.tests.common import PayULatamCommon
@tagged('post_install', '-at_install') @tagged('post_install', '-at_install')
class PayULatamTest(PayULatamCommon, PaymentHttpCommon): class PayULatamTest(PayULatamCommon, PaymentHttpCommon):
def test_compatibility_with_supported_currencies(self):
""" Test that the PayULatam provider is compatible with all supported currencies. """
for supported_currency_code in SUPPORTED_CURRENCIES:
supported_currency = self._prepare_currency(supported_currency_code)
compatible_providers = self.env['payment.provider']._get_compatible_providers(
self.company.id, self.partner.id, self.amount, currency_id=supported_currency.id
)
self.assertIn(self.payulatam, compatible_providers)
def test_incompatibility_with_unsupported_currency(self): def test_incompatibility_with_unsupported_currency(self):
""" Test that the PayULatam provider is not compatible with an unsupported currency. """ """ Test that the PayULatam provider is not compatible with an unsupported currency. """
compatible_providers = self.env['payment.provider']._get_compatible_providers( compatible_providers = self.env['payment.provider']._get_compatible_providers(
@@ -2,7 +2,7 @@
import hashlib import hashlib
from odoo import api, fields, models from odoo import fields, models
class PaymentProvider(models.Model): class PaymentProvider(models.Model):
@@ -16,16 +16,12 @@ class PaymentProvider(models.Model):
payumoney_merchant_salt = fields.Char( payumoney_merchant_salt = fields.Char(
string="Merchant Salt", required_if_provider='payumoney', groups='base.group_system') string="Merchant Salt", required_if_provider='payumoney', groups='base.group_system')
@api.model def _get_supported_currencies(self):
def _get_compatible_providers(self, *args, currency_id=None, **kwargs): """ Override of `payment` to return EUR as the only supported currency. """
""" Override of payment to unlist PayUmoney providers when the currency is not INR. """ supported_currencies = super()._get_supported_currencies()
providers = super()._get_compatible_providers(*args, currency_id=currency_id, **kwargs) if self.code == 'payumoney':
supported_currencies = supported_currencies.filtered(lambda c: c.name == 'INR')
currency = self.env['res.currency'].browse(currency_id).exists() return supported_currencies
if currency and currency.name != 'INR':
providers = providers.filtered(lambda p: p.code != 'payumoney')
return providers
def _payumoney_generate_sign(self, values, incoming=True): def _payumoney_generate_sign(self, values, incoming=True):
""" Generate the shasign for incoming or outgoing communications. """ Generate the shasign for incoming or outgoing communications.
+93 -8
View File
@@ -2,15 +2,100 @@
# The currencies supported by Razorpay, in ISO 4217 format. Last updated on May 26, 2021. # The currencies supported by Razorpay, in ISO 4217 format. Last updated on May 26, 2021.
# See https://razorpay.com/docs/payments/payments/international-payments/#supported-currencies. # See https://razorpay.com/docs/payments/payments/international-payments/#supported-currencies.
# Last seen online: 16 November 2022.
SUPPORTED_CURRENCIES = [ SUPPORTED_CURRENCIES = [
'AED', 'ALL', 'AMD', 'ARS', 'AUD', 'AWG', 'BBD', 'BDT', 'BMD', 'BND', 'BOB', 'BSD', 'BWP', 'AED',
'BZD', 'CAD', 'CHF', 'CNY', 'COP', 'CRC', 'CUP', 'CZK', 'DKK', 'DOP', 'DZD', 'EGP', 'ETB', 'ALL',
'EUR', 'FJD', 'GBP', 'GHS', 'GIP', 'GMD', 'GTQ', 'GYD', 'HKD', 'HNL', 'HRK', 'HTG', 'HUF', 'AMD',
'IDR', 'ILS', 'INR', 'JMD', 'KES', 'KGS', 'KHR', 'KYD', 'KZT', 'LAK', 'LBP', 'LKR', 'LRD', 'ARS',
'LSL', 'MAD', 'MDL', 'MKD', 'MMK', 'MNT', 'MOP', 'MUR', 'MVR', 'MWK', 'MXN', 'MYR', 'NAD', 'AUD',
'NGN', 'NIO', 'NOK', 'NPR', 'NZD', 'PEN', 'PGK', 'PHP', 'PKR', 'QAR', 'RUB', 'SAR', 'SCR', 'AWG',
'SEK', 'SGD', 'SLL', 'SOS', 'SSP', 'SVC', 'SZL', 'THB', 'TTD', 'TZS', 'USD', 'UYU', 'UZS', 'BBD',
'YER', 'ZAR', 'BDT',
'BMD',
'BND',
'BOB',
'BSD',
'BWP',
'BZD',
'CAD',
'CHF',
'CNY',
'COP',
'CRC',
'CUP',
'CZK',
'DKK',
'DOP',
'DZD',
'EGP',
'ETB',
'EUR',
'FJD',
'GBP',
'GHS',
'GIP',
'GMD',
'GTQ',
'GYD',
'HKD',
'HNL',
'HRK',
'HTG',
'HUF',
'IDR',
'ILS',
'INR',
'JMD',
'KES',
'KGS',
'KHR',
'KYD',
'KZT',
'LAK',
'LKR',
'LRD',
'LSL',
'MAD',
'MDL',
'MKD',
'MMK',
'MNT',
'MOP',
'MUR',
'MVR',
'MWK',
'MXN',
'MYR',
'NAD',
'NGN',
'NIO',
'NOK',
'NPR',
'NZD',
'PEN',
'PGK',
'PHP',
'PKR',
'QAR',
'RUB',
'SAR',
'SCR',
'SEK',
'SGD',
'SLL',
'SOS',
'SSP',
'SVC',
'SZL',
'THB',
'TTD',
'TZS',
'USD',
'UYU',
'UZS',
'YER',
'ZAR',
] ]
# Mapping of transaction states to Razorpay's payment statuses. # Mapping of transaction states to Razorpay's payment statuses.
@@ -8,7 +8,7 @@ import pprint
import requests import requests
from werkzeug.urls import url_join from werkzeug.urls import url_join
from odoo import _, api, fields, models from odoo import _, fields, models
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from odoo.addons.payment_razorpay.const import SUPPORTED_CURRENCIES from odoo.addons.payment_razorpay.const import SUPPORTED_CURRENCIES
@@ -51,16 +51,14 @@ class PaymentProvider(models.Model):
# === BUSINESS METHODS ===# # === BUSINESS METHODS ===#
@api.model def _get_supported_currencies(self):
def _get_compatible_providers(self, *args, currency_id=None, **kwargs): """ Override of `payment` to return the supported currencies. """
""" Override of `payment` to filter out Razorpay providers for unsupported currencies. """ supported_currencies = super()._get_supported_currencies()
providers = super()._get_compatible_providers(*args, currency_id=currency_id, **kwargs) if self.code == 'razorpay':
supported_currencies = supported_currencies.filtered(
currency = self.env['res.currency'].browse(currency_id).exists() lambda c: c.name in SUPPORTED_CURRENCIES
if currency and currency.name not in SUPPORTED_CURRENCIES: )
providers = providers.filtered(lambda p: p.code != 'razorpay') return supported_currencies
return providers
def _razorpay_make_request(self, endpoint, payload=None, method='POST'): def _razorpay_make_request(self, endpoint, payload=None, method='POST'):
""" Make a request to Razorpay API at the specified endpoint. """ Make a request to Razorpay API at the specified endpoint.
@@ -3,12 +3,7 @@
# ISO 4217 Data for currencies supported by sips # ISO 4217 Data for currencies supported by sips
# NOTE: these are listed on the Atos Wordline SIPS POST documentation page # NOTE: these are listed on the Atos Wordline SIPS POST documentation page
# at https://documentation.sips.worldline.com/en/WLSIPS.001-GD-Data-dictionary.html#Sips.001_DD_en-Value-currencyCode # at https://documentation.sips.worldline.com/en/WLSIPS.001-GD-Data-dictionary.html#Sips.001_DD_en-Value-currencyCode
# Yet with the simu environment, some of these currencies are *not* working # Last seen on: 22 September 2022.
# I have no way to know if this is caused by the SIMU environment, or if it's
# the doc of SIPS that lists currencies that don't work, but since this list is
# restrictive, I'm gonna assume they are supported when using the right flow
# and payment methods, which may not work in SIMU...
# Since SIPS advises to use 'in production', well...
SUPPORTED_CURRENCIES = { SUPPORTED_CURRENCIES = {
'ARS': '032', 'ARS': '032',
'AUD': '036', 'AUD': '036',
+10 -12
View File
@@ -3,9 +3,9 @@
from hashlib import sha256 from hashlib import sha256
from odoo import api, fields, models from odoo import fields, models
from .const import SUPPORTED_CURRENCIES from odoo.addons.payment_sips.const import SUPPORTED_CURRENCIES
class PaymentProvider(models.Model): class PaymentProvider(models.Model):
@@ -29,16 +29,14 @@ class PaymentProvider(models.Model):
sips_version = fields.Char( sips_version = fields.Char(
string="Interface Version", required_if_provider='sips', default="HP_2.31") string="Interface Version", required_if_provider='sips', default="HP_2.31")
@api.model def _get_supported_currencies(self):
def _get_compatible_providers(self, *args, currency_id=None, **kwargs): """ Override of `payment` to return the supported currencies. """
""" Override of payment to unlist Sips providers when the currency is not supported. """ supported_currencies = super()._get_supported_currencies()
providers = super()._get_compatible_providers(*args, currency_id=currency_id, **kwargs) if self.code == 'sips':
supported_currencies = supported_currencies.filtered(
currency = self.env['res.currency'].browse(currency_id).exists() lambda c: c.name in SUPPORTED_CURRENCIES.keys()
if currency and currency.name not in SUPPORTED_CURRENCIES: )
providers = providers.filtered(lambda p: p.code != 'sips') return supported_currencies
return providers
def _sips_generate_shasign(self, data): def _sips_generate_shasign(self, data):
""" Generate the shasign for incoming or outgoing communications. """ Generate the shasign for incoming or outgoing communications.
@@ -10,8 +10,8 @@ from odoo import _, api, models
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from odoo.addons.payment import utils as payment_utils from odoo.addons.payment import utils as payment_utils
from odoo.addons.payment_sips.const import RESPONSE_CODES_MAPPING, SUPPORTED_CURRENCIES
from odoo.addons.payment_sips.controllers.main import SipsController from odoo.addons.payment_sips.controllers.main import SipsController
from .const import RESPONSE_CODES_MAPPING, SUPPORTED_CURRENCIES
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
-8
View File
@@ -12,7 +12,6 @@ from odoo.tools import mute_logger
from odoo.addons.payment.tests.http_common import PaymentHttpCommon from odoo.addons.payment.tests.http_common import PaymentHttpCommon
from odoo.addons.payment_sips.controllers.main import SipsController from odoo.addons.payment_sips.controllers.main import SipsController
from odoo.addons.payment_sips.models.payment_provider import SUPPORTED_CURRENCIES
from odoo.addons.payment_sips.tests.common import SipsCommon from odoo.addons.payment_sips.tests.common import SipsCommon
@@ -20,13 +19,6 @@ from odoo.addons.payment_sips.tests.common import SipsCommon
class SipsTest(SipsCommon, PaymentHttpCommon): class SipsTest(SipsCommon, PaymentHttpCommon):
def test_compatible_providers(self): def test_compatible_providers(self):
for curr in SUPPORTED_CURRENCIES:
currency = self._prepare_currency(curr)
providers = self.env['payment.provider']._get_compatible_providers(
self.company.id, self.partner.id, self.amount, currency_id=currency.id
)
self.assertIn(self.sips, providers)
unsupported_currency = self._prepare_currency('VEF') unsupported_currency = self._prepare_currency('VEF')
providers = self.env['payment.provider']._get_compatible_providers( providers = self.env['payment.provider']._get_compatible_providers(
self.company.id, self.partner.id, self.amount, currency_id=unsupported_currency.id self.company.id, self.partner.id, self.amount, currency_id=unsupported_currency.id