diff options
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">&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">&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 < MAX_BRAND_LINE) else ' d-none'}"> <span t-field="pm_icon.image_payment_form" - t-options='{"widget": "image", "alt-field": "name"}' /> + t-options='{"widget": "image", "alt-field": "name"}'/> </li> <li t-if="i==MAX_BRAND_LINE" style="display:block;" class="list-inline-item"> <span class="float-right more_option text-info"> @@ -81,46 +80,27 @@ </t> </ul> </div> - <div t-attf-id="o_payment_add_token_acq_{{acq.id}}" t-attf-class="card-footer {{'d-none' if(acquirers_count > 1 and pms_count==0 and s2s_acquirers[0]!=acq) else 'd-none' if pms_count >0 else ''}}"> - <div class="clearfix"> - <t t-call="{{acq.sudo().get_s2s_form_xml_id()}}"> - <t t-set="id" t-value="acq.id"/> - <t t-set="partner_id" t-value="partner_id"/> - <t t-if="not return_url" t-set="return_url" t-value="''"/> - </t> + <t t-if="acq.payment_flow == 'form'"> + <div t-attf-id="o_payment_form_acq_{{acq.id}}" + t-attf-class="d-none {{'card-footer' if acq.save_token == 'ask' else ''}}"> + <label t-if="acq.save_token == 'ask'"> + <input type="checkbox" name="o_payment_form_save_token" data-remove-me=""/> + Save my payment data + </label> </div> - </div> - </t> - <t t-if="mode == 'payment'" t-foreach="form_acquirers" t-as="acq"> - <div class="card-body"> - <label> - <input type="radio" t-att-data-acquirer-id="acq.id" - t-att-data-form-payment="true" - t-att-data-provider="acq.provider" - t-att-class="'d-none' if (acquirers_count==1 and pms_count==0) else ''" - t-att-checked="acquirers_count==1 and pms_count==0" - name="pm_id" t-attf-value="form_{{acq.id}}"/> - <span class="payment_icon_name" t-esc="acq.name"/> - </label> - <ul class="float-right list-inline payment_icon_list"> - <t t-set="i" t-value="0"/> - <t t-foreach="acq.payment_icon_ids" t-as="pm_icon"> - <li t-attf-class="list-inline-item#{'' if (i < MAX_BRAND_LINE) else ' d-none'}"> - <span t-field="pm_icon.image_payment_form" - t-options='{"widget": "image", "alt-field": "name"}' /> - </li> - <li t-if="i==MAX_BRAND_LINE" style="display:block;" class="list-inline-item"> - <span class="float-right more_option text-info"> - <a href="#" class="o_payment_form_pay_icon_more" data-toggle="tooltip" t-att-title="', '.join([opt.name for opt in acq.payment_icon_ids[MAX_BRAND_LINE:]])">and more</a> - </span> - </li> - <t t-set="i" t-value="i+1"/> - </t> - </ul> - </div> - <div t-attf-id="o_payment_form_acq_{{acq.id}}" t-attf-class="d-none {{'card-footer' if acq.save_token == 'ask' else ''}}"> - <label t-if="acq.save_token == 'ask'"><input type="checkbox" name="o_payment_form_save_token" data-remove-me=""/> Save my payment data</label> - </div> + </t> + <t t-else="acq.payment_flow == 's2s'"> + <div t-attf-id="o_payment_add_token_acq_{{acq.id}}" + t-attf-class="card-footer {{'d-none' if(acquirers_count > 1 and pms_count==0 and acquirers[0]!=acq) else 'd-none' if pms_count >0 else ''}}"> + <div class="clearfix"> + <t t-call="{{acq.sudo().get_s2s_form_xml_id()}}"> + <t t-set="id" t-value="acq.id"/> + <t t-set="partner_id" t-value="partner_id"/> + <t t-if="not return_url" t-set="return_url" t-value="''"/> + </t> + </div> + </div> + </t> </t> <t t-foreach="pms" t-as="pm"> <div class="card-body"> @@ -157,10 +137,37 @@ <i class="fa fa-lock"> Pay</i> </t> </button> - <button t-if="mode == 'manage' and s2s_acquirers" id="o_payment_form_add_pm" class="btn btn-primary btn-lg mb8 mt8"> + <button t-if="mode == 'manage' and acquirers.filtered(lambda x: x.payment_flow == 's2s')" id="o_payment_form_add_pm" class="btn btn-primary btn-lg mb8 mt8"> <i class="fa fa-plus-circle"> Add new card</i> </button> </div> </form> </template> + + <template id="payment_process_page" name="Payment processing page"> + <!-- If you are looking at this template and tries to understand how it works, + You should take a look at the file located in /static/src/js/payment_processing.js + --> + <t t-call="portal.frontend_layout"> + <div class="wrap"> + <div class="container o_website_payment"> + <div class="o_payment_processing"> + <div class="row"> + <div class="o_payment_processing_content col-sm-6 col-sm-offset-3"> + <!-- here should be the content generated by payment_processing.js --> + </div> + </div> + </div> + </div> + </div> + <!-- We load and attach the payment processing widget --> + <script> + odoo.define('payment.processing_bootstrap', function(require) { + var PaymentProcessing = require('payment.processing'); + var processingWidget = new PaymentProcessing(null, <t t-esc="payment_tx_ids"/>); + processingWidget.attachTo($('.o_payment_processing')); + }); + </script> + </t> + </template> </odoo> 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&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"> |
