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

@ -1,10 +1,11 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import urllib.parse
import werkzeug
from werkzeug.exceptions import BadRequest, Forbidden, NotFound
from odoo import _, http
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.exceptions import AccessError
from odoo.http import request
from odoo.addons.payment import utils as payment_utils
@ -38,7 +39,7 @@ class PaymentPortal(portal.CustomerPortal):
)
def payment_pay(
self, reference=None, amount=None, currency_id=None, partner_id=None, company_id=None,
provider_id=None, access_token=None, **kwargs
access_token=None, **kwargs
):
""" Display the payment form with optional filtering of payment options.
@ -47,33 +48,34 @@ class PaymentPortal(portal.CustomerPortal):
In addition to the desired filtering, a second one ensures that none of the following
rules is broken:
- Public users are not allowed to save their payment method as a token.
- Payments made by public users should either *not* be made on behalf of a specific
partner or have an access token validating the partner, amount and currency.
We let access rights and security rules do their job for logged in users.
:param str reference: The custom prefix to compute the full reference
:param str amount: The amount to pay
:param str currency_id: The desired currency, as a `res.currency` id
:param str partner_id: The partner making the payment, as a `res.partner` id
:param str company_id: The related company, as a `res.company` id
:param str provider_id: The desired provider, as a `payment.provider` id
:param str access_token: The access token used to authenticate the partner
- Public users are not allowed to save their payment method as a token.
- Payments made by public users should either *not* be made on behalf of a specific partner
or have an access token validating the partner, amount and currency.
We let access rights and security rules do their job for logged users.
:param str reference: The custom prefix to compute the full reference.
:param str amount: The amount to pay.
:param str currency_id: The desired currency, as a `res.currency` id.
:param str partner_id: The partner making the payment, as a `res.partner` id.
:param str company_id: The related company, as a `res.company` id.
:param str access_token: The access token used to authenticate the partner.
:param dict kwargs: Optional data passed to helper methods.
:return: The rendered checkout form
:return: The rendered payment form.
:rtype: str
:raise: werkzeug.exceptions.NotFound if the access token is invalid
:raise NotFound: If the access token is invalid.
"""
# Cast numeric parameters as int or float and void them if their str value is malformed
currency_id, provider_id, partner_id, company_id = tuple(map(
self._cast_as_int, (currency_id, provider_id, partner_id, company_id)
currency_id, partner_id, company_id = tuple(map(
self._cast_as_int, (currency_id, partner_id, company_id)
))
amount = self._cast_as_float(amount)
# Raise an HTTP 404 if a partner is provided with an invalid access token
if partner_id:
if not payment_utils.check_access_token(access_token, partner_id, amount, currency_id):
raise werkzeug.exceptions.NotFound() # Don't leak information about ids.
raise NotFound() # Don't leak information about ids.
user_sudo = request.env.user
logged_in = not user_sudo._is_public()
@ -104,60 +106,76 @@ class PaymentPortal(portal.CustomerPortal):
# Make sure that the currency exists and is active
currency = request.env['res.currency'].browse(currency_id).exists()
if not currency or not currency.active:
raise werkzeug.exceptions.NotFound() # The currency must exist and be active.
raise NotFound() # The currency must exist and be active.
# Select all providers and tokens that match the constraints
availability_report = {}
# Select all the payment methods and tokens that match the payment context.
providers_sudo = request.env['payment.provider'].sudo()._get_compatible_providers(
company_id, partner_sudo.id, amount, currency_id=currency.id, **kwargs
) # In sudo mode to read the fields of providers and partner (if not logged in)
if provider_id in providers_sudo.ids: # Only keep the desired provider if it's suitable
providers_sudo = providers_sudo.browse(provider_id)
payment_tokens = request.env['payment.token'].search(
[('provider_id', 'in', providers_sudo.ids), ('partner_id', '=', partner_sudo.id)]
) if logged_in else request.env['payment.token']
company_id,
partner_sudo.id,
amount,
currency_id=currency.id,
report=availability_report,
**kwargs,
) # In sudo mode to read the fields of providers and partner (if logged out).
payment_methods_sudo = request.env['payment.method'].sudo()._get_compatible_payment_methods(
providers_sudo.ids,
partner_sudo.id,
currency_id=currency.id,
report=availability_report,
**kwargs,
) # In sudo mode to read the fields of providers.
tokens_sudo = request.env['payment.token'].sudo()._get_available_tokens(
providers_sudo.ids, partner_sudo.id
) # In sudo mode to be able to read tokens of other partners and the fields of providers.
# Make sure that the partner's company matches the company passed as parameter.
if not PaymentPortal._can_partner_pay_in_company(partner_sudo, company):
providers_sudo = request.env['payment.provider'].sudo()
payment_tokens = request.env['payment.token']
# Compute the fees taken by providers supporting the feature
fees_by_provider = {
provider_sudo: provider_sudo._compute_fees(amount, currency, partner_sudo.country_id)
for provider_sudo in providers_sudo.filtered('fees_active')
}
company_mismatch = not PaymentPortal._can_partner_pay_in_company(partner_sudo, company)
# Generate a new access token in case the partner id or the currency id was updated
access_token = payment_utils.generate_access_token(partner_sudo.id, amount, currency.id)
rendering_context = {
'providers': providers_sudo,
'tokens': payment_tokens,
'fees_by_provider': fees_by_provider,
'show_tokenize_input': self._compute_show_tokenize_input_mapping(
providers_sudo, logged_in=logged_in, **kwargs
portal_page_values = {
'res_company': company, # Display the correct logo in a multi-company environment.
'company_mismatch': company_mismatch,
'expected_company': company,
'partner_is_different': partner_is_different,
}
payment_form_values = {
'show_tokenize_input_mapping': self._compute_show_tokenize_input_mapping(
providers_sudo, **kwargs
),
}
payment_context = {
'reference_prefix': reference,
'amount': amount,
'currency': currency,
'partner_id': partner_sudo.id,
'access_token': access_token,
'providers_sudo': providers_sudo,
'payment_methods_sudo': payment_methods_sudo,
'tokens_sudo': tokens_sudo,
'availability_report': availability_report,
'transaction_route': '/payment/transaction',
'landing_route': '/payment/confirmation',
'res_company': company, # Display the correct logo in a multi-company environment
'partner_is_different': partner_is_different,
**self._get_custom_rendering_context_values(**kwargs),
'access_token': access_token,
}
rendering_context = {
**portal_page_values,
**payment_form_values,
**payment_context,
**self._get_extra_payment_form_values(
**payment_context, currency_id=currency.id, **kwargs
), # Pass the payment context to allow overriding modules to check document access.
}
return request.render(self._get_payment_page_template_xmlid(**kwargs), rendering_context)
@staticmethod
def _compute_show_tokenize_input_mapping(providers_sudo, logged_in=False, **kwargs):
def _compute_show_tokenize_input_mapping(providers_sudo, **kwargs):
""" Determine for each provider whether the tokenization input should be shown or not.
:param recordset providers_sudo: The providers for which to determine whether the
tokenization input should be shown or not, as a sudoed
`payment.provider` recordset.
:param bool logged_in: Whether the user is logged in or not.
:param dict kwargs: The optional data passed to the helper methods.
:return: The mapping of the computed value for each provider id.
:rtype: dict
@ -165,8 +183,7 @@ class PaymentPortal(portal.CustomerPortal):
show_tokenize_input_mapping = {}
for provider_sudo in providers_sudo:
show_tokenize_input = provider_sudo.allow_tokenization \
and not provider_sudo._is_tokenization_required(**kwargs) \
and logged_in
and not provider_sudo._is_tokenization_required(**kwargs)
show_tokenize_input_mapping[provider_sudo.id] = show_tokenize_input
return show_tokenize_input_mapping
@ -182,43 +199,63 @@ class PaymentPortal(portal.CustomerPortal):
:rtype: str
"""
partner_sudo = request.env.user.partner_id # env.user is always sudoed
availability_report = {}
# Select all the payment methods and tokens that match the payment context.
providers_sudo = request.env['payment.provider'].sudo()._get_compatible_providers(
request.env.company.id,
partner_sudo.id,
0., # There is no amount to pay with validation transactions.
force_tokenization=True,
is_validation=True,
)
# Get all partner's tokens for which providers are not disabled.
tokens_sudo = request.env['payment.token'].sudo().search([
('partner_id', 'in', [partner_sudo.id, partner_sudo.commercial_partner_id.id]),
('provider_id.state', 'in', ['enabled', 'test']),
])
report=availability_report,
**kwargs,
) # In sudo mode to read the fields of providers and partner (if logged out).
payment_methods_sudo = request.env['payment.method'].sudo()._get_compatible_payment_methods(
providers_sudo.ids,
partner_sudo.id,
force_tokenization=True,
report=availability_report,
) # In sudo mode to read the fields of providers.
tokens_sudo = request.env['payment.token'].sudo()._get_available_tokens(
None, partner_sudo.id, is_validation=True
) # In sudo mode to read the commercial partner's and providers' fields.
access_token = payment_utils.generate_access_token(partner_sudo.id, None, None)
rendering_context = {
'providers': providers_sudo,
'tokens': tokens_sudo,
payment_form_values = {
'mode': 'validation',
'allow_token_selection': False,
'allow_token_deletion': True,
}
payment_context = {
'reference_prefix': payment_utils.singularize_reference_prefix(prefix='V'),
'partner_id': partner_sudo.id,
'access_token': access_token,
'providers_sudo': providers_sudo,
'payment_methods_sudo': payment_methods_sudo,
'tokens_sudo': tokens_sudo,
'availability_report': availability_report,
'transaction_route': '/payment/transaction',
'landing_route': '/my/payment_method',
**self._get_custom_rendering_context_values(**kwargs),
'access_token': access_token,
}
rendering_context = {
**payment_form_values,
**payment_context,
**self._get_extra_payment_form_values(**kwargs),
}
return request.render('payment.payment_methods', rendering_context)
def _get_custom_rendering_context_values(self, **kwargs):
""" Return a dict of additional rendering context values.
def _get_extra_payment_form_values(self, **kwargs):
""" Return a dict of extra payment form values to include in the rendering context.
:param dict kwargs: Optional data. This parameter is not used here
:return: The dict of additional rendering context values
:param dict kwargs: Optional data. This parameter is not used here.
:return: The dict of extra payment form values.
:rtype: dict
"""
return {}
@http.route('/payment/transaction', type='json', auth='public')
@http.route('/payment/transaction', type='jsonrpc', auth='public')
def payment_transaction(self, amount, currency_id, partner_id, access_token, **kwargs):
""" Create a draft transaction and return its processing values.
@ -231,58 +268,61 @@ class PaymentPortal(portal.CustomerPortal):
:param dict kwargs: Locally unused data passed to `_create_transaction`
:return: The mandatory values for the processing of the transaction
:rtype: dict
:raise: ValidationError if the access token is invalid
:raise Forbidden: If the access token is invalid.
"""
# Check the access token against the transaction values
amount = amount and float(amount) # Cast as float in case the JS stripped the '.0'
if not payment_utils.check_access_token(access_token, partner_id, amount, currency_id):
raise ValidationError(_("The access token is invalid."))
raise Forbidden()
kwargs.pop('custom_create_values', None) # Don't allow passing arbitrary create values
self._validate_transaction_kwargs(kwargs, additional_allowed_keys=('reference_prefix',))
tx_sudo = self._create_transaction(
amount=amount, currency_id=currency_id, partner_id=partner_id, **kwargs
)
self._update_landing_route(tx_sudo, access_token) # Add the required parameters to the route
self._update_landing_route(tx_sudo, access_token) # Add the required params to the route.
return tx_sudo._get_processing_values()
def _create_transaction(
self, payment_option_id, reference_prefix, amount, currency_id, partner_id, flow,
tokenization_requested, landing_route, is_validation=False,
self, provider_id, payment_method_id, token_id, amount, currency_id, partner_id, flow,
tokenization_requested, landing_route, reference_prefix=None, is_validation=False,
custom_create_values=None, **kwargs
):
""" Create a draft transaction based on the payment context and return it.
:param int payment_option_id: The payment option handling the transaction, as a
`payment.provider` id or a `payment.token` id
:param str reference_prefix: The custom prefix to compute the full reference
:param float|None amount: The amount to pay in the given currency.
None if in a payment method validation operation
:param int|None currency_id: The currency of the transaction, as a `res.currency` id.
None if in a payment method validation operation
:param int partner_id: The partner making the payment, as a `res.partner` id
:param str flow: The online payment flow of the transaction: 'redirect', 'direct' or 'token'
:param bool tokenization_requested: Whether the user requested that a token is created
:param str landing_route: The route the user is redirected to after the transaction
:param bool is_validation: Whether the operation is a validation
:param dict custom_create_values: Additional create values overwriting the default ones
:param int provider_id: The provider of the provider payment method or token, as a
`payment.provider` id.
:param int|None payment_method_id: The payment method, if any, as a `payment.method` id.
:param int|None token_id: The token, if any, as a `payment.token` id.
:param float|None amount: The amount to pay, or `None` if in a validation operation.
:param int|None currency_id: The currency of the amount, as a `res.currency` id, or `None`
if in a validation operation.
:param int partner_id: The partner making the payment, as a `res.partner` id.
:param str flow: The online payment flow of the transaction: 'redirect', 'direct' or 'token'.
:param bool tokenization_requested: Whether the user requested that a token is created.
:param str landing_route: The route the user is redirected to after the transaction.
:param str reference_prefix: The custom prefix to compute the full reference.
:param bool is_validation: Whether the operation is a validation.
:param dict custom_create_values: Additional create values overwriting the default ones.
:param dict kwargs: Locally unused data passed to `_is_tokenization_required` and
`_compute_reference`
:return: The sudoed transaction that was created
:rtype: recordset of `payment.transaction`
:raise: UserError if the flow is invalid
`_compute_reference`.
:return: The sudoed transaction that was created.
:rtype: payment.transaction
"""
# Prepare create values
# Prepare the create values.
provider_sudo = request.env['payment.provider'].sudo().browse(provider_id)
tokenize = False
if flow in ['redirect', 'direct']: # Direct payment or payment with redirection
provider_sudo = request.env['payment.provider'].sudo().browse(payment_option_id)
payment_method_sudo = request.env['payment.method'].sudo().browse(payment_method_id)
token_id = None
tokenize = bool(
# Don't tokenize if the user tried to force it through the browser's developer tools
provider_sudo.allow_tokenization
and payment_method_sudo.support_tokenization
# Token is only created if required by the flow or requested by the user
and (provider_sudo._is_tokenization_required(**kwargs) or tokenization_requested)
)
elif flow == 'token': # Payment by token
token_sudo = request.env['payment.token'].sudo().browse(payment_option_id)
token_sudo = request.env['payment.token'].sudo().browse(token_id)
# Prevent from paying with a token that doesn't belong to the current partner (either
# the current user's partner if logged in, or the partner on behalf of whom the payment
@ -291,13 +331,7 @@ class PaymentPortal(portal.CustomerPortal):
if partner_sudo.commercial_partner_id != token_sudo.partner_id.commercial_partner_id:
raise AccessError(_("You do not have access to this payment token."))
provider_sudo = token_sudo.provider_id
token_id = payment_option_id
tokenize = False
else:
raise UserError(
_("The payment should either be direct, with redirection, or made by a token.")
)
payment_method_id = token_sudo.payment_method_id.id
reference = request.env['payment.transaction']._compute_reference(
provider_sudo.code,
@ -307,11 +341,15 @@ class PaymentPortal(portal.CustomerPortal):
)
if is_validation: # Providers determine the amount and currency in validation operations
amount = provider_sudo._get_validation_amount()
currency_id = provider_sudo._get_validation_currency().id
payment_method = request.env['payment.method'].browse(payment_method_id)
currency_id = provider_sudo.with_context(
validation_pm=payment_method # Will be converted to a kwarg in master.
)._get_validation_currency().id
# Create the transaction
tx_sudo = request.env['payment.transaction'].sudo().create({
'provider_id': provider_sudo.id,
'payment_method_id': payment_method_id,
'reference': reference,
'amount': amount,
'currency_id': currency_id,
@ -323,13 +361,13 @@ class PaymentPortal(portal.CustomerPortal):
**(custom_create_values or {}),
}) # In sudo mode to allow writing on callback fields
if flow == 'token':
tx_sudo._send_payment_request() # Payments by token process transactions immediately
else:
tx_sudo._log_sent_message()
if flow != 'token':
tx_sudo._log_sent_message() # Direct/Redirect payments go through the payment form.
elif not request.env.context.get('delay_token_charge'):
tx_sudo._charge_with_token() # Token payments are charged immediately.
# Monitor the transaction to make it available in the portal
PaymentPostProcessing.monitor_transactions(tx_sudo)
# Monitor the transaction to make it available in the portal.
PaymentPostProcessing.monitor_transaction(tx_sudo)
return tx_sudo
@ -360,7 +398,7 @@ class PaymentPortal(portal.CustomerPortal):
:param str tx_id: The transaction to confirm, as a `payment.transaction` id
:param str access_token: The access token used to verify the user
:param dict kwargs: Optional data. This parameter is not used here
:raise: werkzeug.exceptions.NotFound if the access token is invalid
:raise NotFound: If the access token is invalid.
"""
tx_id = self._cast_as_int(tx_id)
if tx_id:
@ -370,10 +408,7 @@ class PaymentPortal(portal.CustomerPortal):
if not payment_utils.check_access_token(
access_token, tx_sudo.partner_id.id, tx_sudo.amount, tx_sudo.currency_id.id
):
raise werkzeug.exceptions.NotFound() # Don't leak information about ids.
# Stop monitoring the transaction now that it reached a final state.
PaymentPostProcessing.remove_transactions(tx_sudo)
raise NotFound() # Don't leak information about ids.
# Display the payment confirmation page to the user
return request.render('payment.confirm', qcontext={'tx': tx_sudo})
@ -381,7 +416,7 @@ class PaymentPortal(portal.CustomerPortal):
# Display the portal homepage to the user
return request.redirect('/my/home')
@http.route('/payment/archive_token', type='json', auth='user')
@http.route('/payment/archive_token', type='jsonrpc', auth='user')
def archive_token(self, token_id):
""" Check that a user has write access on a token and archive the token if so.
@ -441,3 +476,36 @@ class PaymentPortal(portal.CustomerPortal):
:rtype: str
"""
return not partner.company_id or partner.company_id == document_company
@staticmethod
def _validate_transaction_kwargs(kwargs, additional_allowed_keys=()):
""" Verify that the keys of a transaction route's kwargs are all whitelisted.
The whitelist consists of all the keys that are expected to be passed to a transaction
route, plus optional contextually allowed keys.
This method must be called in all transaction routes to ensure that no undesired kwarg can
be passed as param and then injected in the create values of the transaction.
:param dict kwargs: The transaction route's kwargs to verify.
:param tuple additional_allowed_keys: The keys of kwargs that are contextually allowed.
:return: None
:raise BadRequest: If some kwargs keys are rejected.
"""
whitelist = {
'provider_id',
'payment_method_id',
'token_id',
'amount',
'flow',
'tokenization_requested',
'landing_route',
'is_validation',
'csrf_token',
}
whitelist.update(additional_allowed_keys)
rejected_keys = set(kwargs.keys()) - whitelist
if rejected_keys:
raise BadRequest(
_("The following kwargs are not whitelisted: %s", ', '.join(rejected_keys))
)

View file

@ -1,13 +1,14 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from datetime import timedelta
import psycopg2
from odoo import fields, http
from odoo import http
from odoo.http import request
from odoo.tools.translate import LazyTranslate
_lt = LazyTranslate(__name__)
_logger = logging.getLogger(__name__)
@ -22,118 +23,70 @@ class PaymentPostProcessing(http.Controller):
their post-processing.
"""
MONITORED_TX_IDS_KEY = '__payment_monitored_tx_ids__'
MONITORED_TX_ID_KEY = '__payment_monitored_tx_id__'
@http.route('/payment/status', type='http', auth='public', website=True, sitemap=False)
@http.route('/payment/status', type='http', auth='public', website=True, sitemap=False, list_as_website_content=_lt("Payment Status"))
def display_status(self, **kwargs):
""" Display the payment status page.
""" Fetch the transaction and display it on the payment status page.
:param dict kwargs: Optional data. This parameter is not used here
:return: The rendered status page
:rtype: str
"""
return request.render('payment.payment_status')
monitored_tx = self._get_monitored_transaction()
# The session might have expired, or the transaction never existed.
values = {'tx': monitored_tx} if monitored_tx else {'payment_not_found': True}
return request.render('payment.payment_status', values)
@http.route('/payment/status/poll', type='json', auth='public')
@http.route('/payment/status/poll', type='jsonrpc', auth='public')
def poll_status(self, **_kwargs):
""" Fetch the transactions to display on the status page and finalize their post-processing.
""" Fetch the transaction and trigger its post-processing.
:return: The post-processing values of the transactions
:return: The post-processing values of the transaction.
:rtype: dict
"""
# Retrieve recent user's transactions from the session
limit_date = fields.Datetime.now() - timedelta(days=1)
monitored_txs = request.env['payment.transaction'].sudo().search([
('id', 'in', self.get_monitored_transaction_ids()),
('last_state_change', '>=', limit_date)
])
if not monitored_txs: # The transaction was not correctly created
return {
'success': False,
'error': 'no_tx_found',
}
# We only poll the payment status if a payment was found, so the transaction should exist.
monitored_tx = self._get_monitored_transaction()
# Build the list of display values with the display message and post-processing values
display_values_list = []
for tx in monitored_txs:
display_message = None
if tx.state == 'pending':
display_message = tx.provider_id.pending_msg
elif tx.state == 'done':
display_message = tx.provider_id.done_msg
elif tx.state == 'cancel':
display_message = tx.provider_id.cancel_msg
display_values_list.append({
'display_message': display_message,
**tx._get_post_processing_values(),
})
# Stop monitoring already post-processed transactions
post_processed_txs = monitored_txs.filtered('is_post_processed')
self.remove_transactions(post_processed_txs)
# Finalize post-processing of transactions before displaying them to the user
txs_to_post_process = (monitored_txs - post_processed_txs).filtered(
lambda t: t.state == 'done'
)
success, error = True, None
try:
txs_to_post_process._finalize_post_processing()
except psycopg2.OperationalError: # A collision of accounting sequences occurred
request.env.cr.rollback() # Rollback and try later
success = False
error = 'tx_process_retry'
except Exception as e:
request.env.cr.rollback()
success = False
error = str(e)
_logger.exception(
"encountered an error while post-processing transactions with ids %s:\n%s",
', '.join([str(tx_id) for tx_id in txs_to_post_process.ids]), e
)
# Post-process the transaction before redirecting the user to the landing route and its
# document.
if not monitored_tx.is_post_processed:
try:
monitored_tx._post_process()
except (
psycopg2.OperationalError, psycopg2.IntegrityError
): # The database cursor could not be committed.
request.env.cr.rollback() # Rollback and try later.
raise Exception('retry')
except Exception as e:
request.env.cr.rollback()
_logger.exception(
"Encountered an error while post-processing transaction with id %s:\n%s",
monitored_tx.id, e
)
raise
return {
'success': success,
'error': error,
'display_values_list': display_values_list,
'provider_code': monitored_tx.provider_code,
'state': monitored_tx.state,
'landing_route': monitored_tx.landing_route,
}
@classmethod
def monitor_transactions(cls, transactions):
""" Add the ids of the provided transactions to the list of monitored transaction ids.
def monitor_transaction(cls, transaction):
""" Make the provided transaction id monitored.
:param recordset transactions: The transactions to monitor, as a `payment.transaction`
recordset
:param payment.transaction transaction: The transaction to monitor.
:return: None
"""
if transactions:
monitored_tx_ids = request.session.get(cls.MONITORED_TX_IDS_KEY, [])
request.session[cls.MONITORED_TX_IDS_KEY] = list(
set(monitored_tx_ids).union(transactions.ids)
)
request.session[cls.MONITORED_TX_ID_KEY] = transaction.id
@classmethod
def get_monitored_transaction_ids(cls):
""" Return the ids of transactions being monitored.
def _get_monitored_transaction(self):
""" Retrieve the user's last transaction from the session (the transaction being monitored).
Only the ids and not the recordset itself is returned to allow the caller browsing the
recordset with sudo privileges, and using the ids in a custom query.
:return: The ids of transactions being monitored
:rtype: list
:return: the user's last transaction
:rtype: payment.transaction
"""
return request.session.get(cls.MONITORED_TX_IDS_KEY, [])
@classmethod
def remove_transactions(cls, transactions):
""" Remove the ids of the provided transactions from the list of monitored transaction ids.
:param recordset transactions: The transactions to remove, as a `payment.transaction`
recordset
:return: None
"""
if transactions:
monitored_tx_ids = request.session.get(cls.MONITORED_TX_IDS_KEY, [])
request.session[cls.MONITORED_TX_IDS_KEY] = [
tx_id for tx_id in monitored_tx_ids if tx_id not in transactions.ids
]
return request.env['payment.transaction'].sudo().browse(
request.session.get(self.MONITORED_TX_ID_KEY)
).exists()