Files
odoo_source/addons/payment/controllers/portal.py
T
Toufik Benjaa 6ed44181d0 [IMP] payment_*: payment acquirers error handling
This commit aims to improve the user experience when using payment acquirers. There currently are no error feedback with some acquirers, which leaves the user wondering what is going on and what is the real status of its payment.
In some cases, the user is currently being redirected to the home page even though the payment has failed. We want to make it more obvious to the user that something unexpected has happened by redirecting to an intermediate page that will provide good feedback on payments status.

Another goal of this commit is to order acquirers by sequence instead of by flow and to select the first acquirer by default. This feature was already implmented in commit fe294fd43e521bd2d339e962f43acf46c3d4cb97, some UI adaptations were needed though.

Related to task #36680
Closes #26958
2018-09-19 18:25:32 +02:00

274 lines
11 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from odoo import http, _
from odoo.http import request
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
from datetime import datetime, timedelta
_logger = logging.getLogger(__name__)
class PaymentProcessing(http.Controller):
@staticmethod
def remove_payment_transaction(transactions):
tx_ids_list = request.session.get("__payment_tx_ids__", [])
if transactions:
for tx in transactions:
if tx.id in tx_ids_list:
tx_ids_list.remove(tx.id)
else:
return False
request.session["__payment_tx_ids__"] = tx_ids_list
return True
@staticmethod
def add_payment_transaction(transactions):
if not transactions:
return False
tx_ids_list = set(request.session.get("__payment_tx_ids__", [])) | set(transactions.ids)
request.session["__payment_tx_ids__"] = tx_ids_list
return True
@staticmethod
def get_payment_transaction_ids():
# return the ids and not the recordset, since we might need to
# sudo the browse to access all the record
# I prefer to let the controller chose when to access to payment.transaction using sudo
return request.session.get("__payment_tx_ids__", [])
@http.route(['/payment/process'], type="http", auth="public", website=True)
def payment_status_page(self, **kwargs):
# When the customer is redirect to this website page,
# we retrieve the payment transaction list from his session
tx_ids_list = self.get_payment_transaction_ids()
payment_transaction_ids = request.env['payment.transaction'].sudo().browse(tx_ids_list).exists()
render_ctx = {
'payment_tx_ids': payment_transaction_ids.ids,
}
return request.render("payment.payment_process_page", render_ctx)
@http.route(['/payment/process/poll'], type="json", auth="public")
def payment_status_poll(self):
# retrieve the transactions
tx_ids_list = self.get_payment_transaction_ids()
payment_transaction_ids = request.env['payment.transaction'].sudo().search([
('id', 'in', list(tx_ids_list)),
('date', '>=', (datetime.now() - timedelta(days=1)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
])
if not payment_transaction_ids:
return {
'success': False,
'error': 'no_tx_found',
}
# create the returned dictionnary
result = {
'success': True,
'transactions': [],
}
# populate the returned dictionnary with the transactions data
for tx in payment_transaction_ids:
message_to_display = tx.acquirer_id[tx.state + '_msg'] if tx.state in ['done', 'pending', 'cancel', 'error'] else None
result['transactions'].append({
'reference': tx.reference,
'state': tx.state,
'return_url': tx.return_url,
'is_processed': tx.is_processed,
'state_message': tx.state_message,
'message_to_display': message_to_display,
'amount': tx.amount,
'currency': tx.currency_id.name,
'acquirer_provider': tx.acquirer_id.provider,
})
tx_to_process = payment_transaction_ids.filtered(lambda x: x.state == 'done' and x.is_processed is False)
try:
tx_to_process._post_process_after_done()
except Exception as e:
request.env.cr.rollback()
result['success'] = False
result['error'] = str(e)
_logger.error("Error while processing transaction(s) %s, exception \"%s\"", tx_to_process.ids, str(e))
return result
class WebsitePayment(http.Controller):
@http.route(['/my/payment_method'], type='http', auth="user", website=True)
def payment_method(self, **kwargs):
acquirers = list(request.env['payment.acquirer'].search([('website_published', '=', True), ('registration_view_template_id', '!=', False), ('payment_flow', '=', 's2s')]))
partner = request.env.user.partner_id
payment_tokens = partner.payment_token_ids
payment_tokens |= partner.commercial_partner_id.sudo().payment_token_ids
return_url = request.params.get('redirect', '/my/payment_method')
values = {
'pms': payment_tokens,
'acquirers': acquirers,
'error_message': [kwargs['error']] if kwargs.get('error') else False,
'return_url': return_url,
'bootstrap_formatting': True,
'partner_id': partner.id
}
return request.render("payment.pay_methods", values)
@http.route(['/website_payment/pay'], type='http', auth='public', website=True)
def pay(self, reference='', order_id=None, amount=False, currency_id=None, acquirer_id=None, **kw):
env = request.env
user = env.user.sudo()
# Default values
values = {
'amount': 0.0,
'currency': user.company_id.currency_id,
}
# Check sale order
if order_id:
try:
order_id = int(order_id)
order = env['sale.order'].browse(order_id)
values.update({
'currency': order.currency_id,
'amount': order.amount_total,
'order_id': order_id
})
except:
order_id = None
# Check currency
if currency_id:
try:
currency_id = int(currency_id)
values['currency'] = env['res.currency'].browse(currency_id)
except:
pass
# Check amount
if amount:
try:
amount = float(amount)
values['amount'] = amount
except:
pass
# Check reference
reference_values = order_id and {'sale_order_ids': [(4, order_id)]} or {}
values['reference'] = env['payment.transaction']._compute_reference(values=reference_values, prefix=reference)
# Check acquirer
acquirers = None
if acquirer_id:
acquirers = env['payment.acquirer'].browse(int(acquirer_id))
if not acquirers:
acquirers = env['payment.acquirer'].search([('website_published', '=', True), ('company_id', '=', user.company_id.id)])
# Check partner
partner_id = user.partner_id.id if not user._is_public() else False
values.update({
'partner_id': partner_id,
'bootstrap_formatting': True,
'error_msg': kw.get('error_msg')
})
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)
@http.route(['/website_payment/transaction/<string:reference>/<string:amount>/<string:currency_id>',
'/website_payment/transaction/v2/<string:amount>/<string:currency_id>/<path:reference>',], type='json', auth='public')
def transaction(self, acquirer_id, reference, amount, currency_id, **kwargs):
partner_id = request.env.user.partner_id.id if not request.env.user._is_public() else False
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,
'amount': float(amount),
'currency_id': int(currency_id),
'partner_id': partner_id,
'type': 'form_save' if acquirer.save_token != 'none' and partner_id else 'form',
}
if order_id:
values['sale_order_ids'] = [(6, 0, [order_id])]
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 = {
'partner_id': partner_id,
}
return acquirer.sudo().render(tx.reference, float(amount), int(currency_id), values=render_values)
@http.route(['/website_payment/token/<string:reference>/<string:amount>/<string:currency_id>',
'/website_payment/token/v2/<string:amount>/<string:currency_id>/<path:reference>'], type='http', auth='public', website=True)
def payment_token(self, pm_id, reference, amount, currency_id, return_url=None, **kwargs):
token = request.env['payment.token'].browse(int(pm_id))
order_id = kwargs.get('order_id')
if not token:
return request.redirect('/website_payment/pay?error_msg=%s' % _('Cannot setup the payment.'))
partner_id = request.env.user.partner_id.id if not request.env.user._is_public() else False
values = {
'acquirer_id': token.acquirer_id.id,
'reference': reference,
'amount': float(amount),
'currency_id': int(currency_id),
'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('/payment/process')
@http.route(['/website_payment/confirm'], type='http', auth='public', website=True)
def confirm(self, **kw):
tx_id = int(kw.get('tx_id', 0))
if tx_id:
tx = request.env['payment.transaction'].browse(tx_id)
if tx.state == 'done':
status = 'success'
message = tx.acquirer_id.done_msg
elif tx.state == 'pending':
status = 'warning'
message = tx.acquirer_id.pending_msg
else:
status = 'danger'
message = tx.acquirer_id.error_msg
return request.render('payment.confirm', {'tx': tx, 'status': status, 'message': message})
else:
return request.redirect('/my/home')