diff --git a/addons/payment/models/res_config.py b/addons/payment/models/res_config.py
index a74e595b52e..b64b698c83d 100644
--- a/addons/payment/models/res_config.py
+++ b/addons/payment/models/res_config.py
@@ -19,4 +19,7 @@ class AccountPaymentConfig(osv.TransientModel):
'module_payment_buckaroo': fields.boolean(
'Manage Payments Using Buckaroo',
help='-It installs the module payment_buckaroo.'),
+ 'module_payment_authorize': fields.boolean(
+ 'Manage Payments Using Authorize.Net',
+ help='-It installs the module payment_authorize.'),
}
diff --git a/addons/payment/views/res_config_view.xml b/addons/payment/views/res_config_view.xml
index e4e6fc96652..8f5e5f31321 100644
--- a/addons/payment/views/res_config_view.xml
+++ b/addons/payment/views/res_config_view.xml
@@ -24,6 +24,10 @@
+
+
+
+
diff --git a/addons/payment_authorize/__init__.py b/addons/payment_authorize/__init__.py
new file mode 100644
index 00000000000..9332521b0ee
--- /dev/null
+++ b/addons/payment_authorize/__init__.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Odoo, Open Source Management Solution
+# Copyright (C) 2014-TODAY Odoo 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_authorize/__openerp__.py b/addons/payment_authorize/__openerp__.py
new file mode 100644
index 00000000000..57bf2c7e711
--- /dev/null
+++ b/addons/payment_authorize/__openerp__.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+
+{
+ 'name': 'Authorize.Net Payment Acquirer',
+ 'category': 'Hidden',
+ 'summary': 'Payment Acquirer: Authorize.net Implementation',
+ 'version': '1.0',
+ 'description': """Authorize.Net Payment Acquirer""",
+ 'author': 'Odoo SA',
+ 'depends': ['payment'],
+ 'data': [
+ 'views/authorize.xml',
+ 'views/payment_acquirer.xml',
+ 'data/authorize.xml',
+ 'views/payment_authorize_template.xml',
+ ],
+ 'installable': True,
+}
diff --git a/addons/payment_authorize/controllers/__init__.py b/addons/payment_authorize/controllers/__init__.py
new file mode 100644
index 00000000000..bbd183e955b
--- /dev/null
+++ b/addons/payment_authorize/controllers/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+import main
diff --git a/addons/payment_authorize/controllers/main.py b/addons/payment_authorize/controllers/main.py
new file mode 100644
index 00000000000..1279c23ce26
--- /dev/null
+++ b/addons/payment_authorize/controllers/main.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+import pprint
+import logging
+import urlparse
+
+from openerp import http
+from openerp.http import request
+
+_logger = logging.getLogger(__name__)
+
+
+class AuthorizeController(http.Controller):
+ _return_url = '/payment/authorize/return/'
+ _cancel_url = '/payment/authorize/cancel/'
+
+ @http.route([
+ '/payment/authorize/return/',
+ '/payment/authorize/cancel/',
+ ], type='http', auth='public')
+ def authorize_form_feedback(self, **post):
+ _logger.info('Authorize: entering form_feedback with post data %s', pprint.pformat(post))
+ return_url = '/'
+ if post:
+ request.env['payment.transaction'].sudo().form_feedback(post, 'authorize')
+ return_url = post.pop('return_url', '/')
+ base_url = request.env['ir.config_parameter'].get_param('web.base.url')
+ # Authorize.Net is expecting a response to the POST sent by their server.
+ # This response is in the form of a URL that Authorize.Net will pass on to the
+ # client's browser to redirect them to the desired location need javascript.
+ return request.render('payment_authorize.payment_authorize_redirect', {
+ 'return_url': '%s' % urlparse.urljoin(base_url, return_url)
+ })
diff --git a/addons/payment_authorize/data/authorize.xml b/addons/payment_authorize/data/authorize.xml
new file mode 100644
index 00000000000..d1d38106252
--- /dev/null
+++ b/addons/payment_authorize/data/authorize.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+ Authorize.Net
+ authorize
+
+
+ test
+ You will be redirected to the Authorize website after clicking on the payment button.]]>
+ dummy
+ dummy
+
+
+
+
diff --git a/addons/payment_authorize/models/__init__.py b/addons/payment_authorize/models/__init__.py
new file mode 100644
index 00000000000..92e55eeff24
--- /dev/null
+++ b/addons/payment_authorize/models/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+import authorize
diff --git a/addons/payment_authorize/models/authorize.py b/addons/payment_authorize/models/authorize.py
new file mode 100644
index 00000000000..2708daa37a6
--- /dev/null
+++ b/addons/payment_authorize/models/authorize.py
@@ -0,0 +1,165 @@
+# -*- coding: utf-'8' "-*-"
+
+import hashlib
+import hmac
+import logging
+import time
+import urlparse
+
+from openerp import api, fields, models
+from openerp.addons.payment.models.payment_acquirer import ValidationError
+from openerp.addons.payment_authorize.controllers.main import AuthorizeController
+from openerp.tools.float_utils import float_compare
+
+_logger = logging.getLogger(__name__)
+
+
+class PaymentAcquirerAuthorize(models.Model):
+ _inherit = 'payment.acquirer'
+
+ def _get_authorize_urls(self, environment):
+ """ Authorize URLs """
+ if environment == 'prod':
+ return {'authorize_form_url': 'https://secure.authorize.net/gateway/transact.dll'}
+ else:
+ return {'authorize_form_url': 'https://test.authorize.net/gateway/transact.dll'}
+
+ @api.model
+ def _get_providers(self):
+ providers = super(PaymentAcquirerAuthorize, self)._get_providers()
+ providers.append(['authorize', 'Authorize.Net'])
+ return providers
+
+ authorize_login = fields.Char(string='API Login Id', required_if_provider='authorize')
+ authorize_transaction_key = fields.Char(string='API Transaction Key', required_if_provider='authorize')
+
+ def _authorize_generate_hashing(self, values):
+ data = '^'.join([
+ values['x_login'],
+ values['x_fp_sequence'],
+ values['x_fp_timestamp'],
+ values['x_amount'],
+ values['x_currency_code']])
+ return hmac.new(str(values['x_trans_key']), data, hashlib.md5).hexdigest()
+
+ @api.multi
+ def authorize_form_generate_values(self, partner_values, tx_values):
+ self.ensure_one()
+ base_url = self.env['ir.config_parameter'].get_param('web.base.url')
+ authorize_tx_values = dict(tx_values)
+ temp_authorize_tx_values = {
+ 'x_login': self.authorize_login,
+ 'x_trans_key': self.authorize_transaction_key,
+ 'x_amount': str(tx_values['amount']),
+ 'x_show_form': 'PAYMENT_FORM',
+ 'x_type': 'AUTH_CAPTURE',
+ 'x_method': 'CC',
+ 'x_fp_sequence': '%s%s' % (self.id, int(time.time())),
+ 'x_version': '3.1',
+ 'x_relay_response': 'TRUE',
+ 'x_fp_timestamp': str(int(time.time())),
+ 'x_relay_url': '%s' % urlparse.urljoin(base_url, AuthorizeController._return_url),
+ 'x_cancel_url': '%s' % urlparse.urljoin(base_url, AuthorizeController._cancel_url),
+ 'x_currency_code': tx_values['currency'] and tx_values['currency'].name or '',
+ 'address': partner_values['address'],
+ 'city': partner_values['city'],
+ 'country': partner_values['country'] and partner_values['country'].name or '',
+ 'email': partner_values['email'],
+ 'zip': partner_values['zip'],
+ 'first_name': partner_values['first_name'],
+ 'last_name': partner_values['last_name'],
+ 'phone': partner_values['phone'],
+ 'state': partner_values.get('state') and partner_values['state'].name or '',
+ }
+ temp_authorize_tx_values['returndata'] = authorize_tx_values.pop('return_url', '')
+ temp_authorize_tx_values['x_fp_hash'] = self._authorize_generate_hashing(temp_authorize_tx_values)
+ authorize_tx_values.update(temp_authorize_tx_values)
+ return partner_values, authorize_tx_values
+
+ @api.multi
+ def authorize_get_form_action_url(self):
+ self.ensure_one()
+ return self._get_authorize_urls(self.environment)['authorize_form_url']
+
+
+class TxAuthorize(models.Model):
+ _inherit = 'payment.transaction'
+
+ authorize_txnid = fields.Char(string='Transaction ID')
+
+ _authorize_valid_tx_status = 1
+ _authorize_pending_tx_status = 4
+ _authorize_cancel_tx_status = 2
+
+ # --------------------------------------------------
+ # FORM RELATED METHODS
+ # --------------------------------------------------
+
+ @api.model
+ def _authorize_form_get_tx_from_data(self, data):
+ """ Given a data dict coming from authorize, verify it and find the related
+ transaction record. """
+ reference, trans_id, fingerprint = data.get('x_invoice_num'), data.get('x_trans_id'), data.get('x_MD5_Hash')
+ if not reference or not trans_id or not fingerprint:
+ error_msg = 'Authorize: received data with missing reference (%s) or trans_id (%s) or fingerprint (%s)' % (reference, trans_id, fingerprint)
+ _logger.error(error_msg)
+ raise ValidationError(error_msg)
+ tx = self.search([('reference', '=', reference)])
+ if not tx or len(tx) > 1:
+ error_msg = 'Authorize: received data for reference %s' % (reference)
+ if not tx:
+ error_msg += '; no order found'
+ else:
+ error_msg += '; multiple order found'
+ _logger.error(error_msg)
+ raise ValidationError(error_msg)
+ return tx[0]
+
+ @api.model
+ def _authorize_form_get_invalid_parameters(self, tx, data):
+ invalid_parameters = []
+
+ if tx.authorize_txnid and data.get('x_trans_id') != tx.authorize_txnid:
+ invalid_parameters.append(('Transaction Id', data.get('x_trans_id'), tx.authorize_txnid))
+ # check what is buyed
+ if float_compare(float(data.get('x_amount', '0.0')), tx.amount, 2) != 0:
+ invalid_parameters.append(('Amount', data.get('x_amount'), '%.2f' % tx.amount))
+ return invalid_parameters
+
+ @api.model
+ def _authorize_form_validate(self, tx, data):
+ if tx.state == 'done':
+ _logger.warning('Authorize: trying to validate an already validated tx (ref %s)' % tx.reference)
+ return True
+ status_code = int(data.get('x_response_code', '0'))
+ if status_code == self._authorize_valid_tx_status:
+ tx.write({
+ 'state': 'done',
+ 'authorize_txnid': data.get('x_trans_id'),
+ 'acquirer_reference': data['x_invoice_num'],
+ })
+ return True
+ elif status_code == self._authorize_pending_tx_status:
+ tx.write({
+ 'state': 'pending',
+ 'authorize_txnid': data.get('x_trans_id'),
+ 'acquirer_reference': data['x_invoice_num'],
+ })
+ return True
+ elif status_code == self._authorize_cancel_tx_status:
+ tx.write({
+ 'state': 'cancel',
+ 'authorize_txnid': data.get('x_trans_id'),
+ 'acquirer_reference': data['x_invoice_num'],
+ })
+ return True
+ else:
+ error = data.get('x_response_reason_text')
+ _logger.info(error)
+ tx.write({
+ 'state': 'error',
+ 'state_message': error,
+ 'authorize_txnid': data.get('x_trans_id'),
+ 'acquirer_reference': data['x_invoice_num'],
+ })
+ return False
diff --git a/addons/payment_authorize/static/description/icon.png b/addons/payment_authorize/static/description/icon.png
new file mode 100644
index 00000000000..c2bab4f7a07
Binary files /dev/null and b/addons/payment_authorize/static/description/icon.png differ
diff --git a/addons/payment_authorize/static/src/img/authorize_icon.png b/addons/payment_authorize/static/src/img/authorize_icon.png
new file mode 100644
index 00000000000..5f0393871e7
Binary files /dev/null and b/addons/payment_authorize/static/src/img/authorize_icon.png differ
diff --git a/addons/payment_authorize/tests/__init__.py b/addons/payment_authorize/tests/__init__.py
new file mode 100644
index 00000000000..7c3bcfce559
--- /dev/null
+++ b/addons/payment_authorize/tests/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from openerp.addons.payment_authorize.tests import test_authorize
diff --git a/addons/payment_authorize/tests/test_authorize.py b/addons/payment_authorize/tests/test_authorize.py
new file mode 100644
index 00000000000..f49aefaee40
--- /dev/null
+++ b/addons/payment_authorize/tests/test_authorize.py
@@ -0,0 +1,173 @@
+# -*- coding: utf-8 -*-
+
+import hashlib
+import hmac
+import time
+import urlparse
+from lxml import objectify
+
+import openerp
+from openerp.addons.payment.models.payment_acquirer import ValidationError
+from openerp.addons.payment.tests.common import PaymentAcquirerCommon
+from openerp.addons.payment_authorize.controllers.main import AuthorizeController
+from openerp.tools import mute_logger
+
+
+@openerp.tests.common.at_install(True)
+@openerp.tests.common.post_install(True)
+class AuthorizeCommon(PaymentAcquirerCommon):
+
+ def setUp(self):
+ super(AuthorizeCommon, self).setUp()
+ self.base_url = self.env['ir.config_parameter'].get_param('web.base.url')
+ # authorize only support USD in test environment
+ self.currency_usd = self.env['res.currency'].search([('name', '=', 'USD')], limit=1)[0]
+ # get the authorize account
+ model, self.authorize_id = self.env['ir.model.data'].get_object_reference('payment_authorize', 'payment_acquirer_authorize')
+
+
+@openerp.tests.common.at_install(True)
+@openerp.tests.common.post_install(True)
+class AuthorizeForm(AuthorizeCommon):
+
+ def _authorize_generate_hashing(self, values):
+ data = '^'.join([
+ values['x_login'],
+ values['x_fp_sequence'],
+ values['x_fp_timestamp'],
+ values['x_amount'],
+ ]) + '^'
+ return hmac.new(str(values['x_trans_key']), data, hashlib.md5).hexdigest()
+
+ def test_10_Authorize_form_render(self):
+ authorize = self.env['payment.acquirer'].browse(self.authorize_id)
+ self.assertEqual(authorize.environment, 'test', 'test without test environment')
+
+ # ----------------------------------------
+ # Test: button direct rendering
+ # ----------------------------------------
+ form_values = {
+ 'x_login': authorize.authorize_login,
+ 'x_trans_key': authorize.authorize_transaction_key,
+ 'x_amount': '320.0',
+ 'x_show_form': 'PAYMENT_FORM',
+ 'x_type': 'AUTH_CAPTURE',
+ 'x_method': 'CC',
+ 'x_fp_sequence': '%s%s' % (authorize.id, int(time.time())),
+ 'x_version': '3.1',
+ 'x_relay_response': 'TRUE',
+ 'x_fp_timestamp': str(int(time.time())),
+ 'x_relay_url': '%s' % urlparse.urljoin(self.base_url, AuthorizeController._return_url),
+ 'x_cancel_url': '%s' % urlparse.urljoin(self.base_url, AuthorizeController._cancel_url),
+ 'return_url': None,
+ 'x_currency_code': 'USD',
+ 'x_invoice_num': 'SO004',
+ 'x_first_name': 'Buyer',
+ 'x_last_name': 'Norbert',
+ 'x_address': 'Huge Street 2/543',
+ 'x_city': 'Sin City',
+ 'x_zip': '1000',
+ 'x_country': 'Belgium',
+ 'x_phone': '0032 12 34 56 78',
+ 'x_email': 'norbert.buyer@example.com',
+ 'x_state': None,
+ }
+
+ form_values['x_fp_hash'] = self._authorize_generate_hashing(form_values)
+ # render the button
+ cr, uid, context = self.env.cr, self.env.uid, {}
+ res = self.payment_acquirer.render(
+ cr, uid, self.authorize_id, 'SO004', 320.0, self.currency_usd.id,
+ partner_id=None, partner_values=self.buyer_values, context=context)
+ # check form result
+ tree = objectify.fromstring(res)
+ self.assertEqual(tree.get('action'), 'https://test.authorize.net/gateway/transact.dll', 'Authorize: wrong form POST url')
+ for form_input in tree.input:
+ # Generated and received 'x_fp_hash' are always different so skeep it.
+ if form_input.get('name') in ['submit', 'x_fp_hash']:
+ continue
+ self.assertEqual(
+ form_input.get('value'),
+ form_values[form_input.get('name')],
+ 'Authorize: wrong value for input %s: received %s instead of %s' % (form_input.get('name'), form_input.get('value'), form_values[form_input.get('name')])
+ )
+
+ @mute_logger('openerp.addons.payment_authorize.models.authorize', 'ValidationError')
+ def test_20_authorize_form_management(self):
+ cr, uid, context = self.env.cr, self.env.uid, {}
+ # be sure not to do stupid thing
+ authorize = self.env['payment.acquirer'].browse(self.authorize_id)
+ self.assertEqual(authorize.environment, 'test', 'test without test environment')
+
+ # typical data posted by authorize after client has successfully paid
+ authorize_post_data = {
+ 'return_url': u'/shop/payment/validate',
+ 'x_MD5_Hash': u'7934485E1C105940BE854208D10FAB4F',
+ 'x_account_number': u'XXXX0027',
+ 'x_address': u'Huge Street 2/543',
+ 'x_amount': u'320.00',
+ 'x_auth_code': u'E4W7IU',
+ 'x_avs_code': u'Y',
+ 'x_card_type': u'Visa',
+ 'x_cavv_response': u'2',
+ 'x_city': u'Sun City',
+ 'x_company': u'',
+ 'x_country': u'Belgium',
+ 'x_cust_id': u'',
+ 'x_cvv2_resp_code': u'',
+ 'x_description': u'',
+ 'x_duty': u'0.00',
+ 'x_email': u'norbert.buyer@exampl',
+ 'x_fax': u'',
+ 'x_first_name': u'Norbert',
+ 'x_freight': u'0.00',
+ 'x_invoice_num': u'SO004',
+ 'x_last_name': u'Buyer',
+ 'x_method': u'CC',
+ 'x_phone': u'0032 12 34 56 78',
+ 'x_po_num': u'',
+ 'x_response_code': u'1',
+ 'x_response_reason_code': u'1',
+ 'x_response_reason_text': u'This transaction has been approved.',
+ 'x_ship_to_address': u'Huge Street 2/543',
+ 'x_ship_to_city': u'Sun City',
+ 'x_ship_to_company': u'',
+ 'x_ship_to_country': u'Belgium',
+ 'x_ship_to_first_name': u'Norbert',
+ 'x_ship_to_last_name': u'Buyer',
+ 'x_ship_to_state': u'',
+ 'x_ship_to_zip': u'1000',
+ 'x_state': u'',
+ 'x_tax': u'0.00',
+ 'x_tax_exempt': u'FALSE',
+ 'x_test_request': u'false',
+ 'x_trans_id': u'2217460311',
+ 'x_type': u'auth_capture',
+ 'x_zip': u'1000'
+ }
+
+ # should raise error about unknown tx
+ with self.assertRaises(ValidationError):
+ self.payment_transaction.form_feedback(cr, uid, authorize_post_data, 'authorize', context=context)
+
+ tx = self.env['payment.transaction'].create({
+ 'amount': 320.0,
+ 'acquirer_id': self.authorize_id,
+ 'currency_id': self.currency_usd.id,
+ 'reference': 'SO004',
+ 'partner_name': 'Norbert Buyer',
+ 'partner_country_id': self.country_france_id})
+ # validate it
+ self.payment_transaction.form_feedback(cr, uid, authorize_post_data, 'authorize', context=context)
+ # check state
+ self.assertEqual(tx.state, 'done', 'Authorize: validation did not put tx into done state')
+ self.assertEqual(tx.authorize_txnid, authorize_post_data.get('x_trans_id'), 'Authorize: validation did not update tx payid')
+
+ # reset tx
+ tx.write({'state': 'draft', 'date_validate': False, 'authorize_txnid': False})
+
+ # simulate an error
+ authorize_post_data['x_response_code'] = u'3'
+ self.payment_transaction.form_feedback(cr, uid, authorize_post_data, 'authorize', context=context)
+ # check state
+ self.assertEqual(tx.state, 'error', 'Authorize: erroneous validation did not put tx into error state')
diff --git a/addons/payment_authorize/views/authorize.xml b/addons/payment_authorize/views/authorize.xml
new file mode 100644
index 00000000000..7285ccb860a
--- /dev/null
+++ b/addons/payment_authorize/views/authorize.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/payment_authorize/views/payment_acquirer.xml b/addons/payment_authorize/views/payment_acquirer.xml
new file mode 100644
index 00000000000..a370f9f7a2e
--- /dev/null
+++ b/addons/payment_authorize/views/payment_acquirer.xml
@@ -0,0 +1,33 @@
+
+
+
+
+ acquirer.form.authorize
+ payment.acquirer
+
+
+
+
+
+
+
+
+
+
+
+
+ acquirer.transaction.form.authorize
+ payment.transaction
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/payment_authorize/views/payment_authorize_template.xml b/addons/payment_authorize/views/payment_authorize_template.xml
new file mode 100644
index 00000000000..58b1ee85e67
--- /dev/null
+++ b/addons/payment_authorize/views/payment_authorize_template.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+