summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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">