[ADD] payment_payulatam: Add new payment acquirer for Latin America

TaskID 1826303

closes odoo/odoo#24802
This commit is contained in:
Nikunj Ladava
2019-02-11 11:36:14 +00:00
committed by Yannick Tivisse
parent 890e47ea7f
commit 0e71f91fa1
15 changed files with 429 additions and 0 deletions
@@ -243,4 +243,26 @@
ref("payment.payment_icon_cc_visa")])]'/>
</record>
<record id="payment_acquirer_payulatam" model="payment.acquirer">
<field name="name">PayUlatam</field>
<field name="image" type="base64" file="payment_payulatam/static/src/img/payulatam_icon.png"/>
<field name="view_template_id" ref="default_acquirer_button"/>
<field name="module_id" ref="base.module_payment_payulatam"/>
<field name="description" type="html">
<p>
PayU is a leading financial services provider in Colombia, Argentina, Brazil, Chile, Mexico, Panama, and Peru. It allows merchant to accept local payments with just one account and integration.
</p>
<ul class="list-inline">
<li><i class="fa fa-check"/>Online Payment</li>
<li><i class="fa fa-check"/>Payment Status Tracking</li>
</ul>
</field>
<!-- https://www.payulatam.com/medios-de-pago/ -->
<field name="payment_icon_ids" eval='[(6, 0, [ref("payment.payment_icon_cc_diners_club_intl"),
ref("payment.payment_icon_cc_mastercard"),
ref("payment.payment_icon_cc_american_express"),
ref("payment.payment_icon_cc_visa"),
ref("payment.payment_icon_cc_codensa_easy_credit")])]'/>
</record>
</odoo>
@@ -49,4 +49,9 @@
<field name="name">Bancontact</field>
<field name="image" type="base64" file="payment/static/img/bancontact.png"/>
</record>
<record id="payment_icon_cc_codensa_easy_credit" model="payment.icon">
<field name="name">Codensa Easy Credit</field>
<field name="image" type="base64" file="payment/static/img/codensa_easy_credit.png"/>
</record>
</odoo>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

+6
View File
@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
from odoo.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers
+16
View File
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'PayuLatam Payment Acquirer',
'category': 'Payment Acquirer',
'summary': 'Payment Acquirer: PayuLatam Implementation',
'description': """Payulatam payment acquirer""",
'depends': ['payment'],
'data': [
'views/payment_views.xml',
'views/payment_payulatam_templates.xml',
'data/payment_acquirer_data.xml',
],
'post_init_hook': 'create_missing_journal_for_acquirers',
}
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import pprint
import werkzeug
from odoo import http
from odoo.http import request
_logger = logging.getLogger(__name__)
class PayuLatamController(http.Controller):
@http.route('/payment/payulatam/response', type='http', auth='public', csrf=False)
def payulatam_response(self, **post):
""" PayUlatam."""
_logger.info('PayUlatam: entering form_feedback with post response data %s', pprint.pformat(post))
if post:
request.env['payment.transaction'].sudo().form_feedback(post, 'payulatam')
return werkzeug.utils.redirect('/payment/process')
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="payment.payment_acquirer_payulatam" model="payment.acquirer">
<field name="provider">payulatam</field>
<field name="company_id" ref="base.main_company"/>
<field name="view_template_id" ref="payulatam_form"/>
<field name="environment">test</field>
<field name="pre_msg" type="html">
<p>You will be redirected to the PayUlatam website after clicking on the payment button.</p>
</field>
<field name="payulatam_account_id">dummy</field>
<field name="payulatam_merchant_id">dummy</field>
<field name="payulatam_api_key">dummy</field>
</record>
</data>
</odoo>
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import payment
+144
View File
@@ -0,0 +1,144 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import uuid
from hashlib import md5
from werkzeug import urls
from odoo import api, fields, models, _
from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.tools.float_utils import float_compare
_logger = logging.getLogger(__name__)
class PaymentAcquirerPayulatam(models.Model):
_inherit = 'payment.acquirer'
provider = fields.Selection(selection_add=[('payulatam', 'PayUlatam')])
payulatam_merchant_id = fields.Char(string="PayUlatam Merchant ID", required_if_provider='payulatam', groups='base.group_user')
payulatam_account_id = fields.Char(string="PayUlatam Account ID", required_if_provider='payulatam', groups='base.group_user')
payulatam_api_key = fields.Char(string="PayUlatam API Key", required_if_provider='payulatam', groups='base.group_user')
def _get_payulatam_urls(self, environment):
""" PayUlatam URLs"""
if environment == 'prod':
return 'https://checkout.payulatam.com/ppp-web-gateway-payu/'
return 'https://sandbox.checkout.payulatam.com/ppp-web-gateway-payu/'
def _payulatam_generate_sign(self, inout, values):
if inout not in ('in', 'out'):
raise Exception("Type must be 'in' or 'out'")
if inout == 'in':
data_string = ('~').join((self.payulatam_api_key, self.payulatam_merchant_id, values['referenceCode'],
str(values['amount']), values['currency']))
else:
data_string = ('~').join((self.payulatam_api_key, self.payulatam_merchant_id, values['referenceCode'],
str(float(values.get('TX_VALUE'))), values['currency'], values.get('transactionState')))
return md5(data_string.encode('utf-8')).hexdigest()
@api.multi
def payulatam_form_generate_values(self, values):
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
tx = self.env['payment.transaction'].search([('reference', '=', values.get('reference'))])
# payulatam will not allow any payment twise even if payment was failed last time.
# so, replace reference code if payment is not done or pending.
if tx.state not in ['done', 'pending']:
tx.acquirer_reference = str(uuid.uuid4())
payulatam_values = dict(
values,
merchantId=self.payulatam_merchant_id,
accountId=self.payulatam_account_id,
description=values.get('reference'),
referenceCode=tx.acquirer_reference,
amount=values['amount'],
tax='0', # This is the transaction VAT. If VAT zero is sent the system, 19% will be applied automatically. It can contain two decimals. Eg 19000.00. In the where you do not charge VAT, it should should be set as 0.
taxReturnBase='0',
currency=values['currency'].name,
buyerEmail=values['partner_email'],
responseUrl=urls.url_join(base_url, '/payment/payulatam/response'),
)
payulatam_values['signature'] = self._payulatam_generate_sign("in", payulatam_values)
return payulatam_values
@api.multi
def payulatam_get_form_action_url(self):
self.ensure_one()
return self._get_payulatam_urls(self.environment)
class PaymentTransactionPayulatam(models.Model):
_inherit = 'payment.transaction'
@api.model
def _payulatam_form_get_tx_from_data(self, data):
""" Given a data dict coming from payulatam, verify it and find the related
transaction record. """
reference, txnid, sign = data.get('referenceCode'), data.get('transactionId'), data.get('signature')
if not reference or not txnid or not sign:
raise ValidationError(_('PayUlatam: received data with missing reference (%s) or transaction id (%s) or sign (%s)') % (reference, txnid, sign))
transaction = self.search([('acquirer_reference', '=', reference)])
if not transaction:
error_msg = (_('PayUlatam: received data for reference %s; no order found') % (reference))
raise ValidationError(error_msg)
elif len(transaction) > 1:
error_msg = (_('PayUlatam: received data for reference %s; multiple orders found') % (reference))
raise ValidationError(error_msg)
# verify shasign
sign_check = transaction.acquirer_id._payulatam_generate_sign('out', data)
if sign_check.upper() != sign.upper():
raise ValidationError(('PayUlatam: invalid sign, received %s, computed %s, for data %s') % (sign, sign_check, data))
return transaction
@api.multi
def _payulatam_form_get_invalid_parameters(self, data):
invalid_parameters = []
if self.acquirer_reference and data.get('referenceCode') != self.acquirer_reference:
invalid_parameters.append(('Reference code', data.get('referenceCode'), self.acquirer_reference))
if float_compare(float(data.get('TX_VALUE', '0.0')), self.amount, 2) != 0:
invalid_parameters.append(('Amount', data.get('TX_VALUE'), '%.2f' % self.amount))
if data.get('merchantId') != self.acquirer_id.payulatam_merchant_id:
invalid_parameters.append(('Merchant Id', data.get('merchantId'), self.acquirer_id.payulatam_merchant_id))
return invalid_parameters
@api.multi
def _payulatam_form_validate(self, data):
self.ensure_one()
status = data.get('lapTransactionState') or data.find('transactionResponse').find('state').text
res = {
'acquirer_reference': data.get('transactionId') or data.find('transactionResponse').find('transactionId').text,
'state_message': data.get('message') or ""
}
if status == 'APPROVED':
_logger.info('Validated PayUlatam payment for tx %s: set as done' % (self.reference))
res.update(state='done', date=fields.Datetime.now())
self._set_transaction_done()
self.write(res)
self.execute_callback()
return True
elif status == 'PENDING':
_logger.info('Received notification for PayUlatam payment %s: set as pending' % (self.reference))
res.update(state='pending')
self._set_transaction_pending()
return self.write(res)
elif status in ['EXPIRED', 'DECLINED']:
_logger.info('Received notification for PayUlatam payment %s: set as Cancel' % (self.reference))
res.update(state='cancel')
self._set_transaction_cancel()
return self.write(res)
else:
error = 'Received unrecognized status for PayUlatam payment %s: %s, set as error' % (self.reference, status)
_logger.info(error)
res.update(state='cancel', state_message=error)
self._set_transaction_cancel()
return self.write(res)
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_payulatam
@@ -0,0 +1,148 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from lxml import objectify
from werkzeug import urls
from odoo.addons.payment.tests.common import PaymentAcquirerCommon
from odoo.tests import tagged
class PayUlatamCommon(PaymentAcquirerCommon):
def setUp(self):
super(PayUlatamCommon, self).setUp()
self.payulatam = self.env.ref('payment.payment_acquirer_payulatam')
@tagged('post_install', '-at_install', 'external', '-standard')
class PayUlatamForm(PayUlatamCommon):
def test_10_payulatam_form_render(self):
base_url = self.env['ir.config_parameter'].get_param('web.base.url')
self.assertEqual(self.payulatam.environment, 'test', 'test without test environment')
self.payulatam.write({
'payulatam_merchant_id': 'dummy',
'payulatam_account_id': 'dummy',
'payulatam_api_key': 'dummy',
})
# ----------------------------------------
# Test: button direct rendering
# ----------------------------------------
self.env['payment.transaction'].create({
'reference': 'test_ref0',
'amount': 0.001,
'currency_id': self.currency_euro.id,
'acquirer_id': self.payulatam.id,
'partner_id': self.buyer_id
})
# render the button
res = self.payulatam.render(
'test_ref0', 0.01, self.currency_euro.id,
values=self.buyer_values)
form_values = {
'merchantId': 'dummy',
'accountId': 'dummy',
'description': 'test_ref0',
'referenceCode': 'test',
'amount': '0.01',
'currency': 'EUR',
'tax': '0',
'taxReturnBase': '0',
'buyerEmail': 'norbert.buyer@example.com',
'responseUrl': urls.url_join(base_url, '/payment/payulatam/response'),
'extra1': None
}
# check form result
tree = objectify.fromstring(res)
data_set = tree.xpath("//input[@name='data_set']")
self.assertEqual(len(data_set), 1, 'payulatam: Found %d "data_set" input instead of 1' % len(data_set))
self.assertEqual(data_set[0].get('data-action-url'), 'https://sandbox.checkout.payulatam.com/ppp-web-gateway-payu/', 'payulatam: wrong form POST url')
for form_input in tree.input:
if form_input.get('name') in ['submit', 'data_set', 'signature', 'referenceCode']:
continue
self.assertEqual(
form_input.get('value'),
form_values[form_input.get('name')],
'payulatam: wrong value for input %s: received %s instead of %s' % (form_input.get('name'), form_input.get('value'), form_values[form_input.get('name')])
)
def test_20_payulatam_form_management(self):
self.assertEqual(self.payulatam.environment, 'test', 'test without test environment')
# typical data posted by payulatam after client has successfully paid
payulatam_post_data = {
'installmentsNumber': '1',
'lapPaymentMethod': 'VISA',
'description': 'test_ref0',
'currency': 'EUR',
'extra2': '',
'lng': 'es',
'transactionState': '7',
'polPaymentMethod': '211',
'pseCycle': '',
'pseBank': '',
'referenceCode': 'test_ref_10',
'reference_pol': '844164756',
'signature': '88f11d693d3551419f86850948d731ba',
'pseReference3': '',
'buyerEmail': 'admin@yourcompany.example.com',
'lapResponseCode': 'PENDING_TRANSACTION_CONFIRMATION',
'pseReference2': '',
'cus': '',
'orderLanguage': 'es',
'TX_VALUE': '0.01',
'risk': '',
'trazabilityCode': '',
'extra3': '',
'pseReference1': '',
'polTransactionState': '14',
'polResponseCode': '25',
'merchant_name': 'Test PayU Test comercio',
'merchant_url': 'http://pruebaslapv.xtrweb.com',
'extra1': '/shop/payment/validate',
'message': 'PENDING',
'lapPaymentMethodType': 'CARD',
'polPaymentMethodType': '7',
'telephone': '7512354',
'merchantId': 'dummy',
'transactionId': 'b232989a-4aa8-42d1-bace-153236eee791',
'authorizationCode': '',
'lapTransactionState': 'PENDING',
'TX_TAX': '.00',
'merchant_address': 'Av 123 Calle 12'
}
# create tx
tx = self.env['payment.transaction'].create({
'amount': 0.01,
'acquirer_id': self.payulatam.id,
'currency_id': self.currency_euro.id,
'reference': 'test_ref_2',
'partner_name': 'Norbert Buyer',
'partner_country_id': self.country_france.id,
'acquirer_reference': 'test_ref_10',
'partner_id': self.buyer_id})
# validate transaction
tx.form_feedback(payulatam_post_data, 'payulatam')
# check
self.assertEqual(tx.state, 'pending', 'Payulatam: wrong state after receiving a valid pending notification')
self.assertEqual(tx.state_message, 'PENDING', 'Payulatam: wrong state message after receiving a valid pending notification')
self.assertEqual(tx.acquirer_reference, 'b232989a-4aa8-42d1-bace-153236eee791', 'PayUlatam: wrong txn_id after receiving a valid pending notification')
# update transaction
tx.write({
'state': 'draft',
'acquirer_reference': False})
# update notification from payulatam
payulatam_post_data['lapTransactionState'] = 'APPROVED'
# validate transaction
tx.form_feedback(payulatam_post_data, 'payulatam')
# check transaction
self.assertEqual(tx.state, 'done', 'payulatam: wrong state after receiving a valid pending notification')
self.assertEqual(tx.acquirer_reference, 'b232989a-4aa8-42d1-bace-153236eee791', 'payulatam: wrong txn_id after receiving a valid pending notification')
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="payulatam_form">
<div>
<input type="hidden" name="data_set" t-att-data-action-url="tx_url" data-remove-me=""/>
<input type="hidden" name="merchantId" t-att-value='merchantId'/>
<input type="hidden" name="accountId" t-att-value='accountId'/>
<input type="hidden" name="description" t-att-value='description'/>
<input type="hidden" name="referenceCode" t-att-value='referenceCode'/>
<input type="hidden" name="amount" t-att-value='amount'/>
<input type="hidden" name="currency" t-att-value='currency'/>
<input type="hidden" name="signature" t-att-value='signature'/>
<input type="hidden" name="tax" t-att-value='tax'/>
<input type="hidden" name="taxReturnBase" t-att-value="taxReturnBase"/>
<input type="hidden" name="buyerEmail" t-att-value='buyerEmail'/>
<input type="hidden" name="responseUrl" t-att-value='responseUrl'/>
<input type="hidden" name="extra1" t-att-value="extra1"/>
</div>
</template>
</odoo>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="payment_acquirer_form_inherit_payment_payulatam" model="ir.ui.view">
<field name="name">payment.acquirer.form.inherit.payment.payulatam</field>
<field name="model">payment.acquirer</field>
<field name="inherit_id" ref="payment.acquirer_form"/>
<field name="arch" type="xml">
<xpath expr='//group[@name="acquirer"]' position='after'>
<group attrs="{'invisible': [('provider', '!=', 'payulatam')]}">
<field name="payulatam_account_id"/>
<field name="payulatam_merchant_id"/>
<field name="payulatam_api_key"/>
</group>
</xpath>
</field>
</record>
</odoo>