[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:
committed by
Antoine Vandevenne (anv)
co-authored by
mano-odoo
parent
23faf6f7ef
commit
cf52e381bd
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user