From c1000ff29e842ce2e8433b94a861762628f30130 Mon Sep 17 00:00:00 2001
From: "Bruno Zanotti (ADHOC)"
Date: Tue, 23 Mar 2021 13:07:27 -0300
Subject: [PATCH] [ADD] payment_mercadopago: MercadoPago Redirect
---
payment_mercadopago/__manifest__.py | 4 +-
payment_mercadopago/controllers/main.py | 55 +++++-
.../data/payment_acquirer_data.xml | 17 +-
.../demo/payment_acquirer_demo.xml | 27 +++
.../models/mercadopago_request.py | 18 +-
payment_mercadopago/models/payment.py | 182 ++++++++++++++++++
.../views/payment_mercadopago_templates.xml | 10 +-
.../views/payment_transaction_views.xml | 6 +-
payment_mercadopago/views/payment_views.xml | 4 +
9 files changed, 298 insertions(+), 25 deletions(-)
diff --git a/payment_mercadopago/__manifest__.py b/payment_mercadopago/__manifest__.py
index c2cb3417..082e3614 100644
--- a/payment_mercadopago/__manifest__.py
+++ b/payment_mercadopago/__manifest__.py
@@ -2,8 +2,10 @@
{
'name': 'MercadoPago Payment Acquirer',
'category': 'Accounting/Payment',
- 'summary': 'Payment Acquirer: MercadoPago Implementation',
+ 'summary': 'Payment Acquirer: MercadoPago',
'version': '13.0.1.0.0',
+ 'author': 'ADHOC SA',
+ 'website': 'www.adhoc.com.ar',
'description': """MercadoPago Payment Acquirer""",
'depends': ['payment'],
'external_dependencies': {
diff --git a/payment_mercadopago/controllers/main.py b/payment_mercadopago/controllers/main.py
index 6b01b767..e2027809 100644
--- a/payment_mercadopago/controllers/main.py
+++ b/payment_mercadopago/controllers/main.py
@@ -3,19 +3,68 @@
# directory
##############################################################################
-# import pprint
import logging
-
+import pprint
+import werkzeug
from odoo import http
from odoo.http import request
+from odoo.tools.safe_eval import safe_eval
from odoo.addons.payment_mercadopago.models.mercadopago_request import MercadoPagoAPI
from urllib import parse
-
+# TODO: remove
+from ..static.sdkpython.mercadopago import mercadopago
_logger = logging.getLogger(__name__)
class MercadoPagoController(http.Controller):
+ # MercadoPago redirect controller
+ _success_url = '/payment/mercadopago/success/'
+ _pending_url = '/payment/mercadopago/pending/'
+ _failure_url = '/payment/mercadopago/failure/'
+ _create_preference_url = '/payment/mercadopago/create_preference'
+
+ @http.route(['/payment/mercadopago/create_preference'], type='http', auth="none", csrf=False)
+ def mercadopago_create_preference(self, **post):
+ # TODO podriamos pasar cada elemento por separado para no necesitar
+ # el literal eval
+ # mercadopago_data = safe_eval(post.get('mercadopago_data', {}))
+ acquirer = request.env['payment.acquirer'].browse(safe_eval(post.get('acquirer_id'))).sudo()
+ mercadopago_preference = safe_eval(post.get('mercadopago_preference'))
+
+ if not acquirer:
+ return werkzeug.utils.redirect("/")
+
+ # TODO: Remove this with sdk 1.2.0
+ if (not mercadopago_preference or not acquirer.mercadopago_secret_key or not acquirer.mercadopago_client_id):
+ _logger.warning('Missing parameters!')
+ return werkzeug.utils.redirect("/")
+
+ MP = mercadopago.MP(acquirer.mercadopago_client_id, acquirer.mercadopago_secret_key)
+ MP.sandbox_mode(True) if acquirer.state == "enabled" else MP.sandbox_mode(False)
+ resp = MP.post("/checkout/preferences", mercadopago_preference)
+ linkpay = resp['response']['init_point'] if acquirer.state == "enabled" else resp['response']['sandbox_init_point']
+ # TODO: We should do this with skd 1.2.0
+ # MP = MercadoPagoAPI(acquirer)
+ # linkpay = MP.create_preference(mercadopago_preference)
+ return werkzeug.utils.redirect(linkpay)
+
+ @http.route([
+ '/payment/mercadopago/success',
+ '/payment/mercadopago/pending',
+ '/payment/mercadopago/failure'
+ ], type='http', auth="none")
+ def mercadopago_back_no_return(self, **post):
+ """
+ Odoo, si usas el boton de pago desde una sale order o email, no manda
+ una return url, desde website si y la almacena en un valor que vuelve
+ desde el agente de pago. Como no podemos mandar esta "return_url" para
+ que vuelva, directamente usamos dos distintas y vovemos con una u otra
+ """
+ _logger.info('Mercadopago: entering mecadopago_back with post data %s', pprint.pformat(post))
+ request.env['payment.transaction'].sudo().form_feedback(post, 'mercadopago')
+ return werkzeug.utils.redirect("/payment/process")
+
@http.route(['/payment/mercadopago/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
def mercadopago_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
if not kwargs.get('partner_id'):
diff --git a/payment_mercadopago/data/payment_acquirer_data.xml b/payment_mercadopago/data/payment_acquirer_data.xml
index 9b0b2d7f..9087b198 100644
--- a/payment_mercadopago/data/payment_acquirer_data.xml
+++ b/payment_mercadopago/data/payment_acquirer_data.xml
@@ -3,21 +3,21 @@
MercadoPago
- Credit Card (powered by MercadoPago)
mercadopago
-
- dummy
- dummy
- True
+
+ Credit Card (powered by MercadoPago)
+
s2s
-
+ True
- A payment gateway to accept online payments via credit cards and e-checks.
+ A payment platform to accept online payments.
- Online Payment
@@ -28,8 +28,5 @@
- Embedded Credit Card Form
-
diff --git a/payment_mercadopago/demo/payment_acquirer_demo.xml b/payment_mercadopago/demo/payment_acquirer_demo.xml
index 9a813939..c610ca16 100644
--- a/payment_mercadopago/demo/payment_acquirer_demo.xml
+++ b/payment_mercadopago/demo/payment_acquirer_demo.xml
@@ -6,4 +6,31 @@
TEST-2775253347293690-081210-4dede21e22738444d5fe2f092ee478f3__LC_LB__-113996959
+
+ MercadoPago Redirect
+ mercadopago
+
+
+ 2285999378925428
+ BRiYAPa3QAozapMpIFcT1JmilVAYOHNz
+ MercadoPago (redirect)
+
+
+
+
+ A payment platform to accept online payments.
+
+
+ - Online Payment
+ - Payment Status Tracking
+
+
+ You will be redirected to the MercadoPago website after cliking on the payment button.
]]>
+
+ Pendiente de acreditación. Su pago todavía no ha sido confirmado por Mercadopago, le informaremos cuando esto suceda y validaremos el pedido.]]>
+
+
+
diff --git a/payment_mercadopago/models/mercadopago_request.py b/payment_mercadopago/models/mercadopago_request.py
index 0fc6c05a..2ad825f4 100644
--- a/payment_mercadopago/models/mercadopago_request.py
+++ b/payment_mercadopago/models/mercadopago_request.py
@@ -19,7 +19,8 @@ class MercadoPagoAPI():
def __init__(self, acquirer):
self.mp = mercadopago.MP(acquirer.mercadopago_access_token)
- self.mp.sandbox_mode(False) if acquirer.state == "prod" else self.mp.sandbox_mode(True)
+ self.mp.sandbox_mode(False) if acquirer.state == "enabled" else self.mp.sandbox_mode(True)
+ self.sandbox = not acquirer.state == "enabled"
self.mp.set_platform_id("BVH38T5N7QOK0PPDGC2G")
def check_response(self, resp):
@@ -41,6 +42,18 @@ def check_response(self, resp):
'err_msg': "Server Error"
}
+ # Preference
+ def create_preference(self, preference):
+ resp = self.mp.create_preference(preference)
+ if self.sandbox:
+ _logger.info('Preference Result:\n%s' % resp)
+ resp = self.check_response(resp)
+
+ if resp.get('err_code'):
+ raise UserError(_("MercadoPago Error:\nCode: %s\nMessage: %s" % (resp.get('err_code'), resp.get('err_msg'))))
+ else:
+ return resp['sandbox_init_point'] if self.sandbox else resp['init_point']
+
# Customers
def get_customer_profile(self, email):
resp = self.mp.get('/v1/customers/search?%s' % email)
@@ -64,7 +77,6 @@ def create_customer_profile(self, email):
# Cards
def get_customer_cards(self, customer_id):
resp = self.mp.get('/v1/customers/%s/cards' % customer_id)
- import pdb; pdb.set_trace()
resp = self.check_response(resp)
if type(resp) != list and resp.get('err_code'):
raise UserError(_("MercadoPago Error:\nCode: %s\nMessage: %s" % (resp.get('err_code'), resp.get('err_msg'))))
@@ -123,6 +135,8 @@ def payment(self, acquirer, token, amount, capture=True, cvv_token=None):
values.update({"payer": {"type": 'customer', 'id': customer_id}})
resp = self.mp.post("/v1/payments", values)
+ if self.sandbox:
+ _logger.info('Payment Result:\n%s' % resp)
resp = self.check_response(resp)
if resp.get('err_code'):
raise UserError(_("MercadoPago Error:\nCode: %s\nMessage: %s" % (resp.get('err_code'), resp.get('err_msg'))))
diff --git a/payment_mercadopago/models/payment.py b/payment_mercadopago/models/payment.py
index ac5c8148..85213618 100644
--- a/payment_mercadopago/models/payment.py
+++ b/payment_mercadopago/models/payment.py
@@ -1,10 +1,13 @@
# coding: utf-8
from .mercadopago_request import MercadoPagoAPI
import logging
+import urllib.parse as urlparse
+import werkzeug
from odoo import _, api, fields, models
from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.http import request
+from ..controllers.main import MercadoPagoController
_logger = logging.getLogger(__name__)
@@ -33,6 +36,11 @@ class PaymentAcquirerMercadoPago(models.Model):
mercadopago_publishable_key = fields.Char('MercadoPago Public Key', required_if_provider='mercadopago')
mercadopago_access_token = fields.Char('MercadoPago Access Token', required_if_provider='mercadopago')
+ # Fields add by MercadoPago redirect
+ # TODO Can we use mercadopago_publishable_key and mercadopago_access_token?
+ mercadopago_client_id = fields.Char('MercadoPago Client Id', required_if_provider='mercadopago')
+ mercadopago_secret_key = fields.Char('MercadoPago Secret Key', required_if_provider='mercadopago')
+
def _get_feature_support(self):
"""Get advanced feature support by provider.
@@ -46,8 +54,98 @@ def _get_feature_support(self):
"""
res = super(PaymentAcquirerMercadoPago, self)._get_feature_support()
res['tokenize'].append('mercadopago')
+ res['fees'].append('mercadopago')
return res
+ def mercadopago_compute_fees(self, amount, currency_id, country_id):
+ self.ensure_one()
+ if not self.fees_active:
+ return 0.0
+ country = self.env['res.country'].browse(country_id)
+ if country and self.company_id.country_id.id == country.id:
+ percentage = self.fees_dom_var
+ fixed = self.fees_dom_fixed
+ else:
+ percentage = self.fees_int_var
+ fixed = self.fees_int_fixed
+ fees = percentage / 100.0 * amount + fixed
+ return fees
+
+ def mercadopago_form_generate_values(self, values):
+ self.ensure_one()
+ tx_values = dict(values)
+ base_url = self.get_base_url()
+ if (not self.mercadopago_client_id or not self.mercadopago_secret_key):
+ raise ValidationError(
+ _('YOU MUST COMPLETE acquirer.mercadopago_client_id and acquirer.mercadopago_secret_key'))
+
+ success_url = MercadoPagoController._success_url
+ failure_url = MercadoPagoController._failure_url
+ pending_url = MercadoPagoController._pending_url
+ return_url = tx_values.get('return_url')
+ # si hay return_url se la pasamos codificada asi cuando vuelve
+ # nos devuelve la misma
+ if return_url:
+ url_suffix = '{}{}'.format('?', werkzeug.urls.url_encode({'return_url': return_url}))
+ success_url += url_suffix
+ failure_url += url_suffix
+ pending_url += url_suffix
+
+ # TODO, implement, not implemented yet because mercadopago only
+ # shows description of first line and we would need to send taxes too
+ # sale_order = self.env['sale.order'].search(
+ # [('name', '=', tx_values["reference"])], limit=1)
+ # if self.mercadopago_description == 'so_lines' and sale_order:
+ # items = [{
+ # "title": line.name,
+ # "quantity": line.product_uom_qty,
+ # "currency_id": (
+ # tx_values['currency'] and
+ # tx_values['currency'].name or ''),
+ # "unit_price": line.price_unit,
+ # } for line in sale_order.order_line]
+ # else:
+ items = [{
+ "title": _("Order %s") % (tx_values["reference"]),
+ "quantity": 1,
+ "currency_id": (tx_values['currency'] and tx_values['currency'].name or ''),
+ "unit_price": tx_values["amount"]
+ }]
+
+ if self.fees_active:
+ items.append({
+ "title": _('Recargo por Mercadopago'),
+ "quantity": 1,
+ "currency_id": (tx_values['currency'] and tx_values['currency'].name or ''),
+ "unit_price": tx_values.pop('fees', 0.0)
+ })
+
+ preference = {
+ "items": items,
+ "payer": {
+ "name": values["billing_partner_first_name"],
+ "surname": values["billing_partner_last_name"],
+ "email": values["partner_email"]
+ },
+ "back_urls": {
+ "success": '%s' % urlparse.urljoin(base_url, success_url),
+ "failure": '%s' % urlparse.urljoin(base_url, failure_url),
+ "pending": '%s' % urlparse.urljoin(base_url, pending_url)
+ },
+ "auto_return": "approved",
+ "external_reference": tx_values["reference"],
+ "expires": False
+ }
+ tx_values.update({
+ 'acquirer_id': self.id,
+ 'mercadopago_preference': preference
+ })
+ return tx_values
+
+ def mercadopago_get_form_action_url(self):
+ self.ensure_one()
+ return MercadoPagoController._create_preference_url
+
@api.model
def mercadopago_s2s_form_process(self, data):
values = {
@@ -76,6 +174,90 @@ def mercadopago_s2s_form_validate(self, data):
class PaymentTransactionMercadoPago(models.Model):
_inherit = 'payment.transaction'
+ # Fields add by MercadoPago redirect
+ mercadopago_txn_id = fields.Char('Transaction ID')
+ mercadopago_txn_type = fields.Char('Transaction type', help='Informative field computed after payment')
+ # ----------------------------------
+
+ # --------------------------------------------------
+ # FORM RELATED METHODS
+ # --------------------------------------------------
+
+ @api.model
+ def _mercadopago_form_get_tx_from_data(self, data):
+ reference = data.get('external_reference')
+ collection_id = data.get('collection_id')
+ if not reference or not collection_id:
+ error_msg = (
+ 'MercadoPago: received data with missing reference (%s) or '
+ 'collection_id (%s)' % (reference, collection_id))
+ _logger.error(error_msg)
+ raise ValidationError(error_msg)
+
+ # find tx -> @TDENOTE use txn_id ?
+ txs = self.env['payment.transaction'].search(
+ [('reference', '=', reference)])
+ if not txs or len(txs) > 1:
+ error_msg = (
+ 'MercadoPago: received data for reference %s' % (reference))
+ if not txs:
+ error_msg += '; no order found'
+ else:
+ error_msg += '; multiple order found'
+ _logger.error(error_msg)
+ raise ValidationError(error_msg)
+ return txs[0]
+
+ @api.model
+ def _mercadopago_form_get_invalid_parameters(self, data):
+ invalid_parameters = []
+ # TODO implementar invalid paramters desde
+ # https://www.mercadopago.com.ar/developers/es/api-docs/basic-checkout/checkout-preferences/
+ # if data.get('pspReference'):
+ # _logger.ValidationError('Received a notification from MercadoLibre.')
+ return invalid_parameters
+
+ @api.model
+ def _mercadopago_form_validate(self, data):
+ """
+ From:
+ https://developers.mercadopago.com/documentacion/notificaciones-de-pago
+ Por lo que vi nunca se devuelve la "cancel_reason" o "pending_reason"
+ """
+ status = data.get('collection_status')
+ data = {
+ 'acquirer_reference': data.get('external_reference'),
+ 'mercadopago_txn_type': data.get('payment_type'),
+ 'mercadopago_txn_id': data.get('merchant_order_id', False),
+ # otros parametros que vuelven son 'collection_id'
+ }
+ if status in ['approved', 'processed']:
+ _logger.info('Validated MercadoPago payment for tx %s: set as done' % (self.reference))
+ self.write(data)
+ self._set_transaction_done()
+ return True
+ elif status in ['pending', 'in_process', 'in_mediation']:
+ _logger.info('Received notification for MercadoPago payment %s: set as pending' % (self.reference))
+ data.update(state_message=data.get('pending_reason', ''))
+ self.write(data)
+ self._set_transaction_pending()
+ return True
+ elif status in ['cancelled', 'refunded', 'charged_back', 'rejected']:
+ _logger.info('Received notification for MercadoPago payment %s: set as cancelled' % (self.reference))
+ data.update(state_message=data.get('cancel_reason', ''))
+ self.write(data)
+ self._set_transaction_cancel()
+ return True
+ else:
+ error = (
+ 'Received unrecognized status for MercadoPago payment %s: %s, '
+ 'set as error' % (self.reference, status))
+ _logger.info(error)
+ data.update(state_message=error)
+ self.write(data)
+ self._set_transaction_error(error)
+ return True
+
# --------------------------------------------------
# SERVER2SERVER RELATED METHODS
# --------------------------------------------------
diff --git a/payment_mercadopago/views/payment_mercadopago_templates.xml b/payment_mercadopago/views/payment_mercadopago_templates.xml
index c1e4c287..a838107a 100644
--- a/payment_mercadopago/views/payment_mercadopago_templates.xml
+++ b/payment_mercadopago/views/payment_mercadopago_templates.xml
@@ -2,16 +2,12 @@
-
+
+
+
-
-
diff --git a/payment_mercadopago/views/payment_transaction_views.xml b/payment_mercadopago/views/payment_transaction_views.xml
index b9bb81ac..1f1a1912 100644
--- a/payment_mercadopago/views/payment_transaction_views.xml
+++ b/payment_mercadopago/views/payment_transaction_views.xml
@@ -8,8 +8,10 @@
-
-
+
+
+
+
diff --git a/payment_mercadopago/views/payment_views.xml b/payment_mercadopago/views/payment_views.xml
index 71409426..c04b2664 100644
--- a/payment_mercadopago/views/payment_views.xml
+++ b/payment_mercadopago/views/payment_views.xml
@@ -9,6 +9,10 @@
+
+
+
+
How to generate this credentials