[IMP] payment_*: payment acquirers error handling
This commit aims to improve the user experience when using payment acquirers. There currently are no error feedback with some acquirers, which leaves the user wondering what is going on and what is the real status of its payment. In some cases, the user is currently being redirected to the home page even though the payment has failed. We want to make it more obvious to the user that something unexpected has happened by redirecting to an intermediate page that will provide good feedback on payments status. Another goal of this commit is to order acquirers by sequence instead of by flow and to select the first acquirer by default. This feature was already implmented in commit fe294fd43e521bd2d339e962f43acf46c3d4cb97, some UI adaptations were needed though. Related to task #36680 Closes #26958
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
from odoo import http, _
|
||||
from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager
|
||||
from odoo.addons.payment.controllers.portal import PaymentProcessing
|
||||
from odoo.exceptions import AccessError, MissingError
|
||||
from odoo.http import request
|
||||
|
||||
@@ -87,6 +88,7 @@ class PortalAccount(CustomerPortal):
|
||||
return self._show_report(model=invoice_sudo, report_type=report_type, report_ref='account.account_invoices', download=download)
|
||||
|
||||
values = self._invoice_get_page_view_values(invoice_sudo, access_token, **kw)
|
||||
PaymentProcessing.remove_payment_transaction(invoice_sudo.transaction_ids)
|
||||
return request.render("account.portal_invoice_page", values)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
from odoo import http, _
|
||||
from odoo.addons.portal.controllers.portal import _build_url_w_params
|
||||
from odoo.addons.payment.controllers.portal import PaymentProcessing
|
||||
from odoo.http import request, route
|
||||
|
||||
|
||||
@@ -31,16 +32,17 @@ class PaymentPortal(http.Controller):
|
||||
save_token = False # we avoid to create a token for the public user
|
||||
vals = {
|
||||
'acquirer_id': acquirer_id,
|
||||
'return_url': success_url,
|
||||
}
|
||||
|
||||
if save_token:
|
||||
vals['type'] = 'form_save'
|
||||
|
||||
transaction = invoice_sudo._create_payment_transaction(vals)
|
||||
PaymentProcessing.add_payment_transaction(transaction)
|
||||
|
||||
return transaction.render_invoice_button(
|
||||
invoice_sudo,
|
||||
success_url,
|
||||
submit_txt=_('Pay & Confirm'),
|
||||
render_values={
|
||||
'type': 'form_save' if save_token else 'form',
|
||||
@@ -75,9 +77,11 @@ class PaymentPortal(http.Controller):
|
||||
vals = {
|
||||
'payment_token_id': token.id,
|
||||
'type': 'server2server',
|
||||
'return_url': success_url,
|
||||
}
|
||||
|
||||
invoice_sudo._create_payment_transaction(vals)
|
||||
tx = invoice_sudo._create_payment_transaction(vals)
|
||||
PaymentProcessing.add_payment_transaction(tx)
|
||||
|
||||
params['success'] = 'pay_invoice'
|
||||
return request.redirect(_build_url_w_params(success_url, params))
|
||||
|
||||
@@ -12,9 +12,8 @@ _logger = logging.getLogger(__name__)
|
||||
class PaymentTransaction(models.Model):
|
||||
_inherit = 'payment.transaction'
|
||||
|
||||
def render_invoice_button(self, invoice, return_url, submit_txt=None, render_values=None):
|
||||
def render_invoice_button(self, invoice, submit_txt=None, render_values=None):
|
||||
values = {
|
||||
'return_url': return_url,
|
||||
'partner_id': invoice.partner_id.id,
|
||||
}
|
||||
if render_values:
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<button type="button" class="close" data-dismiss="modal">&times;</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div t-if="pms or s2s_acquirers or form_acquirers" id="payment_method" class="text-left col-md-13">
|
||||
<div t-if="pms or acquirers" id="payment_method" class="text-left col-md-13">
|
||||
<t t-call="payment.payment_tokens_list">
|
||||
<t t-set="mode" t-value="'payment'"/>
|
||||
<t t-set="partner_id" t-value="invoice.partner_id.id"/>
|
||||
@@ -92,11 +92,11 @@
|
||||
</div>
|
||||
<t t-set="tx_ids" t-value="invoice.transaction_ids.filtered(lambda tx: tx.state in ('authorized', 'done'))"/>
|
||||
<div t-if="not tx_ids and invoice.state == 'open' and invoice.amount_total" id="portal_pay">
|
||||
<div t-if="pms or s2s_acquirers or form_acquirers" id="payment_method">
|
||||
<div t-if="pms or acquirers" id="payment_method">
|
||||
<t t-call="account_payment.portal_invoice_payment"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body" t-if="existing_token"><!-- FIXME -->
|
||||
<div class="panel-body" t-if="existing_token">
|
||||
<div class="offset-lg-3 col-lg-6">
|
||||
<i class="fa fa-info"></i> You have credits card registered, you can log-in to be able to use them.
|
||||
</div>
|
||||
@@ -137,7 +137,7 @@
|
||||
<b>Communication: </b><span t-esc='invoice.reference'/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="success == 'pay_invoice' and invoice.state == 'paid' and not invoice.payment_acquirer_id">
|
||||
<t t-if="success == 'pay_invoice' and invoice.state == 'paid'">
|
||||
Done, your online payment has been successfully processed. Thank you for your order.
|
||||
</t>
|
||||
</xpath>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
'data/account_data.xml',
|
||||
'data/payment_icon_data.xml',
|
||||
'data/payment_acquirer_data.xml',
|
||||
'data/payment_cron.xml',
|
||||
'views/payment_views.xml',
|
||||
'views/account_payment_views.xml',
|
||||
'views/account_invoice_views.xml',
|
||||
|
||||
@@ -1,10 +1,102 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import http, _
|
||||
from odoo.http import request
|
||||
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class PaymentProcessing(http.Controller):
|
||||
|
||||
@staticmethod
|
||||
def remove_payment_transaction(transactions):
|
||||
tx_ids_list = request.session.get("__payment_tx_ids__", [])
|
||||
if transactions:
|
||||
for tx in transactions:
|
||||
if tx.id in tx_ids_list:
|
||||
tx_ids_list.remove(tx.id)
|
||||
else:
|
||||
return False
|
||||
request.session["__payment_tx_ids__"] = tx_ids_list
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def add_payment_transaction(transactions):
|
||||
if not transactions:
|
||||
return False
|
||||
tx_ids_list = set(request.session.get("__payment_tx_ids__", [])) | set(transactions.ids)
|
||||
request.session["__payment_tx_ids__"] = tx_ids_list
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_payment_transaction_ids():
|
||||
# return the ids and not the recordset, since we might need to
|
||||
# sudo the browse to access all the record
|
||||
# I prefer to let the controller chose when to access to payment.transaction using sudo
|
||||
return request.session.get("__payment_tx_ids__", [])
|
||||
|
||||
@http.route(['/payment/process'], type="http", auth="public", website=True)
|
||||
def payment_status_page(self, **kwargs):
|
||||
# When the customer is redirect to this website page,
|
||||
# we retrieve the payment transaction list from his session
|
||||
tx_ids_list = self.get_payment_transaction_ids()
|
||||
payment_transaction_ids = request.env['payment.transaction'].sudo().browse(tx_ids_list).exists()
|
||||
|
||||
render_ctx = {
|
||||
'payment_tx_ids': payment_transaction_ids.ids,
|
||||
}
|
||||
return request.render("payment.payment_process_page", render_ctx)
|
||||
|
||||
@http.route(['/payment/process/poll'], type="json", auth="public")
|
||||
def payment_status_poll(self):
|
||||
# retrieve the transactions
|
||||
tx_ids_list = self.get_payment_transaction_ids()
|
||||
|
||||
payment_transaction_ids = request.env['payment.transaction'].sudo().search([
|
||||
('id', 'in', list(tx_ids_list)),
|
||||
('date', '>=', (datetime.now() - timedelta(days=1)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
|
||||
])
|
||||
if not payment_transaction_ids:
|
||||
return {
|
||||
'success': False,
|
||||
'error': 'no_tx_found',
|
||||
}
|
||||
# create the returned dictionnary
|
||||
result = {
|
||||
'success': True,
|
||||
'transactions': [],
|
||||
}
|
||||
# populate the returned dictionnary with the transactions data
|
||||
for tx in payment_transaction_ids:
|
||||
message_to_display = tx.acquirer_id[tx.state + '_msg'] if tx.state in ['done', 'pending', 'cancel', 'error'] else None
|
||||
result['transactions'].append({
|
||||
'reference': tx.reference,
|
||||
'state': tx.state,
|
||||
'return_url': tx.return_url,
|
||||
'is_processed': tx.is_processed,
|
||||
'state_message': tx.state_message,
|
||||
'message_to_display': message_to_display,
|
||||
'amount': tx.amount,
|
||||
'currency': tx.currency_id.name,
|
||||
'acquirer_provider': tx.acquirer_id.provider,
|
||||
})
|
||||
|
||||
tx_to_process = payment_transaction_ids.filtered(lambda x: x.state == 'done' and x.is_processed is False)
|
||||
try:
|
||||
tx_to_process._post_process_after_done()
|
||||
except Exception as e:
|
||||
request.env.cr.rollback()
|
||||
result['success'] = False
|
||||
result['error'] = str(e)
|
||||
_logger.error("Error while processing transaction(s) %s, exception \"%s\"", tx_to_process.ids, str(e))
|
||||
|
||||
return result
|
||||
|
||||
class WebsitePayment(http.Controller):
|
||||
@http.route(['/my/payment_method'], type='http', auth="user", website=True)
|
||||
def payment_method(self, **kwargs):
|
||||
@@ -78,15 +170,13 @@ class WebsitePayment(http.Controller):
|
||||
partner_id = user.partner_id.id if not user._is_public() else False
|
||||
|
||||
values.update({
|
||||
'return_url': '/website_payment/confirm',
|
||||
'partner_id': partner_id,
|
||||
'bootstrap_formatting': True,
|
||||
'error_msg': kw.get('error_msg')
|
||||
})
|
||||
|
||||
values['s2s_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 's2s']
|
||||
values['form_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 'form']
|
||||
values['pms'] = request.env['payment.token'].search([('acquirer_id', 'in', [acq.id for acq in values['s2s_acquirers']])])
|
||||
values['acquirers'] = [acq for acq in acquirers if acq.payment_flow in ['form', 's2s']]
|
||||
values['pms'] = request.env['payment.token'].search([('acquirer_id', 'in', acquirers.filtered(lambda x: x.payment_flow == 's2s').ids)])
|
||||
|
||||
return request.render('payment.pay', values)
|
||||
|
||||
@@ -97,6 +187,9 @@ class WebsitePayment(http.Controller):
|
||||
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
|
||||
order_id = kwargs.get('order_id')
|
||||
|
||||
reference_values = order_id and {'sale_order_ids': [(4, order_id)]} or {}
|
||||
reference = request.env['payment.transaction']._compute_reference(values=reference_values, prefix=reference)
|
||||
|
||||
values = {
|
||||
'acquirer_id': int(acquirer_id),
|
||||
'reference': reference,
|
||||
@@ -112,9 +205,11 @@ class WebsitePayment(http.Controller):
|
||||
reference_values = order_id and {'sale_order_ids': [(4, order_id)]} or {}
|
||||
values['reference'] = request.env['payment.transaction']._compute_reference(values=reference_values, prefix=reference)
|
||||
tx = request.env['payment.transaction'].sudo().with_context(lang=None).create(values)
|
||||
tx.return_url = '/website_payment/confirm?tx_id=%d' % tx.id
|
||||
|
||||
PaymentProcessing.add_payment_transaction(tx)
|
||||
|
||||
render_values = {
|
||||
'return_url': '/website_payment/confirm?tx_id=%d' % tx.id,
|
||||
'partner_id': partner_id,
|
||||
}
|
||||
|
||||
@@ -139,23 +234,25 @@ class WebsitePayment(http.Controller):
|
||||
'partner_id': partner_id,
|
||||
'payment_token_id': pm_id,
|
||||
'type': 'form_save' if token.acquirer_id.save_token != 'none' and partner_id else 'form',
|
||||
'return_url': return_url,
|
||||
}
|
||||
|
||||
if order_id:
|
||||
values['sale_order_ids'] = [(6, 0, [order_id])]
|
||||
|
||||
tx = request.env['payment.transaction'].sudo().with_context(lang=None).create(values)
|
||||
PaymentProcessing.add_payment_transaction(tx)
|
||||
|
||||
try:
|
||||
res = tx.s2s_do_transaction()
|
||||
if tx.state == 'done':
|
||||
tx.return_url = return_url or '/website_payment/confirm?tx_id=%d' % tx.id
|
||||
valid_state = 'authorized' if tx.acquirer_id.capture_manually else 'done'
|
||||
if not res or tx.state != valid_state:
|
||||
tx.return_url = '/website_payment/pay?error_msg=%s' % _('Payment transaction failed.')
|
||||
return request.redirect('/payment/process')
|
||||
except Exception as e:
|
||||
return request.redirect('/website_payment/pay?error_msg=%s' % _('Payment transaction failed.'))
|
||||
|
||||
valid_state = 'authorized' if tx.acquirer_id.capture_manually else 'done'
|
||||
if not res or tx.state != valid_state:
|
||||
return request.redirect('/website_payment/pay?error_msg=%s' % _('Payment transaction failed.'))
|
||||
|
||||
return request.redirect(return_url if return_url else '/website_payment/confirm?tx_id=%d' % tx.id)
|
||||
return request.redirect('/payment/process')
|
||||
|
||||
@http.route(['/website_payment/confirm'], type='http', auth='public', website=True)
|
||||
def confirm(self, **kw):
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record model="ir.cron" id="cron_post_process_payment_tx">
|
||||
<field name="name">Post process payment transactions</field>
|
||||
<field name="model_id" ref="payment.model_payment_transaction"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_post_process_after_done()</field>
|
||||
<field name="active" eval="True"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="interval_number">10</field> <!-- To decide clearly -->
|
||||
<field name="interval_type">minutes</field>
|
||||
<field name="numbercall">-1</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -308,10 +308,7 @@ class PaymentAcquirer(models.Model):
|
||||
|
||||
It contains
|
||||
|
||||
* form_acquirers: record set of acquirers based on a local form that
|
||||
sends customer to the acquirer website;
|
||||
* s2s_acquirers: reset set of acquirers that send customer data to
|
||||
acquirer without redirecting to any other website;
|
||||
* acquirers: record set of both form and s2s acquirers;
|
||||
* pms: record set of stored credit card data (aka payment.token)
|
||||
connected to a given partner to allow customers to reuse them """
|
||||
if not company:
|
||||
@@ -319,14 +316,13 @@ class PaymentAcquirer(models.Model):
|
||||
if not partner:
|
||||
partner = self.env.user.partner_id
|
||||
active_acquirers = self.sudo().search([('website_published', '=', True), ('company_id', '=', company.id)])
|
||||
form_acquirers = active_acquirers.filtered(lambda acq: acq.payment_flow == 'form' and acq.view_template_id)
|
||||
s2s_acquirers = active_acquirers.filtered(lambda acq: acq.payment_flow == 's2s' and acq.registration_view_template_id)
|
||||
acquirers = active_acquirers.filtered(lambda acq: (acq.payment_flow == 'form' and acq.view_template_id) or
|
||||
(acq.payment_flow == 's2s' and acq.registration_view_template_id))
|
||||
return {
|
||||
'form_acquirers': form_acquirers,
|
||||
's2s_acquirers': s2s_acquirers,
|
||||
'acquirers': acquirers,
|
||||
'pms': self.env['payment.token'].search([
|
||||
('partner_id', '=', partner.id),
|
||||
('acquirer_id', 'in', s2s_acquirers.ids)]),
|
||||
('acquirer_id', 'in', acquirers.filtered(lambda acq: acq.payment_flow == 's2s').ids)]),
|
||||
}
|
||||
|
||||
@api.multi
|
||||
@@ -571,7 +567,8 @@ class PaymentTransaction(models.Model):
|
||||
('pending', 'Pending'),
|
||||
('authorized', 'Authorized'),
|
||||
('done', 'Done'),
|
||||
('cancel', 'Canceled')],
|
||||
('cancel', 'Canceled'),
|
||||
('error', 'Error'),],
|
||||
string='Status', copy=False, default='draft', required=True, readonly=True)
|
||||
state_message = fields.Text(string='Message', readonly=True,
|
||||
help='Field used to store error and/or validation messages for information')
|
||||
@@ -599,6 +596,10 @@ class PaymentTransaction(models.Model):
|
||||
callback_method = fields.Char('Callback Method', groups="base.group_system")
|
||||
callback_hash = fields.Char('Callback Hash', groups="base.group_system")
|
||||
|
||||
# Fields used for user redirection & payment post processing
|
||||
return_url = fields.Char('Return URL after payment')
|
||||
is_processed = fields.Boolean('Has the payment been post processed', default=False)
|
||||
|
||||
# Fields used for payment.transaction traceability.
|
||||
|
||||
payment_token_id = fields.Many2one('payment.token', 'Payment Token', readonly=True,
|
||||
@@ -730,8 +731,6 @@ class PaymentTransaction(models.Model):
|
||||
raise ValidationError(_('Only draft/authorized transaction can be posted.'))
|
||||
|
||||
self.write({'state': 'done', 'date': datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
|
||||
self._reconcile_after_transaction_done()
|
||||
self._log_payment_transaction_received()
|
||||
|
||||
@api.multi
|
||||
def _reconcile_after_transaction_done(self):
|
||||
@@ -768,6 +767,39 @@ class PaymentTransaction(models.Model):
|
||||
self.write({'state': 'cancel', 'date': datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
|
||||
self._log_payment_transaction_received()
|
||||
|
||||
@api.multi
|
||||
def _set_transaction_error(self, msg):
|
||||
'''Move the transaction to the error state (Third party returning error e.g. Paypal).'''
|
||||
if any(trans.state != 'draft' for trans in self):
|
||||
raise ValidationError(_('Only draft transaction can be processed.'))
|
||||
|
||||
self.write({
|
||||
'state': 'error',
|
||||
'date': datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
|
||||
'state_message': msg,
|
||||
})
|
||||
|
||||
@api.multi
|
||||
def _post_process_after_done(self):
|
||||
self._reconcile_after_transaction_done()
|
||||
self._log_payment_transaction_received()
|
||||
self.write({'is_processed': True})
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def _cron_post_process_after_done(self):
|
||||
if not self:
|
||||
# we retrieve all the payment tx that need to be post processed
|
||||
self = self.search([('state', '=', 'done'),
|
||||
('is_processed', '=', False)
|
||||
])
|
||||
for tx in self:
|
||||
try:
|
||||
tx._post_process_after_done()
|
||||
self.env.cr.commit()
|
||||
except Exception as e:
|
||||
_logger.error("Transaction post processing failed, reason \"%s\"", e)
|
||||
|
||||
@api.model
|
||||
def _compute_reference_prefix(self, values):
|
||||
if values and values.get('invoice_ids'):
|
||||
@@ -919,10 +951,6 @@ class PaymentTransaction(models.Model):
|
||||
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def _post_process_after_done(self, **kwargs):
|
||||
return True
|
||||
|
||||
# --------------------------------------------------
|
||||
# SERVER2SERVER RELATED METHODS
|
||||
# --------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
odoo.define('payment.processing', function (require) {
|
||||
'use strict';
|
||||
|
||||
var Widget = require('web.Widget');
|
||||
var Ajax = require('web.ajax');
|
||||
var Core = require('web.core');
|
||||
var Qweb = Core.qweb;
|
||||
var _t = Core._t;
|
||||
|
||||
$.blockUI.defaults.css.border = '0';
|
||||
$.blockUI.defaults.css["background-color"] = '';
|
||||
$.blockUI.defaults.overlayCSS["opacity"] = '0.9';
|
||||
|
||||
return Widget.extend({
|
||||
/* Members */
|
||||
_payment_tx_ids: null,
|
||||
_pollCount: 0,
|
||||
/* Events */
|
||||
events: {
|
||||
|
||||
},
|
||||
/* deps */
|
||||
xmlDependencies: ['/payment/static/src/xml/payment_processing.xml'],
|
||||
/* Widget overrides */
|
||||
init: function (parent, payment_tx_ids) {
|
||||
this._super.apply(this, arguments);
|
||||
//
|
||||
this._payment_tx_ids = payment_tx_ids;
|
||||
},
|
||||
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;
|
||||
Ajax.jsonRpc('/payment/process/poll', 'call', {}).then(function(data) {
|
||||
if(data.success === true) {
|
||||
self.processPolledData(data.transactions);
|
||||
}
|
||||
else {
|
||||
switch(data.error) {
|
||||
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();
|
||||
}).always(function(){
|
||||
if ($.blockUI) {
|
||||
$.unblockUI();
|
||||
}
|
||||
}).fail(function(e) {
|
||||
self.displayContent("payment.rpc_error", {});
|
||||
self.startPolling();
|
||||
});
|
||||
},
|
||||
processPolledData: function (transactions) {
|
||||
var render_values = {
|
||||
'tx_draft': [],
|
||||
'tx_pending': [],
|
||||
'tx_authorized': [],
|
||||
'tx_done': [],
|
||||
'tx_cancel': [],
|
||||
'tx_error': [],
|
||||
};
|
||||
// group the transaction according to their state
|
||||
transactions.forEach(tx => {
|
||||
var key = 'tx_' + tx.state;
|
||||
if(key in render_values) {
|
||||
render_values[key].push(tx);
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
// if there's only one tx to manage
|
||||
if(countTxInState(['tx_done', 'tx_error']) === 1) {
|
||||
var tx = render_values['tx_done'][0] || render_values['tx_error'][0];
|
||||
if (tx) {
|
||||
window.location = tx.return_url;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.displayContent("payment.display_tx_list", render_values);
|
||||
},
|
||||
displayContent: function (xmlid, render_values) {
|
||||
var html = Qweb.render(xmlid, render_values);
|
||||
this.$el.find('.o_payment_processing_content').html(html);
|
||||
},
|
||||
displayLoading: function () {
|
||||
var msg = _t("We are processing your payments, please wait ...");
|
||||
$.blockUI({
|
||||
'message': '<h2 class="text-white"><img src="/web/static/src/img/spin.png" class="fa-pulse"/>' +
|
||||
' <br />' + msg +
|
||||
'</h2>'
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="payment" xml:space="preserve">
|
||||
<!-- The templates here as rendered by 'payment_processing.js', you can also take
|
||||
a look at payment_templates.xml (xmlid: payment_process_page) for more infos-->
|
||||
<t t-name="payment.display_tx_list">
|
||||
<div>
|
||||
<!-- Error transactions -->
|
||||
<div t-if="tx_error.length > 0">
|
||||
<h1>Payments failed</h1>
|
||||
<ul class="list-group">
|
||||
<t t-foreach="tx_error" t-as="tx">
|
||||
<li class="list-group-item">
|
||||
<h4 class="list-group-item-heading mb5">
|
||||
<t t-esc="tx['reference']"/>
|
||||
<span class="badge pull-right"><t t-esc="tx['amount']"/> <t t-esc="tx['currency']"/></span>
|
||||
</h4>
|
||||
<small class="list-group-item-text">
|
||||
An error occured during the processing of this payment.<br/>
|
||||
<strong>Reason:</strong> <t t-esc="tx['state_message']"/>
|
||||
</small>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
<div t-if="tx_done.length > 0 || tx_authorized.length > 0 || tx_pending.length > 0">
|
||||
<h1>Payments received</h1>
|
||||
<div class="list-group">
|
||||
<!-- Done transactions -->
|
||||
<t t-foreach="tx_done" t-as="tx">
|
||||
<a t-att-href="tx['return_url']" class="list-group-item">
|
||||
<h4 class="list-group-item-heading mb5">
|
||||
<t t-esc="tx['reference']"/>
|
||||
<span class="badge pull-right"><t t-esc="tx['amount']"/> <t t-esc="tx['currency']"/></span>
|
||||
</h4>
|
||||
<small class="list-group-item-text">
|
||||
<t t-if="!tx['is_processed']">
|
||||
Your order is being processed, please wait ... <i class="fa fa-cog fa-spin"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
Your order has been processed.<br/>
|
||||
Click here to be redirected to the confirmation page.
|
||||
</t>
|
||||
</small>
|
||||
</a>
|
||||
</t>
|
||||
<!-- Pending transactions -->
|
||||
<t t-foreach="tx_pending" t-as="tx">
|
||||
<a t-att-href="tx['return_url']" class="list-group-item">
|
||||
<h4 class="list-group-item-heading mb5">
|
||||
<t t-esc="tx['reference']"/>
|
||||
<span class="badge pull-right"><t t-esc="tx['amount']"/> <t t-esc="tx['currency']"/></span>
|
||||
</h4>
|
||||
<small class="list-group-item-text">
|
||||
<t t-if="tx['message_to_display']">
|
||||
<t t-raw="tx['message_to_display']"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
Your payment is in pending state.<br/>
|
||||
You will be notified when the payment is fully confirmed.<br/>
|
||||
You can click here to be redirected to the confirmation page.
|
||||
</t>
|
||||
</small>
|
||||
</a>
|
||||
</t>
|
||||
<!-- Authorized transactions -->
|
||||
<t t-foreach="tx_authorized" t-as="tx">
|
||||
<li class="list-group-item">
|
||||
<h4 class="list-group-item-heading mb5">
|
||||
<t t-esc="tx['reference']"/>
|
||||
<span class="badge pull-right"><t t-esc="tx['amount']"/> <t t-esc="tx['currency']"/></span>
|
||||
</h4>
|
||||
<small class="list-group-item-text">
|
||||
<t t-if="tx['message_to_display']">
|
||||
<t t-raw="tx['message_to_display']"/>
|
||||
</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>
|
||||
</li>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Draft transactions -->
|
||||
<div t-if="tx_draft.length > 0">
|
||||
<h1>Waiting for payment</h1>
|
||||
<ul class="list-group">
|
||||
<t t-foreach="tx_draft" t-as="tx">
|
||||
<li class="list-group-item">
|
||||
<h4 class="list-group-item-heading mb5">
|
||||
<t t-esc="tx['reference']"/>
|
||||
<span class="badge pull-right"><t t-esc="tx['amount']"/> <t t-esc="tx['currency']"/></span>
|
||||
</h4>
|
||||
<small class="list-group-item-text">
|
||||
<t t-if="tx['message_to_display']">
|
||||
<t t-raw="tx['message_to_display']"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
We are waiting for the payment acquirer to confirm the payment.
|
||||
</t>
|
||||
</small>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Cancel transactions -->
|
||||
<div t-if="tx_cancel.length > 0">
|
||||
<h1>Cancelled payments</h1>
|
||||
<ul class="list-group">
|
||||
<t t-foreach="tx_cancel" t-as="tx">
|
||||
<li class="list-group-item">
|
||||
<h4 class="list-group-item-heading mb5">
|
||||
<t t-esc="tx['reference']"/>
|
||||
<span class="badge pull-right"><t t-esc="tx['amount']"/> <t t-esc="tx['currency']"/></span>
|
||||
</h4>
|
||||
<small class="list-group-item-text">
|
||||
This transaction has been cancelled.<br/>
|
||||
No payment has been processed.
|
||||
</small>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="payment.no_tx_found">
|
||||
<div class="text-center">
|
||||
<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>
|
||||
</div>
|
||||
</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>
|
||||
</t>
|
||||
|
||||
<t t-name="payment.exception">
|
||||
<div class="text-center">
|
||||
<h2>Internal server error</h2>
|
||||
<pre><t t-esc="exception_msg"/></pre>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -10,6 +10,7 @@
|
||||
<script type="text/javascript" src="/payment/static/src/js/payment_portal.js"></script>
|
||||
<script type="text/javascript" src="/payment/static/src/js/payment_transaction_portal.js"></script>
|
||||
<script type="text/javascript" src="/payment/static/src/js/payment_form.js"></script>
|
||||
<script type="text/javascript" src="/payment/static/src/js/payment_processing.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<div class="col-md-8">
|
||||
<h3>Manage your Payment Methods</h3>
|
||||
<t t-call="payment.payment_tokens_list">
|
||||
<t t-set="s2s_acquirers" t-value="acquirers"/>
|
||||
<t t-set="acquirers" t-value="acquirers"/>
|
||||
<t t-set="mode" t-value="'manage'"/>
|
||||
<t t-set="verify_validity" t-value="True"/>
|
||||
</t>
|
||||
@@ -58,7 +58,7 @@
|
||||
<t t-set="prepare_tx_url" t-value="'/website_payment/transaction/v2/' + str(amount) + '/' + str(currency.id) + '/' + reference"/>
|
||||
<t t-set="form_action" t-value="'/website_payment/token/v2/' + str(amount) + '/' + str(currency.id) + '/' + reference"/>
|
||||
</t>
|
||||
<div t-if="not (s2s_acquirers or form_acquirers)" class="alert alert-danger" role="alert">
|
||||
<div t-if="not acquirers" class="alert alert-danger" role="alert">
|
||||
<p>No payment acquirer found.</p>
|
||||
<p>Please configure a payment acquirer.</p>
|
||||
</div>
|
||||
@@ -165,7 +165,7 @@
|
||||
<button type="button" class="close" data-dismiss="alert" title="Dismiss" aria-label="Dismiss">&times;</button>
|
||||
<span t-if='payment_tx_id.acquirer_id.cancel_msg' t-raw="payment_tx_id.acquirer_id.cancel_msg"/>
|
||||
</div>
|
||||
<div t-if="payment_tx_id.state_message">
|
||||
<div t-if="payment_tx_id and payment_tx_id.state_message">
|
||||
<span t-esc="payment_tx_id.state_message"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -17,12 +17,11 @@
|
||||
- 'checked_pm_id' the payment token that should be checked (for radio buttons)
|
||||
- 'mode' can take two values, either 'payment' or 'manage'. 'manage' displays the add a new card and delete buttons. 'payment'
|
||||
display a form that is used to pay and send the information to the form action url.
|
||||
- 's2s_acquirers' the list of the server2server acquirers
|
||||
- 'form_acquirers' the list of the payment acquirers to pay with forms
|
||||
- 'acquirers' the list of both server2server and form payment acquirers
|
||||
- 'verify_validity' if we need to verify if the payment method is valid when adding a new one
|
||||
- 'prepare_tx_url' the url of the route which will handle the creation of a transaction for a form base payment (handles if the transaction is form or form_save)
|
||||
-->
|
||||
<form t-if="s2s_acquirers or pms or form_acquirers" method="post" class="o_payment_form mt-3 clearfix"
|
||||
<form t-if="pms or acquirers" method="post" class="o_payment_form mt-3 clearfix"
|
||||
t-att-action="form_action if form_action else '#'"
|
||||
t-att-data-success-url="success_url or ''"
|
||||
t-att-data-error-url="error_url or ''"
|
||||
@@ -41,27 +40,27 @@
|
||||
<input type="hidden" t-if="callback_method" name="callback_method" t-att-value="callback_method"/>
|
||||
|
||||
<div class="card">
|
||||
<t t-set="acquirers_count" t-value="(len(s2s_acquirers) if s2s_acquirers else 0) + (len(form_acquirers) if form_acquirers else 0)"/>
|
||||
<t t-set="acquirers_count" t-value="len(acquirers) if acquirers else 0"/>
|
||||
<t t-set="pms_count" t-value="len(pms) if pms else 0"/>
|
||||
<t t-set="MAX_BRAND_LINE" t-value="3"/>
|
||||
<t t-foreach="s2s_acquirers" t-as="acq">
|
||||
<t t-foreach="acquirers" t-as="acq">
|
||||
<div class="card-body">
|
||||
<label>
|
||||
<t t-if="mode == 'manage'">
|
||||
<t t-if="acq.payment_flow == 'form'">
|
||||
<input type="radio" t-att-data-acquirer-id="acq.id"
|
||||
t-att-data-s2s-payment="true"
|
||||
t-att-data-provider="acq.provider"
|
||||
name="pm_id" t-attf-value="new_{{acq.id}}"
|
||||
t-att-class="'d-none' if acquirers_count==1 else ''"
|
||||
t-att-checked="acquirers_count == 1"/>
|
||||
t-att-data-form-payment="true"
|
||||
t-att-data-provider="acq.provider"
|
||||
t-att-class="'d-none' if (acquirers_count==1 and pms_count==0) else ''"
|
||||
name="pm_id" t-attf-value="form_{{acq.id}}"
|
||||
t-att-checked="acquirers_count==1 and pms_count==0 or acquirers[0] == acq"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-else="acq.payment_flow == 's2s'">
|
||||
<input type="radio" t-att-data-acquirer-id="acq.id"
|
||||
t-att-data-s2s-payment="true"
|
||||
t-att-data-provider="acq.provider"
|
||||
name="pm_id" t-attf-value="new_{{acq.id}}"
|
||||
t-att-class="'d-none' if (acquirers_count==1 and pms_count==0) else ''"
|
||||
t-att-checked="acquirers_count==1 and pms_count==0"/>
|
||||
t-att-data-s2s-payment="true"
|
||||
t-att-data-provider="acq.provider"
|
||||
name="pm_id" t-attf-value="new_{{acq.id}}"
|
||||
t-att-class="'d-none' if (acquirers_count==1 and pms_count==0) else ''"
|
||||
t-att-checked="acquirers_count==1 and pms_count==0 or acquirers[0] == acq"/>
|
||||
</t>
|
||||
<span class="payment_option_name" t-esc="acq.name"/>
|
||||
</label>
|
||||
@@ -70,7 +69,7 @@
|
||||
<t t-foreach="acq.payment_icon_ids" t-as="pm_icon">
|
||||
<li t-attf-class="list-inline-item#{'' if (i < MAX_BRAND_LINE) else ' d-none'}">
|
||||
<span t-field="pm_icon.image_payment_form"
|
||||
t-options='{"widget": "image", "alt-field": "name"}' />
|
||||
t-options='{"widget": "image", "alt-field": "name"}'/>
|
||||
</li>
|
||||
<li t-if="i==MAX_BRAND_LINE" style="display:block;" class="list-inline-item">
|
||||
<span class="float-right more_option text-info">
|
||||
@@ -81,46 +80,27 @@
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
<div t-attf-id="o_payment_add_token_acq_{{acq.id}}" t-attf-class="card-footer {{'d-none' if(acquirers_count > 1 and pms_count==0 and s2s_acquirers[0]!=acq) else 'd-none' if pms_count >0 else ''}}">
|
||||
<div class="clearfix">
|
||||
<t t-call="{{acq.sudo().get_s2s_form_xml_id()}}">
|
||||
<t t-set="id" t-value="acq.id"/>
|
||||
<t t-set="partner_id" t-value="partner_id"/>
|
||||
<t t-if="not return_url" t-set="return_url" t-value="''"/>
|
||||
</t>
|
||||
<t t-if="acq.payment_flow == 'form'">
|
||||
<div t-attf-id="o_payment_form_acq_{{acq.id}}"
|
||||
t-attf-class="d-none {{'card-footer' if acq.save_token == 'ask' else ''}}">
|
||||
<label t-if="acq.save_token == 'ask'">
|
||||
<input type="checkbox" name="o_payment_form_save_token" data-remove-me=""/>
|
||||
Save my payment data
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="mode == 'payment'" t-foreach="form_acquirers" t-as="acq">
|
||||
<div class="card-body">
|
||||
<label>
|
||||
<input type="radio" t-att-data-acquirer-id="acq.id"
|
||||
t-att-data-form-payment="true"
|
||||
t-att-data-provider="acq.provider"
|
||||
t-att-class="'d-none' if (acquirers_count==1 and pms_count==0) else ''"
|
||||
t-att-checked="acquirers_count==1 and pms_count==0"
|
||||
name="pm_id" t-attf-value="form_{{acq.id}}"/>
|
||||
<span class="payment_icon_name" t-esc="acq.name"/>
|
||||
</label>
|
||||
<ul class="float-right list-inline payment_icon_list">
|
||||
<t t-set="i" t-value="0"/>
|
||||
<t t-foreach="acq.payment_icon_ids" t-as="pm_icon">
|
||||
<li t-attf-class="list-inline-item#{'' if (i < MAX_BRAND_LINE) else ' d-none'}">
|
||||
<span t-field="pm_icon.image_payment_form"
|
||||
t-options='{"widget": "image", "alt-field": "name"}' />
|
||||
</li>
|
||||
<li t-if="i==MAX_BRAND_LINE" style="display:block;" class="list-inline-item">
|
||||
<span class="float-right more_option text-info">
|
||||
<a href="#" class="o_payment_form_pay_icon_more" data-toggle="tooltip" t-att-title="', '.join([opt.name for opt in acq.payment_icon_ids[MAX_BRAND_LINE:]])">and more</a>
|
||||
</span>
|
||||
</li>
|
||||
<t t-set="i" t-value="i+1"/>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
<div t-attf-id="o_payment_form_acq_{{acq.id}}" t-attf-class="d-none {{'card-footer' if acq.save_token == 'ask' else ''}}">
|
||||
<label t-if="acq.save_token == 'ask'"><input type="checkbox" name="o_payment_form_save_token" data-remove-me=""/> Save my payment data</label>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="acq.payment_flow == 's2s'">
|
||||
<div t-attf-id="o_payment_add_token_acq_{{acq.id}}"
|
||||
t-attf-class="card-footer {{'d-none' if(acquirers_count > 1 and pms_count==0 and acquirers[0]!=acq) else 'd-none' if pms_count >0 else ''}}">
|
||||
<div class="clearfix">
|
||||
<t t-call="{{acq.sudo().get_s2s_form_xml_id()}}">
|
||||
<t t-set="id" t-value="acq.id"/>
|
||||
<t t-set="partner_id" t-value="partner_id"/>
|
||||
<t t-if="not return_url" t-set="return_url" t-value="''"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-foreach="pms" t-as="pm">
|
||||
<div class="card-body">
|
||||
@@ -157,10 +137,37 @@
|
||||
<i class="fa fa-lock"> Pay</i>
|
||||
</t>
|
||||
</button>
|
||||
<button t-if="mode == 'manage' and s2s_acquirers" id="o_payment_form_add_pm" class="btn btn-primary btn-lg mb8 mt8">
|
||||
<button t-if="mode == 'manage' and acquirers.filtered(lambda x: x.payment_flow == 's2s')" id="o_payment_form_add_pm" class="btn btn-primary btn-lg mb8 mt8">
|
||||
<i class="fa fa-plus-circle"> Add new card</i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<template id="payment_process_page" name="Payment processing page">
|
||||
<!-- If you are looking at this template and tries to understand how it works,
|
||||
You should take a look at the file located in /static/src/js/payment_processing.js
|
||||
-->
|
||||
<t t-call="portal.frontend_layout">
|
||||
<div class="wrap">
|
||||
<div class="container o_website_payment">
|
||||
<div class="o_payment_processing">
|
||||
<div class="row">
|
||||
<div class="o_payment_processing_content col-sm-6 col-sm-offset-3">
|
||||
<!-- here should be the content generated by payment_processing.js -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- We load and attach the payment processing widget -->
|
||||
<script>
|
||||
odoo.define('payment.processing_bootstrap', function(require) {
|
||||
var PaymentProcessing = require('payment.processing');
|
||||
var processingWidget = new PaymentProcessing(null, <t t-esc="payment_tx_ids"/>);
|
||||
processingWidget.attachTo($('.o_payment_processing'));
|
||||
});
|
||||
</script>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
@@ -21,11 +21,7 @@ class AdyenController(http.Controller):
|
||||
_logger.info('Beginning Adyen form_feedback with post data %s', pprint.pformat(post)) # debug
|
||||
if post.get('authResult') not in ['CANCELLED']:
|
||||
request.env['payment.transaction'].sudo().form_feedback(post, 'adyen')
|
||||
return_url = post.pop('return_url', '')
|
||||
if not return_url:
|
||||
custom = json.loads(post.pop('merchantReturnData', '{}'))
|
||||
return_url = custom.pop('return_url', '/')
|
||||
return werkzeug.utils.redirect(return_url)
|
||||
return werkzeug.utils.redirect('/payment/process')
|
||||
|
||||
@http.route([
|
||||
'/payment/adyen/notification',
|
||||
|
||||
@@ -5,7 +5,7 @@ from werkzeug import urls, utils
|
||||
|
||||
from odoo import http, _
|
||||
from odoo.http import request
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -20,16 +20,14 @@ class AuthorizeController(http.Controller):
|
||||
], type='http', auth='public', csrf=False)
|
||||
def authorize_form_feedback(self, **post):
|
||||
_logger.info('Authorize: entering form_feedback with post data %s', pprint.pformat(post))
|
||||
return_url = '/'
|
||||
if post:
|
||||
request.env['payment.transaction'].sudo().form_feedback(post, 'authorize')
|
||||
return_url = post.pop('return_url', '/')
|
||||
base_url = request.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
# Authorize.Net is expecting a response to the POST sent by their server.
|
||||
# This response is in the form of a URL that Authorize.Net will pass on to the
|
||||
# client's browser to redirect them to the desired location need javascript.
|
||||
return request.render('payment_authorize.payment_authorize_redirect', {
|
||||
'return_url': urls.url_join(base_url, return_url)
|
||||
'return_url': urls.url_join(base_url, "/payment/process")
|
||||
})
|
||||
|
||||
@http.route(['/payment/authorize/s2s/create_json'], type='json', auth='public')
|
||||
@@ -38,13 +36,18 @@ class AuthorizeController(http.Controller):
|
||||
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
|
||||
if not kwargs.get('partner_id'):
|
||||
kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
|
||||
return acquirer.s2s_process(kwargs).id
|
||||
try:
|
||||
return acquirer.s2s_process(kwargs).id
|
||||
except (ValidationError, UserError) as e:
|
||||
return {
|
||||
'error': e.name,
|
||||
}
|
||||
|
||||
@http.route(['/payment/authorize/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
|
||||
def authorize_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
|
||||
token = False
|
||||
acquirer = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id')))
|
||||
|
||||
|
||||
try:
|
||||
if not kwargs.get('partner_id'):
|
||||
kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
|
||||
@@ -65,6 +68,10 @@ class AuthorizeController(http.Controller):
|
||||
return {
|
||||
'error': message
|
||||
}
|
||||
except UserError as e:
|
||||
return {
|
||||
'error': e.name,
|
||||
}
|
||||
|
||||
if not token:
|
||||
res = {
|
||||
@@ -91,4 +98,4 @@ class AuthorizeController(http.Controller):
|
||||
acquirer_id = int(post.get('acquirer_id'))
|
||||
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
|
||||
acquirer.s2s_process(post)
|
||||
return utils.redirect(post.get('return_url', '/'))
|
||||
return utils.redirect("/payment/process")
|
||||
|
||||
@@ -28,4 +28,4 @@ class BuckarooController(http.Controller):
|
||||
request.env['payment.transaction'].sudo().form_feedback(post, 'buckaroo')
|
||||
post = {key.upper(): value for key, value in post.items()}
|
||||
return_url = post.get('ADD_RETURNDATA') or '/'
|
||||
return werkzeug.utils.redirect(return_url)
|
||||
return werkzeug.utils.redirect('/payment/process')
|
||||
|
||||
@@ -7,6 +7,7 @@ from werkzeug.urls import url_unquote_plus
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.payment.models.payment_acquirer import ValidationError
|
||||
from odoo.addons.payment.controllers.portal import PaymentProcessing
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,7 +28,7 @@ class OgoneController(http.Controller):
|
||||
""" Ogone contacts using GET, at least for accept """
|
||||
_logger.info('Ogone: entering form_feedback with post data %s', pprint.pformat(post)) # debug
|
||||
request.env['payment.transaction'].sudo().form_feedback(post, 'ogone')
|
||||
return werkzeug.utils.redirect(url_unquote_plus(post.pop('return_url', '/')))
|
||||
return werkzeug.utils.redirect("/payment/process")
|
||||
|
||||
@http.route(['/payment/ogone/s2s/create_json'], type='json', auth='public', csrf=False)
|
||||
def ogone_s2s_create_json(self, **kwargs):
|
||||
@@ -40,11 +41,18 @@ class OgoneController(http.Controller):
|
||||
def ogone_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
|
||||
if not kwargs.get('partner_id'):
|
||||
kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
|
||||
token = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
|
||||
token = False
|
||||
error = None
|
||||
|
||||
try:
|
||||
token = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
|
||||
except Exception as e:
|
||||
error = str(e)
|
||||
|
||||
if not token:
|
||||
res = {
|
||||
'result': False,
|
||||
'error': error,
|
||||
}
|
||||
return res
|
||||
|
||||
@@ -94,7 +102,9 @@ class OgoneController(http.Controller):
|
||||
tx = token.validate(**params)
|
||||
if tx and tx.html_3ds:
|
||||
return tx.html_3ds
|
||||
return werkzeug.utils.redirect(post.get('return_url', '/') + (error and '#error=%s' % werkzeug.url_quote(error) or ''))
|
||||
# add the payment transaction into the session to let the page /payment/process to handle it
|
||||
PaymentProcessing.add_payment_transaction(tx)
|
||||
return werkzeug.utils.redirect("/payment/process")
|
||||
|
||||
@http.route([
|
||||
'/payment/ogone/validate/accept',
|
||||
@@ -104,7 +114,7 @@ class OgoneController(http.Controller):
|
||||
def ogone_validation_form_feedback(self, **post):
|
||||
""" Feedback from 3d secure for a bank card validation """
|
||||
request.env['payment.transaction'].sudo().form_feedback(post, 'ogone')
|
||||
return werkzeug.utils.redirect(werkzeug.url_unquote(post.pop('return_url', '/')))
|
||||
return werkzeug.utils.redirect("/payment/process")
|
||||
|
||||
@http.route(['/payment/ogone/s2s/feedback'], auth='none', csrf=False)
|
||||
def feedback(self, **kwargs):
|
||||
|
||||
@@ -20,14 +20,6 @@ class PaypalController(http.Controller):
|
||||
_return_url = '/payment/paypal/dpn/'
|
||||
_cancel_url = '/payment/paypal/cancel/'
|
||||
|
||||
def _get_return_url(self, **post):
|
||||
""" Extract the return URL from the data coming from paypal. """
|
||||
return_url = post.pop('return_url', '')
|
||||
if not return_url:
|
||||
custom = json.loads(urls.url_unquote_plus(post.pop('custom', False) or post.pop('cm', False) or '{}'))
|
||||
return_url = custom.get('return_url', '/')
|
||||
return return_url
|
||||
|
||||
def _parse_pdt_response(self, response):
|
||||
""" Parse a text response for a PDT verification.
|
||||
|
||||
@@ -63,20 +55,20 @@ class PaypalController(http.Controller):
|
||||
|
||||
Once data is validated, process it. """
|
||||
res = False
|
||||
new_post = dict(post, cmd='_notify-validate', charset='UTF-8')
|
||||
post['cmd'] = '_notify-validate'
|
||||
reference = post.get('item_number')
|
||||
tx = None
|
||||
if reference:
|
||||
tx = request.env['payment.transaction'].search([('reference', '=', reference)])
|
||||
paypal_urls = request.env['payment.acquirer']._get_paypal_urls(tx and tx.acquirer_id.environment or 'prod')
|
||||
pdt_request = bool(new_post.get('amt')) # check for spefific pdt param
|
||||
pdt_request = bool(post.get('amt')) # check for spefific pdt param
|
||||
if pdt_request:
|
||||
# this means we are in PDT instead of DPN like before
|
||||
# fetch the PDT token
|
||||
new_post['at'] = tx and tx.acquirer_id.paypal_pdt_token or ''
|
||||
new_post['cmd'] = '_notify-synch' # command is different in PDT than IPN/DPN
|
||||
post['at'] = tx and tx.acquirer_id.paypal_pdt_token or ''
|
||||
post['cmd'] = '_notify-synch' # command is different in PDT than IPN/DPN
|
||||
validate_url = paypal_urls['paypal_form_url']
|
||||
urequest = requests.post(validate_url, new_post)
|
||||
urequest = requests.post(validate_url, post)
|
||||
urequest.raise_for_status()
|
||||
resp = urequest.text
|
||||
if pdt_request:
|
||||
@@ -84,10 +76,14 @@ class PaypalController(http.Controller):
|
||||
if resp in ['VERIFIED', 'SUCCESS']:
|
||||
_logger.info('Paypal: validated data')
|
||||
res = request.env['payment.transaction'].sudo().form_feedback(post, 'paypal')
|
||||
if not res:
|
||||
tx.sudo()._set_transaction_error('Validation error occured. Please contact your administrator.')
|
||||
elif resp in ['INVALID', 'FAIL']:
|
||||
_logger.warning('Paypal: answered INVALID/FAIL on data verification')
|
||||
tx.sudo()._set_transaction_error('Invalid response from Paypal. Please contact your administrator.')
|
||||
else:
|
||||
_logger.warning('Paypal: unrecognized paypal answer, received %s instead of VERIFIED/SUCCESS or INVALID/FAIL (validation: %s)' % (resp, 'PDT' if pdt_request else 'IPN/DPN'))
|
||||
tx.sudo()._set_transaction_error('Unrecognized error from Paypal. Please contact your administrator.')
|
||||
return res
|
||||
|
||||
@http.route('/payment/paypal/ipn/', type='http', auth='none', methods=['POST'], csrf=False)
|
||||
@@ -104,13 +100,14 @@ class PaypalController(http.Controller):
|
||||
def paypal_dpn(self, **post):
|
||||
""" Paypal DPN """
|
||||
_logger.info('Beginning Paypal DPN form_feedback with post data %s', pprint.pformat(post)) # debug
|
||||
return_url = self._get_return_url(**post)
|
||||
self.paypal_validate_data(**post)
|
||||
return werkzeug.utils.redirect(return_url)
|
||||
try:
|
||||
res = self.paypal_validate_data(**post)
|
||||
except ValidationError:
|
||||
_logger.exception('Unable to validate the Paypal payment')
|
||||
return werkzeug.utils.redirect('/payment/process')
|
||||
|
||||
@http.route('/payment/paypal/cancel', type='http', auth="none", csrf=False)
|
||||
def paypal_cancel(self, **post):
|
||||
""" When the user cancels its Paypal payment: GET on this route """
|
||||
_logger.info('Beginning Paypal cancel with post data %s', pprint.pformat(post)) # debug
|
||||
return_url = self._get_return_url(**post)
|
||||
return werkzeug.utils.redirect(return_url)
|
||||
return werkzeug.utils.redirect('/payment/process')
|
||||
|
||||
@@ -25,7 +25,7 @@ class AcquirerPaypal(models.Model):
|
||||
'Paypal Merchant ID', groups='base.group_user',
|
||||
help='The Merchant ID is used to ensure communications coming from Paypal are valid and secured.')
|
||||
paypal_use_ipn = fields.Boolean('Use IPN', default=True, help='Paypal Instant Payment Notification', groups='base.group_user')
|
||||
paypal_pdt_token = fields.Char(string='Paypal PDT Token', required_if_provider='paypal', help='Payment Data Transfer allows you to receive notification of successful payments as they are made.', groups='base.group_user')
|
||||
paypal_pdt_token = fields.Char(string='Paypal PDT Token', help='Payment Data Transfer allows you to receive notification of successful payments as they are made.', groups='base.group_user')
|
||||
# Server 2 server
|
||||
paypal_api_enabled = fields.Boolean('Use Rest API', default=False)
|
||||
paypal_api_username = fields.Char('Rest API Username', groups='base.group_user')
|
||||
|
||||
@@ -17,8 +17,6 @@ class PayuMoneyController(http.Controller):
|
||||
""" PayUmoney."""
|
||||
_logger.info(
|
||||
'PayUmoney: entering form_feedback with post data %s', pprint.pformat(post))
|
||||
return_url = '/'
|
||||
if post:
|
||||
request.env['payment.transaction'].sudo().form_feedback(post, 'payumoney')
|
||||
return_url = post.get('udf1')
|
||||
return werkzeug.utils.redirect(return_url)
|
||||
return werkzeug.utils.redirect('/payment/process')
|
||||
|
||||
@@ -16,16 +16,6 @@ class SipsController(http.Controller):
|
||||
_notify_url = '/payment/sips/ipn/'
|
||||
_return_url = '/payment/sips/dpn/'
|
||||
|
||||
def _get_return_url(self, **post):
|
||||
""" Extract the return URL from the data coming from sips. """
|
||||
return_url = post.pop('return_url', '')
|
||||
if not return_url:
|
||||
Tx = request.env['payment.transaction']
|
||||
data = Tx._sips_data_to_object(post.get('Data'))
|
||||
custom = json.loads(data.pop('returnContext', False) or '{}')
|
||||
return_url = custom.get('return_url', '/')
|
||||
return return_url
|
||||
|
||||
def sips_validate_data(self, **post):
|
||||
sips = request.env['payment.acquirer'].search([('provider', '=', 'sips')], limit=1)
|
||||
security = sips.sudo()._sips_generate_shasign(post)
|
||||
@@ -47,6 +37,8 @@ class SipsController(http.Controller):
|
||||
'/payment/sips/dpn'], type='http', auth="none", methods=['POST'], csrf=False)
|
||||
def sips_dpn(self, **post):
|
||||
""" Sips DPN """
|
||||
return_url = self._get_return_url(**post)
|
||||
self.sips_validate_data(**post)
|
||||
return werkzeug.utils.redirect(return_url)
|
||||
try:
|
||||
self.sips_validate_data(**post)
|
||||
except:
|
||||
pass
|
||||
return werkzeug.utils.redirect('/payment/process')
|
||||
|
||||
@@ -5,6 +5,7 @@ import werkzeug
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.payment.controllers.portal import PaymentProcessing
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -93,4 +94,6 @@ class StripeController(http.Controller):
|
||||
_logger.info('Stripe: entering form_feedback with post data %s', pprint.pformat(response))
|
||||
if response:
|
||||
request.env['payment.transaction'].sudo().with_context(lang=None).form_feedback(response, 'stripe')
|
||||
return post.pop('return_url', '/')
|
||||
# add the payment transaction into the session to let the page /payment/process to handle it
|
||||
PaymentProcessing.add_payment_transaction(tx)
|
||||
return "/payment/process"
|
||||
|
||||
@@ -54,7 +54,6 @@ class PaymentAcquirerStripe(models.Model):
|
||||
'phone': tx_values.get('partner_phone'),
|
||||
}
|
||||
|
||||
temp_stripe_tx_values['returndata'] = stripe_tx_values.pop('return_url', '')
|
||||
stripe_tx_values.update(temp_stripe_tx_values)
|
||||
return stripe_tx_values
|
||||
|
||||
|
||||
@@ -18,4 +18,4 @@ class OgoneController(http.Controller):
|
||||
def transfer_form_feedback(self, **post):
|
||||
_logger.info('Beginning form_feedback with post data %s', pprint.pformat(post)) # debug
|
||||
request.env['payment.transaction'].sudo().form_feedback(post, 'transfer')
|
||||
return werkzeug.utils.redirect(post.pop('return_url', '/'))
|
||||
return werkzeug.utils.redirect('/payment/process')
|
||||
|
||||
@@ -175,11 +175,11 @@ class CustomerPortal(CustomerPortal):
|
||||
])
|
||||
acquirers = request.env['payment.acquirer'].sudo().search(domain)
|
||||
|
||||
values['form_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 'form' and acq.view_template_id]
|
||||
values['s2s_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 's2s' and acq.registration_view_template_id]
|
||||
values['acquirers'] = acquirers.filtered(lambda acq: (acq.payment_flow == 'form' and acq.view_template_id) or
|
||||
(acq.payment_flow == 's2s' and acq.registration_view_template_id))
|
||||
values['pms'] = request.env['payment.token'].search(
|
||||
[('partner_id', '=', order_sudo.partner_id.id),
|
||||
('acquirer_id', 'in', [acq.id for acq in values['s2s_acquirers']])])
|
||||
('acquirer_id', 'in', acquirers.filtered(lambda acq: acq.payment_flow == 's2s').ids)])
|
||||
|
||||
if order_sudo.state in ('draft', 'sent', 'cancel'):
|
||||
history = request.session.get('my_quotations_history', [])
|
||||
|
||||
@@ -108,7 +108,7 @@ class PaymentTransaction(models.Model):
|
||||
|
||||
@api.multi
|
||||
def _invoice_sale_orders(self):
|
||||
if self.env['ir.config_parameter'].sudo().get_param('website_sale.automatic_invoice'):
|
||||
if self.env['ir.config_parameter'].sudo().get_param('sale.automatic_invoice'):
|
||||
ctx_company = {'company_id': self.acquirer_id.company_id.id,
|
||||
'force_company': self.acquirer_id.company_id.id}
|
||||
for trans in self.filtered(lambda t: t.sale_order_ids):
|
||||
@@ -146,9 +146,8 @@ class PaymentTransaction(models.Model):
|
||||
# Tools for payment
|
||||
# --------------------------------------------------
|
||||
|
||||
def render_sale_button(self, order, return_url, submit_txt=None, render_values=None):
|
||||
def render_sale_button(self, order, submit_txt=None, render_values=None):
|
||||
values = {
|
||||
'return_url': return_url,
|
||||
'partner_id': order.partner_shipping_id.id or order.partner_invoice_id.id,
|
||||
'billing_partner_id': order.partner_invoice_id.id,
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
<li t-if="sale_order.payment_term_id"><span>With payment terms:</span> <b t-field="sale_order.payment_term_id"/></li>
|
||||
</ul>
|
||||
</p>
|
||||
<div t-if="pms or s2s_acquirers or form_acquirers" id="payment_method" class="text-left">
|
||||
<div t-if="pms or acquirers" id="payment_method" class="text-left">
|
||||
<h3 class="mb24">Pay with</h3>
|
||||
<t t-call="payment.payment_tokens_list">
|
||||
<t t-set="mode" t-value="'payment'"/>
|
||||
|
||||
@@ -85,9 +85,14 @@ tour.register('event_buy_tickets', {
|
||||
extra_trigger: '#payment_method label:contains("Wire Transfer") input:checked,#payment_method:not(:has("input:radio:visible"))',
|
||||
trigger: 'button[id="o_payment_form_pay"]:visible',
|
||||
},
|
||||
{
|
||||
content: "Payment processing",
|
||||
trigger: 'a[class="list-group-item"]',
|
||||
timeout: 5000,
|
||||
},
|
||||
{
|
||||
content: "Last step",
|
||||
trigger: '.oe_website_sale:contains("Thank you for your order")',
|
||||
trigger: '.oe_website_sale:contains("Pending... The order will be validated after the payment.")',
|
||||
timeout: 30000,
|
||||
}
|
||||
]
|
||||
|
||||
@@ -8,6 +8,7 @@ from odoo import fields, http, tools, _
|
||||
from odoo.http import request
|
||||
from odoo.addons.base.models.ir_qweb_fields import nl2br
|
||||
from odoo.addons.http_routing.models.ir_http import slug
|
||||
from odoo.addons.payment.controllers.portal import PaymentProcessing
|
||||
from odoo.addons.website.controllers.main import QueryURL
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.addons.website.controllers.main import Website
|
||||
@@ -798,8 +799,8 @@ class WebsiteSale(http.Controller):
|
||||
acquirers = request.env['payment.acquirer'].search(domain)
|
||||
|
||||
values['access_token'] = order.access_token
|
||||
values['form_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 'form' and acq.view_template_id]
|
||||
values['s2s_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 's2s' and acq.registration_view_template_id]
|
||||
values['acquirers'] = [acq for acq in acquirers if (acq.payment_flow == 'form' and acq.view_template_id) or
|
||||
(acq.payment_flow == 's2s' and acq.registration_view_template_id)]
|
||||
values['tokens'] = request.env['payment.token'].search(
|
||||
[('partner_id', '=', order.partner_id.id),
|
||||
('acquirer_id', 'in', acquirers.ids)])
|
||||
@@ -870,7 +871,8 @@ class WebsiteSale(http.Controller):
|
||||
assert order.partner_id.id != request.website.partner_id.id
|
||||
|
||||
# Create transaction
|
||||
vals = {'acquirer_id': acquirer_id}
|
||||
vals = {'acquirer_id': acquirer_id,
|
||||
'return_url': '/shop/payment/validate'}
|
||||
|
||||
if save_token:
|
||||
vals['type'] = 'form_save'
|
||||
@@ -879,7 +881,15 @@ class WebsiteSale(http.Controller):
|
||||
|
||||
transaction = order._create_payment_transaction(vals)
|
||||
|
||||
return transaction.render_sale_button(order, '/shop/payment/validate')
|
||||
# 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:
|
||||
PaymentProcessing.remove_payment_transaction(last_tx)
|
||||
PaymentProcessing.add_payment_transaction(transaction)
|
||||
request.session['__website_sale_last_tx_id'] = transaction.id
|
||||
return transaction.render_sale_button(order)
|
||||
|
||||
@http.route('/shop/payment/token', type='http', auth='public', website=True)
|
||||
def payment_token(self, pm_id=None, **kwargs):
|
||||
@@ -904,10 +914,11 @@ class WebsiteSale(http.Controller):
|
||||
return request.redirect('/shop/?error=token_not_found')
|
||||
|
||||
# Create transaction
|
||||
vals = {'payment_token_id': pm_id}
|
||||
vals = {'payment_token_id': pm_id, 'return_url': '/shop/payment/validate'}
|
||||
|
||||
order._create_payment_transaction(vals)
|
||||
return request.redirect('/shop/payment/validate')
|
||||
tx = order._create_payment_transaction(vals)
|
||||
PaymentProcessing.add_payment_transaction(tx)
|
||||
return request.redirect('/payment/process')
|
||||
|
||||
@http.route('/shop/payment/get_status/<int:sale_order_id>', type='json', auth="public", website=True)
|
||||
def payment_get_status(self, sale_order_id, **post):
|
||||
@@ -950,6 +961,7 @@ class WebsiteSale(http.Controller):
|
||||
if tx and tx.state == 'draft':
|
||||
return request.redirect('/shop')
|
||||
|
||||
PaymentProcessing.remove_payment_transaction(tx)
|
||||
return request.redirect('/shop/confirmation')
|
||||
|
||||
|
||||
|
||||
@@ -74,9 +74,14 @@ tour.register('shop_buy_product', {
|
||||
extra_trigger: '#payment_method label:contains("Wire Transfer") input:checked,#payment_method:not(:has("input:radio:visible"))',
|
||||
trigger: 'button[id="o_payment_form_pay"]:visible:not(:disabled)',
|
||||
},
|
||||
{
|
||||
content: "Payment processing",
|
||||
trigger: 'a[class="list-group-item"]',
|
||||
timeout: 5000,
|
||||
},
|
||||
{
|
||||
content: "finish",
|
||||
trigger: '.oe_website_sale:contains("Thank you for your order")',
|
||||
trigger: '.oe_website_sale:contains("Pending... The order will be validated after the payment.")',
|
||||
timeout: 30000,
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1336,8 +1336,8 @@
|
||||
|
||||
<div class="oe_structure clearfix mt-3"/>
|
||||
|
||||
<div id="payment_method" class="mt-3" t-if="(form_acquirers or s2s_acquirers or tokens) and website_sale_order.amount_total">
|
||||
<h3>Pay with </h3>
|
||||
<div id="payment_method" class="mt-3" t-if="(acquirers or tokens) and website_sale_order.amount_total">
|
||||
<h3 class="mb24">Pay with </h3>
|
||||
<t t-call="payment.payment_tokens_list">
|
||||
<t t-set="mode" t-value="'payment'"/>
|
||||
<t t-set="submit_txt">Pay Now</t>
|
||||
@@ -1356,7 +1356,7 @@
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<div t-if="not (form_acquirers or s2s_acquirers)" class="mt-2">
|
||||
<div t-if="not acquirers" class="mt-2">
|
||||
<a role="button" class="btn-link"
|
||||
groups="base.group_system"
|
||||
t-attf-href="/web#return_label=Website&action=#{payment_action_id}">
|
||||
@@ -1472,11 +1472,14 @@
|
||||
<div class="row">
|
||||
<div class="col-12 col-xl">
|
||||
<div class="oe_cart">
|
||||
<div class="thanks_msg">
|
||||
<h2>Thank you for your order.
|
||||
<a role="button" class="btn btn-primary d-none d-md-inline-block" href="/shop/print" target="_blank" aria-label="Print" title="Print"><i class="fa fa-print"></i> Print</a>
|
||||
</h2>
|
||||
</div>
|
||||
<t t-set="payment_tx_id" t-value="order.get_portal_last_transaction()"/>
|
||||
<t t-if="payment_tx_id.state == 'done'">
|
||||
<div class="thanks_msg">
|
||||
<h2>Thank you for your order.
|
||||
<a role="button" class="btn btn-primary d-none d-md-inline-block" href="/shop/print" target="_blank" aria-label="Print" title="Print"><i class="fa fa-print"></i> Print</a>
|
||||
</h2>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="request.env['ir.config_parameter'].sudo().get_param('auth_signup.invitation_scope', 'b2b') == 'b2c' and request.website.is_public_user()">
|
||||
<p class="alert alert-info mt-3" role="status">
|
||||
<a role="button" t-att-href='order.partner_id.signup_prepare() and order.partner_id.signup_url' class='btn btn-primary'>Sign Up</a>
|
||||
@@ -1598,8 +1601,13 @@
|
||||
<t t-if="payment_tx_id.state == 'cancel'">
|
||||
<t t-raw="payment_tx_id.acquirer_id.cancel_msg"/>
|
||||
</t>
|
||||
<t t-if="payment_tx_id.state == 'error'">
|
||||
<span t-if="payment_tx_id.acquirer_id.error_msg" t-raw="payment_tx_id.acquirer_id.error_msg"/>
|
||||
<span t-else="">An error occured while processing your payment. Please try again or contact your administrator.</span>
|
||||
</t>
|
||||
<t t-if="payment_tx_id.state == 'authorized'">
|
||||
Your payment has been authorized.
|
||||
<span t-if="payment_tx_id.acquirer_id.auth_msg" t-raw="payment_tx_id.acquirer_id.auth_msg"/>
|
||||
<span t-else="">Your payment has been authorized.</span>
|
||||
</t>
|
||||
</div>
|
||||
<div class="card-body" t-if="payment_tx_id.acquirer_id.post_msg">
|
||||
|
||||
Reference in New Issue
Block a user