diff --git a/addons/payment/data/payment_acquirer_data.xml b/addons/payment/data/payment_acquirer_data.xml index 8c8ea1b84a5..27d27ea4258 100644 --- a/addons/payment/data/payment_acquirer_data.xml +++ b/addons/payment/data/payment_acquirer_data.xml @@ -243,4 +243,26 @@ ref("payment.payment_icon_cc_visa")])]'/> + + PayUlatam + + + + +

+ 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. +

+
    +
  • Online Payment
  • +
  • Payment Status Tracking
  • +
+
+ + +
+ diff --git a/addons/payment/data/payment_icon_data.xml b/addons/payment/data/payment_icon_data.xml index 139066edad0..ded11d7dee9 100644 --- a/addons/payment/data/payment_icon_data.xml +++ b/addons/payment/data/payment_icon_data.xml @@ -49,4 +49,9 @@ Bancontact + + + Codensa Easy Credit + + \ No newline at end of file diff --git a/addons/payment/static/img/codensa_easy_credit.png b/addons/payment/static/img/codensa_easy_credit.png new file mode 100644 index 00000000000..7f5d48750b4 Binary files /dev/null and b/addons/payment/static/img/codensa_easy_credit.png differ diff --git a/addons/payment_payulatam/__init__.py b/addons/payment_payulatam/__init__.py new file mode 100644 index 00000000000..e52ad6f83e3 --- /dev/null +++ b/addons/payment_payulatam/__init__.py @@ -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 diff --git a/addons/payment_payulatam/__manifest__.py b/addons/payment_payulatam/__manifest__.py new file mode 100644 index 00000000000..3cb48a950ea --- /dev/null +++ b/addons/payment_payulatam/__manifest__.py @@ -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', +} diff --git a/addons/payment_payulatam/controllers/__init__.py b/addons/payment_payulatam/controllers/__init__.py new file mode 100644 index 00000000000..5d4b25db9c0 --- /dev/null +++ b/addons/payment_payulatam/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import main diff --git a/addons/payment_payulatam/controllers/main.py b/addons/payment_payulatam/controllers/main.py new file mode 100644 index 00000000000..9ada53e5395 --- /dev/null +++ b/addons/payment_payulatam/controllers/main.py @@ -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') diff --git a/addons/payment_payulatam/data/payment_acquirer_data.xml b/addons/payment_payulatam/data/payment_acquirer_data.xml new file mode 100644 index 00000000000..fcb831bd509 --- /dev/null +++ b/addons/payment_payulatam/data/payment_acquirer_data.xml @@ -0,0 +1,17 @@ + + + + + payulatam + + + test + +

You will be redirected to the PayUlatam website after clicking on the payment button.

+
+ dummy + dummy + dummy +
+
+
diff --git a/addons/payment_payulatam/models/__init__.py b/addons/payment_payulatam/models/__init__.py new file mode 100644 index 00000000000..2ec5b9cd2f6 --- /dev/null +++ b/addons/payment_payulatam/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import payment diff --git a/addons/payment_payulatam/models/payment.py b/addons/payment_payulatam/models/payment.py new file mode 100644 index 00000000000..8389f6eb42e --- /dev/null +++ b/addons/payment_payulatam/models/payment.py @@ -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) diff --git a/addons/payment_payulatam/static/src/img/payulatam_icon.png b/addons/payment_payulatam/static/src/img/payulatam_icon.png new file mode 100644 index 00000000000..dfaf42f9dec Binary files /dev/null and b/addons/payment_payulatam/static/src/img/payulatam_icon.png differ diff --git a/addons/payment_payulatam/tests/__init__.py b/addons/payment_payulatam/tests/__init__.py new file mode 100644 index 00000000000..09671a17b96 --- /dev/null +++ b/addons/payment_payulatam/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_payulatam diff --git a/addons/payment_payulatam/tests/test_payulatam.py b/addons/payment_payulatam/tests/test_payulatam.py new file mode 100644 index 00000000000..2bec371b81a --- /dev/null +++ b/addons/payment_payulatam/tests/test_payulatam.py @@ -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') diff --git a/addons/payment_payulatam/views/payment_payulatam_templates.xml b/addons/payment_payulatam/views/payment_payulatam_templates.xml new file mode 100644 index 00000000000..793b1e0e38d --- /dev/null +++ b/addons/payment_payulatam/views/payment_payulatam_templates.xml @@ -0,0 +1,20 @@ + + + + diff --git a/addons/payment_payulatam/views/payment_views.xml b/addons/payment_payulatam/views/payment_views.xml new file mode 100644 index 00000000000..b4092fb98a0 --- /dev/null +++ b/addons/payment_payulatam/views/payment_views.xml @@ -0,0 +1,17 @@ + + + + payment.acquirer.form.inherit.payment.payulatam + payment.acquirer + + + + + + + + + + + +