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 @@ - -