19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -4,6 +4,9 @@ from . import common
from . import http_common
from . import test_flows
from . import test_multicompany_flows
from . import test_payment_capture_wizard
from . import test_payment_method
from . import test_payment_provider
from . import test_payment_token
from . import test_payment_transaction
from . import test_res_company

View file

@ -1,48 +1,36 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from unittest.mock import patch
from lxml import objectify
from odoo.fields import Command
from odoo.tests.common import TransactionCase
from odoo.fields import Command, Domain
from odoo.tools.misc import hmac as hmac_tool
from odoo.addons.base.tests.common import BaseCommon
_logger = logging.getLogger(__name__)
class PaymentCommon(TransactionCase):
class PaymentCommon(BaseCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.currency_euro = cls._prepare_currency('EUR')
cls.currency_usd = cls._prepare_currency('USD')
cls.currency_euro = cls._enable_currency('EUR')
cls.currency_usd = cls._enable_currency('USD')
cls.country_belgium = cls.env.ref('base.be')
cls.country_france = cls.env.ref('base.fr')
cls.europe = cls.env.ref('base.europe')
cls.country_belgium = cls.quick_ref('base.be')
cls.country_france = cls.quick_ref('base.fr')
cls.europe = cls.quick_ref('base.europe')
cls.group_user = cls.env.ref('base.group_user')
cls.group_portal = cls.env.ref('base.group_portal')
cls.group_public = cls.env.ref('base.group_public')
cls.admin_user = cls.env.ref('base.user_admin')
cls.internal_user = cls.env['res.users'].create({
'name': 'Internal User (Test)',
'login': 'internal',
'password': 'internal',
'groups_id': [Command.link(cls.group_user.id)]
})
cls.portal_user = cls.env['res.users'].create({
'name': 'Portal User (Test)',
'login': 'payment_portal',
'password': 'payment_portal',
'groups_id': [Command.link(cls.group_portal.id)]
})
cls.public_user = cls.env.ref('base.public_user')
cls.admin_user = cls.quick_ref('base.user_admin')
cls.internal_user = cls._create_new_internal_user()
cls.portal_user = cls._create_new_portal_user()
cls.public_user = cls.quick_ref('base.public_user')
cls.admin_partner = cls.admin_user.partner_id
cls.internal_partner = cls.internal_user.partner_id
@ -72,16 +60,30 @@ class PaymentCommon(TransactionCase):
'arch': arch,
})
cls.pm_unknown = cls.quick_ref('payment.payment_method_unknown')
cls.dummy_provider = cls.env['payment.provider'].create({
'name': "Dummy Provider",
'code': 'none',
'state': 'test',
'is_published': True,
'payment_method_ids': [Command.set([cls.pm_unknown.id])],
'allow_tokenization': True,
'redirect_form_view_id': redirect_form.id,
'available_currency_ids': [Command.set(
(cls.currency_euro + cls.currency_usd + cls.env.company.currency_id).ids
)],
})
# Activate pm
cls.pm_unknown.write({
'active': True,
'support_tokenization': True,
})
cls.provider = cls.dummy_provider
cls.payment_methods = cls.provider.payment_method_ids
cls.payment_method = cls.payment_methods[:1]
cls.payment_method_id = cls.payment_method.id
cls.payment_method_code = cls.payment_method.code
cls.amount = 1111.11
cls.company = cls.env.company
cls.company_id = cls.company.id
@ -91,65 +93,57 @@ class PaymentCommon(TransactionCase):
account_payment_module = cls.env['ir.module.module']._get('account_payment')
cls.account_payment_installed = account_payment_module.state in ('installed', 'to upgrade')
cls.enable_reconcile_after_done_patcher = True
cls.enable_post_process_patcher = True
def setUp(self):
super().setUp()
if self.account_payment_installed and self.enable_reconcile_after_done_patcher:
if self.account_payment_installed and self.enable_post_process_patcher:
# disable account payment generation if account_payment is installed
# because the accounting setup of providers is not managed in this common
self.reconcile_after_done_patcher = patch(
'odoo.addons.account_payment.models.payment_transaction.PaymentTransaction._reconcile_after_done',
self.post_process_patcher = patch(
'odoo.addons.account_payment.models.payment_transaction.PaymentTransaction._post_process',
)
self.startPatcher(self.reconcile_after_done_patcher)
self.startPatcher(self.post_process_patcher)
#=== Utils ===#
@classmethod
def _prepare_currency(cls, currency_code):
currency = cls.env['res.currency'].with_context(active_test=False).search(
[('name', '=', currency_code.upper())]
)
currency.action_unarchive()
return currency
@classmethod
def _prepare_provider(cls, code='none', company=None, update_values=None):
""" Prepare and return the first provider matching the given provider and company.
If no provider is found in the given company, we duplicate the one from the base company.
def _prepare_provider(cls, code, company=None, update_values=None, **kwargs):
""" Prepare and return the first active provider matching the given code and company.
All other providers belonging to the same company are disabled to avoid any interferences.
:param str code: The code of the provider to prepare
:param recordset company: The company of the provider to prepare, as a `res.company` record
:param dict update_values: The values used to update the provider
:param dict kwargs: The keyword arguments passed as-is to the called function.
:return: The provider to prepare, if found
:rtype: recordset of `payment.provider`
"""
assert code != 'none', "Code 'none' should not be passed to _prepare_provider"
company = company or cls.env.company
update_values = update_values or {}
provider_domain = cls._get_provider_domain(code, **kwargs)
provider = cls.env['payment.provider'].sudo().search(
[('code', '=', code), ('company_id', '=', company.id)], limit=1
Domain.AND([provider_domain, [('company_id', '=', company.id)]]), limit=1
)
if not provider:
base_provider = cls.env['payment.provider'].sudo().search(
[('code', '=', code)], limit=1
)
if not base_provider:
_logger.error("no payment.provider found for code %s", code)
return cls.env['payment.provider']
else:
provider = base_provider.copy({'company_id': company.id})
_logger.error("No payment.provider found for code %s in company %s", code, company.name)
return cls.env['payment.provider']
update_values['state'] = 'test'
provider.write(update_values)
return provider
@classmethod
def _get_provider_domain(cls, code, **kwargs):
return [('code', '=', code)]
@classmethod
def _prepare_user(cls, user, group_xmlid):
user.groups_id = [Command.link(cls.env.ref(group_xmlid).id)]
user.group_ids = [Command.link(cls.env.ref(group_xmlid).id)]
# Flush and invalidate the cache to allow checking access rights.
user.flush_recordset()
user.invalidate_recordset()
@ -157,6 +151,7 @@ class PaymentCommon(TransactionCase):
def _create_transaction(self, flow, sudo=True, **values):
default_values = {
'payment_method_id': self.payment_method_id,
'amount': self.amount,
'currency_id': self.currency.id,
'provider_id': self.provider.id,
@ -168,8 +163,9 @@ class PaymentCommon(TransactionCase):
def _create_token(self, sudo=True, **values):
default_values = {
'payment_details': "1234",
'provider_id': self.provider.id,
'payment_method_id': self.payment_method_id,
'payment_details': "1234",
'partner_id': self.partner.id,
'provider_ref': "provider Ref (TEST)",
'active': True,

View file

@ -1,12 +1,11 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
from uuid import uuid4
from contextlib import contextmanager
from lxml import etree, objectify
from werkzeug import urls
from odoo.tests import HttpCase
from odoo.tests import HttpCase, JsonRpcException
from odoo.tools import urls
from odoo.addons.payment.tests.common import PaymentCommon
@ -22,7 +21,7 @@ class PaymentHttpCommon(PaymentCommon, HttpCase):
###########
def _build_url(self, route):
return urls.url_join(self.base_url(), route)
return urls.urljoin(self.base_url(), route)
def _make_http_get_request(self, url, params=None):
""" Make an HTTP GET request to the provided URL.
@ -33,7 +32,7 @@ class PaymentHttpCommon(PaymentCommon, HttpCase):
:rtype: :class:`requests.models.Response`
"""
formatted_params = self._format_http_request_payload(payload=params)
return self.opener.get(url, params=formatted_params)
return self.url_open(url, params=formatted_params)
def _make_http_post_request(self, url, data=None):
""" Make an HTTP POST request to the provided URL.
@ -44,7 +43,7 @@ class PaymentHttpCommon(PaymentCommon, HttpCase):
:rtype: :class:`requests.models.Response`
"""
formatted_data = self._format_http_request_payload(payload=data)
return self.opener.post(url, data=formatted_data)
return self.url_open(url, data=formatted_data, method='POST')
def _format_http_request_payload(self, payload=None):
""" Format a request payload to replace float values by their string representation.
@ -67,29 +66,19 @@ class PaymentHttpCommon(PaymentCommon, HttpCase):
:return: The response of the request
:rtype: :class:`requests.models.Response`
"""
return self.opener.post(url, json=data)
return self.url_open(url, json=data)
def _make_json_rpc_request(self, url, data=None):
""" Make a JSON-RPC request to the provided URL.
@contextmanager
def _assertNotFound(self):
with self.assertRaises(JsonRpcException) as cm:
yield
self.assertEqual(cm.exception.code, 404)
:param str url: The URL to make the request to
:param dict data: The data to be send in the request body in JSON-RPC 2.0 format
:return: The response of the request
:rtype: :class:`requests.models.Response`
"""
return self.opener.post(url, json={
'jsonrpc': '2.0',
'method': 'call',
'id': str(uuid4()),
'params': data,
})
def _get_tx_context(self, response, form_name):
"""Extracts txContext & other form info (provider & token ids)
from a payment response (with manage/checkout html form)
def _get_payment_context(self, response):
"""Extracts the payment context & other form info (provider & token ids)
from a payment response
:param response: http Response, with a payment form as text
:param str form_name: o_payment_manage / o_payment_checkout
:return: Transaction context (+ provider_ids & token_ids)
:rtype: dict
"""
@ -100,9 +89,9 @@ class PaymentHttpCommon(PaymentCommon, HttpCase):
response.text,
parser=etree.HTMLParser(),
)
checkout_form = html_tree.xpath(f"//form[@name='{form_name}']")[0]
payment_form = html_tree.xpath('//form[@id="o_payment_form"]')[0]
values = {}
for key, val in checkout_form.items():
for key, val in payment_form.items():
if key.startswith("data-"):
formatted_key = key[5:].replace('-', '_')
if formatted_key.endswith('_id'):
@ -114,21 +103,21 @@ class PaymentHttpCommon(PaymentCommon, HttpCase):
values[formatted_key] = formatted_val
payment_options_inputs = html_tree.xpath("//input[@name='o_payment_radio']")
provider_ids = []
token_ids = []
payment_method_ids = []
for p_o_input in payment_options_inputs:
data = dict()
for key, val in p_o_input.items():
if key.startswith('data-'):
data[key[5:]] = val
if data['payment-option-type'] == 'provider':
provider_ids.append(int(data['payment-option-id']))
else:
if data['payment-option-type'] == 'token':
token_ids.append(int(data['payment-option-id']))
else: # 'payment_method'
payment_method_ids.append(int(data['payment-option-id']))
values.update({
'provider_ids': provider_ids,
'token_ids': token_ids,
'payment_method_ids': payment_method_ids,
})
return values
@ -156,7 +145,7 @@ class PaymentHttpCommon(PaymentCommon, HttpCase):
}
def _portal_pay(self, **route_kwargs):
"""/payment/pay txContext feedback
"""/payment/pay payment context feedback
NOTE: must be authenticated before calling method.
Or an access_token should be specified in route_kwargs
@ -165,18 +154,18 @@ class PaymentHttpCommon(PaymentCommon, HttpCase):
url = self._build_url(uri)
return self._make_http_get_request(url, route_kwargs)
def _get_tx_checkout_context(self, **route_kwargs):
def _get_portal_pay_context(self, **route_kwargs):
response = self._portal_pay(**route_kwargs)
self.assertEqual(response.status_code, 200)
return self._get_tx_context(response, 'o_payment_checkout')
return self._get_payment_context(response)
# /my/payment_method #
######################
def _portal_payment_method(self):
"""/my/payment_method txContext feedback
"""/my/payment_method payment context feedback
NOTE: must be authenticated before calling method
validation flow is restricted to logged users
@ -185,56 +174,49 @@ class PaymentHttpCommon(PaymentCommon, HttpCase):
url = self._build_url(uri)
return self._make_http_get_request(url, {})
def _get_tx_manage_context(self):
def _get_portal_payment_method_context(self):
response = self._portal_payment_method()
self.assertEqual(response.status_code, 200)
return self._get_tx_context(response, 'o_payment_manage')
return self._get_payment_context(response)
# payment/transaction #
#######################
def _prepare_transaction_values(self, payment_option_id, flow):
def _prepare_transaction_values(self, payment_method_id, token_id, flow):
""" Prepare the basic payment/transaction route values.
:param int payment_option_id: The payment option handling the transaction, as a
`payment.provider` id or a `payment.token` id
`payment.method` id or a `payment.token` id
:param str flow: The payment flow
:return: The route values
:rtype: dict
"""
return {
'provider_id': self.provider.id,
'payment_method_id': payment_method_id,
'token_id': token_id,
'amount': self.amount,
'currency_id': self.currency.id,
'partner_id': self.partner.id,
'access_token': self._generate_test_access_token(
self.partner.id, self.amount, self.currency.id
),
'payment_option_id': payment_option_id,
'reference_prefix': 'test',
'tokenization_requested': True,
'landing_route': 'Test',
'reference_prefix': 'test',
'is_validation': False,
'flow': flow,
}
def _portal_transaction(self, **route_kwargs):
def _portal_transaction(self, tx_route='/payment/transaction', **route_kwargs):
"""/payment/transaction feedback
:return: The response to the json request
"""
uri = '/payment/transaction'
url = self._build_url(uri)
response = self._make_json_rpc_request(url, route_kwargs)
self.assertEqual(response.status_code, 200) # Check the request went through.
return response
url = self._build_url(tx_route)
return self.make_jsonrpc_request(url, route_kwargs)
def _get_processing_values(self, **route_kwargs):
response = self._portal_transaction(**route_kwargs)
self.assertEqual(response.status_code, 200)
resp_content = json.loads(response.content)
return resp_content['result']
return self._portal_transaction(**route_kwargs)

View file

@ -1,11 +1,11 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from urllib.parse import urlparse, parse_qs
from unittest.mock import patch
from urllib.parse import parse_qs, urlparse
from freezegun import freeze_time
from odoo.tests import tagged
from odoo.tests import JsonRpcException, tagged
from odoo.tools import mute_logger
from odoo.addons.payment.controllers.portal import PaymentPortal
@ -26,34 +26,31 @@ class TestFlows(PaymentHttpCommon):
route_values = self._prepare_pay_values()
# /payment/pay
tx_context = self._get_tx_checkout_context(**route_values)
for key, val in tx_context.items():
payment_context = self._get_portal_pay_context(**route_values)
for key, val in payment_context.items():
if key in route_values:
self.assertEqual(val, route_values[key])
self.assertIn(self.provider.id, tx_context['provider_ids'])
# Route values are taken from tx_context result of /pay route to correctly simulate the flow
# Route values are taken from payment_context result of /pay route to correctly simulate the flow
route_values = {
k: tx_context[k]
k: payment_context[k]
for k in [
'amount',
'currency_id',
'reference_prefix',
'partner_id',
'access_token',
'landing_route',
'reference_prefix',
'access_token',
]
}
route_values.update({
'provider_id': self.provider.id,
'payment_method_id': self.payment_method_id if flow != 'token' else None,
'token_id': self._create_token().id if flow == 'token' else None,
'flow': flow,
'payment_option_id': self.provider.id,
'tokenization_requested': False,
})
if flow == 'token':
route_values['payment_option_id'] = self._create_token().id
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'])
@ -71,6 +68,7 @@ class TestFlows(PaymentHttpCommon):
self.assertEqual(processing_values['currency_id'], self.currency.id)
self.assertEqual(processing_values['partner_id'], self.partner.id)
self.assertEqual(processing_values['reference'], self.reference)
self.assertFalse(processing_values['should_tokenize'])
# Verify computed values not provided, but added during the flow
self.assertIn("tx_id=", tx_sudo.landing_route)
@ -163,26 +161,28 @@ class TestFlows(PaymentHttpCommon):
validation_amount = self.provider._get_validation_amount()
validation_currency = self.provider._get_validation_currency()
tx_context = self._get_tx_manage_context()
payment_context = self._get_portal_payment_method_context()
expected_values = {
'partner_id': self.partner.id,
'access_token': self._generate_test_access_token(self.partner.id, None, None),
'reference_prefix': expected_reference
}
for key, val in tx_context.items():
for key, val in payment_context.items():
if key in expected_values:
self.assertEqual(val, expected_values[key])
transaction_values = {
'provider_id': self.provider.id,
'payment_method_id': self.payment_method_id,
'token_id': None,
'amount': None,
'currency_id': None,
'partner_id': tx_context['partner_id'],
'access_token': tx_context['access_token'],
'partner_id': payment_context['partner_id'],
'access_token': payment_context['access_token'],
'flow': flow,
'payment_option_id': self.provider.id,
'tokenization_requested': True,
'reference_prefix': tx_context['reference_prefix'],
'landing_route': tx_context['landing_route'],
'landing_route': payment_context['landing_route'],
'reference_prefix': payment_context['reference_prefix'],
'is_validation': True,
}
with mute_logger('odoo.addons.payment.models.payment_transaction'):
@ -196,7 +196,6 @@ class TestFlows(PaymentHttpCommon):
self.assertEqual(tx_sudo.partner_id.id, self.partner.id)
self.assertEqual(tx_sudo.reference, expected_reference)
# processing_values == given values
self.assertEqual(processing_values['provider_id'], self.provider.id)
self.assertEqual(processing_values['amount'], validation_amount)
self.assertEqual(processing_values['currency_id'], validation_currency.id)
self.assertEqual(processing_values['partner_id'], self.partner.id)
@ -237,7 +236,7 @@ class TestFlows(PaymentHttpCommon):
# Pay without a partner specified (but logged) --> pay with the partner of current user.
self.authenticate(self.portal_user.login, self.portal_user.login)
tx_context = self._get_tx_checkout_context(**route_values)
tx_context = self._get_portal_pay_context(**route_values)
self.assertEqual(tx_context['partner_id'], self.portal_partner.id)
def test_pay_no_token(self):
@ -253,7 +252,7 @@ class TestFlows(PaymentHttpCommon):
# Pay without a partner specified (but logged) --> pay with the partner of current user.
self.authenticate(self.portal_user.login, self.portal_user.login)
tx_context = self._get_tx_checkout_context(**route_values)
tx_context = self._get_portal_pay_context(**route_values)
self.assertEqual(tx_context['partner_id'], self.portal_partner.id)
def test_pay_wrong_token(self):
@ -279,30 +278,37 @@ class TestFlows(PaymentHttpCommon):
def test_transaction_wrong_flow(self):
transaction_values = self._prepare_pay_values()
transaction_values.pop('reference')
transaction_values.update({
'flow': 'this flow does not exist',
'payment_option_id': self.provider.id,
'tokenization_requested': False,
'reference_prefix': 'whatever',
'landing_route': 'whatever',
'reference_prefix': 'whatever',
})
# Transaction step with a wrong flow --> UserError
with mute_logger('odoo.http'):
response = self._portal_transaction(**transaction_values)
self.assertIn(
"odoo.exceptions.UserError: The payment should either be direct, with redirection, or made by a token.",
response.text)
with mute_logger("odoo.http"), self.assertRaises(
JsonRpcException,
msg='odoo.exceptions.UserError: The payment should either be direct, with redirection, or made by a token.',
):
self._portal_transaction(**transaction_values)
@mute_logger('odoo.http')
def test_transaction_route_rejects_unexpected_kwarg(self):
route_kwargs = {
**self._prepare_pay_values(),
'custom_create_values': 'whatever', # This should be rejected.
}
with self.assertRaises(JsonRpcException, msg='odoo.exceptions.ValidationError'):
self._portal_transaction(**route_kwargs)
def test_transaction_wrong_token(self):
route_values = self._prepare_pay_values()
route_values['access_token'] = "abcde"
# Transaction step with a wrong access token --> ValidationError
with mute_logger('odoo.http'):
response = self._portal_transaction(**route_values)
self.assertIn(
"odoo.exceptions.ValidationError: The access token is invalid.",
response.text)
with mute_logger('odoo.http'), self.assertRaises(JsonRpcException, msg='odoo.exceptions.ValidationError: The access token is invalid.'):
self._portal_transaction(**route_values)
def test_access_disabled_providers_tokens(self):
self.partner = self.portal_partner
@ -311,31 +317,27 @@ class TestFlows(PaymentHttpCommon):
self.authenticate(self.portal_user.login, self.portal_user.login)
token = self._create_token()
provider_b = self.provider.copy()
provider_b.state = 'test'
provider_b = self.provider.copy({'is_published': True, 'state': 'test'})
token_b = self._create_token(provider_id=provider_b.id)
# User must see both enabled providers and tokens
manage_context = self._get_tx_manage_context()
self.assertEqual(manage_context['partner_id'], self.partner.id)
self.assertIn(self.provider.id, manage_context['provider_ids'])
self.assertIn(provider_b.id, manage_context['provider_ids'])
self.assertIn(token.id, manage_context['token_ids'])
self.assertIn(token_b.id, manage_context['token_ids'])
# User must see both tokens and compatible payment methods.
payment_context = self._get_portal_payment_method_context()
self.assertEqual(payment_context['partner_id'], self.partner.id)
self.assertIn(token.id, payment_context['token_ids'])
self.assertIn(token_b.id, payment_context['token_ids'])
self.assertIn(self.payment_method_id, payment_context['payment_method_ids'])
# Token of disabled provider(s) & disabled providers should not be shown
# Token of disabled provider(s) should not be shown.
self.provider.state = 'disabled'
manage_context = self._get_tx_manage_context()
self.assertEqual(manage_context['partner_id'], self.partner.id)
self.assertEqual(manage_context['provider_ids'], [provider_b.id])
self.assertEqual(manage_context['token_ids'], [token_b.id])
payment_context = self._get_portal_payment_method_context()
self.assertEqual(payment_context['partner_id'], self.partner.id)
self.assertEqual(payment_context['token_ids'], [token_b.id])
# Archived tokens must be hidden from the user
token_b.active = False
manage_context = self._get_tx_manage_context()
self.assertEqual(manage_context['partner_id'], self.partner.id)
self.assertEqual(manage_context['provider_ids'], [provider_b.id])
self.assertEqual(manage_context['token_ids'], [])
payment_context = self._get_portal_payment_method_context()
self.assertEqual(payment_context['partner_id'], self.partner.id)
self.assertEqual(payment_context['token_ids'], [])
@mute_logger('odoo.addons.payment.models.payment_transaction')
def test_direct_payment_triggers_no_payment_request(self):
@ -344,10 +346,10 @@ class TestFlows(PaymentHttpCommon):
self.user = self.portal_user
with patch(
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
'._send_payment_request'
'._charge_with_token'
) as patched:
self._portal_transaction(
**self._prepare_transaction_values(self.provider.id, 'direct')
**self._prepare_transaction_values(self.payment_method_id, None, 'direct')
)
self.assertEqual(patched.call_count, 0)
@ -358,10 +360,10 @@ class TestFlows(PaymentHttpCommon):
self.user = self.portal_user
with patch(
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
'._send_payment_request'
'._charge_with_token'
) as patched:
self._portal_transaction(
**self._prepare_transaction_values(self.provider.id, 'redirect')
**self._prepare_transaction_values(self.payment_method_id, None, 'redirect')
)
self.assertEqual(patched.call_count, 0)
@ -372,23 +374,30 @@ class TestFlows(PaymentHttpCommon):
self.user = self.portal_user
with patch(
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
'._send_payment_request'
'._charge_with_token'
) as patched:
self._portal_transaction(
**self._prepare_transaction_values(self._create_token().id, 'token')
**self._prepare_transaction_values(None, self._create_token().id, 'token')
)
self.assertEqual(patched.call_count, 1)
def test_tokenization_input_is_show_to_logged_in_users(self):
def test_tokenization_input_is_shown_to_logged_in_users(self):
# Test both for portal and internal users
self.user = self.portal_user
self.provider.allow_tokenization = True
show_tokenize_input = PaymentPortal._compute_show_tokenize_input_mapping(
self.provider, logged_in=True
)
show_tokenize_input = PaymentPortal._compute_show_tokenize_input_mapping(self.provider)
self.assertDictEqual(show_tokenize_input, {self.provider.id: True})
def test_tokenization_input_is_hidden_for_logged_out_users(self):
self.provider.allow_tokenization = False
show_tokenize_input = PaymentPortal._compute_show_tokenize_input_mapping(
self.provider, logged_in=True
)
self.assertDictEqual(show_tokenize_input, {self.provider.id: False})
self.user = self.internal_user
self.provider.allow_tokenization = True
show_tokenize_input = PaymentPortal._compute_show_tokenize_input_mapping(self.provider)
self.assertDictEqual(show_tokenize_input, {self.provider.id: True})
def test_tokenization_input_is_shown_to_logged_out_users(self):
self.user = self.public_user
self.provider.allow_tokenization = True
show_tokenize_input = PaymentPortal._compute_show_tokenize_input_mapping(self.provider)
self.assertDictEqual(show_tokenize_input, {self.provider.id: True})

View file

@ -24,7 +24,7 @@ class TestMultiCompanyFlows(PaymentHttpCommon):
'password': 'user_company_b',
'company_id': cls.company_b.id,
'company_ids': [Command.set(cls.company_b.ids)],
'groups_id': [Command.link(cls.group_user.id)],
'group_ids': [Command.link(cls.group_user.id)],
})
cls.user_multi_company = cls.env['res.users'].create({
'name': "Multi Company User (TEST)",
@ -32,10 +32,11 @@ class TestMultiCompanyFlows(PaymentHttpCommon):
'password': 'user_multi_company',
'company_id': cls.company_a.id,
'company_ids': [Command.set([cls.company_a.id, cls.company_b.id])],
'groups_id': [Command.link(cls.group_user.id)],
'group_ids': [Command.link(cls.group_user.id)],
})
cls.provider_company_b = cls._prepare_provider(company=cls.company_b)
cls.provider = cls.dummy_provider.copy({'company_id': cls.company_b.id})
cls.provider.state = 'test'
def test_pay_logged_in_another_company(self):
"""User pays for an amount in another company."""
@ -48,8 +49,8 @@ class TestMultiCompanyFlows(PaymentHttpCommon):
# Pay in company B
route_values['company_id'] = self.company_b.id
tx_context = self._get_tx_checkout_context(**route_values)
for key, val in tx_context.items():
payment_context = self._get_portal_pay_context(**route_values)
for key, val in payment_context.items():
if key in route_values:
if key == 'access_token':
continue # access_token was modified due to the change of partner.
@ -59,24 +60,22 @@ class TestMultiCompanyFlows(PaymentHttpCommon):
else:
self.assertEqual(val, route_values[key])
available_providers = self.env['payment.provider'].sudo().browse(tx_context['provider_ids'])
self.assertIn(self.provider_company_b, available_providers)
self.assertEqual(available_providers.company_id, self.company_b)
validation_values = {
k: tx_context[k]
k: payment_context[k]
for k in [
'amount',
'currency_id',
'reference_prefix',
'partner_id',
'access_token',
'landing_route',
'reference_prefix',
'access_token',
]
}
validation_values.update({
'provider_id': self.provider.id,
'payment_method_id': self.provider.payment_method_ids[:1].id,
'token_id': None,
'flow': 'direct',
'payment_option_id': self.provider_company_b.id,
'tokenization_requested': False,
})
with mute_logger('odoo.addons.payment.models.payment_transaction'):
@ -84,14 +83,14 @@ class TestMultiCompanyFlows(PaymentHttpCommon):
tx_sudo = self._get_tx(processing_values['reference'])
# Tx values == given values
self.assertEqual(tx_sudo.provider_id.id, self.provider_company_b.id)
self.assertEqual(tx_sudo.provider_id.id, self.provider.id)
self.assertEqual(tx_sudo.amount, self.amount)
self.assertEqual(tx_sudo.currency_id.id, self.currency.id)
self.assertEqual(tx_sudo.partner_id.id, self.user_company_a.partner_id.id)
self.assertEqual(tx_sudo.reference, self.reference)
self.assertEqual(tx_sudo.company_id, self.company_b)
# processing_values == given values
self.assertEqual(processing_values['provider_id'], self.provider_company_b.id)
self.assertEqual(processing_values['provider_id'], self.provider.id)
self.assertEqual(processing_values['amount'], self.amount)
self.assertEqual(processing_values['currency_id'], self.currency.id)
self.assertEqual(processing_values['partner_id'], self.user_company_a.partner_id.id)
@ -104,15 +103,13 @@ class TestMultiCompanyFlows(PaymentHttpCommon):
self.authenticate(self.portal_user.login, self.portal_user.login)
token = self._create_token()
token_company_b = self._create_token(provider_id=self.provider_company_b.id)
token_company_b = self._create_token(provider_id=self.provider.id)
# A partner should see all his tokens on the /my/payment_method route,
# even if they are in other companies otherwise he won't ever see them.
manage_context = self._get_tx_manage_context()
self.assertEqual(manage_context['partner_id'], self.partner.id)
self.assertEqual(manage_context['provider_ids'], self.provider.ids)
self.assertIn(token.id, manage_context['token_ids'])
self.assertIn(token_company_b.id, manage_context['token_ids'])
payment_context = self._get_portal_payment_method_context()
self.assertIn(token.id, payment_context['token_ids'])
self.assertIn(token_company_b.id, payment_context['token_ids'])
def test_archive_token_logged_in_another_company(self):
"""User archives his token from another company."""
@ -128,6 +125,6 @@ class TestMultiCompanyFlows(PaymentHttpCommon):
# Archive token in company A
url = self._build_url('/payment/archive_token')
self._make_json_rpc_request(url, {'token_id': token.id})
self.make_jsonrpc_request(url, {'token_id': token.id})
self.assertFalse(token.active)

View file

@ -0,0 +1,57 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.addons.payment.tests.common import PaymentCommon
@tagged('post_install', '-at_install')
class TestPaymentCaptureWizard(PaymentCommon):
def test_partial_capture_wizard(self):
self.provider.update({
'capture_manually': True,
'support_manual_capture': 'partial',
})
source_tx = self._create_transaction('direct', state='authorized')
wizard = self.env['payment.capture.wizard'].create({
'transaction_ids': source_tx.ids,
})
wizard.amount_to_capture = 511.11
wizard.action_capture()
child_tx_1 = source_tx.child_transaction_ids
self.assertEqual(child_tx_1.state, 'draft')
child_tx_1._set_done()
self.env['payment.capture.wizard'].create({
'transaction_ids': source_tx.ids,
}).action_capture()
child_tx_2 = (source_tx.child_transaction_ids - child_tx_1).ensure_one()
child_tx_2._set_done()
self.assertAlmostEqual(
sum(source_tx.child_transaction_ids.mapped('amount')),
source_tx.amount,
)
self.assertEqual(source_tx.state, 'done')
def test_support_partial_capture_computation_with_brands(self):
self.provider.update({
'capture_manually': True,
'support_manual_capture': 'partial',
})
dummy_brand = self.env['payment.method'].create({
'name': "Dummy Brand",
'code': 'dumbrand',
'primary_payment_method_id': self.payment_method.id,
'provider_ids': self.provider.ids,
})
source_tx = self._create_transaction(
'direct', state='authorized', payment_method_id=dummy_brand.id,
)
wizard = self.env['payment.capture.wizard'].create({
'transaction_ids': source_tx.ids,
})
self.assertTrue(wizard.support_partial_capture)

View file

@ -0,0 +1,252 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.exceptions import UserError, ValidationError
from odoo.fields import Command
from odoo.tests import tagged
from odoo.addons.payment import utils as payment_utils
from odoo.addons.payment.const import REPORT_REASONS_MAPPING
from odoo.addons.payment.tests.common import PaymentCommon
@tagged('-at_install', 'post_install')
class TestPaymentMethod(PaymentCommon):
def test_unlinking_payment_method_from_provider_state_archives_tokens(self):
""" Test that the active tokens of a payment method created through a provider are archived
when the method is unlinked from the provider. """
token = self._create_token()
self.payment_method.provider_ids = [Command.unlink(self.payment_method.provider_ids[:1].id)]
self.assertFalse(token.active)
def test_payment_method_requires_provider_to_be_activated(self):
""" Test that activating a payment method that is not linked to an enabled provider is
forbidden. """
self.provider.state = 'disabled'
with self.assertRaises(UserError):
self.payment_methods.active = True
def test_brand_compatible_with_manual_capture(self):
""" Test that a "brand" can be enabled for providers which support manual capture. """
self.provider.update({
'capture_manually': True,
'support_manual_capture': 'partial',
})
self.payment_method.support_manual_capture = 'partial'
brand_payment_method = self.env['payment.method'].create({
'name': "Dummy Brand",
'code': 'dumbrand',
'primary_payment_method_id': self.payment_method.id,
'active': False,
'provider_ids': self.provider.ids,
})
self._assert_does_not_raise(ValidationError, brand_payment_method.action_unarchive)
self.assertTrue(brand_payment_method.active)
def test_payment_method_compatible_when_provider_is_enabled(self):
""" Test that a payment method is available when it is supported by an enabled provider. """
compatible_payment_methods = self.env['payment.method']._get_compatible_payment_methods(
self.provider.ids, self.partner.id
)
self.assertIn(self.payment_method, compatible_payment_methods)
def test_payment_method_not_compatible_when_provider_is_disabled(self):
""" Test that a payment method is not available when there is no enabled provider that
supports it. """
self.provider.state = 'disabled'
compatible_payment_methods = self.env['payment.method']._get_compatible_payment_methods(
self.provider.ids, self.partner.id
)
self.assertNotIn(self.payment_method, compatible_payment_methods)
def test_non_primary_payment_method_not_compatible(self):
""" Test that a "brand" (i.e., non-primary) payment method is never available. """
brand_payment_method = self.payment_method.copy()
brand_payment_method.primary_payment_method_id = self.payment_method_id # Make it a brand.
compatible_payment_methods = self.env['payment.method']._get_compatible_payment_methods(
self.provider.ids, self.partner.id
)
self.assertNotIn(brand_payment_method, compatible_payment_methods)
def test_payment_method_compatible_with_supported_countries(self):
""" Test that the payment method is compatible with its supported countries. """
belgium = self.env.ref('base.be')
self.payment_method.supported_country_ids = [Command.set([belgium.id])]
self.partner.country_id = belgium
compatible_payment_methods = self.env['payment.method']._get_compatible_payment_methods(
self.provider.ids, self.partner.id
)
self.assertIn(self.payment_method, compatible_payment_methods)
def test_payment_method_not_compatible_with_unsupported_countries(self):
""" Test that the payment method is not compatible with a country that is not supported. """
belgium = self.env.ref('base.be')
self.payment_method.supported_country_ids = [Command.set([belgium.id])]
france = self.env.ref('base.fr')
self.partner.country_id = france
compatible_payment_methods = self.env['payment.method']._get_compatible_payment_methods(
self.provider.ids, self.partner.id
)
self.assertNotIn(self.payment_method, compatible_payment_methods)
def test_payment_method_compatible_when_no_supported_countries_set(self):
""" Test that the payment method is always compatible when no supported countries are
set. """
self.payment_method.supported_country_ids = [Command.clear()]
belgium = self.env.ref('base.be')
self.partner.country_id = belgium
compatible_payment_methods = self.env['payment.method']._get_compatible_payment_methods(
self.provider.ids, self.partner.id
)
self.assertIn(self.payment_method, compatible_payment_methods)
def test_payment_method_compatible_with_supported_currencies(self):
""" Test that the payment method is compatible with its supported currencies. """
self.payment_method.supported_currency_ids = [Command.set([self.currency_euro.id])]
compatible_payment_methods = self.env['payment.method']._get_compatible_payment_methods(
self.provider.ids, self.partner.id, currency_id=self.currency_euro.id
)
self.assertIn(self.payment_method, compatible_payment_methods)
def test_payment_method_not_compatible_with_unsupported_currencies(self):
""" Test that the payment method is not compatible with a currency that is not
supported. """
self.payment_method.supported_currency_ids = [Command.set([self.currency_euro.id])]
compatible_payment_methods = self.env['payment.method']._get_compatible_payment_methods(
self.provider.ids, self.partner.id, currency_id=self.currency_usd.id
)
self.assertNotIn(self.payment_method, compatible_payment_methods)
def test_payment_method_compatible_when_no_supported_currencies_set(self):
""" Test that the payment method is always compatible when no supported currencies are
set. """
self.payment_method.supported_currency_ids = [Command.clear()]
compatible_payment_methods = self.env['payment.method']._get_compatible_payment_methods(
self.provider.ids, self.partner.id, currency_id=self.currency_euro.id
)
self.assertIn(self.payment_method, compatible_payment_methods)
def test_payment_method_compatible_when_tokenization_forced(self):
""" Test that the payment method is compatible when it supports tokenization while it is
forced by the calling module. """
self.payment_method.support_tokenization = True
compatible_payment_methods = self.env['payment.method']._get_compatible_payment_methods(
self.provider.ids, self.partner.id, force_tokenization=True
)
self.assertIn(self.payment_method, compatible_payment_methods)
def test_payment_method_not_compatible_when_tokenization_forced(self):
""" Test that the payment method is not compatible when it does not support tokenization
while it is forced by the calling module. """
self.payment_method.support_tokenization = False
compatible_payment_methods = self.env['payment.method']._get_compatible_payment_methods(
self.provider.ids, self.partner.id, force_tokenization=True
)
self.assertNotIn(self.payment_method, compatible_payment_methods)
def test_payment_method_compatible_with_express_checkout(self):
""" Test that the payment method is compatible when it supports express checkout while it is
an express checkout flow. """
self.payment_method.support_express_checkout = True
compatible_payment_methods = self.env['payment.method']._get_compatible_payment_methods(
self.provider.ids, self.partner.id, is_express_checkout=True
)
self.assertIn(self.payment_method, compatible_payment_methods)
def test_payment_method_not_compatible_with_express_checkout(self):
""" Test that the payment method is not compatible when it does not support express checkout
while it is an express checkout flow. """
self.payment_method.support_express_checkout = False
compatible_payment_methods = self.env['payment.method']._get_compatible_payment_methods(
self.provider.ids, self.partner.id, is_express_checkout=True
)
self.assertNotIn(self.payment_method, compatible_payment_methods)
def test_availability_report_covers_all_reasons(self):
""" Test that every possible unavailability reason is correctly reported. """
# Disable all payment methods.
pms = self.env['payment.method'].search([('is_primary', '=', True)])
pms.active = False
# Prepare a base payment method.
self.payment_method.write({
'active': True,
'support_express_checkout': True,
'support_tokenization': True,
})
# Prepare the report with a provider to allow checking provider availability.
report = {}
payment_utils.add_to_report(report, self.provider)
# Prepare a payment method with an unavailable provider.
unavailable_provider = self.provider.copy()
payment_utils.add_to_report(report, unavailable_provider, available=False, reason="test")
no_provider_pm = self.payment_method.copy()
no_provider_pm.provider_ids = [Command.set([unavailable_provider.id])]
unavailable_provider.payment_method_ids = [Command.set([no_provider_pm.id])]
# Prepare a payment method with an incompatible country.
invalid_country_pm = self.payment_method.copy()
belgium = self.env.ref('base.be')
invalid_country_pm.supported_country_ids = [Command.set([belgium.id])]
france = self.env.ref('base.fr')
self.partner.country_id = france
# Prepare a payment method with an incompatible currency.
invalid_currency_pm = self.payment_method.copy()
invalid_currency_pm.supported_currency_ids = [Command.set([self.currency_euro.id])]
# Prepare a payment method without support for tokenization.
no_tokenization_pm = self.payment_method.copy()
no_tokenization_pm.support_tokenization = False
# Prepare a payment method without support for express checkout.
no_express_checkout_pm = self.payment_method.copy()
no_express_checkout_pm.support_express_checkout = False
# Get compatible payment methods to generate their availability report.
self.env['payment.method']._get_compatible_payment_methods(
self.provider.ids,
self.partner.id,
currency_id=self.currency_usd.id,
force_tokenization=True,
is_express_checkout=True,
report=report,
)
# Compare the generated payment methods report with the expected one.
expected_pms_report = {
self.payment_method: {
'available': True,
'reason': '',
'supported_providers': [(self.provider, True)],
},
no_provider_pm: {
'available': False,
'reason': REPORT_REASONS_MAPPING['provider_not_available'],
'supported_providers': [(unavailable_provider, False)],
},
invalid_country_pm: {
'available': False,
'reason': REPORT_REASONS_MAPPING['incompatible_country'],
'supported_providers': [(self.provider, True)],
},
invalid_currency_pm: {
'available': False,
'reason': REPORT_REASONS_MAPPING['incompatible_currency'],
'supported_providers': [(self.provider, True)],
},
no_tokenization_pm: {
'available': False,
'reason': REPORT_REASONS_MAPPING['tokenization_not_supported'],
'supported_providers': [(self.provider, True)],
},
no_express_checkout_pm: {
'available': False,
'reason': REPORT_REASONS_MAPPING['express_checkout_not_supported'],
'supported_providers': [(self.provider, True)],
},
}
self.maxDiff = None
self.assertDictEqual(report['payment_methods'], expected_pms_report)

View file

@ -1,12 +1,100 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from json.decoder import JSONDecodeError
from unittest.mock import patch
import requests
from odoo.exceptions import ValidationError
from odoo.fields import Command
from odoo.tests import tagged
from odoo.tools import mute_logger
from odoo.addons.payment.const import REPORT_REASONS_MAPPING
from odoo.addons.payment.tests.common import PaymentCommon
@tagged('-at_install', 'post_install')
class TestPaymentprovider(PaymentCommon):
class TestPaymentProvider(PaymentCommon):
def test_changing_provider_state_archives_tokens(self):
""" Test that all active tokens of a provider are archived when its state is changed. """
for old_state in ('enabled', 'test'): # No need to check when the provided was disabled.
for new_state in ('enabled', 'test', 'disabled'):
if old_state != new_state: # No need to check when the state is unchanged.
self.provider.state = old_state
token = self._create_token()
self.provider.state = new_state
self.assertFalse(token.active)
def test_enabling_provider_activates_default_payment_methods(self):
""" Test that the default payment methods of a provider are activated when it is
enabled. """
self.payment_methods.active = False
for new_state in ('enabled', 'test'):
self.provider.state = 'disabled'
with patch(
'odoo.addons.payment.models.payment_provider.PaymentProvider'
'._get_default_payment_method_codes', return_value={self.payment_method_code},
):
self.provider.state = new_state
self.assertTrue(self.payment_methods.active)
def test_enabling_manual_capture_provider_activates_compatible_default_pms(self):
"""Test that only payment methods supporting manual capture are activated when a provider
requiring manual capture is enabled."""
payment_method_with_manual_capture = self.env['payment.method'].create({
'name': 'Payment Method With Manual Capture',
'code': 'pm_with_manual_capture',
'support_manual_capture': 'full_only',
})
self.provider.state = 'disabled'
self.provider.capture_manually = True
self.provider.payment_method_ids = [Command.set([
self.payment_method.id, payment_method_with_manual_capture.id
])]
self.payment_method.support_manual_capture = 'none'
default_codes = {self.payment_method_code, payment_method_with_manual_capture.code}
with patch(
'odoo.addons.payment.models.payment_provider.PaymentProvider'
'._get_default_payment_method_codes', return_value=default_codes,
):
self.provider.state = 'test'
self.assertFalse(self.payment_methods.active)
self.assertTrue(payment_method_with_manual_capture.active)
def test_disabling_provider_deactivates_default_payment_methods(self):
""" Test that the default payment methods of a provider are deactivated when it is
disabled. """
self.payment_methods.active = True
for old_state in ('enabled', 'test'):
self.provider.state = old_state
with patch(
'odoo.addons.payment.models.payment_provider.PaymentProvider'
'._get_default_payment_method_codes', return_value=self.payment_method_code,
):
self.provider.state = 'disabled'
self.assertFalse(self.payment_methods.active)
def test_enabling_provider_activates_processing_cron(self):
""" Test that the post-processing cron is activated when a provider is enabled. """
self.env['payment.provider'].search([]).state = 'disabled' # Reset providers' state.
post_processing_cron = self.env.ref('payment.cron_post_process_payment_tx')
for enabled_state in ('enabled', 'test'):
post_processing_cron.active = False # Reset the cron's active field.
self.provider.state = 'disabled' # Prepare the dummy provider for enabling.
self.provider.state = enabled_state
self.assertTrue(post_processing_cron.active)
def test_disabling_provider_deactivates_processing_cron(self):
""" Test that the post-processing cron is deactivated when a provider is disabled. """
self.env['payment.provider'].search([]).state = 'disabled' # Reset providers' state.
post_processing_cron = self.env.ref('payment.cron_post_process_payment_tx')
for enabled_state in ('enabled', 'test'):
post_processing_cron.active = True # Reset the cron's active field.
self.provider.state = enabled_state # Prepare the dummy provider for disabling.
self.provider.state = 'disabled'
self.assertFalse(post_processing_cron.active)
def test_published_provider_compatible_with_all_users(self):
""" Test that a published provider is always available to all users. """
@ -38,13 +126,56 @@ class TestPaymentprovider(PaymentCommon):
)
self.assertNotIn(self.provider, compatible_providers)
def test_provider_compatible_with_branch_companies(self):
""" Test that the provider is available to branch companies. """
branch_company = self.env['res.company'].create({
'name': "Provider Branch Company",
'parent_id': self.provider.company_id.id,
})
compatible_providers = self.provider._get_compatible_providers(
branch_company.id, self.partner.id, self.amount,
)
self.assertIn(self.provider, compatible_providers)
def test_provider_compatible_with_available_countries(self):
""" Test that the provider is compatible with its available countries. """
belgium = self.env.ref('base.be')
self.provider.available_country_ids = [Command.set([belgium.id])]
self.partner.country_id = belgium
compatible_providers = self.provider._get_compatible_providers(
self.company.id, self.partner.id, self.amount
)
self.assertIn(self.provider, compatible_providers)
def test_provider_not_compatible_with_unavailable_countries(self):
""" Test that the provider is not compatible with a country that is not available. """
belgium = self.env.ref('base.be')
self.provider.available_country_ids = [Command.set([belgium.id])]
france = self.env.ref('base.fr')
self.partner.country_id = france
compatible_providers = self.provider._get_compatible_providers(
self.company.id, self.partner.id, self.amount
)
self.assertNotIn(self.provider, compatible_providers)
def test_provider_compatible_when_no_available_countries_set(self):
""" Test that the provider is always compatible when no available countries are set. """
self.provider.available_country_ids = [Command.clear()]
belgium = self.env.ref('base.be')
self.partner.country_id = belgium
compatible_providers = self.provider._get_compatible_providers(
self.company.id, self.partner.id, self.amount
)
self.assertIn(self.provider, compatible_providers)
def test_provider_compatible_when_maximum_amount_is_zero(self):
""" Test that the maximum amount has no effect on the provider's compatibility when it is
set to 0. """
self.provider.maximum_amount = 0.
currency = self.provider.main_currency_id.id
compatible_providers = self.env['payment.provider']._get_compatible_providers(
self.company.id, self.partner.id, self.amount, currency_id=self.env.company.currency_id.id,
self.company.id, self.partner.id, self.amount, currency_id=currency
)
self.assertIn(self.provider, compatible_providers)
@ -52,9 +183,10 @@ class TestPaymentprovider(PaymentCommon):
""" Test that a provider is compatible when the payment amount is less than the maximum
amount. """
self.provider.maximum_amount = self.amount + 10.0
currency = self.provider.main_currency_id.id
compatible_providers = self.env['payment.provider']._get_compatible_providers(
self.company.id, self.partner.id, self.amount, currency_id=self.env.company.currency_id.id,
self.company.id, self.partner.id, self.amount, currency_id=currency
)
self.assertIn(self.provider, compatible_providers)
@ -62,8 +194,240 @@ class TestPaymentprovider(PaymentCommon):
""" Test that a provider is not compatible when the payment amount is more than the maximum
amount. """
self.provider.maximum_amount = self.amount - 10.0
currency = self.provider.main_currency_id.id
compatible_providers = self.env['payment.provider']._get_compatible_providers(
self.company.id, self.partner.id, self.amount, currency_id=self.env.company.currency_id.id,
self.company.id, self.partner.id, self.amount, currency_id=currency
)
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_currencies_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_provider_compatible_when_tokenization_forced(self):
""" Test that the provider is compatible when it allows tokenization while it is forced by
the calling module. """
self.provider.allow_tokenization = True
compatible_providers = self.provider._get_compatible_providers(
self.company.id, self.partner.id, self.amount, force_tokenization=True
)
self.assertIn(self.provider, compatible_providers)
def test_provider_not_compatible_when_tokenization_forced(self):
""" Test that the provider is not compatible when it does not allow tokenization while it
is forced by the calling module. """
self.provider.allow_tokenization = False
compatible_providers = self.provider._get_compatible_providers(
self.company.id, self.partner.id, self.amount, force_tokenization=True
)
self.assertNotIn(self.provider, compatible_providers)
def test_provider_compatible_when_tokenization_required(self):
""" Test that the provider is compatible when it allows tokenization while it is required by
the payment context (e.g., when paying for a subscription). """
self.provider.allow_tokenization = True
with patch(
'odoo.addons.payment.models.payment_provider.PaymentProvider._is_tokenization_required',
return_value=True,
):
compatible_providers = self.provider._get_compatible_providers(
self.company.id, self.partner.id, self.amount
)
self.assertIn(self.provider, compatible_providers)
def test_provider_not_compatible_when_tokenization_required(self):
""" Test that the provider is not compatible when it does not allow tokenization while it
is required by the payment context (e.g., when paying for a subscription). """
self.provider.allow_tokenization = False
with patch(
'odoo.addons.payment.models.payment_provider.PaymentProvider._is_tokenization_required',
return_value=True,
):
compatible_providers = self.provider._get_compatible_providers(
self.company.id, self.partner.id, self.amount
)
self.assertNotIn(self.provider, compatible_providers)
def test_provider_compatible_with_express_checkout(self):
""" Test that the provider is compatible when it allows express checkout while it is an
express checkout flow. """
self.provider.allow_express_checkout = True
compatible_providers = self.provider._get_compatible_providers(
self.company.id, self.partner.id, self.amount, is_express_checkout=True
)
self.assertIn(self.provider, compatible_providers)
def test_provider_not_compatible_with_express_checkout(self):
""" Test that the provider is not compatible when it does not allow express checkout while
it is an express checkout flow. """
self.provider.allow_express_checkout = False
compatible_providers = self.provider._get_compatible_providers(
self.company.id, self.partner.id, self.amount, is_express_checkout=True
)
self.assertNotIn(self.provider, compatible_providers)
def test_availability_report_covers_all_reasons(self):
""" Test that every possible unavailability reason is correctly reported. """
# Disable all providers.
providers = self.env['payment.provider'].search([])
providers.state = 'disabled'
# Prepare a base provider.
self.provider.write({
'state': 'test',
'allow_express_checkout': True,
'allow_tokenization': True,
})
# Prepare a provider with an incompatible country.
invalid_country_provider = self.provider.copy()
belgium = self.env.ref('base.be')
invalid_country_provider.write({
'state': 'test',
'available_country_ids': [Command.set([belgium.id])],
})
france = self.env.ref('base.fr')
self.partner.country_id = france
# Prepare a provider with a maximum amount lower than the payment amount.
exceeding_max_provider = self.provider.copy()
exceeding_max_provider.write({
'state': 'test',
'maximum_amount': self.amount - 10.0,
})
# Prepare a provider with an incompatible currency.
invalid_currency_provider = self.provider.copy()
invalid_currency_provider.write({
'state': 'test',
'available_currency_ids': [Command.unlink(self.currency_usd.id)],
})
# Prepare a provider without tokenization support.
no_tokenization_provider = self.provider.copy()
no_tokenization_provider.write({
'state': 'test',
'allow_tokenization': False,
})
# Prepare a provider without express checkout support.
no_express_checkout_provider = self.provider.copy()
no_express_checkout_provider.write({
'state': 'test',
'allow_express_checkout': False,
})
# Get compatible providers to generate their availability report.
report = {}
self.env['payment.provider']._get_compatible_providers(
self.company_id,
self.partner.id,
self.amount,
currency_id=self.currency_usd.id,
force_tokenization=True,
is_express_checkout=True,
report=report,
)
# Compare the generated providers report with the expected one.
expected_providers_report = {
self.provider: {
'available': True,
'reason': '',
},
invalid_country_provider: {
'available': False,
'reason': REPORT_REASONS_MAPPING['incompatible_country'],
},
exceeding_max_provider: {
'available': False,
'reason': REPORT_REASONS_MAPPING['exceed_max_amount'],
},
invalid_currency_provider: {
'available': False,
'reason': REPORT_REASONS_MAPPING['incompatible_currency'],
},
no_tokenization_provider: {
'available': False,
'reason': REPORT_REASONS_MAPPING['tokenization_not_supported'],
},
no_express_checkout_provider: {
'available': False,
'reason': REPORT_REASONS_MAPPING['express_checkout_not_supported'],
},
}
self.maxDiff = None
self.assertDictEqual(report['providers'], expected_providers_report)
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)
@mute_logger('odoo.addons.payment.models.payment_provider')
def test_parsing_non_json_response_falls_back_to_text_response(self):
"""Test that a non-JSON response is smoothly parsed as a text response."""
response = requests.Response()
response.status_code = 502
response._content = b"<html><body>Cloudflare Error</body></html>"
with (
patch('requests.request', return_value=response),
patch(
'odoo.addons.payment.models.payment_provider.PaymentProvider._parse_response_error',
new=lambda _self, _response: _response.json(),
),
):
try:
self.provider._send_api_request('GET', '/dummy')
except Exception as e: # noqa: BLE001
self.assertNotIsInstance(e, JSONDecodeError)
self.assertIsInstance(e, ValidationError)
self.assertIn("Cloudflare Error", e.args[0])

View file

@ -4,8 +4,9 @@ from datetime import date
from freezegun import freeze_time
from odoo.exceptions import UserError
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tests import tagged
from odoo.tools import mute_logger
from odoo.addons.payment.tests.common import PaymentCommon
@ -13,9 +14,31 @@ from odoo.addons.payment.tests.common import PaymentCommon
@tagged('-at_install', 'post_install')
class TestPaymentToken(PaymentCommon):
def test_token_cannot_be_unarchived(self):
""" Test that unarchiving disabled tokens is forbidden. """
@mute_logger('odoo.addons.base.models.ir_rule')
def test_users_have_no_access_to_other_users_tokens(self):
users = [self.public_user, self.portal_user, self.internal_user]
token = self._create_token(partner_id=self.admin_partner.id)
for user in users:
with self.assertRaises(AccessError):
token.with_user(user).read()
def test_cannot_assign_token_to_public_partner(self):
""" Test that no token can be assigned to the public partner. """
token = self._create_token()
with self.assertRaises(ValidationError):
token.partner_id = self.public_user.partner_id
def test_unarchiving_token_requires_active_provider(self):
""" Test that unarchiving disabled tokens is forbidden if the provider is disabled. """
token = self._create_token(active=False)
token.provider_id.state = 'disabled'
with self.assertRaises(UserError):
token.active = True
def test_unarchiving_token_requires_active_payment_method(self):
""" Test that unarchiving disabled tokens is forbidden if the method is disabled. """
token = self._create_token(active=False)
token.payment_method_id.active = False
with self.assertRaises(UserError):
token.active = True
@ -24,7 +47,7 @@ class TestPaymentToken(PaymentCommon):
token = self._create_token()
self.assertEqual(token._build_display_name(), '•••• 1234')
@freeze_time('2024-1-31 10:00:00')
@freeze_time('2024-01-31 10:00:00')
def test_display_name_for_empty_payment_details(self):
""" Test that the display name is still built for token without payment details. """
token = self._create_token(payment_details='')

View file

@ -1,7 +1,10 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import patch
from odoo.exceptions import AccessError
from odoo.tests import tagged
from odoo.tools import mute_logger
from odoo.addons.payment.tests.common import PaymentCommon
@ -9,28 +12,38 @@ from odoo.addons.payment.tests.common import PaymentCommon
@tagged('-at_install', 'post_install')
class TestPaymentTransaction(PaymentCommon):
def test_is_live_when_created_by_enabled_provider(self):
self.provider.state = 'enabled'
tx = self._create_transaction('redirect')
self.assertTrue(tx.is_live)
def test_is_not_live_when_created_by_test_provider(self):
self.provider.state = 'test' # Will work with anything other than 'enabled'
tx = self._create_transaction('redirect')
self.assertFalse(tx.is_live)
def test_capture_allowed_for_authorized_users(self):
""" Test that users who have access to a transaction can capture it. """
if 'account' not in self.env["ir.module.module"]._installed():
self.skipTest("account module is not installed")
self.provider.support_manual_capture = True
if not self.env.ref('account.group_account_invoice', raise_if_not_found=False):
self.skipTest("account needed for test")
self.provider.support_manual_capture = 'full_only'
tx = self._create_transaction('redirect', state='authorized')
user = self._prepare_user(self.internal_user, 'account.group_account_invoice')
self._assert_does_not_raise(AccessError, tx.with_user(user).action_capture)
def test_void_allowed_for_authorized_users(self):
""" Test that users who have access to a transaction can void it. """
if 'account' not in self.env["ir.module.module"]._installed():
self.skipTest("account module is not installed")
self.provider.support_manual_capture = True
if not self.env.ref('account.group_account_invoice', raise_if_not_found=False):
self.skipTest("account needed for test")
self.provider.support_manual_capture = 'full_only'
tx = self._create_transaction('redirect', state='authorized')
user = self._prepare_user(self.internal_user, 'account.group_account_invoice')
self._assert_does_not_raise(AccessError, tx.with_user(user).action_void)
def test_refund_allowed_for_authorized_users(self):
""" Test that users who have access to a transaction can refund it. """
if 'account' not in self.env["ir.module.module"]._installed():
self.skipTest("account module is not installed")
if not self.env.ref('account.group_account_invoice', raise_if_not_found=False):
self.skipTest("account needed for test")
self.provider.support_refund = 'full_only'
tx = self._create_transaction('redirect', state='done')
user = self._prepare_user(self.internal_user, 'account.group_account_invoice')
@ -38,13 +51,13 @@ class TestPaymentTransaction(PaymentCommon):
def test_capture_blocked_for_unauthorized_user(self):
""" Test that users who don't have access to a transaction cannot capture it. """
self.provider.support_manual_capture = True
self.provider.support_manual_capture = 'full_only'
tx = self._create_transaction('redirect', state='authorized')
self.assertRaises(AccessError, tx.with_user(self.internal_user).action_capture)
def test_void_blocked_for_unauthorized_user(self):
""" Test that users who don't have access to a transaction cannot void it. """
self.provider.support_manual_capture = True
self.provider.support_manual_capture = 'full_only'
tx = self._create_transaction('redirect', state='authorized')
self.assertRaises(AccessError, tx.with_user(self.internal_user).action_void)
@ -66,7 +79,7 @@ class TestPaymentTransaction(PaymentCommon):
state='done',
operation=operation, # Override the computed flow
source_transaction_id=tx.id,
)._reconcile_after_done()
)._post_process()
self.assertEqual(
tx.refunds_count,
@ -74,12 +87,30 @@ class TestPaymentTransaction(PaymentCommon):
msg="The refunds count should only consider transactions with operation 'refund'."
)
def test_capturing_tx_creates_child_tx(self):
"""Test that capturing a transaction creates a child capture transaction."""
self.provider.capture_manually = True
self.provider.support_manual_capture = 'partial'
source_tx = self._create_transaction('direct', state='authorized')
child_tx = source_tx._capture()
self.assertTrue(child_tx)
self.assertNotEqual(child_tx, source_tx)
def test_voiding_tx_creates_child_tx(self):
"""Test that voiding a transaction creates a child void transaction."""
self.provider.capture_manually = True
self.provider.support_manual_capture = 'partial'
source_tx = self._create_transaction('direct', state='authorized')
child_tx = source_tx._void()
self.assertTrue(child_tx)
self.assertNotEqual(child_tx, source_tx)
def test_refund_transaction_values(self):
self.provider.support_refund = 'partial'
tx = self._create_transaction('redirect', state='done')
# Test the default values of a full refund transaction
refund_tx = tx._create_refund_transaction()
refund_tx = tx._create_child_transaction(tx.amount, is_refund=True)
self.assertEqual(
refund_tx.reference,
f'R-{tx.reference}',
@ -99,7 +130,7 @@ class TestPaymentTransaction(PaymentCommon):
self.assertEqual(
refund_tx.currency_id,
tx.currency_id,
msg="The currency of the refund transaction should that of the source transaction."
msg="The currency of the refund transaction should be that of the source transaction."
)
self.assertEqual(
refund_tx.operation,
@ -114,11 +145,11 @@ class TestPaymentTransaction(PaymentCommon):
self.assertEqual(
refund_tx.partner_id,
tx.partner_id,
msg="The partner of the refund transaction should that of the source transaction."
msg="The partner of the refund transaction should be that of the source transaction."
)
# Test the values of a partial refund transaction with custom refund amount
partial_refund_tx = tx._create_refund_transaction(amount_to_refund=11.11)
partial_refund_tx = tx._create_child_transaction(11.11, is_refund=True)
self.assertAlmostEqual(
partial_refund_tx.amount,
-11.11,
@ -126,3 +157,199 @@ class TestPaymentTransaction(PaymentCommon):
msg="The amount of the refund transaction should be the negative value of the amount "
"to refund."
)
def test_partial_capture_transaction_values(self):
self.provider.support_manual_capture = 'partial'
self.provider.capture_manually = True
tx = self._create_transaction('redirect', state='authorized')
capture_tx = tx._create_child_transaction(11.11)
self.assertEqual(
capture_tx.reference,
f'P-{tx.reference}',
msg="The reference of a partial capture should be the prefixed reference of the source "
"transaction.",
)
self.assertEqual(
capture_tx.amount,
11.11,
msg="The amount of a partial capture should be the one passed as argument.",
)
self.assertEqual(
capture_tx.currency_id,
tx.currency_id,
msg="The currency of the partial capture should be that of the source transaction.",
)
self.assertEqual(
capture_tx.operation,
tx.operation,
msg="The operation of the partial capture should be the same as the source"
" transaction.",
)
self.assertEqual(
tx,
capture_tx.source_transaction_id,
msg="The partial capture transaction should be linked to the source transaction.",
)
self.assertEqual(
capture_tx.partner_id,
tx.partner_id,
msg="The partner of the partial capture should be that of the source transaction.",
)
def test_capturing_child_tx_triggers_source_tx_state_update(self):
self.provider.support_manual_capture = 'partial'
self.provider.capture_manually = True
source_tx = self._create_transaction(flow='direct', state='authorized')
child_tx_1 = source_tx._create_child_transaction(100)
with patch(
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
'._update_source_transaction_state'
) as patched:
child_tx_1._set_done()
patched.assert_called_once()
def test_voiding_child_tx_triggers_source_tx_state_update(self):
self.provider.support_manual_capture = 'partial'
self.provider.capture_manually = True
source_tx = self._create_transaction(flow='direct', state='authorized')
child_tx_1 = source_tx._create_child_transaction(100)
child_tx_1._set_done()
child_tx_2 = source_tx._create_child_transaction(source_tx.amount-100)
with patch(
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
'._update_source_transaction_state'
) as patched:
child_tx_2._set_canceled()
patched.assert_called_once()
def test_capturing_partial_amount_leaves_source_tx_authorized(self):
self.provider.support_manual_capture = 'partial'
self.provider.capture_manually = True
source_tx = self._create_transaction(flow='direct', state='authorized')
child_tx_1 = source_tx._create_child_transaction(100)
child_tx_1._set_done()
self.assertEqual(
source_tx.state,
'authorized',
msg="The whole amount of the source transaction has not been processed yet, it's state "
"should still be 'authorized'.",
)
def test_capturing_full_amount_confirms_source_tx(self):
self.provider.support_manual_capture = 'partial'
self.provider.capture_manually = True
source_tx = self._create_transaction(flow='direct', state='authorized')
child_tx_1 = source_tx._create_child_transaction(100)
child_tx_1._set_done()
child_tx_2 = source_tx._create_child_transaction(source_tx.amount - 100)
child_tx_2._set_canceled()
self.assertEqual(
source_tx.state,
'done',
msg="The whole amount of the source transaction has been processed, it's state is now "
"'done'."
)
def test_validate_amount_skips_validation_transactions(self):
"""Test that the amount validation is skipped for validation transactions."""
tx = self._create_transaction('redirect', operation='validation')
with patch(
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
'._extract_amount_data', return_value={'amount': None, 'currency_code': None},
):
tx._validate_amount({})
self.assertNotEqual(tx.state, 'error')
def test_processing_applies_updates_to_error_txs_with_valid_amount_data(self):
tx = self._create_transaction('redirect', state='error')
with patch(
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
'._validate_amount'
), patch(
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
'._apply_updates'
) as apply_updates_mock:
tx._process('test', {})
self.assertEqual(apply_updates_mock.call_count, 1)
def test_processing_does_not_apply_updates_when_amount_data_is_invalid(self):
tx = self._create_transaction('redirect', state='draft', amount=100)
with patch(
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
'._extract_amount_data', return_value={'amount': 10, 'currency_code': 'USD'}
), patch(
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
'._apply_updates'
) as apply_updates_mock:
tx._process('test', {})
self.assertEqual(tx.state, 'error')
self.assertEqual(apply_updates_mock.call_count, 0)
def test_processing_tokenizes_validated_transaction(self):
"""Test that `_process` tokenizes 'authorized' and 'done' transactions when possible."""
self.provider.support_manual_capture = 'partial'
self.provider.capture_manually = True
for state in ['authorized', 'done']:
tx = self._create_transaction(
'redirect', reference=f'Test {state}', state=state, tokenize=True
)
with patch(
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
'._validate_amount', return_value=None
), patch(
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
'._extract_token_values', return_value={'provider_ref': 'test'}
):
tx._process('test', {})
self.assertTrue(tx.token_id)
def test_processing_only_tokenizes_when_requested(self):
"""Test that `_process` only triggers tokenization if the user requested it."""
tx = self._create_transaction('redirect', state='done', tokenize=False)
with patch(
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
'._validate_amount', return_value=None
), patch(
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
'._tokenize'
) as tokenize_mock:
tx._process('test', {})
self.assertEqual(tokenize_mock.call_count, 0)
@mute_logger('odoo.addons.payment.models.payment_transaction')
def test_update_state_to_illegal_target_state(self):
tx = self._create_transaction('redirect', state='done')
tx._update_state(['draft', 'pending', 'authorized'], 'cancel', None)
self.assertEqual(tx.state, 'done')
def test_update_state_to_extra_allowed_state(self):
tx = self._create_transaction('redirect', state='done')
tx._update_state(
['draft', 'pending', 'authorized', 'done'], 'cancel', None
)
self.assertEqual(tx.state, 'cancel')
def test_updating_state_resets_post_processing_status(self):
if self.account_payment_installed:
self.skipTest("This test should not be run after account_payment is installed.")
tx = self._create_transaction('redirect', state='draft')
tx._set_pending()
self.assertFalse(tx.is_post_processed)
tx._post_process()
self.assertTrue(tx.is_post_processed)
tx._set_done()
self.assertFalse(tx.is_post_processed)
def test_validate_amount_uses_payment_minor_unit(self):
self.currency_euro.rounding = 0.001
tx = self._create_transaction('direct', amount=123.452, currency_id=self.currency_euro.id)
with patch(
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
'._extract_amount_data',
return_value={'amount': 123.45, 'currency_code': self.currency_euro.name},
):
tx._validate_amount({})
self.assertNotEqual(tx.state, 'error')

View file

@ -0,0 +1,24 @@
from odoo.tests import tagged
from odoo.addons.payment.tests.common import PaymentCommon
@tagged('-at_install', 'post_install')
class TestResCompany(PaymentCommon):
def test_creating_company_duplicates_providers(self):
"""Ensure that installed payment providers of an existing company are correctly duplicated
when a new company is created."""
main_company = self.env.company
main_company_providers_count = self.env['payment.provider'].search_count([
('company_id', '=', main_company.id),
('module_state', '=', 'installed'),
])
new_company = self.env['res.company'].create({'name': 'New Company'})
new_company_providers_count = self.env['payment.provider'].search_count([
('company_id', '=', new_company.id),
('module_state', '=', 'installed'),
])
self.assertEqual(new_company_providers_count, main_company_providers_count)