mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-22 10:32:02 +02:00
payment
This commit is contained in:
parent
12c29a983b
commit
95fcc8bd63
189 changed files with 170858 additions and 0 deletions
|
|
@ -0,0 +1,3 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import portal
|
||||
443
odoo-bringout-oca-ocb-payment/payment/controllers/portal.py
Normal file
443
odoo-bringout-oca-ocb-payment/payment/controllers/portal.py
Normal file
|
|
@ -0,0 +1,443 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import urllib.parse
|
||||
import werkzeug
|
||||
|
||||
from odoo import _, http
|
||||
from odoo.exceptions import AccessError, UserError, ValidationError
|
||||
from odoo.http import request
|
||||
|
||||
from odoo.addons.payment import utils as payment_utils
|
||||
from odoo.addons.payment.controllers.post_processing import PaymentPostProcessing
|
||||
from odoo.addons.portal.controllers import portal
|
||||
|
||||
|
||||
class PaymentPortal(portal.CustomerPortal):
|
||||
|
||||
""" This controller contains the foundations for online payments through the portal.
|
||||
|
||||
It allows to complete a full payment flow without the need of going through a document-based
|
||||
flow made available by another module's controller.
|
||||
|
||||
Such controllers should extend this one to gain access to the _create_transaction static method
|
||||
that implements the creation of a transaction before its processing, or to override specific
|
||||
routes and change their behavior globally (e.g. make the /pay route handle sale orders).
|
||||
|
||||
The following routes are exposed:
|
||||
- `/payment/pay` allows for arbitrary payments.
|
||||
- `/my/payment_method` allows the user to create and delete tokens. It's its own `landing_route`
|
||||
- `/payment/transaction` is the `transaction_route` for the standard payment flow. It creates a
|
||||
draft transaction, and return the processing values necessary for the completion of the
|
||||
transaction.
|
||||
- `/payment/confirmation` is the `landing_route` for the standard payment flow. It displays the
|
||||
payment confirmation page to the user when the transaction is validated.
|
||||
"""
|
||||
|
||||
@http.route(
|
||||
'/payment/pay', type='http', methods=['GET'], auth='public', website=True, sitemap=False,
|
||||
)
|
||||
def payment_pay(
|
||||
self, reference=None, amount=None, currency_id=None, partner_id=None, company_id=None,
|
||||
provider_id=None, access_token=None, **kwargs
|
||||
):
|
||||
""" Display the payment form with optional filtering of payment options.
|
||||
|
||||
The filtering takes place on the basis of provided parameters, if any. If a parameter is
|
||||
incorrect or malformed, it is skipped to avoid preventing the user from making the payment.
|
||||
|
||||
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
|
||||
:param dict kwargs: Optional data passed to helper methods.
|
||||
:return: The rendered checkout form
|
||||
:rtype: str
|
||||
:raise: werkzeug.exceptions.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)
|
||||
))
|
||||
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.
|
||||
|
||||
user_sudo = request.env.user
|
||||
logged_in = not user_sudo._is_public()
|
||||
# If the user is logged in, take their partner rather than the partner set in the params.
|
||||
# This is something that we want, since security rules are based on the partner, and created
|
||||
# tokens should not be assigned to the public user. This should have no impact on the
|
||||
# transaction itself besides making reconciliation possibly more difficult (e.g. The
|
||||
# transaction and invoice partners are different).
|
||||
partner_is_different = False
|
||||
if logged_in:
|
||||
partner_is_different = partner_id and partner_id != user_sudo.partner_id.id
|
||||
partner_sudo = user_sudo.partner_id
|
||||
else:
|
||||
partner_sudo = request.env['res.partner'].sudo().browse(partner_id).exists()
|
||||
if not partner_sudo:
|
||||
return request.redirect(
|
||||
# Escape special characters to avoid loosing original params when redirected
|
||||
f'/web/login?redirect={urllib.parse.quote(request.httprequest.full_path)}'
|
||||
)
|
||||
|
||||
# Instantiate transaction values to their default if not set in parameters
|
||||
reference = reference or payment_utils.singularize_reference_prefix(prefix='tx')
|
||||
amount = amount or 0.0 # If the amount is invalid, set it to 0 to stop the payment flow
|
||||
company_id = company_id or partner_sudo.company_id.id or user_sudo.company_id.id
|
||||
company = request.env['res.company'].sudo().browse(company_id)
|
||||
currency_id = currency_id or company.currency_id.id
|
||||
|
||||
# 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.
|
||||
|
||||
# Select all providers and tokens that match the constraints
|
||||
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']
|
||||
|
||||
# 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')
|
||||
}
|
||||
|
||||
# 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
|
||||
),
|
||||
'reference_prefix': reference,
|
||||
'amount': amount,
|
||||
'currency': currency,
|
||||
'partner_id': partner_sudo.id,
|
||||
'access_token': access_token,
|
||||
'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),
|
||||
}
|
||||
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):
|
||||
""" 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
|
||||
"""
|
||||
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
|
||||
show_tokenize_input_mapping[provider_sudo.id] = show_tokenize_input
|
||||
return show_tokenize_input_mapping
|
||||
|
||||
def _get_payment_page_template_xmlid(self, **kwargs):
|
||||
return 'payment.pay'
|
||||
|
||||
@http.route('/my/payment_method', type='http', methods=['GET'], auth='user', website=True)
|
||||
def payment_method(self, **kwargs):
|
||||
""" Display the form to manage payment methods.
|
||||
|
||||
:param dict kwargs: Optional data. This parameter is not used here
|
||||
:return: The rendered manage form
|
||||
:rtype: str
|
||||
"""
|
||||
partner_sudo = request.env.user.partner_id # env.user is always sudoed
|
||||
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']),
|
||||
])
|
||||
|
||||
access_token = payment_utils.generate_access_token(partner_sudo.id, None, None)
|
||||
rendering_context = {
|
||||
'providers': providers_sudo,
|
||||
'tokens': tokens_sudo,
|
||||
'reference_prefix': payment_utils.singularize_reference_prefix(prefix='V'),
|
||||
'partner_id': partner_sudo.id,
|
||||
'access_token': access_token,
|
||||
'transaction_route': '/payment/transaction',
|
||||
'landing_route': '/my/payment_method',
|
||||
**self._get_custom_rendering_context_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.
|
||||
|
||||
:param dict kwargs: Optional data. This parameter is not used here
|
||||
:return: The dict of additional rendering context values
|
||||
:rtype: dict
|
||||
"""
|
||||
return {}
|
||||
|
||||
@http.route('/payment/transaction', type='json', auth='public')
|
||||
def payment_transaction(self, amount, currency_id, partner_id, access_token, **kwargs):
|
||||
""" Create a draft transaction and return its processing values.
|
||||
|
||||
: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 access_token: The access token used to authenticate the partner
|
||||
: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
|
||||
"""
|
||||
# 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."))
|
||||
|
||||
kwargs.pop('custom_create_values', None) # Don't allow passing arbitrary create values
|
||||
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
|
||||
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,
|
||||
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 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
|
||||
"""
|
||||
# Prepare create values
|
||||
if flow in ['redirect', 'direct']: # Direct payment or payment with redirection
|
||||
provider_sudo = request.env['payment.provider'].sudo().browse(payment_option_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
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
# is being made).
|
||||
partner_sudo = request.env['res.partner'].sudo().browse(partner_id)
|
||||
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.")
|
||||
)
|
||||
|
||||
reference = request.env['payment.transaction']._compute_reference(
|
||||
provider_sudo.code,
|
||||
prefix=reference_prefix,
|
||||
**(custom_create_values or {}),
|
||||
**kwargs
|
||||
)
|
||||
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
|
||||
|
||||
# Create the transaction
|
||||
tx_sudo = request.env['payment.transaction'].sudo().create({
|
||||
'provider_id': provider_sudo.id,
|
||||
'reference': reference,
|
||||
'amount': amount,
|
||||
'currency_id': currency_id,
|
||||
'partner_id': partner_id,
|
||||
'token_id': token_id,
|
||||
'operation': f'online_{flow}' if not is_validation else 'validation',
|
||||
'tokenize': tokenize,
|
||||
'landing_route': landing_route,
|
||||
**(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()
|
||||
|
||||
# Monitor the transaction to make it available in the portal
|
||||
PaymentPostProcessing.monitor_transactions(tx_sudo)
|
||||
|
||||
return tx_sudo
|
||||
|
||||
@staticmethod
|
||||
def _update_landing_route(tx_sudo, access_token):
|
||||
""" Add the mandatory parameters to the route and recompute the access token if needed.
|
||||
|
||||
The generic landing route requires the tx id and access token to be provided since there is
|
||||
no document to rely on. The access token is recomputed in case we are dealing with a
|
||||
validation transaction (provider-specific amount and currency).
|
||||
|
||||
:param recordset tx_sudo: The transaction whose landing routes to update, as a
|
||||
`payment.transaction` record.
|
||||
:param str access_token: The access token used to authenticate the partner
|
||||
:return: None
|
||||
"""
|
||||
if tx_sudo.operation == 'validation':
|
||||
access_token = payment_utils.generate_access_token(
|
||||
tx_sudo.partner_id.id, tx_sudo.amount, tx_sudo.currency_id.id
|
||||
)
|
||||
tx_sudo.landing_route = f'{tx_sudo.landing_route}' \
|
||||
f'?tx_id={tx_sudo.id}&access_token={access_token}'
|
||||
|
||||
@http.route('/payment/confirmation', type='http', methods=['GET'], auth='public', website=True)
|
||||
def payment_confirm(self, tx_id, access_token, **kwargs):
|
||||
""" Display the payment confirmation page to the user.
|
||||
|
||||
: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
|
||||
"""
|
||||
tx_id = self._cast_as_int(tx_id)
|
||||
if tx_id:
|
||||
tx_sudo = request.env['payment.transaction'].sudo().browse(tx_id)
|
||||
|
||||
# Raise an HTTP 404 if the access token is invalid
|
||||
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)
|
||||
|
||||
# Display the payment confirmation page to the user
|
||||
return request.render('payment.confirm', qcontext={'tx': tx_sudo})
|
||||
else:
|
||||
# Display the portal homepage to the user
|
||||
return request.redirect('/my/home')
|
||||
|
||||
@http.route('/payment/archive_token', type='json', auth='user')
|
||||
def archive_token(self, token_id):
|
||||
""" Check that a user has write access on a token and archive the token if so.
|
||||
|
||||
:param int token_id: The token to archive, as a `payment.token` id
|
||||
:return: None
|
||||
"""
|
||||
partner_sudo = request.env.user.partner_id
|
||||
token_sudo = request.env['payment.token'].sudo().search([
|
||||
('id', '=', token_id),
|
||||
# Check that the user owns the token before letting them archive anything
|
||||
('partner_id', 'in', [partner_sudo.id, partner_sudo.commercial_partner_id.id])
|
||||
])
|
||||
if token_sudo:
|
||||
token_sudo.active = False
|
||||
|
||||
@staticmethod
|
||||
def _cast_as_int(str_value):
|
||||
""" Cast a string as an `int` and return it.
|
||||
|
||||
If the conversion fails, `None` is returned instead.
|
||||
|
||||
:param str str_value: The value to cast as an `int`
|
||||
:return: The casted value, possibly replaced by None if incompatible
|
||||
:rtype: int|None
|
||||
"""
|
||||
try:
|
||||
return int(str_value)
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _cast_as_float(str_value):
|
||||
""" Cast a string as a `float` and return it.
|
||||
|
||||
If the conversion fails, `None` is returned instead.
|
||||
|
||||
:param str str_value: The value to cast as a `float`
|
||||
:return: The casted value, possibly replaced by None if incompatible
|
||||
:rtype: float|None
|
||||
"""
|
||||
try:
|
||||
return float(str_value)
|
||||
except (TypeError, ValueError, OverflowError):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _can_partner_pay_in_company(partner, document_company):
|
||||
""" Return whether the provided partner can pay in the provided company.
|
||||
|
||||
The payment is allowed either if the partner's company is not set or if the companies match.
|
||||
|
||||
:param recordset partner: The partner on behalf on which the payment is made, as a
|
||||
`res.partner` record.
|
||||
:param recordset document_company: The company of the document being paid, as a
|
||||
`res.company` record.
|
||||
:return: Whether the payment is allowed.
|
||||
:rtype: str
|
||||
"""
|
||||
return not partner.company_id or partner.company_id == document_company
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
# 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.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PaymentPostProcessing(http.Controller):
|
||||
|
||||
"""
|
||||
This controller is responsible for the monitoring and finalization of the post-processing of
|
||||
transactions.
|
||||
|
||||
It exposes the route `/payment/status`: All payment flows must go through this route at some
|
||||
point to allow the user checking on the transactions' status, and to trigger the finalization of
|
||||
their post-processing.
|
||||
"""
|
||||
|
||||
MONITORED_TX_IDS_KEY = '__payment_monitored_tx_ids__'
|
||||
|
||||
@http.route('/payment/status', type='http', auth='public', website=True, sitemap=False)
|
||||
def display_status(self, **kwargs):
|
||||
""" Display 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')
|
||||
|
||||
@http.route('/payment/status/poll', type='json', auth='public')
|
||||
def poll_status(self, **_kwargs):
|
||||
""" Fetch the transactions to display on the status page and finalize their post-processing.
|
||||
|
||||
:return: The post-processing values of the transactions
|
||||
: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',
|
||||
}
|
||||
|
||||
# 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
|
||||
)
|
||||
|
||||
return {
|
||||
'success': success,
|
||||
'error': error,
|
||||
'display_values_list': display_values_list,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def monitor_transactions(cls, transactions):
|
||||
""" Add the ids of the provided transactions to the list of monitored transaction ids.
|
||||
|
||||
:param recordset transactions: The transactions to monitor, 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] = list(
|
||||
set(monitored_tx_ids).union(transactions.ids)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_monitored_transaction_ids(cls):
|
||||
""" Return the ids of transactions 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 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
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue