diff --git a/addons/payment_acquirer_adyen/__init__.py b/addons/payment_acquirer_adyen/__init__.py new file mode 100644 index 00000000000..dde6f25c728 --- /dev/null +++ b/addons/payment_acquirer_adyen/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2013-Today OpenERP SA (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import models +import controllers diff --git a/addons/payment_acquirer_adyen/__openerp__.py b/addons/payment_acquirer_adyen/__openerp__.py new file mode 100644 index 00000000000..e2344c88406 --- /dev/null +++ b/addons/payment_acquirer_adyen/__openerp__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +{ + 'name': 'Adyen Payment Acquirer', + 'category': 'Hidden', + 'summary': 'Payment Acquirer: Adyen Implementation', + 'version': '1.0', + 'description': """Adyen Payment Acquirer""", + 'author': 'OpenERP SA', + 'depends': ['payment_acquirer'], + 'data': [ + 'views/adyen.xml', + 'views/payment_acquirer.xml', + 'data/adyen.xml', + ], + 'installable': True, +} diff --git a/addons/payment_acquirer_adyen/controllers/__init__.py b/addons/payment_acquirer_adyen/controllers/__init__.py new file mode 100644 index 00000000000..bbd183e955b --- /dev/null +++ b/addons/payment_acquirer_adyen/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +import main diff --git a/addons/payment_acquirer_adyen/controllers/main.py b/addons/payment_acquirer_adyen/controllers/main.py new file mode 100644 index 00000000000..2ea9fb00c51 --- /dev/null +++ b/addons/payment_acquirer_adyen/controllers/main.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +from openerp.addons.web import http +from openerp.addons.web.http import request +from openerp.addons.website.models import website + +try: + import simplejson as json +except ImportError: + import json +import logging +import pprint + +_logger = logging.getLogger(__name__) + + +class AdyenController(http.Controller): + _return_url = '/payment/adyen/return/' + + @website.route([ + '/payment/adyen/return/', + ], type='http', auth='public') + def adyen_return(self, **post): + """ Paypal IPN. """ + _logger.info('Beginning Adyen form_feedback with post data %s', pprint.pformat(post)) # debug + request.registry['payment.transaction'].form_feedback(request.cr, request.uid, post, 'adyen', context=request.context) + return_url = post.pop('return_url', '') + if not return_url: + custom = json.loads(post.pop('merchantReturnData', '{}')) + return_url = custom.pop('return_url', '/') + return request.redirect(return_url) diff --git a/addons/payment_acquirer_adyen/data/adyen.xml b/addons/payment_acquirer_adyen/data/adyen.xml new file mode 100644 index 00000000000..ec5cb3c7373 --- /dev/null +++ b/addons/payment_acquirer_adyen/data/adyen.xml @@ -0,0 +1,15 @@ + + + + + + adyen + + test + OpenERPCOM + cbqYWvVL + cbqYWvVL + + + + diff --git a/addons/payment_acquirer_adyen/doc/changelog.rst b/addons/payment_acquirer_adyen/doc/changelog.rst new file mode 100644 index 00000000000..d0c24af384d --- /dev/null +++ b/addons/payment_acquirer_adyen/doc/changelog.rst @@ -0,0 +1,9 @@ +.. _changelog: + +Changelog +========= + +`trunk (saas-3)` +---------------- + +- Module creation \ No newline at end of file diff --git a/addons/payment_acquirer_adyen/doc/index.rst b/addons/payment_acquirer_adyen/doc/index.rst new file mode 100644 index 00000000000..85028ed0d3e --- /dev/null +++ b/addons/payment_acquirer_adyen/doc/index.rst @@ -0,0 +1,14 @@ +Payment module documentation +============================ + +Payment documentation topics +'''''''''''''''''''''''''''' + + +Changelog +''''''''' + +.. toctree:: + :maxdepth: 1 + + changelog.rst diff --git a/addons/payment_acquirer_adyen/models/__init__.py b/addons/payment_acquirer_adyen/models/__init__.py new file mode 100644 index 00000000000..fdc540ef04d --- /dev/null +++ b/addons/payment_acquirer_adyen/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +import adyen diff --git a/addons/payment_acquirer_adyen/models/adyen.py b/addons/payment_acquirer_adyen/models/adyen.py new file mode 100644 index 00000000000..54c5cdfeb9f --- /dev/null +++ b/addons/payment_acquirer_adyen/models/adyen.py @@ -0,0 +1,198 @@ +# -*- coding: utf-'8' "-*-" + +import base64 +try: + import simplejson as json +except ImportError: + import json +from hashlib import sha1 +import hmac +import logging +import urlparse + +from openerp.addons.payment_acquirer.models.payment_acquirer import ValidationError +from openerp.addons.payment_acquirer_adyen.controllers.main import AdyenController +from openerp.osv import osv, fields +from openerp.tools import float_round + +_logger = logging.getLogger(__name__) + + +class AcquirerAdyen(osv.Model): + _inherit = 'payment.acquirer' + + def _get_adyen_urls(self, cr, uid, ids, name, args, context=None): + """ Adyen URLs + + - yhpp: hosted payment page: pay.shtml for single, select.shtml for multiple + """ + res = {} + for acquirer in self.browse(cr, uid, ids, context=context): + qualif = acquirer.env + res[acquirer.id] = { + 'adyen_form_url': 'https://%s.adyen.com/hpp/pay.shtml' % qualif, + } + return res + + _columns = { + 'adyen_merchant_account': fields.char('Merchant Account', required_if_provider='adyen'), + 'adyen_skin_code': fields.char('Skin Code', required_if_provider='adyen'), + 'adyen_skin_hmac_key': fields.char('Skin HMAC Key', required_if_provider='adyen'), + 'adyen_form_url': fields.function( + _get_adyen_urls, multi='_get_adyen_urls', + type='char', string='Transaction URL', required_if_provider='adyen'), + } + + def _adyen_generate_merchant_sig(self, acquirer, inout, values): + """ Generate the shasign for incoming or outgoing communications. + + :param browse acquirer: the payment.acquirer browse record. It should + have a shakey in shaky out + :param string inout: 'in' (openerp contacting ogone) or 'out' (adyen + contacting openerp). In this last case only some + fields should be contained (see e-Commerce basic) + :param dict values: transaction values + + :return string: shasign + """ + assert inout in ('in', 'out') + assert acquirer.name == 'adyen' + + if inout == 'in': + keys = "paymentAmount currencyCode shipBeforeDate merchantReference skinCode merchantAccount sessionValidity shopperEmail shopperReference recurringContract allowedMethods blockedMethods shopperStatement merchantReturnData billingAddressType deliveryAddressType offset".split() + else: + keys = "authResult pspReference merchantReference skinCode paymentMethod shopperLocale merchantReturnData".split() + + def get_value(key): + if values.get(key): + return values[key] + return '' + + sign = ''.join('%s' % get_value(k) for k in keys).encode('ascii') + key = acquirer.adyen_skin_hmac_key.encode('ascii') + return base64.b64encode(hmac.new(key, sign, sha1).digest()) + + def adyen_form_generate_values(self, cr, uid, id, reference, amount, currency, partner_id=False, partner_values=None, tx_custom_values=None, context=None): + if partner_values is None: + partner_values = {} + base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url') + acquirer = self.browse(cr, uid, id, context=context) + # tmp + import datetime + from dateutil import relativedelta + tmp_date = datetime.date.today() + relativedelta.relativedelta(days=1) + + partner = None + if partner_id: + partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context) + tx_values = { + 'merchantReference': reference, + 'paymentAmount': '%d' % int(float_round(amount, 2) * 100), + 'currencyCode': currency and currency.name or 'EUR', + 'shipBeforeDate': tmp_date, + 'skinCode': acquirer.adyen_skin_code, + 'merchantAccount': acquirer.adyen_merchant_account, + 'shopperLocale': partner and partner.lang or partner_values.get('lang', 'en_US'), + 'sessionValidity': tmp_date, + 'merchantSig': 'oij', + 'resURL': '%s' % urlparse.urljoin(base_url, AdyenController._return_url), + } + if tx_custom_values and tx_custom_values.get('return_url'): + tx_values['merchantReturnData'] = json.dumps({'return_url': '%s' % tx_custom_values.pop('return_url')}) + if tx_custom_values: + tx_values.update(tx_custom_values) + tx_values['merchantSig'] = self._adyen_generate_merchant_sig(acquirer, 'in', tx_values) + return tx_values + + def adyen_get_form_action_url(self, cr, uid, id, context=None): + acquirer = self.browse(cr, uid, id, context=context) + return acquirer.adyen_form_url + + +class TxAdyen(osv.Model): + _inherit = 'payment.transaction' + + _columns = { + 'adyen_psp_reference': fields.char('Adyen PSP Reference'), + } + + # -------------------------------------------------- + # FORM RELATED METHODS + # -------------------------------------------------- + + def adyen_form_generate_values(self, cr, uid, id, tx_custom_values=None, context=None): + tx = self.browse(cr, uid, id, context=context) + + tx_data = { + 'shopperLocale': tx.partner_lang, + } + if tx_custom_values: + tx_data.update(tx_custom_values) + return self.pool['payment.acquirer'].paypal_form_generate_values( + cr, uid, tx.acquirer_id.id, + tx.reference, tx.amount, tx.currency_id, + tx_custom_values=tx_data, + context=context + ) + + def _adyen_form_get_tx_from_data(self, cr, uid, data, context=None): + reference, pspReference = data.get('merchantReference'), data.get('pspReference') + if not reference or not pspReference: + error_msg = 'Adyen: received data with missing reference (%s) or missing pspReference (%s)' % (reference, pspReference) + _logger.error(error_msg) + raise ValidationError(error_msg) + + # find tx -> @TDENOTE use pspReference ? + tx_ids = self.pool['payment.transaction'].search(cr, uid, [('reference', '=', reference)], context=context) + if not tx_ids or len(tx_ids) > 1: + error_msg = 'Adyen: received data for reference %s' % (reference) + if not tx_ids: + error_msg += '; no order found' + else: + error_msg += '; multiple order found' + _logger.error(error_msg) + raise ValidationError(error_msg) + tx = self.pool['payment.transaction'].browse(cr, uid, tx_ids[0], context=context) + + # verify shasign + shasign_check = self.pool['payment.acquirer']._adyen_generate_merchant_sig(tx.acquirer_id, 'out', data) + if shasign_check != data.get('merchantSig'): + error_msg = 'Adyen: invalid merchantSig, received %s, computed %s' % (data.get('merchantSig'), shasign_check) + _logger.warning(error_msg) + # raise ValidationError(error_msg) + + return tx + + def _adyen_form_get_invalid_parameters(self, cr, uid, tx, data, context=None): + # TODO: txn_id: shoudl be false at draft, set afterwards, and verified with txn details + invalid_parameters = [] + if data.get('skinCode') != tx.acquirer_id.adyen_skin_code: + invalid_parameters.append(('skinCode', data.get('skinCode'), tx.acquirer_id.adyen_skin_code)) + if not data.get('authResult'): + invalid_parameters.append(('authResult', data.get('authResult'), 'something')) + return invalid_parameters + + def _adyen_form_validate(self, cr, uid, tx, data, context=None): + status = data.get('authResult', 'PENDING') + if status == 'AUTHORISED': + tx.write({ + 'state': 'done', + 'adyen_psp_reference': data.get('pspReference'), + # 'date_validate': data.get('payment_date', fields.datetime.now()), + # 'paypal_txn_type': data.get('express_checkout') + }) + return True + elif status == 'PENDING': + tx.write({ + 'state': 'pending', + 'adyen_psp_reference': data.get('pspReference'), + }) + return True + else: + error = 'Paypal: feedback error' + _logger.info(error) + tx.write({ + 'state': 'error', + 'state_message': error + }) + return False diff --git a/addons/payment_acquirer_adyen/static/description/icon.png b/addons/payment_acquirer_adyen/static/description/icon.png new file mode 100644 index 00000000000..ccbc55b8c5d Binary files /dev/null and b/addons/payment_acquirer_adyen/static/description/icon.png differ diff --git a/addons/payment_acquirer_adyen/static/src/img/logo.jpg b/addons/payment_acquirer_adyen/static/src/img/logo.jpg new file mode 100644 index 00000000000..63a58006324 Binary files /dev/null and b/addons/payment_acquirer_adyen/static/src/img/logo.jpg differ diff --git a/addons/payment_acquirer_adyen/tests/__init__.py b/addons/payment_acquirer_adyen/tests/__init__.py new file mode 100644 index 00000000000..b201c351ca7 --- /dev/null +++ b/addons/payment_acquirer_adyen/tests/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +from . import test_adyen + +checks = [ + test_adyen, +] diff --git a/addons/payment_acquirer_adyen/tests/test_adyen.py b/addons/payment_acquirer_adyen/tests/test_adyen.py new file mode 100644 index 00000000000..4fe53b6f2ca --- /dev/null +++ b/addons/payment_acquirer_adyen/tests/test_adyen.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- + +from openerp.addons.payment_acquirer.models.payment_acquirer import ValidationError +from openerp.addons.payment_acquirer.tests.common import PaymentAcquirerCommon +from openerp.addons.payment_acquirer_adyen.controllers.main import AdyenController +from openerp.osv.orm import except_orm +from openerp.tools import mute_logger + +from lxml import objectify +import urlparse + + +class AdyenCommon(PaymentAcquirerCommon): + + def setUp(self): + super(AdyenCommon, self).setUp() + cr, uid = self.cr, self.uid + + self.base_url = self.registry('ir.config_parameter').get_param(cr, uid, 'web.base.url') + model, self.paypal_view_id = self.registry('ir.model.data').get_object_reference(cr, uid, 'payment_acquirer_adyen', 'adyen_acquirer_button') + + # create a new ogone account + self.adyen_id = self.payment_acquirer.create( + cr, uid, { + 'name': 'adyen', + 'env': 'test', + 'view_template_id': self.paypal_view_id, + 'adyen_merchant_account': 'OpenERPCOM', + 'adyen_skin_code': 'cbqYWvVL', + 'adyen_skin_hmac_key': 'cbqYWvVL', + }) + + # some CC (always use expiration date 06 / 2016, cvc 737, cid 7373 (amex)) + self.amex = (('370000000000002', '7373')) + self.dinersclub = (('36006666333344', '737')) + self.discover = (('6011601160116611', '737'), ('644564456445644', '737')) + self.jcb = (('3530111333300000', '737')) + self.mastercard = (('5555444433331111', '737'), ('5555555555554444', '737')) + self.visa = (('4111 1111 1111 1111', '737'), ('4444333322221111', '737')) + self.mcdebit = (('5500000000000004', '737')) + self.visadebit = (('4400000000000008', '737')) + self.maestro = (('6731012345678906', '737')) + self.laser = (('630495060000000000', '737')) + self.hipercard = (('6062828888666688', '737')) + self.dsmastercard = (('521234567890 1234', '737', 'user', 'password')) + self.dsvisa = (('4212345678901237', '737', 'user', 'password')) + self.mistercash = (('6703444444444449', None, 'user', 'password')) + + +class AdyenServer2Server(AdyenCommon): + + def test_00_tx_management(self): + cr, uid, context = self.cr, self.uid, {} + + +class AdyenForm(AdyenCommon): + + def test_10_adyen_form_render(self): + cr, uid, context = self.cr, self.uid, {} + + # ---------------------------------------- + # Test: button direct rendering + # ---------------------------------------- + + form_values = { + 'merchantAccount': 'OpenERPCOM', + 'merchantReference': 'test_ref0', + 'skinCode': 'cbqYWvVL', + 'paymentAmount': '1', + 'currencyCode': 'EUR', + 'resURL': '%s' % urlparse.urljoin(self.base_url, AdyenController._return_url), + } + + # render the button + res = self.payment_acquirer.render( + cr, uid, self.adyen_id, + 'test_ref0', 0.01, self.currency_euro, + partner_id=None, + partner_values=self.buyer_values, + context=context) + + # check form result + tree = objectify.fromstring(res) + self.assertEqual(tree.get('action'), 'https://test.adyen.com/hpp/pay.shtml', 'adyen: wrong form POST url') + for form_input in tree.input: + if form_input.get('name') in ['submit', 'shipBeforeDate', 'sessionValidity', 'shopperLocale', 'merchantSig']: + continue + self.assertEqual( + form_input.get('value'), + form_values[form_input.get('name')], + 'adyen: wrong value for form: received %s instead of %s' % (form_input.get('value'), form_values[form_input.get('name')]) + ) + + @mute_logger('openerp.addons.payment_acquirer_paypal.models.paypal', 'ValidationError') + def test_20_paypal_form_management(self): + cr, uid, context = self.cr, self.uid, {} + +# {'authResult': u'AUTHORISED', +# 'merchantReference': u'SO014', +# 'merchantReturnData': u'return_url=/shop/payment/validate', +# 'merchantSig': u'GaLRO8aMHFaQX3gQ5BVP/YETzeA=', +# 'paymentMethod': u'visa', +# 'pspReference': u'8813859935907337', +# 'shopperLocale': u'en_US', +# 'skinCode': u'cbqYWvVL'} \ No newline at end of file diff --git a/addons/payment_acquirer_adyen/views/adyen.xml b/addons/payment_acquirer_adyen/views/adyen.xml new file mode 100644 index 00000000000..aeaa5260b11 --- /dev/null +++ b/addons/payment_acquirer_adyen/views/adyen.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/addons/payment_acquirer_adyen/views/payment_acquirer.xml b/addons/payment_acquirer_adyen/views/payment_acquirer.xml new file mode 100644 index 00000000000..9b75828f315 --- /dev/null +++ b/addons/payment_acquirer_adyen/views/payment_acquirer.xml @@ -0,0 +1,35 @@ + + + + + + acquirer.form.adyen + payment.acquirer + + + + + + + + + + + + acquirer.transaction.form.adyen + payment.transaction + + + + + + + + + + + + + + diff --git a/addons/website_sale/views/website_sale.xml b/addons/website_sale/views/website_sale.xml index 13af89ea937..32d43f2dafa 100644 --- a/addons/website_sale/views/website_sale.xml +++ b/addons/website_sale/views/website_sale.xml @@ -879,17 +879,20 @@
-

Payment method:

-
+

Choose your payment method

+
+ + +
+
- -