summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorToufik Benjaa <tbe@odoo.com>2018-07-11 17:47:29 +0200
committerToufik Benjaa <tbe@odoo.com>2018-09-19 18:25:32 +0200
commit6ed44181d081b3cfd8875dc82e4e155970cc07f5 (patch)
tree5449ac99ea4191b9b747918d0d1d8702ebad3cc5 /addons
parent46dc8ceef90ec5472b1aa4b45509f23d6c3e2624 (diff)
[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
Diffstat (limited to 'addons')
-rw-r--r--addons/account/controllers/portal.py2
-rw-r--r--addons/account_payment/controllers/payment.py8
-rw-r--r--addons/account_payment/models/payment.py3
-rw-r--r--addons/account_payment/views/account_portal_templates.xml8
-rw-r--r--addons/payment/__manifest__.py1
-rw-r--r--addons/payment/controllers/portal.py121
-rw-r--r--addons/payment/data/payment_cron.xml14
-rw-r--r--addons/payment/models/payment_acquirer.py60
-rw-r--r--addons/payment/static/src/js/payment_processing.js125
-rw-r--r--addons/payment/static/src/xml/payment_processing.xml150
-rw-r--r--addons/payment/views/assets.xml1
-rw-r--r--addons/payment/views/payment_portal_templates.xml6
-rw-r--r--addons/payment/views/payment_templates.xml123
-rw-r--r--addons/payment_adyen/controllers/main.py6
-rw-r--r--addons/payment_authorize/controllers/main.py21
-rw-r--r--addons/payment_buckaroo/controllers/main.py2
-rw-r--r--addons/payment_ogone/controllers/main.py18
-rw-r--r--addons/payment_paypal/controllers/main.py33
-rw-r--r--addons/payment_paypal/models/payment.py2
-rw-r--r--addons/payment_payumoney/controllers/main.py4
-rw-r--r--addons/payment_sips/controllers/main.py18
-rw-r--r--addons/payment_stripe/controllers/main.py5
-rw-r--r--addons/payment_stripe/models/payment.py1
-rw-r--r--addons/payment_transfer/controllers/main.py2
-rw-r--r--addons/sale/controllers/portal.py6
-rw-r--r--addons/sale/models/payment.py5
-rw-r--r--addons/sale/views/sale_portal_templates.xml2
-rw-r--r--addons/website_event_sale/static/src/js/website.tour.event_sale.js7
-rw-r--r--addons/website_sale/controllers/main.py26
-rw-r--r--addons/website_sale/static/src/js/website_sale_tour_buy.js7
-rw-r--r--addons/website_sale/views/templates.xml26
31 files changed, 636 insertions, 177 deletions
diff --git a/addons/account/controllers/portal.py b/addons/account/controllers/portal.py
index 0da2bd0ce5d..e66e6a68d35 100644
--- a/addons/account/controllers/portal.py
+++ b/addons/account/controllers/portal.py
@@ -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)
# ------------------------------------------------------------
diff --git a/addons/account_payment/controllers/payment.py b/addons/account_payment/controllers/payment.py
index 5c3f6f0ea31..c9ef4530f99 100644
--- a/addons/account_payment/controllers/payment.py
+++ b/addons/account_payment/controllers/payment.py
@@ -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))
diff --git a/addons/account_payment/models/payment.py b/addons/account_payment/models/payment.py
index 5669d2b300d..7d20ec0d6b0 100644
--- a/addons/account_payment/models/payment.py
+++ b/addons/account_payment/models/payment.py
@@ -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:
diff --git a/addons/account_payment/views/account_portal_templates.xml b/addons/account_payment/views/account_portal_templates.xml
index ffcbb7f99c6..0c7ddc0ce88 100644
--- a/addons/account_payment/views/account_portal_templates.xml
+++ b/addons/account_payment/views/account_portal_templates.xml
@@ -45,7 +45,7 @@
<button type="button" class="close" data-dismiss="modal">&amp;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>
diff --git a/addons/payment/__manifest__.py b/addons/payment/__manifest__.py
index c353f9b0ad3..97890e9b3ce 100644
--- a/addons/payment/__manifest__.py
+++ b/addons/payment/__manifest__.py
@@ -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',
diff --git a/addons/payment/controllers/portal.py b/addons/payment/controllers/portal.py
index 904c440bcac..057ee72aa3b 100644
--- a/addons/payment/controllers/portal.py
+++ b/addons/payment/controllers/portal.py
@@ -1,9 +1,101 @@
# -*- 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)
@@ -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):
diff --git a/addons/payment/data/payment_cron.xml b/addons/payment/data/payment_cron.xml
new file mode 100644
index 00000000000..c0bbe62b785
--- /dev/null
+++ b/addons/payment/data/payment_cron.xml
@@ -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>
diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py
index b3560e236f4..3da56665b26 100644
--- a/addons/payment/models/payment_acquirer.py
+++ b/addons/payment/models/payment_acquirer.py
@@ -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
# --------------------------------------------------
diff --git a/addons/payment/static/src/js/payment_processing.js b/addons/payment/static/src/js/payment_processing.js
new file mode 100644
index 00000000000..b40c2074131
--- /dev/null
+++ b/addons/payment/static/src/js/payment_processing.js
@@ -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>'
+ });
+ },
+ });
+});
diff --git a/addons/payment/static/src/xml/payment_processing.xml b/addons/payment/static/src/xml/payment_processing.xml
new file mode 100644
index 00000000000..386a03bb117
--- /dev/null
+++ b/addons/payment/static/src/xml/payment_processing.xml
@@ -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>
diff --git a/addons/payment/views/assets.xml b/addons/payment/views/assets.xml
index 06fc316ca39..62fe9d2455f 100644
--- a/addons/payment/views/assets.xml
+++ b/addons/payment/views/assets.xml
@@ -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>
diff --git a/addons/payment/views/payment_portal_templates.xml b/addons/payment/views/payment_portal_templates.xml
index 762bc2015aa..0676107c698 100644
--- a/addons/payment/views/payment_portal_templates.xml
+++ b/addons/payment/views/payment_portal_templates.xml
@@ -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">&amp;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>
diff --git a/addons/payment/views/payment_templates.xml b/addons/payment/views/payment_templates.xml
index e8fd05685dc..3b1f21d21ab 100644
--- a/addons/payment/views/payment_templates.xml
+++ b/addons/payment/views/payment_templates.xml
@@ -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 &lt; 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 &gt; 1 and pms_count==0 and s2s_acquirers[0]!=acq) else 'd-none' if pms_count &gt;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 &lt; 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 &gt; 1 and pms_count==0 and acquirers[0]!=acq) else 'd-none' if pms_count &gt;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>
diff --git a/addons/payment_adyen/controllers/main.py b/addons/payment_adyen/controllers/main.py
index b6b4331e578..7de5a6dc7ef 100644
--- a/addons/payment_adyen/controllers/main.py
+++ b/addons/payment_adyen/controllers/main.py
@@ -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',
diff --git a/addons/payment_authorize/controllers/main.py b/addons/payment_authorize/controllers/main.py
index 9614327401a..dd8d96f6e90 100644
--- a/addons/payment_authorize/controllers/main.py
+++ b/addons/payment_authorize/controllers/main.py
@@ -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")
diff --git a/addons/payment_buckaroo/controllers/main.py b/addons/payment_buckaroo/controllers/main.py
index 40e4ae539e4..7fe17c8344f 100644
--- a/addons/payment_buckaroo/controllers/main.py
+++ b/addons/payment_buckaroo/controllers/main.py
@@ -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')
diff --git a/addons/payment_ogone/controllers/main.py b/addons/payment_ogone/controllers/main.py
index 409e09b8a9e..6bebff17f34 100644
--- a/addons/payment_ogone/controllers/main.py
+++ b/addons/payment_ogone/controllers/main.py
@@ -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):
diff --git a/addons/payment_paypal/controllers/main.py b/addons/payment_paypal/controllers/main.py
index e14b7d59ca6..f9166ecf302 100644
--- a/addons/payment_paypal/controllers/main.py
+++ b/addons/payment_paypal/controllers/main.py
@@ -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')
diff --git a/addons/payment_paypal/models/payment.py b/addons/payment_paypal/models/payment.py
index e967456f28c..fb12cabdc50 100644
--- a/addons/payment_paypal/models/payment.py
+++ b/addons/payment_paypal/models/payment.py
@@ -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')
diff --git a/addons/payment_payumoney/controllers/main.py b/addons/payment_payumoney/controllers/main.py
index 78bc73dfe2d..a565afddeee 100644
--- a/addons/payment_payumoney/controllers/main.py
+++ b/addons/payment_payumoney/controllers/main.py
@@ -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')
diff --git a/addons/payment_sips/controllers/main.py b/addons/payment_sips/controllers/main.py
index 3af8e2c7b3e..de3e90810ee 100644
--- a/addons/payment_sips/controllers/main.py
+++ b/addons/payment_sips/controllers/main.py
@@ -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')
diff --git a/addons/payment_stripe/controllers/main.py b/addons/payment_stripe/controllers/main.py
index f446375d429..a92e3cb2752 100644
--- a/addons/payment_stripe/controllers/main.py
+++ b/addons/payment_stripe/controllers/main.py
@@ -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"
diff --git a/addons/payment_stripe/models/payment.py b/addons/payment_stripe/models/payment.py
index ee2b6db26d5..78920e93580 100644
--- a/addons/payment_stripe/models/payment.py
+++ b/addons/payment_stripe/models/payment.py
@@ -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
diff --git a/addons/payment_transfer/controllers/main.py b/addons/payment_transfer/controllers/main.py
index 6411414c597..29a1d20d8e1 100644
--- a/addons/payment_transfer/controllers/main.py
+++ b/addons/payment_transfer/controllers/main.py
@@ -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')
diff --git a/addons/sale/controllers/portal.py b/addons/sale/controllers/portal.py
index 6b1fd33f87f..1b38e714e40 100644
--- a/addons/sale/controllers/portal.py
+++ b/addons/sale/controllers/portal.py
@@ -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', [])
diff --git a/addons/sale/models/payment.py b/addons/sale/models/payment.py
index 950aa41c1f9..d6af752c531 100644
--- a/addons/sale/models/payment.py
+++ b/addons/sale/models/payment.py
@@ -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,
}
diff --git a/addons/sale/views/sale_portal_templates.xml b/addons/sale/views/sale_portal_templates.xml
index bf382905b3f..5293c7c6f79 100644
--- a/addons/sale/views/sale_portal_templates.xml
+++ b/addons/sale/views/sale_portal_templates.xml
@@ -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'"/>
diff --git a/addons/website_event_sale/static/src/js/website.tour.event_sale.js b/addons/website_event_sale/static/src/js/website.tour.event_sale.js
index 3aec6f28638..c6dafbb019e 100644
--- a/addons/website_event_sale/static/src/js/website.tour.event_sale.js
+++ b/addons/website_event_sale/static/src/js/website.tour.event_sale.js
@@ -86,8 +86,13 @@ tour.register('event_buy_tickets', {
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,
}
]
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index ef709f02a5d..10027f17655 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -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')
diff --git a/addons/website_sale/static/src/js/website_sale_tour_buy.js b/addons/website_sale/static/src/js/website_sale_tour_buy.js
index 0872f5ae347..c2ffef9b81e 100644
--- a/addons/website_sale/static/src/js/website_sale_tour_buy.js
+++ b/addons/website_sale/static/src/js/website_sale_tour_buy.js
@@ -75,8 +75,13 @@ tour.register('shop_buy_product', {
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,
}
]
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 15e4800e028..eab9ebc7618 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -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&amp;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">