[IMP] payment: post-process transactions immediately

Instead of monitoring a list of transactions to post-process in the
session, we now track only the last processed transaction, thus allowing
to drop the 10 min delay of the post-processing cron.

task-3125913

Part-of: odoo/odoo#119473
Co-authored-by: mano-odoo <mano@odoo.com>
This commit is contained in:
Anita (anko)
2023-09-19 16:37:06 +00:00
committed by Antoine Vandevenne (anv)
co-authored by mano-odoo
parent 23faf6f7ef
commit cf52e381bd
14 changed files with 342 additions and 455 deletions
+2 -5
View File
@@ -317,8 +317,8 @@ class PaymentPortal(portal.CustomerPortal):
else:
tx_sudo._log_sent_message()
# 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
@@ -361,9 +361,6 @@ class PaymentPortal(portal.CustomerPortal):
):
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:
+37 -88
View File
@@ -1,11 +1,10 @@
# 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
_logger = logging.getLogger(__name__)
@@ -22,7 +21,7 @@ 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)
def display_status(self, **kwargs):
@@ -36,104 +35,54 @@ class PaymentPostProcessing(http.Controller):
@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.
""" Fetch the transaction to display on the status page and finalize 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',
}
# Retrieve the last user's transaction from the session.
monitored_tx = request.env['payment.transaction'].sudo().browse(
self.get_monitored_transaction_id()
).exists()
if not monitored_tx: # The session might have expired, or the tx has never existed.
raise Exception('tx_not_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(),
})
# Finalize the post-processing of the transaction before redirecting the user to the landing
# route and its document.
if monitored_tx.state == 'done' and not monitored_tx.is_post_processed:
try:
monitored_tx._finalize_post_processing()
except psycopg2.OperationalError: # 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
# 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,
}
# Return the post-processing values to display the transaction summary to the customer.
return monitored_tx._get_post_processing_values()
@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_id(cls):
""" Return the id of 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.
Only the id and not the recordset itself is returned to allow the caller browsing the
recordset with sudo privileges, and using the id in a custom query.
:return: The ids of transactions being monitored
:return: The id 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
]
return request.session.get(cls.MONITORED_TX_ID_KEY)
+1 -1
View File
@@ -142,7 +142,7 @@ class PaymentProvider(models.Model):
done_msg = fields.Html(
string="Done Message",
help="The message displayed if the order is successfully done after the payment process",
default=lambda self: _("Your payment has been successfully processed. Thank you!"),
default=lambda self: _("Your payment has been successfully processed."),
translate=True)
cancel_msg = fields.Html(
string="Canceled Message",
+11 -6
View File
@@ -911,9 +911,10 @@ class PaymentTransaction(models.Model):
The returned dict contains the following entries:
- `provider_code`: The code of the provider.
- `provider_name`: The name of the provider.
- `reference`: The reference of the transaction.
- `amount`: The rounded amount of the transaction.
- `currency_id`: The currency of the transaction, as a `res.currency` id.
- `currency_code`: The currency of the transaction, as a `res.currency` id.
- `state`: The transaction state: `draft`, `pending`, `authorized`, `done`, `cancel`, or
`error`.
- `state_message`: The information message about the state.
@@ -929,15 +930,23 @@ class PaymentTransaction(models.Model):
"""
self.ensure_one()
display_message = None
if self.state == 'pending':
display_message = self.provider_id.pending_msg
elif self.state == 'done':
display_message = self.provider_id.done_msg
elif self.state == 'cancel':
display_message = self.provider_id.cancel_msg
post_processing_values = {
'provider_code': self.provider_code,
'provider_name': self.provider_id.name,
'reference': self.reference,
'amount': self.amount,
'currency_code': self.currency_id.name,
'state': self.state,
'state_message': self.state_message,
'display_message': display_message,
'operation': self.operation,
'is_post_processed': self.is_post_processed,
'landing_route': self.landing_route,
}
_logger.debug(
@@ -953,8 +962,6 @@ class PaymentTransaction(models.Model):
"""
txs_to_post_process = self
if not txs_to_post_process:
# Let the client post-process transactions so that they remain available in the portal
client_handling_limit_date = datetime.now() - relativedelta.relativedelta(minutes=10)
# Don't try forever to post-process a transaction that doesn't go through. Set the limit
# to 4 days because some providers (PayPal) need that much for the payment verification.
retry_limit_date = datetime.now() - relativedelta.relativedelta(days=4)
@@ -962,8 +969,6 @@ class PaymentTransaction(models.Model):
txs_to_post_process = self.search([
('state', '=', 'done'),
('is_post_processed', '=', False),
'|', ('last_state_change', '<=', client_handling_limit_date),
('source_transaction_id', '!=', False),
('last_state_change', '>=', retry_limit_date),
])
for tx in txs_to_post_process:
+76 -112
View File
@@ -1,132 +1,96 @@
/** @odoo-module **/
import publicWidget from "@web/legacy/js/public/public_widget";
import { renderToElement } from "@web/core/utils/render";
import { _t } from "@web/core/l10n/translation";
import publicWidget from "@web/legacy/js/public/public_widget";
import { renderToElement } from "@web/core/utils/render";
import { markup } from "@odoo/owl";
import { _t } from "@web/core/l10n/translation";
import { markup } from "@odoo/owl";
publicWidget.registry.PaymentPostProcessing = publicWidget.Widget.extend({
selector: 'div[name="o_payment_status"]',
publicWidget.registry.PaymentPostProcessing = publicWidget.Widget.extend({
selector: 'div[name="o_payment_status"]',
timeout: 0,
pollCount: 0,
_pollCount: 0,
async start() {
this.call('ui', 'block', {
'message': _t("We are processing your payment. Please wait."),
});
this._poll();
return this._super.apply(this, arguments);
},
start: function() {
this.displayLoading();
this.poll();
return this._super.apply(this, arguments);
},
/* Methods */
startPolling: function () {
var timeout = 3000;
//
if(this._pollCount >= 10 && this._pollCount < 20) {
timeout = 10000;
}
else if(this._pollCount >= 20) {
timeout = 30000;
}
//
setTimeout(this.poll.bind(this), timeout);
this._pollCount ++;
},
poll: function () {
var self = this;
_poll() {
this._updateTimeout();
setTimeout(() => {
// Fetch the post-processing values from the server.
const self = this;
this._rpc({
route: '/payment/status/poll',
params: {
'csrf_token': odoo.csrf_token,
}
}).then(function(data) {
if(data.success === true) {
self.processPolledData(data.display_values_list);
}
else {
switch(data.error) {
case "tx_process_retry":
break;
case "no_tx_found":
self.displayContent("payment.no_tx_found", {});
break;
default: // if an exception is raised
self.displayContent("payment.exception", {exception_msg: data.error});
break;
}
}
self.startPolling();
}).then(postProcessingValues => {
let { state, display_message, landing_route } = postProcessingValues;
}).guardedCatch(function() {
self.displayContent("payment.rpc_error", {});
self.startPolling();
});
},
processPolledData: function (display_values_list) {
var render_values = {
'tx_draft': [],
'tx_pending': [],
'tx_authorized': [],
'tx_done': [],
'tx_cancel': [],
'tx_error': [],
};
// Display the transaction details before redirection to show something ASAP.
if (display_message) {
postProcessingValues.display_message = markup(display_message);
}
this._renderTemplate('payment.transactionDetails', postProcessingValues);
// group the transaction according to their state
display_values_list.forEach(function (display_values) {
var key = 'tx_' + display_values.state;
if(key in render_values) {
if (display_values["display_message"]) {
display_values.display_message = markup(display_values.display_message)
// Redirect the user to the landing route if the transaction reached a final state.
if (self._getFinalStates(postProcessingValues['provider_code']).includes(state)) {
window.location = landing_route;
} else {
self._poll();
}
}).guardedCatch(error => {
error.event.preventDefault();
if (error.message.data) { // Server error.
switch (error.message.data.message) {
case 'retry':
self._poll();
break;
case 'tx_not_found':
self._renderTemplate('payment.tx_not_found');
break;
default:
self._renderTemplate(
'payment.exception', { error_message: error.message.data.message }
);
break;
}
render_values[key].push(display_values);
} else { // RPC error (server unreachable).
self._renderTemplate('payment.rpc_error');
self._poll();
}
});
}, this.timeout);
},
function countTxInState(states) {
var nbTx = 0;
for (var prop in render_values) {
if (states.indexOf(prop) > -1 && render_values.hasOwnProperty(prop)) {
nbTx += render_values[prop].length;
}
}
return nbTx;
}
_getFinalStates(providerCode) {
return ['authorized', 'done'];
},
/*
* When the server sends the list of monitored transactions, it tries to post-process
* all the successful ones. If it succeeds or if the post-process has already been made,
* the transaction is removed from the list of monitored transactions and won't be
* included in the next response. We assume that successful and post-process
* transactions should always prevail on others, regardless of their number or state.
*/
if (render_values['tx_done'].length === 1 &&
render_values['tx_done'][0].is_post_processed) {
window.location = render_values['tx_done'][0].landing_route;
return;
}
// If there are multiple transactions monitored, display them all to the customer. If
// there is only one transaction monitored, redirect directly the customer to the
// landing route.
if(countTxInState(['tx_done', 'tx_error', 'tx_pending', 'tx_authorized']) === 1) {
// We don't want to redirect customers to the landing page when they have a pending
// transaction. The successful transactions are dealt with before.
var tx = render_values['tx_authorized'][0] || render_values['tx_error'][0];
if (tx) {
window.location = tx.landing_route;
return;
}
}
_updateTimeout() {
if (this.pollCount >= 1 && this.pollCount < 10) {
this.timeout = 3000;
}
if (this.pollCount >= 10 && this.pollCount < 20) {
this.timeout = 10000;
}
else if (this.pollCount >= 20) {
this.timeout = 30000;
}
this.pollCount++;
},
this.displayContent("payment.display_tx_list", render_values);
},
displayContent: function (xmlid, render_values) {
this.call("ui", "unblock");
this.$el.find('div[name="o_payment_status_content"]').empty().append(renderToElement(xmlid, render_values));
},
displayLoading: function () {
this.call("ui", "block", {
'message': _t("We are processing your payment, please wait ..."),
});
},
});
_renderTemplate(xmlid, display_values={}) {
this.call('ui', 'unblock');
const statusContainer = document.querySelector('div[name="o_payment_status_content"]');
statusContainer.innerHTML = renderToElement(xmlid, display_values).innerHTML;
},
export default publicWidget.registry.PaymentPostProcessing;
});
export default publicWidget.registry.PaymentPostProcessing;
@@ -1,3 +1,12 @@
div#o_payment_status_alert > p {
div[name="o_payment_status_alert"] div > p {
margin-bottom: 0;
}
.o_payment_details .col:not(:last-child) {
border-bottom: $border-width solid $border-color;
@include media-breakpoint-up(md) {
border-right: $border-width solid $border-color;
border-bottom: none;
}
}
@@ -1,150 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="payment" xml:space="preserve">
<!-- The templates here as rendered by 'post_processing.js', you can also take
a look at payment_portal_templates.xml (xmlid: payment_status) for more infos-->
<t t-name="payment.display_tx_list">
<div>
<!-- Error transactions -->
<div t-if="tx_error.length > 0">
<h1>Failed operations</h1>
<ul class="list-group">
<t t-foreach="tx_error" t-as="tx" t-key="tx_index">
<a t-att-href="tx['landing_route']" class="list-group-item">
<h4 class="list-group-item-heading mb5">
<t t-esc="tx['reference']"/>
<span class="badge float-end"><t t-esc="tx['amount']"/> <t t-esc="tx['currency_code']"/></span>
</h4>
<small class="list-group-item-text">
An error occurred during the processing of this payment.<br/>
<strong>Reason:</strong> <t t-esc="tx['state_message']"/>
</small>
</a>
</t>
</ul>
</div>
<!-- Pending/Authorized/Confirmed transactions -->
<div t-if="tx_done.length > 0 || tx_authorized.length > 0 || tx_pending.length > 0">
<h1>Operations in progress</h1>
<div class="list-group">
<!-- Done transactions -->
<t t-foreach="tx_done" t-as="tx" t-key="tx_index">
<a t-att-href="tx['landing_route']" class="list-group-item">
<h4 class="list-group-item-heading mb5">
<t t-esc="tx['reference']"/>
<span class="badge float-end"><t t-esc="tx['amount']"/> <t t-esc="tx['currency_code']"/></span>
</h4>
<small class="list-group-item-text">
<t t-if="!tx['is_post_processed']">
<t t-if="tx['operation'] != 'validation'">
Your payment is being processed, please wait... <i class="fa fa-cog fa-spin"/>
</t>
<t t-else="">
Saving your payment method, please wait... <i class="fa fa-cog fa-spin"/>
</t>
</t>
<t t-else="">
<t t-if="tx['operation'] != 'validation'">
Your payment has been processed.<br/>
Click here to be redirected to the confirmation page.
</t>
<t t-else="">
Your payment method has been saved.<br/>
Click here to be redirected to the confirmation page.
</t>
</t>
</small>
</a>
</t>
<!-- Pending transactions -->
<t t-foreach="tx_pending" t-as="tx" t-key="tx_index">
<a t-att-href="tx['landing_route']" class="list-group-item">
<h4 class="list-group-item-heading mb5">
<t t-esc="tx['reference']"/>
<span class="badge float-end"><t t-esc="tx['amount']"/> <t t-esc="tx['currency_code']"/></span>
</h4>
<small class="list-group-item-text">
<t t-if="tx['display_message']">
<!-- display_message is the content of the HTML field associated
with the current transaction state, set on the provider. -->
<t t-out="tx['display_message']"/>
</t>
<t t-else="">
Your payment is in pending state.<br/>
You will be notified when the payment is fully confirmed.<br/>
Click here to be redirected to the confirmation page.
</t>
</small>
</a>
</t>
<!-- Authorized transactions -->
<t t-foreach="tx_authorized" t-as="tx" t-key="tx_index">
<a t-att-href="tx['landing_route']" class="list-group-item">
<h4 class="list-group-item-heading mb5">
<t t-esc="tx['reference']"/>
<span class="badge float-end"><t t-esc="tx['amount']"/> <t t-esc="tx['currency_code']"/></span>
</h4>
<small class="list-group-item-text">
<t t-if="tx['display_message']">
<!-- display_message is the content of the HTML field associated
with the current transaction state, set on the provider. -->
<t t-out="tx['display_message']"/>
</t>
<t t-else="">
Your payment has been received but need to be confirmed manually.<br/>
You will be notified when the payment is confirmed.
</t>
</small>
</a>
</t>
<t t-name="payment.transactionDetails">
<div> <!-- Single parent element to allow using renderToElement. -->
<t t-set="alert_style" t-value="'info'"/>
<t t-set="alert_header" t-value="'Please wait...'"/>
<t t-if="state == 'draft'">
<t t-set="alert_message"
t-value="display_message ? display_message
: 'Your payment has not been processed yet.'"
/>
</t>
<t t-elif="state == 'pending'">
<t t-set="alert_style" t-value="'warning'"/>
<t t-set="alert_message" t-value="display_message"/>
</t>
<t t-elif="state == 'authorized'">
<t t-set="alert_style" t-value="'success'"/>
<t t-set="alert_message" t-value="display_message"/>
</t>
<t t-elif="state == 'done'">
<t t-set="alert_style" t-value="'success'"/>
<t t-set="alert_header"
t-value="operation !== 'validation' ? 'Your payment has been processed' :
'Your payment method has been saved'"
/>
</t>
<t t-elif="state == 'cancel'">
<t t-set="alert_style" t-value="'danger'"/>
<t t-set="alert_header" t-value="'This payment has been canceled'"/>
<t t-set="alert_message" t-value="'No payment has been processed.'"/>
</t>
<t t-elif="state == 'error'">
<t t-set="alert_style" t-value="'danger'"/>
<t t-set="alert_header" t-value="'Error'"/>
<t t-set="alert_message" t-value="state_message"/>
</t>
<div name="o_payment_status_alert"
t-attf-class="alert alert-{{alert_style}} d-flex gap-3"
>
<div>
<i t-attf-class="fa fa-{{alert_style === 'danger' ? 'exclamation-triangle'
: 'cog fa-spin'}}"
/>
</div>
<div>
<h5 class="alert-heading mb-0" t-out="alert_header"/>
<t t-if="alert_message" t-out="alert_message"/>
</div>
<a t-att-href="landing_route" class="alert-link ms-auto text-nowrap">
Skip <i class="oi oi-arrow-right ms-1 small"/>
</a>
</div>
<!-- Draft transactions -->
<div t-if="tx_draft.length > 0">
<h1>Waiting for operations to process</h1>
<ul class="list-group">
<t t-foreach="tx_draft" t-as="tx" t-key="tx_index">
<a t-att-href="tx['landing_route']" class="list-group-item">
<h4 class="list-group-item-heading mb5">
<t t-esc="tx['reference']"/>
<span class="badge float-end"><t t-esc="tx['amount']"/> <t t-esc="tx['currency_code']"/></span>
</h4>
<small class="list-group-item-text">
<t t-if="tx['display_message']">
<!-- display_message is the content of the HTML field associated
with the current transaction state, set on the provider. -->
<t t-out="tx['display_message']"/>
</t>
<t t-else="">
We are waiting for the payment provider to confirm the payment.
</t>
</small>
</a>
</t>
</ul>
</div>
<!-- Cancel transactions -->
<div t-if="tx_cancel.length > 0">
<h1>Canceled operations</h1>
<ul class="list-group">
<t t-foreach="tx_cancel" t-as="tx" t-key="tx_index">
<a t-att-href="tx['landing_route']" class="list-group-item">
<h4 class="list-group-item-heading mb5">
<t t-esc="tx['reference']"/>
<span class="badge float-end"><t t-esc="tx['amount']"/> <t t-esc="tx['currency_code']"/></span>
</h4>
<small class="list-group-item-text">
This payment has been canceled.<br/>
No payment has been processed.
</small>
</a>
</t>
</ul>
<div class="o_payment_details o_cc2 row row-cols-1 row-cols-md-3 mx-0 mb-3 py-2 rounded">
<div class="col py-3">
<label for="o_payment_summary_reference" class="d-block small text-muted">
Reference
</label>
<span id="o_payment_summary_reference" t-out="reference"/>
</div>
</div>
</div>
</t>
<t t-name="payment.no_tx_found">
<div class="text-center">
<t t-name="payment.tx_not_found">
<div class="text-center"> <!-- Single parent element to allow using renderToElement. -->
<p>We are not able to find your payment, but don't worry.</p>
<p>You should receive an email confirming your payment in a few minutes.</p>
<p>If the payment hasn't been confirmed you can contact us.</p>
@@ -152,16 +74,15 @@
</t>
<t t-name="payment.rpc_error">
<div class="text-center">
<p><strong>Server error:</strong> Unable to contact the Odoo server.</p>
<p>Please wait ... <i class="fa fa-refresh fa-spin"></i></p>
<div class="text-center"> <!-- Single parent element to allow using renderToElement. -->
<p>Unable to contact the server. Please wait. <i class="fa fa-refresh fa-spin"/></p>
</div>
</t>
<t t-name="payment.exception">
<div class="text-center">
<div class="text-center"> <!-- Single parent element to allow using renderToElement. -->
<h2>Internal server error</h2>
<pre><t t-esc="exception_msg"/></pre>
<pre><t t-out="error_message"/></pre>
</div>
</t>
@@ -103,9 +103,9 @@
<!-- Portal breadcrumb -->
<t t-call="payment.portal_breadcrumb"/>
<!-- Status page -->
<div name="o_payment_status">
<div name="o_payment_status" class="row justify-content-center my-3">
<div name="o_payment_status_content"
class="col-sm-6 offset-sm-3">
class="col-12 col-lg-8">
<!-- The content is generated in JavaScript -->
</div>
</div>
@@ -126,53 +126,49 @@
<div class="container">
<!-- Portal breadcrumb -->
<t t-call="payment.portal_breadcrumb"/>
<!-- Confirmation page -->
<div class="row">
<div class="col-lg-6">
<div>
<div class="row justify-content-center my-3">
<div class="col-12 col-lg-8">
<!-- Confirmation page -->
<div class="row">
<div class="col">
<t t-call="payment.transaction_status"/>
<div class="mb-3 row">
<label for="form_partner_name" class="col-md-3 col-form-label">
From
</label>
<span name="form_partner_name"
class="col-md-9 col-form-label"
t-esc="tx.partner_name"/>
</div>
<hr/>
<div class="mb-3 row">
<label for="form_reference" class="col-md-3 col-form-label">
Reference
</label>
<span name="form_reference"
class="col-md-9 col-form-label"
t-esc="tx.reference"/>
</div>
<hr/>
<div class="mb-3 row">
<label for="form_amount" class="col-md-3 col-form-label">
Amount
</label>
<span name="form_amount"
class="col-md-9 col-form-label"
t-esc="tx.amount"
t-options="{'widget': 'monetary', 'display_currency': tx.currency_id}"/>
</div>
<hr/>
<div class="row">
<div class="col-md-5 text-muted">
Processed by <t t-esc="tx.provider_id.sudo().name"/>
</div>
<div class="col-md-4 offset-md-3 mt-2 ps-0">
<a role="button"
t-attf-class="btn btn-primary float-end"
href="/my/home">
<i class="fa fa-arrow-circle-right"/> Back to My Account
</a>
</div>
</div>
</div>
</div>
<div class="o_payment_details o_cc2 row row-cols-1 row-cols-md-3 mx-0 mb-3 py-2 rounded">
<div class="col py-3">
<label for="form_reference" class="d-block small text-muted">
Reference
</label>
<span name="form_reference"
t-esc="tx.reference"/>
</div>
<div class="col py-3">
<label for="form_amount" class="d-block small text-muted">
Amount
</label>
<span name="form_amount"
t-esc="tx.amount"
t-options="{'widget': 'monetary', 'display_currency': tx.currency_id}"/>
</div>
<div class="col py-3">
<label for="form_provider" class="d-block small text-muted">
Powered by
</label>
<span name="form_provider"
t-esc="tx.provider_id.sudo().name"/>
</div>
</div>
<div class="row">
<div class="col offset-md-3 ps-0">
<a role="button"
class="btn btn-primary float-end"
href="/my/home">
Go to My Account<i class="oi oi-arrow-right ms-2"/>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -186,7 +182,7 @@
-->
<div class="row">
<div class="col-md-6">
<ol class="breadcrumb mt8">
<ol class="breadcrumb px-0 mt16">
<li class="breadcrumb-item">
<a href="/my/home">
<i class="fa fa-home"
+19 -7
View File
@@ -472,6 +472,7 @@
</t>
<t t-elif="tx.state == 'done'">
<t t-set="alert_style">success</t>
<t t-set="status_heading" t-value="'Thank you!'"/>
<t t-set="status_message" t-value="tx.provider_id.sudo().done_msg"/>
</t>
<t t-elif="tx.state == 'cancel'">
@@ -481,18 +482,29 @@
<t t-elif="tx.state == 'error'">
<t t-set="alert_style">danger</t>
<t t-set="status_message">
<p>An error occurred during the processing of your payment.</p>
<p class="mb-0">An error occurred during the processing of your payment.</p>
</t>
</t>
<t t-if="is_html_empty(status_message)" t-set="status_message" t-value="''"/>
<t t-set="o_payment_status_alert_class"
t-value="'alert alert-'+alert_style +' d-flex gap-3'"
/>
<div t-if="status_message or tx.state_message"
id="o_payment_status_alert"
t-attf-class="alert alert-{{alert_style}} alert-dismissible">
<button class="btn-close" data-bs-dismiss="alert" title="Dismiss"/>
<t t-if="status_message" t-out="status_message"/>
<t t-if="tx.state_message" t-out="tx.state_message"/>
name="o_payment_status_alert"
t-attf-class="{{o_payment_status_alert_class}}"
>
<div id="o_payment_status_icon">
<i t-attf-class="fa fa-{{'check' if alert_style == 'success' else 'info-circle'
if alert_style == 'info' else 'exclamation-triangle'}}"
/>
</div>
<div id="o_payment_status_message" class="w-100">
<h5 t-if="status_heading" t-out="status_heading" class="alert-heading mb-0"/>
<t t-if="status_message" t-out="status_message" class="mb-0"/>
<t t-if="tx.state_message" t-out="tx.state_message" class="mb-0"/>
</div>
</div>
</template>
@@ -54,13 +54,13 @@ class PaymentProvider(models.Model):
*self.env['account.journal']._check_company_domain(company_id),
('type', '=', 'bank'),
]).bank_account_id
account_names = "".join(f"<li>{account.display_name}</li>" for account in accounts)
account_names = "".join(f"<li><pre>{account.display_name}</pre></li>" for account in accounts)
provider.pending_msg = f'<div>' \
f'<h3>{_("Please use the following transfer details")}</h3>' \
f'<h4>{_("Bank Account") if len(accounts) == 1 else _("Bank Accounts")}</h4>' \
f'<ul>{account_names}</ul>' \
f'<h4>{_("Communication")}</h4>' \
f'<p>{_("Please use the order name as communication reference.")}</p>' \
f'<h5>{_("Please use the following transfer details")}</h5>' \
f'<p><br></p>' \
f'<h6>{_("Bank Account") if len(accounts) == 1 else _("Bank Accounts")}</h6>' \
f'<ul>{account_names}</ul>'\
f'<p><br></p>' \
f'</div>'
def _get_removal_values(self):
@@ -1,23 +1,20 @@
/** @odoo-module **/
import paymentPostProcessing from "@payment/js/post_processing";
import paymentPostProcessing from "@payment/js/post_processing";
paymentPostProcessing.include({
/**
* Don't wait for the transaction to be confirmed before redirecting customers to the
* landing route because custom transactions remain in the state 'pending' forever.
*
* @override method from `@payment/js/post_processing`
* @param {Object} display_values_list - The post-processing values of the transactions
*/
processPolledData: function (display_values_list) {
// In almost every case, there will be a single transaction to display. If there are
// more than one transaction, the last one will most likely be the one that counts. We
// use that one to redirect the user to the landing page.
if (display_values_list.length > 0 && display_values_list[0].provider_code === 'custom') {
window.location = display_values_list[0].landing_route;
} else {
return this._super(...arguments);
}
paymentPostProcessing.include({
/**
* Don't wait for the transaction to be confirmed before redirecting customers to the
* landing route because custom transactions remain in the state 'pending' forever.
*
* @override method from `@payment/js/post_processing`
* @param {string} providerCode - The code of the provider handling the transaction.
*/
_getFinalStates(providerCode) {
const finalStates = this._super(...arguments);
if (providerCode === 'custom') {
finalStates.push('pending');
}
});
return finalStates;
}
});
@@ -8,20 +8,67 @@
</template>
<template id="custom_transaction_status" inherit_id="payment.transaction_status">
<xpath expr="//div[@id='o_payment_status_alert']" position="inside">
<t t-if="tx.provider_id.sudo().code == 'custom'">
<div t-if="tx.reference">
<strong>Communication: </strong><span t-esc="tx.reference"/>
<xpath expr="//div[@name='o_payment_status_alert']" position="before">
<h1 t-if=" tx.provider_id.sudo().code == 'custom' and tx.state == 'pending'"
class="mb-3">
Finalize your payment
</h1>
</xpath>
<xpath expr="//div[@name='o_payment_status_alert']" position="inside">
<t t-set="qr_code"
t-value="tx.provider_id.sudo().qr_code and tx.company_id.sudo().partner_id.bank_ids[:1].build_qr_code_base64(tx.amount, tx.reference, None, tx.currency_id, tx.partner_id)"
/>
<t t-if="tx.provider_id.sudo().code == 'custom' and qr_code">
<div class="position-relative order-2 d-flex flex-md-column justify-content-center align-items-center align-items-md-stretch w-100 w-md-auto">
<hr class="d-inline d-md-none w-100"/>
<div class="vr d-none d-md-block h-100 mx-auto"/>
<h6 class="my-1 my-md-3 px-3 px-md-0 text-muted">OR</h6>
<hr class="d-inline d-md-none w-100"/>
<div class="vr d-none d-md-block h-100 mx-auto"/>
</div>
<div t-if="tx.provider_id.sudo().qr_code">
<t t-set="qr_code" t-value="tx.company_id.sudo().partner_id.bank_ids[:1].build_qr_code_base64(tx.amount, tx.reference, None, tx.currency_id, tx.partner_id)"/>
<div t-if="qr_code" class="mt-2">
<h3>Or scan me with your banking app.</h3>
<img class="border border-dark rounded" t-att-src="qr_code"/>
<div class="o_qr_code_card card order-1 order-md-3 bg-info">
<div class="card-body d-flex flex-column align-items-center justify-content-center pb-3">
<img class="mb-2 border border-dark rounded" t-att-src="qr_code"/>
<small class="text-center text-wrap lh-sm">Scan me in your banking app</small>
</div>
</div>
</t>
</xpath>
<xpath expr="//t[@t-set='o_payment_status_alert_class']" position="replace">
<t t-if="tx.provider_id.sudo().code == 'custom'">
<t t-set="o_payment_status_alert_class"
t-value="'d-flex flex-column flex-md-row align-items-stretch gap-2 gap-md-3 mb32'"
/>
</t>
<t t-else="">$0</t>
</xpath>
<xpath expr="//div[@id='o_payment_status_icon']" position="attributes">
<attribute name="t-if">tx.provider_id.sudo().code != 'custom'</attribute>
</xpath>
<xpath expr="//div[@id='o_payment_status_message']" position="replace">
<div t-if="tx.provider_id.sudo().code == 'custom'"
id="o_payment_status_message"
class="order-3 order-md-1 flex-grow-1"
>
<div class="card flex-grow-1">
<div class="card-body">
<t>$0</t>
<t t-if="tx.reference">
<hr class="w-100"/>
<strong class="mt-auto">Communication: </strong>
<span t-out="tx.reference"/>
</t>
</div>
</div>
</div>
<t t-else="">$0</t>
</xpath>
</template>
</odoo>
@@ -6,7 +6,6 @@ from werkzeug.urls import url_encode
from odoo import _, http, tools
from odoo.http import request
from odoo.exceptions import AccessError, ValidationError, UserError
from odoo.addons.payment.controllers.post_processing import PaymentPostProcessing
from odoo.addons.payment.controllers import portal as payment_portal
@@ -267,9 +266,6 @@ class PaymentPortal(payment_portal.PaymentPortal):
tx=tx_sudo, # for the payment.transaction_status template
)
# Stop monitoring the transaction now that it reached a final state.
PaymentPostProcessing.remove_transactions(tx_sudo)
if tx_sudo.state not in ('authorized', 'done'):
rendering_context['state'] = 'tx_error'
return self._render_pay_confirmation(rendering_context)
-6
View File
@@ -15,7 +15,6 @@ from odoo.addons.base.models.ir_qweb_fields import nl2br_enclose
from odoo.addons.http_routing.models.ir_http import slug
from odoo.addons.payment import utils as payment_utils
from odoo.addons.payment.controllers import portal as payment_portal
from odoo.addons.payment.controllers.post_processing import PaymentPostProcessing
from odoo.addons.website.controllers.main import QueryURL
from odoo.addons.website.models.ir_http import sitemap_qs2dom
from odoo.exceptions import AccessError, MissingError, ValidationError
@@ -1691,7 +1690,6 @@ class WebsiteSale(http.Controller):
if tx_sudo and tx_sudo.state == 'draft':
return request.redirect('/shop')
PaymentPostProcessing.remove_transactions(tx_sudo)
return request.redirect('/shop/confirmation')
@http.route(['/shop/confirmation'], type='http', auth="public", website=True, sitemap=False)
@@ -1895,10 +1893,6 @@ class PaymentPortal(payment_portal.PaymentPortal):
# Store the new transaction into the transaction list and if there's an old one, we remove
# it until the day the ecommerce supports multiple orders at the same time.
last_tx_id = request.session.get('__website_sale_last_tx_id')
last_tx = request.env['payment.transaction'].browse(last_tx_id).sudo().exists()
if last_tx:
PaymentPostProcessing.remove_transactions(last_tx)
request.session['__website_sale_last_tx_id'] = tx_sudo.id
self._validate_transaction_for_order(tx_sudo, order_id)