diff --git a/addons/email_template/wizard/mail_compose_message.py b/addons/email_template/wizard/mail_compose_message.py
index bae7a0d8f9c..ac2f79b56f1 100644
--- a/addons/email_template/wizard/mail_compose_message.py
+++ b/addons/email_template/wizard/mail_compose_message.py
@@ -41,6 +41,21 @@ def _reopen(self, res_id, model):
class mail_compose_message(osv.TransientModel):
_inherit = 'mail.compose.message'
+ def default_get(self, cr, uid, fields, context=None):
+ """
+ """
+ if context is None:
+ context = {}
+ res = super(mail_compose_message, self).default_get(cr, uid, fields, context=context)
+ if context.get('default_template_id'):
+ res.update(
+ self.onchange_template_id(
+ cr, uid, [], context['default_template_id'], res.get('composition_mode'),
+ res.get('model'), res.get('res_id'), context=context
+ )['value']
+ )
+ return res
+
_columns = {
'template_id': fields.many2one('email.template', 'Use template', select=True),
'partner_to': fields.char('To (Partner IDs)',
diff --git a/addons/payment_acquirer/__init__.py b/addons/payment_acquirer/__init__.py
new file mode 100644
index 00000000000..d95d47860af
--- /dev/null
+++ b/addons/payment_acquirer/__init__.py
@@ -0,0 +1,22 @@
+# -*- 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
diff --git a/addons/payment_acquirer/__openerp__.py b/addons/payment_acquirer/__openerp__.py
new file mode 100644
index 00000000000..4102c4fe276
--- /dev/null
+++ b/addons/payment_acquirer/__openerp__.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+
+{
+ 'name': 'Payment Acquirer',
+ 'category': 'Hidden',
+ 'summary': 'Payment Acquirer Base Module',
+ 'version': '1.0',
+ 'description': """Payment Acquirer Base Module""",
+ 'author': 'OpenERP SA',
+ 'depends': ['mail'],
+ 'data': [
+ 'views/payment_acquirer.xml',
+ 'security/ir.model.access.csv',
+ ],
+ 'installable': True,
+}
diff --git a/addons/payment_acquirer/doc/changelog.rst b/addons/payment_acquirer/doc/changelog.rst
new file mode 100644
index 00000000000..d0c24af384d
--- /dev/null
+++ b/addons/payment_acquirer/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/doc/index.rst b/addons/payment_acquirer/doc/index.rst
new file mode 100644
index 00000000000..85028ed0d3e
--- /dev/null
+++ b/addons/payment_acquirer/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/models/__init__.py b/addons/payment_acquirer/models/__init__.py
new file mode 100644
index 00000000000..da51c9c1854
--- /dev/null
+++ b/addons/payment_acquirer/models/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+import payment_acquirer
diff --git a/addons/payment_acquirer/models/payment_acquirer.py b/addons/payment_acquirer/models/payment_acquirer.py
new file mode 100644
index 00000000000..69a026cba81
--- /dev/null
+++ b/addons/payment_acquirer/models/payment_acquirer.py
@@ -0,0 +1,292 @@
+# -*- coding: utf-'8' "-*-"
+
+from openerp.osv import osv, fields
+
+import logging
+
+_logger = logging.getLogger(__name__)
+
+
+class ValidationError(ValueError):
+ """ Used for value error when validating transaction data coming from acquirers. """
+ pass
+
+
+class PaymentAcquirer(osv.Model):
+ """ Acquirer Model. Each specific acquirer can extend the model by adding
+ its own fields. Using the required_if_provider='' attribute on fields
+ it is possible to have required fields that depend on a specific acquirer.
+
+ Each acquirer has a link to an ir.ui.view record that is a template of
+ a button used to display the payment form. See examples in ``payment_acquirer_ogone``
+ and ``payment_acquirer_paypal`` modules.
+
+ Methods that should be added in an acquirer-specific implementation:
+
+ - ``_form_generate_values(self, cr, uid, id, reference, amount, currency,
+ partner_id=False, partner_values=None, tx_custom_values=None, context=None)``:
+ method that generates the values used to render the form button template.
+ - ``_get_form_action_url(self, cr, uid, id, context=None):``: method
+ that returns the url of the button form. It is used for example in
+ ecommerce application, if you want to repost some data to the acquirer.
+
+ Each acquirer should also define controllers to handle communication between
+ OpenERP and the acquirer. It generally consists in return urls given to the
+ button form and that the acquirer uses to send the customer back after the
+ transaction, with transaction details given as a POST request.
+ """
+ _name = 'payment.acquirer'
+ _description = 'Payment Acquirer'
+
+ _columns = {
+ 'name': fields.char('Name', required=True),
+ 'message': fields.html('Message', help='Message displayed to help payment and validation'),
+ 'view_template_id': fields.many2one('ir.ui.view', 'Form Button Template', required=True),
+ 'env': fields.selection(
+ [('test', 'Test'), ('prod', 'Production')],
+ string='Environment'),
+ 'portal_published': fields.boolean('Visible in Portal',
+ help="Make this payment acquirer available (Customer invoices, etc.)"),
+ }
+
+ _defaults = {
+ 'portal_published': True,
+ 'env': 'test',
+ }
+
+ def _check_required_if_provider(self, cr, uid, ids, context=None):
+ """ If the field has 'required_if_provider=""' attribute, then it
+ required if record.name is . """
+ for acquirer in self.browse(cr, uid, ids, context=context):
+ if any(c for c, f in self._all_columns.items() if getattr(f.column, 'required_if_provider', None) == acquirer.name and not acquirer[c]):
+ return False
+ return True
+
+ _constraints = [
+ (_check_required_if_provider, 'Required fields not filled', ['required for this provider']),
+ ]
+
+ def render(self, cr, uid, id, reference, amount, currency, tx_id=None, partner_id=False, partner_values=None, tx_custom_values=None, context=None):
+ """ Renders the form template of the given acquirer as a qWeb template.
+ All templates should handle:
+
+ - acquirer: the payment.acquirer browse record
+ - user: the current user browse record
+ - currency: currency browse record
+ - amount: amount of the transaction
+ - reference: reference of the transaction
+ - partner: the current partner browse record, if any (not necessarily set)
+ - partner_values: a dictionary of partner-related values
+ - tx_custom_values: a dictionary of transaction related values that depends
+ on the acquirer. Some specific keys should be managed
+ in each provider, depending on the features it offers:
+
+ - 'feedback_url': feedback URL, controler that manage answer of the acquirer
+ (without base url)
+ - 'return_url': URL for coming back after payment validation (wihout
+ base url)
+ - 'cancel_url': URL if the client cancels the payment
+ - 'error_url': URL if there is an issue with the payment
+
+ - context: OpenERP context dictionary
+
+ :param string reference: the transaction reference
+ :param float amount: the amount the buyer has to pay
+ :param res.currency browse record currency: currency
+ :param int tx_id: id of a transaction; if set, bypasses all other given
+ values and only render the already-stored transaction
+ :param res.partner browse record partner_id: the buyer
+ :param dict partner_values: a dictionary of values for the buyer (see above)
+ :param dict tx_custom_values: a dictionary of values for the transction
+ that is given to the acquirer-specific method
+ generating the form values
+ :param dict context: OpenERP context
+ """
+ if context is None:
+ context = {}
+ partner = None
+ if partner_id:
+ partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
+ acquirer = self.browse(cr, uid, id, context=context)
+ method_name = '%s_form_generate_values' % (acquirer.name)
+
+ if tx_id and hasattr(self.pool['payment.transaction'], method_name):
+ method = getattr(self.pool['payment.transaction'], method_name)
+ tx_values = method(cr, uid, tx_id, tx_custom_values, context=context)
+ elif hasattr(self, method_name):
+ method = getattr(self, method_name)
+ tx_values = method(cr, uid, id, reference, amount, currency, partner_id, partner_values, tx_custom_values, context=context)
+ else:
+ tx_values = tx_custom_values
+
+ qweb_context = {
+ 'acquirer': acquirer,
+ 'user': self.pool.get("res.users").browse(cr, uid, uid, context=context),
+ 'reference': reference,
+ 'amount': amount,
+ 'currency': currency,
+ 'partner': partner,
+ 'partner_values': partner_values,
+ 'tx_values': tx_values,
+ 'context': context,
+ }
+ # because render accepts view ids but not qweb -> need to find the xml_id
+ return self.pool['ir.ui.view'].render(cr, uid, acquirer.view_template_id.xml_id, qweb_context, engine='ir.qweb', context=context)
+
+ def get_form_action_url(self, cr, uid, id, context=None):
+ acquirer = self.browse(cr, uid, id, context=context)
+ if hasattr(self, '%s_get_form_action_url' % acquirer.name):
+ return getattr(self, '%s_get_form_action_url' % acquirer.name)(cr, uid, id, context=context)
+ return False
+
+
+class PaymentTransaction(osv.Model):
+ """ Transaction Model. Each specific acquirer can extend the model by adding
+ its own fields.
+
+ Methods that should be added in an acquirer-specific implementation:
+
+ - ``_form_generate_values(self, cr, uid, id, tx_custom_values=None,
+ context=None)``: method that generates the values used to render the
+ form button template.
+
+ Methods that can be added in an acquirer-specific implementation:
+
+ - ``_create``: method receiving values used when creating a new
+ transaction and that returns a dictionary that will update those values.
+ This method can be used to tweak some transaction values.
+
+ Methods defined for convention, depending on your controllers:
+
+ - ``_form_feedback(self, cr, uid, data, context=None)``: method that
+ handles the data coming from the acquirer after the transaction. It will
+ generally receives data posted by the acquirer after the transaction.
+ """
+ _name = 'payment.transaction'
+ _description = 'Payment Transaction'
+ _inherit = ['mail.thread']
+ _order = 'id desc'
+ _rec_name = 'reference'
+
+ _columns = {
+ 'date_create': fields.datetime('Creation Date', readonly=True, required=True),
+ 'date_validate': fields.datetime('Validation Date'),
+ 'acquirer_id': fields.many2one(
+ 'payment.acquirer', 'Acquirer',
+ required=True,
+ ),
+ 'type': fields.selection(
+ [('server2server', 'Server To Server'), ('form', 'Form')],
+ string='Type', required=True),
+ 'state': fields.selection(
+ [('draft', 'Draft'), ('pending', 'Pending'),
+ ('done', 'Done'), ('error', 'Error'),
+ ('cancel', 'Canceled')
+ ], 'Status', required=True,
+ track_visiblity='onchange'),
+ 'state_message': fields.text('Message',
+ help='Field used to store error and/or validation messages for information'),
+ # payment
+ 'amount': fields.float('Amount', required=True,
+ help='Amount in cents',
+ track_visibility='always'),
+ 'currency_id': fields.many2one('res.currency', 'Currency', required=True),
+ 'reference': fields.char('Order Reference', required=True),
+ # duplicate partner / transaction data to store the values at transaction time
+ 'partner_id': fields.many2one('res.partner', 'Partner'),
+ 'partner_name': fields.char('Partner Name'),
+ 'partner_lang': fields.char('Lang'),
+ 'partner_email': fields.char('Email'),
+ 'partner_zip': fields.char('Zip'),
+ 'partner_address': fields.char('Address'),
+ 'partner_city': fields.char('City'),
+ 'partner_country_id': fields.many2one('res.country', 'Country'),
+ 'partner_phone': fields.char('Phone'),
+ 'partner_reference': fields.char('Buyer Reference'),
+ }
+
+ _sql_constraints = [
+ ('reference_uniq', 'UNIQUE(reference)', 'The payment transaction reference must be unique!'),
+ ]
+
+ _defaults = {
+ 'date_create': fields.datetime.now,
+ 'type': 'form',
+ 'state': 'draft',
+ 'partner_lang': 'en_US',
+ }
+
+ def create(self, cr, uid, values, context=None):
+ if values.get('partner_id'): # @TDENOTE: not sure
+ values.update(self.on_change_partner_id(cr, uid, None, values.get('partner_id'), context=context)['values'])
+
+ # call custom create method if defined (i.e. ogone_create for ogone)
+ if values.get('acquirer_id'):
+ acquirer = self.pool['payment.acquirer'].browse(cr, uid, values.get('acquirer_id'), context=context)
+ custom_method_name = '%s_create' % acquirer.name
+ if hasattr(self, custom_method_name):
+ values.update(getattr(self, custom_method_name)(cr, uid, values, context=context))
+
+ return super(PaymentTransaction, self).create(cr, uid, values, context=context)
+
+ def on_change_partner_id(self, cr, uid, ids, partner_id, context=None):
+ if partner_id:
+ partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
+ values = {
+ 'partner_name': partner.name,
+ 'partner_lang': partner.lang,
+ 'partner_email': partner.email,
+ 'partner_zip': partner.zip,
+ 'partner_address': ' '.join((partner.street or '', partner.street2 or '')).strip(),
+ 'partner_city': partner.city,
+ 'partner_country_id': partner.country_id.id,
+ 'partner_phone': partner.phone,
+ }
+ else:
+ values = {
+ 'partner_name': False,
+ 'partner_lang': 'en_US',
+ 'partner_email': False,
+ 'partner_zip': False,
+ 'partner_address': False,
+ 'partner_city': False,
+ 'partner_country_id': False,
+ 'partner_phone': False,
+ }
+ return {'values': values}
+
+ # --------------------------------------------------
+ # FORM RELATED METHODS
+ # --------------------------------------------------
+
+ def form_feedback(self, cr, uid, data, acquirer_name, context=None):
+ invalid_parameters, tx = None, None
+
+ tx_find_method_name = '_%s_form_get_tx_from_data' % acquirer_name
+ if hasattr(self, tx_find_method_name):
+ tx = getattr(self, tx_find_method_name)(cr, uid, data, context=context)
+
+ invalid_param_method_name = '_%s_form_get_invalid_parameters' % acquirer_name
+ if hasattr(self, invalid_param_method_name):
+ invalid_parameters = getattr(self, invalid_param_method_name)(cr, uid, tx, data, context=context)
+
+ if invalid_parameters:
+ _error_message = '%s: incorrect tx data:\n' % (acquirer_name)
+ for item in invalid_parameters:
+ _error_message += '\t%s: received %s instead of %s\n' % (item[0], item[1], item[2])
+ _logger.error(_error_message)
+ return False
+
+ feedback_method_name = '_%s_form_validate' % acquirer_name
+ if hasattr(self, feedback_method_name):
+ return getattr(self, feedback_method_name)(cr, uid, tx, data, context=context)
+
+ return True
+
+ # --------------------------------------------------
+ # SERVER2SERVER RELATED METHODS
+ # --------------------------------------------------
+
+ def create_s2s(self, cr, uid, tx_values, cc_values, context=None):
+ tx_id = self.create(cr, uid, tx_values, context=context)
+ return tx_id
diff --git a/addons/payment_acquirer/security/ir.model.access.csv b/addons/payment_acquirer/security/ir.model.access.csv
new file mode 100644
index 00000000000..18f98939472
--- /dev/null
+++ b/addons/payment_acquirer/security/ir.model.access.csv
@@ -0,0 +1,7 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+payment_acquirer_all,payment.acquirer.all,model_payment_acquirer,,1,0,0,0
+payment_acquirer_user,payment.acquirer.user,model_payment_acquirer,base.group_user,1,1,1,0
+payment_acquirer_system,payment.acquirer.system,model_payment_acquirer,base.group_system,1,1,1,1
+payment_transaction_all,payment.transaction.all,model_payment_transaction,,1,1,1,0
+payment_transaction_user,payment.transaction.user,model_payment_transaction,base.group_user,1,1,1,0
+payment_transaction_system,payment.transaction.system,model_payment_transaction,base.group_system,1,1,1,1
\ No newline at end of file
diff --git a/addons/payment_acquirer/static/description/icon.png b/addons/payment_acquirer/static/description/icon.png
new file mode 100644
index 00000000000..51e33ac6ddf
Binary files /dev/null and b/addons/payment_acquirer/static/description/icon.png differ
diff --git a/addons/payment_acquirer/tests/__init__.py b/addons/payment_acquirer/tests/__init__.py
new file mode 100644
index 00000000000..5c193a07c8d
--- /dev/null
+++ b/addons/payment_acquirer/tests/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+import common
diff --git a/addons/payment_acquirer/tests/common.py b/addons/payment_acquirer/tests/common.py
new file mode 100644
index 00000000000..5c915f80cbf
--- /dev/null
+++ b/addons/payment_acquirer/tests/common.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+
+from openerp.tests import common
+
+
+class PaymentAcquirerCommon(common.TransactionCase):
+
+ def setUp(self):
+ super(PaymentAcquirerCommon, self).setUp()
+ self.payment_acquirer = self.registry('payment.acquirer')
+ self.payment_transaction = self.registry('payment.transaction')
+
+ self.currency_euro_id = self.registry('res.currency').search(
+ self.cr, self.uid, [('name', '=', 'EUR')], limit=1)[0]
+ self.currency_euro = self.registry('res.currency').browse(
+ self.cr, self.uid, self.currency_euro_id)
+ country_belgium_id = self.registry('res.country').search(
+ self.cr, self.uid, [('code', 'like', 'BE')], limit=1)[0]
+
+ # dict partner values
+ self.buyer_values = {
+ 'name': 'Norbert Buyer',
+ 'lang': 'en_US',
+ 'email': 'norbert.buyer@example.com',
+ 'street': 'Huge Street',
+ 'street2': '2/543',
+ 'phone': '0032 12 34 56 78',
+ 'city': 'Sin City',
+ 'zip': '1000',
+ 'country_id': country_belgium_id,
+ 'country_name': 'Belgium',
+ }
+
+ # test partner
+ self.buyer_id = self.registry('res.partner').create(
+ self.cr, self.uid, {
+ 'name': 'Norbert Buyer',
+ 'lang': 'en_US',
+ 'email': 'norbert.buyer@example.com',
+ 'street': 'Huge Street',
+ 'street2': '2/543',
+ 'phone': '0032 12 34 56 78',
+ 'city': 'Sin City',
+ 'zip': '1000',
+ 'country_id': country_belgium_id,
+ }
+ )
diff --git a/addons/payment_acquirer/views/payment_acquirer.xml b/addons/payment_acquirer/views/payment_acquirer.xml
new file mode 100644
index 00000000000..916f4915e66
--- /dev/null
+++ b/addons/payment_acquirer/views/payment_acquirer.xml
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+ acquirer.form
+ payment.acquirer
+
+
+
+
+
+
+ payment.acquirer
+
+
+
+
+
+
+
+
+
+
+ payment.acquirer
+
+
+
+
+
+
+
+
+ Payment Acquirers
+ payment.acquirer
+ form
+ tree,form
+
+
+
+
+
+ payment.transaction
+
+
+
+
+
+
+ payment.transaction
+
+
+
+
+
+
+
+
+
+
+
+
+ payment.transaction
+
+
+
+
+
+
+
+
+
+
+
+ Payment Transactions
+ payment.transaction
+ form
+ tree,form
+
+
+
+
+
+
diff --git a/addons/payment_acquirer_ogone/__init__.py b/addons/payment_acquirer_ogone/__init__.py
new file mode 100644
index 00000000000..dde6f25c728
--- /dev/null
+++ b/addons/payment_acquirer_ogone/__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_ogone/__openerp__.py b/addons/payment_acquirer_ogone/__openerp__.py
new file mode 100644
index 00000000000..2c7b5076981
--- /dev/null
+++ b/addons/payment_acquirer_ogone/__openerp__.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+
+{
+ 'name': 'Ogone Payment Acquirer',
+ 'category': 'Hidden',
+ 'summary': 'Payment Acquirer: Ogone Implementation',
+ 'version': '1.0',
+ 'description': """Ogone Payment Acquirer""",
+ 'author': 'OpenERP SA',
+ 'depends': ['payment_acquirer'],
+ 'data': [
+ 'views/ogone.xml',
+ 'views/payment_acquirer.xml',
+ 'data/ogone.xml',
+ ],
+ 'installable': True,
+}
diff --git a/addons/payment_acquirer_ogone/controllers/__init__.py b/addons/payment_acquirer_ogone/controllers/__init__.py
new file mode 100644
index 00000000000..bbd183e955b
--- /dev/null
+++ b/addons/payment_acquirer_ogone/controllers/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+import main
diff --git a/addons/payment_acquirer_ogone/controllers/main.py b/addons/payment_acquirer_ogone/controllers/main.py
new file mode 100644
index 00000000000..69307fcc181
--- /dev/null
+++ b/addons/payment_acquirer_ogone/controllers/main.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+import logging
+import pprint
+
+from openerp.addons.web import http
+from openerp.addons.web.http import request
+from openerp.addons.website.models import website
+
+_logger = logging.getLogger(__name__)
+
+
+class OgoneController(http.Controller):
+ _accept_url = '/payment/ogone/test/accept'
+ _decline_url = '/payment/ogone/test/decline'
+ _exception_url = '/payment/ogone/test/exception'
+ _cancel_url = '/payment/ogone/test/cancel'
+
+ @website.route([
+ '/payment/ogone/accept', '/payment/ogone/test/accept',
+ '/payment/ogone/decline', '/payment/ogone/test/decline',
+ '/payment/ogone/exception', '/payment/ogone/test/exception',
+ '/payment/ogone/cancel', '/payment/ogone/test/cancel',
+ ], type='http', auth='admin')
+ def ogone_form_feedback(self, **post):
+ _logger.info('Ogone: entering form_feedback with post data %s', pprint.pformat(post)) # debug
+ cr, uid, context = request.cr, request.uid, request.context
+ request.registry['payment.transaction'].ogone_form_feedback(cr, uid, post, context)
+ return request.redirect(post.pop('return_url', '/'))
diff --git a/addons/payment_acquirer_ogone/data/__init__.py b/addons/payment_acquirer_ogone/data/__init__.py
new file mode 100644
index 00000000000..349d8f668e8
--- /dev/null
+++ b/addons/payment_acquirer_ogone/data/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+import ogone
diff --git a/addons/payment_acquirer_ogone/data/ogone.py b/addons/payment_acquirer_ogone/data/ogone.py
new file mode 100644
index 00000000000..3a2500ee24b
--- /dev/null
+++ b/addons/payment_acquirer_ogone/data/ogone.py
@@ -0,0 +1,497 @@
+# -*- coding: utf-8 -*-
+
+OGONE_ERROR_MAP = {
+ '0020001001': "Authorization failed, please retry",
+ '0020001002': "Authorization failed, please retry",
+ '0020001003': "Authorization failed, please retry",
+ '0020001004': "Authorization failed, please retry",
+ '0020001005': "Authorization failed, please retry",
+ '0020001006': "Authorization failed, please retry",
+ '0020001007': "Authorization failed, please retry",
+ '0020001008': "Authorization failed, please retry",
+ '0020001009': "Authorization failed, please retry",
+ '0020001010': "Authorization failed, please retry",
+ '0030001999': "Our payment system is currently under maintenance, please try later",
+ '0050001005': "Expiry date error",
+ '0050001007': "Requested Operation code not allowed",
+ '0050001008': "Invalid delay value",
+ '0050001010': "Input date in invalid format",
+ '0050001013': "Unable to parse socket input stream",
+ '0050001014': "Error in parsing stream content",
+ '0050001015': "Currency error",
+ '0050001016': "Transaction still posted at end of wait",
+ '0050001017': "Sync value not compatible with delay value",
+ '0050001019': "Transaction duplicate of a pre-existing transaction",
+ '0050001020': "Acceptation code empty while required for the transaction",
+ '0050001024': "Maintenance acquirer differs from original transaction acquirer",
+ '0050001025': "Maintenance merchant differs from original transaction merchant",
+ '0050001028': "Maintenance operation not accurate for the original transaction",
+ '0050001031': "Host application unknown for the transaction",
+ '0050001032': "Unable to perform requested operation with requested currency",
+ '0050001033': "Maintenance card number differs from original transaction card number",
+ '0050001034': "Operation code not allowed",
+ '0050001035': "Exception occurred in socket input stream treatment",
+ '0050001036': "Card length does not correspond to an acceptable value for the brand",
+ '0050001036': "Card length does not correspond to an acceptable value for the brand",
+ '0050001068': "A technical problem occurred, please contact helpdesk",
+ '0050001069': "Invalid check for CardID and Brand",
+ '0050001070': "A technical problem occurred, please contact helpdesk",
+ '0050001116': "Unknown origin IP",
+ '0050001117': "No origin IP detected",
+ '0050001118': "Merchant configuration problem, please contact support",
+ '10001001': "Communication failure",
+ '10001002': "Communication failure",
+ '10001003': "Communication failure",
+ '10001004': "Communication failure",
+ '10001005': "Communication failure",
+ '20001001': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
+ '20001002': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
+ '20001003': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
+ '20001004': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
+ '20001005': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
+ '20001006': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
+ '20001007': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
+ '20001008': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
+ '20001009': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
+ '20001010': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
+ '20001101': "A technical problem occurred, please contact helpdesk",
+ '20001105': "We received an unknown status for the transaction. We will contact your acquirer and update the status of the transaction within one working day. Please check the status later.",
+ '20001111': "A technical problem occurred, please contact helpdesk",
+ '20002001': "Origin for the response of the bank can not be checked",
+ '20002002': "Beneficiary account number has been modified during processing",
+ '20002003': "Amount has been modified during processing",
+ '20002004': "Currency has been modified during processing",
+ '20002005': "No feedback from the bank server has been detected",
+ '30001001': "Payment refused by the acquirer",
+ '30001002': "Duplicate request",
+ '30001010': "A technical problem occurred, please contact helpdesk",
+ '30001011': "A technical problem occurred, please contact helpdesk",
+ '30001012': "Card black listed - Contact acquirer",
+ '30001015': "Your merchant's acquirer is temporarily unavailable, please try later or choose another payment method.",
+ '30001051': "A technical problem occurred, please contact helpdesk",
+ '30001054': "A technical problem occurred, please contact helpdesk",
+ '30001057': "Your merchant's acquirer is temporarily unavailable, please try later or choose another payment method.",
+ '30001058': "Your merchant's acquirer is temporarily unavailable, please try later or choose another payment method.",
+ '30001060': "Aquirer indicates that a failure occured during payment processing",
+ '30001070': "RATEPAY Invalid Response Type (Failure)",
+ '30001071': "RATEPAY Missing Mandatory status code field (failure)",
+ '30001072': "RATEPAY Missing Mandatory Result code field (failure)",
+ '30001073': "RATEPAY Response parsing Failed",
+ '30001090': "CVC check required by front end and returned invalid by acquirer",
+ '30001091': "ZIP check required by front end and returned invalid by acquirer",
+ '30001092': "Address check required by front end and returned as invalid by acquirer.",
+ '30001100': "Unauthorized buyer's country",
+ '30001101': "IP country <> card country",
+ '30001102': "Number of different countries too high",
+ '30001103': "unauthorized card country",
+ '30001104': "unauthorized ip address country",
+ '30001105': "Anonymous proxy",
+ '30001110': "If the problem persists, please contact Support, or go to paysafecard's card balance page (https://customer.cc.at.paysafecard.com/psccustomer/GetWelcomePanelServlet?language=en) to see when the amount reserved on your card will be available again.",
+ '30001120': "IP address in merchant's black list",
+ '30001130': "BIN in merchant's black list",
+ '30001131': "Wrong BIN for 3xCB",
+ '30001140': "Card in merchant's card blacklist",
+ '30001141': "Email in blacklist",
+ '30001142': "Passenger name in blacklist",
+ '30001143': "Card holder name in blacklist",
+ '30001144': "Passenger name different from owner name",
+ '30001145': "Time to departure too short",
+ '30001149': "Card Configured in Card Supplier Limit for another relation (CSL)",
+ '30001150': "Card not configured in the system for this customer (CSL)",
+ '30001151': "REF1 not allowed for this relationship (Contract number",
+ '30001152': "Card/Supplier Amount limit reached (CSL)",
+ '30001153': "Card not allowed for this supplier (Date out of contract bounds)",
+ '30001154': "You have reached the usage limit allowed",
+ '30001155': "You have reached the usage limit allowed",
+ '30001156': "You have reached the usage limit allowed",
+ '30001157': "Unauthorized IP country for itinerary",
+ '30001158': "email usage limit reached",
+ '30001159': "Unauthorized card country/IP country combination",
+ '30001160': "Postcode in highrisk group",
+ '30001161': "generic blacklist match",
+ '30001162': "Billing Address is a PO Box",
+ '30001180': "maximum scoring reached",
+ '30001997': "Authorization canceled by simulation",
+ '30001998': "A technical problem occurred, please try again.",
+ '30001999': "Your merchant's acquirer is temporarily unavailable, please try later or choose another payment method.",
+ '30002001': "Payment refused by the financial institution",
+ '30002001': "Payment refused by the financial institution",
+ '30021001': "Call acquirer support call number.",
+ '30022001': "Payment must be approved by the acquirer before execution.",
+ '30031001': "Invalid merchant number.",
+ '30041001': "Retain card.",
+ '30051001': "Authorization declined",
+ '30071001': "Retain card - special conditions.",
+ '30121001': "Invalid transaction",
+ '30131001': "Invalid amount",
+ '30131002': "You have reached the total amount allowed",
+ '30141001': "Invalid card number",
+ '30151001': "Unknown acquiring institution.",
+ '30171001': "Payment method cancelled by the buyer",
+ '30171002': "The maximum time allowed is elapsed.",
+ '30191001': "Try again later.",
+ '30201001': "A technical problem occurred, please contact helpdesk",
+ '30301001': "Invalid format",
+ '30311001': "Unknown acquirer ID.",
+ '30331001': "Card expired.",
+ '30341001': "Suspicion of fraud.",
+ '30341002': "Suspicion of fraud (3rdMan)",
+ '30341003': "Suspicion of fraud (Perseuss)",
+ '30341004': "Suspicion of fraud (ETHOCA)",
+ '30381001': "A technical problem occurred, please contact helpdesk",
+ '30401001': "Invalid function.",
+ '30411001': "Lost card.",
+ '30431001': "Stolen card, pick up",
+ '30511001': "Insufficient funds.",
+ '30521001': "No Authorization. Contact the issuer of your card.",
+ '30541001': "Card expired.",
+ '30551001': "Invalid PIN.",
+ '30561001': "Card not in authorizer's database.",
+ '30571001': "Transaction not permitted on card.",
+ '30581001': "Transaction not allowed on this terminal",
+ '30591001': "Suspicion of fraud.",
+ '30601001': "The merchant must contact the acquirer.",
+ '30611001': "Amount exceeds card ceiling.",
+ '30621001': "Restricted card.",
+ '30631001': "Security policy not respected.",
+ '30641001': "Amount changed from ref. trn.",
+ '30681001': "Tardy response.",
+ '30751001': "PIN entered incorrectly too often",
+ '30761001': "Card holder already contesting.",
+ '30771001': "PIN entry required.",
+ '30811001': "Message flow error.",
+ '30821001': "Authorization center unavailable",
+ '30831001': "Authorization center unavailable",
+ '30901001': "Temporary system shutdown.",
+ '30911001': "Acquirer unavailable.",
+ '30921001': "Invalid card type for acquirer.",
+ '30941001': "Duplicate transaction",
+ '30961001': "Processing temporarily not possible",
+ '30971001': "A technical problem occurred, please contact helpdesk",
+ '30981001': "A technical problem occurred, please contact helpdesk",
+ '31011001': "Unknown acceptance code",
+ '31021001': "Invalid currency",
+ '31031001': "Acceptance code missing",
+ '31041001': "Inactive card",
+ '31051001': "Merchant not active",
+ '31061001': "Invalid expiration date",
+ '31071001': "Interrupted host communication",
+ '31081001': "Card refused",
+ '31091001': "Invalid password",
+ '31101001': "Plafond transaction (majoré du bonus) dépassé",
+ '31111001': "Plafond mensuel (majoré du bonus) dépassé",
+ '31121001': "Plafond centre de facturation dépassé",
+ '31131001': "Plafond entreprise dépassé",
+ '31141001': "Code MCC du fournisseur non autorisé pour la carte",
+ '31151001': "Numéro SIRET du fournisseur non autorisé pour la carte",
+ '31161001': "This is not a valid online banking account",
+ '32001004': "A technical problem occurred, please try again.",
+ '34011001': "Bezahlung mit RatePAY nicht möglich.",
+ '39991001': "A technical problem occurred, please contact the helpdesk of your acquirer",
+ '40001001': "A technical problem occurred, please try again.",
+ '40001002': "A technical problem occurred, please try again.",
+ '40001003': "A technical problem occurred, please try again.",
+ '40001004': "A technical problem occurred, please try again.",
+ '40001005': "A technical problem occurred, please try again.",
+ '40001006': "A technical problem occurred, please try again.",
+ '40001007': "A technical problem occurred, please try again.",
+ '40001008': "A technical problem occurred, please try again.",
+ '40001009': "A technical problem occurred, please try again.",
+ '40001010': "A technical problem occurred, please try again.",
+ '40001011': "A technical problem occurred, please contact helpdesk",
+ '40001012': "Your merchant's acquirer is temporarily unavailable, please try later or choose another payment method.",
+ '40001013': "A technical problem occurred, please contact helpdesk",
+ '40001016': "A technical problem occurred, please contact helpdesk",
+ '40001018': "A technical problem occurred, please try again.",
+ '40001019': "Sorry, an error occurred during processing. Please retry the operation (use back button of the browser). If problem persists, contact your merchant's helpdesk.",
+ '40001020': "Sorry, an error occurred during processing. Please retry the operation (use back button of the browser). If problem persists, contact your merchant's helpdesk.",
+ '40001050': "A technical problem occurred, please contact helpdesk",
+ '40001133': "Authentication failed, the signature of your bank access control server is incorrect",
+ '40001134': "Authentication failed, please retry or cancel.",
+ '40001135': "Authentication temporary unavailable, please retry or cancel.",
+ '40001136': "Technical problem with your browser, please retry or cancel",
+ '40001137': "Your bank access control server is temporary unavailable, please retry or cancel",
+ '40001998': "Temporary technical problem. Please retry a little bit later.",
+ '50001001': "Unknown card type",
+ '50001002': "Card number format check failed for given card number.",
+ '50001003': "Merchant data error",
+ '50001004': "Merchant identification missing",
+ '50001005': "Expiry date error",
+ '50001006': "Amount is not a number",
+ '50001007': "A technical problem occurred, please contact helpdesk",
+ '50001008': "A technical problem occurred, please contact helpdesk",
+ '50001009': "A technical problem occurred, please contact helpdesk",
+ '50001010': "A technical problem occurred, please contact helpdesk",
+ '50001011': "Brand not supported for that merchant",
+ '50001012': "A technical problem occurred, please contact helpdesk",
+ '50001013': "A technical problem occurred, please contact helpdesk",
+ '50001014': "A technical problem occurred, please contact helpdesk",
+ '50001015': "Invalid currency code",
+ '50001016': "A technical problem occurred, please contact helpdesk",
+ '50001017': "A technical problem occurred, please contact helpdesk",
+ '50001018': "A technical problem occurred, please contact helpdesk",
+ '50001019': "A technical problem occurred, please contact helpdesk",
+ '50001020': "A technical problem occurred, please contact helpdesk",
+ '50001021': "A technical problem occurred, please contact helpdesk",
+ '50001022': "A technical problem occurred, please contact helpdesk",
+ '50001023': "A technical problem occurred, please contact helpdesk",
+ '50001024': "A technical problem occurred, please contact helpdesk",
+ '50001025': "A technical problem occurred, please contact helpdesk",
+ '50001026': "A technical problem occurred, please contact helpdesk",
+ '50001027': "A technical problem occurred, please contact helpdesk",
+ '50001028': "A technical problem occurred, please contact helpdesk",
+ '50001029': "A technical problem occurred, please contact helpdesk",
+ '50001030': "A technical problem occurred, please contact helpdesk",
+ '50001031': "A technical problem occurred, please contact helpdesk",
+ '50001032': "A technical problem occurred, please contact helpdesk",
+ '50001033': "A technical problem occurred, please contact helpdesk",
+ '50001034': "A technical problem occurred, please contact helpdesk",
+ '50001035': "A technical problem occurred, please contact helpdesk",
+ '50001036': "Card length does not correspond to an acceptable value for the brand",
+ '50001037': "Purchasing card number for a regular merchant",
+ '50001038': "Non Purchasing card for a Purchasing card merchant",
+ '50001039': "Details sent for a non-Purchasing card merchant, please contact helpdesk",
+ '50001040': "Details not sent for a Purchasing card transaction, please contact helpdesk",
+ '50001041': "Payment detail validation failed",
+ '50001042': "Given transactions amounts (tax,discount,shipping,net,etc…) do not compute correctly together",
+ '50001043': "A technical problem occurred, please contact helpdesk",
+ '50001044': "No acquirer configured for this operation",
+ '50001045': "No UID configured for this operation",
+ '50001046': "Operation not allowed for the merchant",
+ '50001047': "A technical problem occurred, please contact helpdesk",
+ '50001048': "A technical problem occurred, please contact helpdesk",
+ '50001049': "A technical problem occurred, please contact helpdesk",
+ '50001050': "A technical problem occurred, please contact helpdesk",
+ '50001051': "A technical problem occurred, please contact helpdesk",
+ '50001052': "A technical problem occurred, please contact helpdesk",
+ '50001053': "A technical problem occurred, please contact helpdesk",
+ '50001054': "Card number incorrect or incompatible",
+ '50001055': "A technical problem occurred, please contact helpdesk",
+ '50001056': "A technical problem occurred, please contact helpdesk",
+ '50001057': "A technical problem occurred, please contact helpdesk",
+ '50001058': "A technical problem occurred, please contact helpdesk",
+ '50001059': "A technical problem occurred, please contact helpdesk",
+ '50001060': "A technical problem occurred, please contact helpdesk",
+ '50001061': "A technical problem occurred, please contact helpdesk",
+ '50001062': "A technical problem occurred, please contact helpdesk",
+ '50001063': "Card Issue Number does not correspond to range or not present",
+ '50001064': "Start Date not valid or not present",
+ '50001066': "Format of CVC code invalid",
+ '50001067': "The merchant is not enrolled for 3D-Secure",
+ '50001068': "The card number or account number (PAN) is invalid",
+ '50001069': "Invalid check for CardID and Brand",
+ '50001070': "The ECI value given is either not supported, or in conflict with other data in the transaction",
+ '50001071': "Incomplete TRN demat",
+ '50001072': "Incomplete PAY demat",
+ '50001073': "No demat APP",
+ '50001074': "Authorisation too old",
+ '50001075': "VERRes was an error message",
+ '50001076': "DCP amount greater than authorisation amount",
+ '50001077': "Details negative amount",
+ '50001078': "Details negative quantity",
+ '50001079': "Could not decode/decompress received PARes (3D-Secure)",
+ '50001080': "Received PARes was an erereor message from ACS (3D-Secure)",
+ '50001081': "Received PARes format was invalid according to the 3DS specifications (3D-Secure)",
+ '50001082': "PAReq/PARes reconciliation failure (3D-Secure)",
+ '50001084': "Maximum amount reached",
+ '50001087': "The transaction type requires authentication, please check with your bank.",
+ '50001090': "CVC missing at input, but CVC check asked",
+ '50001091': "ZIP missing at input, but ZIP check asked",
+ '50001092': "Address missing at input, but Address check asked",
+ '50001095': "Invalid date of birth",
+ '50001096': "Invalid commodity code",
+ '50001097': "The requested currency and brand are incompatible.",
+ '50001111': "Data validation error",
+ '50001113': "This order has already been processed",
+ '50001114': "Error pre-payment check page access",
+ '50001115': "Request not received in secure mode",
+ '50001116': "Unknown IP address origin",
+ '50001117': "NO IP address origin",
+ '50001118': "Pspid not found or not correct",
+ '50001119': "Password incorrect or disabled due to numbers of errors",
+ '50001120': "Invalid currency",
+ '50001121': "Invalid number of decimals for the currency",
+ '50001122': "Currency not accepted by the merchant",
+ '50001123': "Card type not active",
+ '50001124': "Number of lines don't match with number of payments",
+ '50001125': "Format validation error",
+ '50001126': "Overflow in data capture requests for the original order",
+ '50001127': "The original order is not in a correct status",
+ '50001128': "missing authorization code for unauthorized order",
+ '50001129': "Overflow in refunds requests",
+ '50001130': "Error access to original order",
+ '50001131': "Error access to original history item",
+ '50001132': "The Selected Catalog is empty",
+ '50001133': "Duplicate request",
+ '50001134': "Authentication failed, please retry or cancel.",
+ '50001135': "Authentication temporary unavailable, please retry or cancel.",
+ '50001136': "Technical problem with your browser, please retry or cancel",
+ '50001137': "Your bank access control server is temporary unavailable, please retry or cancel",
+ '50001150': "Fraud Detection, Technical error (IP not valid)",
+ '50001151': "Fraud detection : technical error (IPCTY unknown or error)",
+ '50001152': "Fraud detection : technical error (CCCTY unknown or error)",
+ '50001153': "Overflow in redo-authorisation requests",
+ '50001170': "Dynamic BIN check failed",
+ '50001171': "Dynamic country check failed",
+ '50001172': "Error in Amadeus signature",
+ '50001174': "Card Holder Name is too long",
+ '50001175': "Name contains invalid characters",
+ '50001176': "Card number is too long",
+ '50001177': "Card number contains non-numeric info",
+ '50001178': "Card Number Empty",
+ '50001179': "CVC too long",
+ '50001180': "CVC contains non-numeric info",
+ '50001181': "Expiration date contains non-numeric info",
+ '50001182': "Invalid expiration month",
+ '50001183': "Expiration date must be in the future",
+ '50001184': "SHA Mismatch",
+ '50001205': "Missing mandatory fields for billing address.",
+ '50001206': "Missing mandatory field date of birth.",
+ '50001207': "Missing required shopping basket details.",
+ '50001208': "Missing social security number",
+ '50001209': "Invalid country code",
+ '50001210': "Missing yearly salary",
+ '50001211': "Missing gender",
+ '50001212': "Missing email",
+ '50001213': "Missing IP address",
+ '50001214': "Missing part payment campaign ID",
+ '50001215': "Missing invoice number",
+ '50001216': "The alias must be different than the card number",
+ '60000001': "account number unknown",
+ '60000003': "not credited dd-mm-yy",
+ '60000005': "name/number do not correspond",
+ '60000007': "account number blocked",
+ '60000008': "specific direct debit block",
+ '60000009': "account number WKA",
+ '60000010': "administrative reason",
+ '60000011': "account number expired",
+ '60000012': "no direct debit authorisation given",
+ '60000013': "debit not approved",
+ '60000014': "double payment",
+ '60000018': "name/address/city not entered",
+ '60001001': "no original direct debit for revocation",
+ '60001002': "payer’s account number format error",
+ '60001004': "payer’s account at different bank",
+ '60001005': "payee’s account at different bank",
+ '60001006': "payee’s account number format error",
+ '60001007': "payer’s account number blocked",
+ '60001008': "payer’s account number expired",
+ '60001009': "payee’s account number expired",
+ '60001010': "direct debit not possible",
+ '60001011': "creditor payment not possible",
+ '60001012': "payer’s account number unknown WKA-number",
+ '60001013': "payee’s account number unknown WKA-number",
+ '60001014': "impermissible WKA transaction",
+ '60001015': "period for revocation expired",
+ '60001017': "reason for revocation not correct",
+ '60001018': "original run number not numeric",
+ '60001019': "payment ID incorrect",
+ '60001020': "amount not numeric",
+ '60001021': "amount zero not permitted",
+ '60001022': "negative amount not permitted",
+ '60001023': "payer and payee giro account number",
+ '60001025': "processing code (verwerkingscode) incorrect",
+ '60001028': "revocation not permitted",
+ '60001029': "guaranteed direct debit on giro account number",
+ '60001030': "NBC transaction type incorrect",
+ '60001031': "description too large",
+ '60001032': "book account number not issued",
+ '60001034': "book account number incorrect",
+ '60001035': "payer’s account number not numeric",
+ '60001036': "payer’s account number not eleven-proof",
+ '60001037': "payer’s account number not issued",
+ '60001039': "payer’s account number of DNB/BGC/BLA",
+ '60001040': "payee’s account number not numeric",
+ '60001041': "payee’s account number not eleven-proof",
+ '60001042': "payee’s account number not issued",
+ '60001044': "payee’s account number unknown",
+ '60001050': "payee’s name missing",
+ '60001051': "indicate payee’s bank account number instead of 3102",
+ '60001052': "no direct debit contract",
+ '60001053': "amount beyond bounds",
+ '60001054': "selective direct debit block",
+ '60001055': "original run number unknown",
+ '60001057': "payer’s name missing",
+ '60001058': "payee’s account number missing",
+ '60001059': "restore not permitted",
+ '60001060': "bank’s reference (navraaggegeven) missing",
+ '60001061': "BEC/GBK number incorrect",
+ '60001062': "BEC/GBK code incorrect",
+ '60001087': "book account number not numeric",
+ '60001090': "cancelled on request",
+ '60001091': "cancellation order executed",
+ '60001092': "cancelled instead of bended",
+ '60001093': "book account number is a shortened account number",
+ '60001094': "instructing party account number not identical with payer",
+ '60001095': "payee unknown GBK acceptor",
+ '60001097': "instructing party account number not identical with payee",
+ '60001099': "clearing not permitted",
+ '60001101': "payer’s account number not spaces",
+ '60001102': "PAN length not numeric",
+ '60001103': "PAN length outside limits",
+ '60001104': "track number not numeric",
+ '60001105': "track number not valid",
+ '60001106': "PAN sequence number not numeric",
+ '60001107': "domestic PAN not numeric",
+ '60001108': "domestic PAN not eleven-proof",
+ '60001109': "domestic PAN not issued",
+ '60001110': "foreign PAN not numeric",
+ '60001111': "card valid date not numeric",
+ '60001112': "book period number (boekperiodenr) not numeric",
+ '60001113': "transaction number not numeric",
+ '60001114': "transaction time not numeric",
+ '60001115': "transaction no valid time",
+ '60001116': "transaction date not numeric",
+ '60001117': "transaction no valid date",
+ '60001118': "STAN not numeric",
+ '60001119': "instructing party’s name missing",
+ '60001120': "foreign amount (bedrag-vv) not numeric",
+ '60001122': "rate (verrekenkoers) not numeric",
+ '60001125': "number of decimals (aantaldecimalen) incorrect",
+ '60001126': "tariff (tarifering) not B/O/S",
+ '60001127': "domestic costs (kostenbinnenland) not numeric",
+ '60001128': "domestic costs (kostenbinnenland) not higher than zero",
+ '60001129': "foreign costs (kostenbuitenland) not numeric",
+ '60001130': "foreign costs (kostenbuitenland) not higher than zero",
+ '60001131': "domestic costs (kostenbinnenland) not zero",
+ '60001132': "foreign costs (kostenbuitenland) not zero",
+ '60001134': "Euro record not fully filled in",
+ '60001135': "Client currency incorrect",
+ '60001136': "Amount NLG not numeric",
+ '60001137': "Amount NLG not higher than zero",
+ '60001138': "Amount NLG not equal to Amount",
+ '60001139': "Amount NLG incorrectly converted",
+ '60001140': "Amount EUR not numeric",
+ '60001141': "Amount EUR not greater than zero",
+ '60001142': "Amount EUR not equal to Amount",
+ '60001143': "Amount EUR incorrectly converted",
+ '60001144': "Client currency not NLG",
+ '60001145': "rate euro-vv (Koerseuro-vv) not numeric",
+ '60001146': "comma rate euro-vv (Kommakoerseuro-vv) incorrect",
+ '60001147': "acceptgiro distributor not valid",
+ '60001148': "Original run number and/or BRN are missing",
+ '60001149': "Amount/Account number/ BRN different",
+ '60001150': "Direct debit already revoked/restored",
+ '60001151': "Direct debit already reversed/revoked/restored",
+ '60001153': "Payer’s account number not known",
+}
+
+DATA_VALIDATION_ERROR = '50001111'
+
+
+def retryable(error):
+ return error in [
+ '0020001001', '0020001002', '0020001003', '0020001004', '0020001005',
+ '0020001006', '0020001007', '0020001008', '0020001009', '0020001010',
+ '30001010', '30001011', '30001015',
+ '30001057', '30001058',
+ '30001998', '30001999',
+ #'30611001', # amount exceeds card limit
+ '30961001',
+ '40001001', '40001002', '40001003', '40001004', '40001005',
+ '40001006', '40001007', '40001008', '40001009', '40001010',
+ '40001012',
+ '40001018', '40001019', '40001020',
+ '40001134', '40001135', '40001136', '40001137',
+ #'50001174', # cardholder name too long
+ ]
diff --git a/addons/payment_acquirer_ogone/data/ogone.xml b/addons/payment_acquirer_ogone/data/ogone.xml
new file mode 100644
index 00000000000..51f67114b1f
--- /dev/null
+++ b/addons/payment_acquirer_ogone/data/ogone.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+ ogone
+
+ test
+ dummy
+ dummy
+ dummy
+ tINY4Yv14789gUix1130
+ tINYj885Tfvd4P471464
+
+
+
+
diff --git a/addons/payment_acquirer_ogone/doc/changelog.rst b/addons/payment_acquirer_ogone/doc/changelog.rst
new file mode 100644
index 00000000000..d0c24af384d
--- /dev/null
+++ b/addons/payment_acquirer_ogone/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_ogone/doc/index.rst b/addons/payment_acquirer_ogone/doc/index.rst
new file mode 100644
index 00000000000..85028ed0d3e
--- /dev/null
+++ b/addons/payment_acquirer_ogone/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_ogone/models/__init__.py b/addons/payment_acquirer_ogone/models/__init__.py
new file mode 100644
index 00000000000..349d8f668e8
--- /dev/null
+++ b/addons/payment_acquirer_ogone/models/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+import ogone
diff --git a/addons/payment_acquirer_ogone/models/ogone.py b/addons/payment_acquirer_ogone/models/ogone.py
new file mode 100644
index 00000000000..b06505c1cdd
--- /dev/null
+++ b/addons/payment_acquirer_ogone/models/ogone.py
@@ -0,0 +1,408 @@
+# -*- coding: utf-'8' "-*-"
+
+from hashlib import sha1
+import logging
+from lxml import etree, objectify
+from pprint import pformat
+import time
+from urllib import urlencode
+import urllib2
+import urlparse
+
+from openerp.addons.payment_acquirer.models.payment_acquirer import ValidationError
+from openerp.addons.payment_acquirer_ogone.controllers.main import OgoneController
+from openerp.addons.payment_acquirer_ogone.data import ogone
+from openerp.osv import osv, fields
+from openerp.tools import float_round
+
+_logger = logging.getLogger(__name__)
+
+
+class PaymentAcquirerOgone(osv.Model):
+ _inherit = 'payment.acquirer'
+
+ def _get_ogone_urls(self, cr, uid, ids, name, args, context=None):
+ """ Ogone URLS:
+
+ - standard order: POST address for form-based
+
+ @TDETODO: complete me
+ """
+ res = {}
+ for acquirer in self.browse(cr, uid, ids, context=context):
+ qualif = acquirer.env
+ res[acquirer.id] = {
+ 'ogone_standard_order_url': 'https://secure.ogone.com/ncol/%s/orderstandard.asp' % qualif,
+ 'ogone_direct_order_url': 'https://secure.ogone.com/ncol/%s/orderdirect.asp' % qualif,
+ 'ogone_direct_query_url': 'https://secure.ogone.com/ncol/%s/querydirect.asp' % qualif,
+ 'ogone_afu_agree_url': 'https://secure.ogone.com/ncol/%s/AFU_agree.asp' % qualif,
+ }
+ return res
+
+ _columns = {
+ 'ogone_pspid': fields.char(
+ 'PSPID', required_if_provider='ogone'),
+ 'ogone_userid': fields.char(
+ 'API User id', required_if_provider='ogone'),
+ 'ogone_password': fields.char(
+ 'Password', required_if_provider='ogone'),
+ 'ogone_shakey_in': fields.char(
+ 'SHA Key IN', size=32, required_if_provider='ogone'),
+ 'ogone_shakey_out': fields.char(
+ 'SHA Key OUT', size=32, required_if_provider='ogone'),
+ # store ogone contact URLs -> not necessary IMHO
+ 'ogone_standard_order_url': fields.function(
+ _get_ogone_urls, type='char', multi='_get_ogone_urls',
+ string='Stanrd Order URL (form)'),
+ 'ogone_direct_order_url': fields.function(
+ _get_ogone_urls, type='char', multi='_get_ogone_urls',
+ string='Direct Order URL (2)'),
+ 'ogone_direct_query_url': fields.function(
+ _get_ogone_urls, type='char', multi='_get_ogone_urls',
+ string='Direct Query URL'),
+ 'ogone_afu_agree_url': fields.function(
+ _get_ogone_urls, type='char', multi='_get_ogone_urls',
+ string='AFU Agree URL'),
+ }
+
+ def _ogone_generate_shasign(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' (ogone
+ 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 == 'ogone'
+ key = getattr(acquirer, 'ogone_shakey_' + inout)
+
+ def filter_key(key):
+ if inout == 'in':
+ return True
+ else:
+ keys = "ORDERID CURRENCY AMOUNT PM ACCEPTANCE STATUS CARDNO ALIAS ED CN TRXDATE PAYID NCERROR BRAND ECI IP COMPLUS".split()
+ return key.upper() in keys
+
+ items = sorted((k.upper(), v) for k, v in values.items())
+ sign = ''.join('%s=%s%s' % (k, v, key) for k, v in items if v and filter_key(k))
+ shasign = sha1(sign).hexdigest()
+ return shasign
+
+ def ogone_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)
+ partner = None
+ if partner_id:
+ partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
+ tx_values = {
+ 'PSPID': acquirer.ogone_pspid,
+ 'ORDERID': reference,
+ 'AMOUNT': '%d' % int(float_round(amount, 2) * 100),
+ 'CURRENCY': currency and currency.name or 'EUR',
+ 'LANGUAGE': partner and partner.lang or partner_values.get('lang', ''),
+ 'CN': partner and partner.name or partner_values.get('name', ''),
+ 'EMAIL': partner and partner.email or partner_values.get('email', ''),
+ 'OWNERZIP': partner and partner.zip or partner_values.get('zip', ''),
+ 'OWNERADDRESS': partner and ' '.join((partner.street or '', partner.street2 or '')).strip() or ' '.join((partner_values.get('street', ''), partner_values.get('street2', ''))).strip(),
+ 'OWNERTOWN': partner and partner.city or partner_values.get('city', ''),
+ 'OWNERCTY': partner and partner.country_id and partner.country_id.name or partner_values.get('country_name', ''),
+ 'OWNERTELNO': partner and partner.phone or partner_values.get('phone', ''),
+ 'ACCEPTURL': '%s' % urlparse.urljoin(base_url, OgoneController._accept_url),
+ 'DECLINEURL': '%s' % urlparse.urljoin(base_url, OgoneController._decline_url),
+ 'EXCEPTIONURL': '%s' % urlparse.urljoin(base_url, OgoneController._exception_url),
+ 'CANCELURL': '%s' % urlparse.urljoin(base_url, OgoneController._cancel_url),
+ }
+ if tx_custom_values and tx_custom_values.get('return_url'):
+ tx_values['PARAMPLUS'] = 'return_url=%s' % tx_custom_values.pop('return_url')
+ if tx_custom_values:
+ tx_values.update(tx_custom_values)
+ shasign = self._ogone_generate_shasign(acquirer, 'in', tx_values)
+ tx_values['SHASIGN'] = shasign
+ return tx_values
+
+ def ogone_get_form_action_url(self, cr, uid, id, context=None):
+ acquirer = self.browse(cr, uid, id, context=context)
+ return acquirer.ogone_standard_order_url
+
+
+class PaymentTxOgone(osv.Model):
+ _inherit = 'payment.transaction'
+ # ogone status
+ _ogone_valid_tx_status = [5, 9]
+ _ogone_wait_tx_status = [41, 50, 51, 52, 55, 56, 91, 92, 99]
+ _ogone_pending_tx_status = [46] # 3DS HTML response
+ _ogone_cancel_tx_status = [1]
+
+ _columns = {
+ 'ogone_3ds': fields.dummy('3ds Activated'),
+ 'ogone_3ds_html': fields.html('3DS HTML'),
+ 'ogone_feedback_model': fields.char(),
+ 'ogone_feedback_eval': fields.char(),
+ 'ogone_complus': fields.char('Complus'),
+ 'ogone_payid': fields.char('PayID', help='Payment ID, generated by Ogone')
+ }
+
+ # --------------------------------------------------
+ # FORM RELATED METHODS
+ # --------------------------------------------------
+
+ def ogone_form_generate_values(self, cr, uid, id, tx_custom_values=None, context=None):
+ """ Ogone-specific value generation for rendering a transaction-based
+ form button. """
+ tx = self.browse(cr, uid, id, context=context)
+
+ tx_data = {
+ 'LANGUAGE': tx.partner_lang,
+ 'CN': tx.partner_name,
+ 'EMAIL': tx.partner_email,
+ 'OWNERZIP': tx.partner_zip,
+ 'OWNERADDRESS': tx.partner_address,
+ 'OWNERTOWN': tx.partner_city,
+ 'OWNERCTY': tx.partner_country_id and tx.partner_country_id.name or '',
+ 'OWNERTELNO': tx.partner_phone,
+ }
+ if tx_custom_values:
+ tx_data.update(tx_custom_values)
+ return self.pool['payment.acquirer'].ogone_form_generate_values(
+ cr, uid, tx.acquirer_id.id,
+ tx.reference, tx.amount, tx.currency_id,
+ tx_custom_values=tx_data,
+ context=context
+ )
+
+ def _ogone_form_get_tx_from_data(self, cr, uid, data, context=None):
+ """ Given a data dict coming from ogone, verify it and find the related
+ transaction record. """
+ reference, pay_id, shasign = data.get('orderID'), data.get('PAYID'), data.get('SHASIGN')
+ if not reference or not pay_id or not shasign:
+ error_msg = 'Ogone: received data with missing reference (%s) or pay_id (%s) or shashign (%s)' % (reference, pay_id, shasign)
+ _logger.error(error_msg)
+ raise ValidationError(error_msg)
+
+ # find tx -> @TDENOTE use paytid ?
+ tx_ids = self.search(cr, uid, [('reference', '=', reference)], context=context)
+ if not tx_ids or len(tx_ids) > 1:
+ error_msg = 'Ogone: 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']._ogone_generate_shasign(tx.acquirer_id, 'out', data)
+ if shasign_check.upper() != shasign.upper():
+ error_msg = 'Ogone: invalid shasign, received %s, computed %s, for data %s' % (shasign, shasign_check, data)
+ _logger.error(error_msg)
+ raise ValidationError(error_msg)
+
+ return tx
+
+ def ogone_form_feedback(self, cr, uid, data, context=None):
+ tx = self._ogone_form_get_tx_from_data(cr, uid, data, context)
+ if not tx:
+ raise ValidationError('Ogone: feedback: tx not found')
+ if tx.state == 'done':
+ _logger.warning('Ogone: trying to validate an already validated tx (ref %s' % tx.reference)
+ return False
+
+ status = int(data.get('STATUS', '0'))
+ if status in self._ogone_valid_tx_status:
+ tx.write({
+ 'state': 'done',
+ 'date_validate': data['TRXDATE'],
+ 'ogone_payid': data['PAYID'],
+ })
+ return True
+ elif status in self._ogone_cancel_tx_status:
+ tx.write({
+ 'state': 'cancel',
+ })
+ elif status in self._ogone_pending_tx_status:
+ tx.write({
+ 'state': 'pending',
+ })
+ else:
+ error = 'Ogone: feedback error: %(error_str)s\n\n%(error_code)s: %(error_msg)s' % {
+ 'error_str': data.get('NCERROR'),
+ 'error_code': data.get('NCERRORPLUS'),
+ 'error_msg': ogone.OGONE_ERROR_MAP.get(data.get('NCERRORPLUS')),
+ }
+ _logger.info(error)
+ tx.write({'state': 'error', 'state_message': error})
+ return False
+
+ # --------------------------------------------------
+ # S2S RELATED METHODS
+ # --------------------------------------------------
+
+ def ogone_s2s_create_alias(self, cr, uid, id, values, context=None):
+ """ Purpose: create an alias via batch """
+ tx = self.browse(cr, uid, id, context=context)
+ assert tx.type == 'server2server', 'Calling s2s dedicated method for a %s acquirer' % tx.type
+ alias = 'OPENERP-%d-%d' % (tx.partner_id.id, tx.id)
+
+ expiry_date = '%s%s' % (values['expiry_date_mm'], values['expiry_date_yy'][2:])
+ line = 'ADDALIAS;%(alias)s;%(holder_name)s;%(number)s;%(expiry_date)s;%(brand)s;%(pspid)s'
+ line = line % dict(values, alias=alias, expiry_date=expiry_date, pspid=tx.acquirer_id.ogone_pspid)
+
+ tx_data = {
+ 'FILE_REFERENCE': 'OPENERP-NEW-ALIAS-%s' % time.time(), # something unique,
+ 'TRANSACTION_CODE': 'ATR',
+ 'OPERATION': 'SAL',
+ 'NB_PAYMENTS': 1, # even if we do not actually have any payment, ogone want it to not be 0
+ 'FILE': line,
+ 'REPLY_TYPE': 'XML',
+ 'PSPID': tx.acquirer_id.ogone_pspid,
+ 'USERID': tx.acquirer_id.ogone_userid,
+ 'PSWD': tx.acquirer_id.ogone_password,
+ 'PROCESS_MODE': 'CHECKANDPROCESS',
+ }
+
+ request = urllib2.Request(tx.acquirer_id.ogone_afu_agree_url, urlencode(tx_data))
+ result = urllib2.urlopen(request).read()
+
+ try:
+ tree = objectify.fromstring(result)
+ except etree.XMLSyntaxError:
+ _logger.exception('Invalid xml response from ogone')
+ return None
+
+ error_code = error_str = None
+ if hasattr(tree, 'PARAMS_ERROR'):
+ error_code = tree.NCERROR.text
+ error_str = 'PARAMS ERROR: %s' % (tree.PARAMS_ERROR.text or '',)
+ else:
+ node = tree.FORMAT_CHECK
+ error_node = getattr(node, 'FORMAT_CHECK_ERROR', None)
+ if error_node is not None:
+ error_code = error_node.NCERROR.text
+ error_str = 'CHECK ERROR: %s' % (error_node.ERROR.text or '',)
+
+ if error_code:
+ error_msg = ogone.OGONE_ERROR_MAP.get(error_code)
+ error = '%s\n\n%s: %s' % (error_str, error_code, error_msg)
+ _logger.error(error)
+ raise Exception(error) # TODO specific exception
+
+ tx.write({'partner_reference': alias})
+ return True
+
+ def ogone_s2s_generate_values(self, cr, uid, id, custom_values, context=None):
+ tx = self.browse(cr, uid, id, context=context)
+ tx_data = {
+ 'PSPID': tx.acquirer_id.ogone_pspid,
+ 'USERID': tx.acquirer_id.ogone_userid,
+ 'PSWD': tx.acquirer_id.ogone_password,
+ 'OrderID': tx.reference,
+ 'amount': '%d' % int(float_round(tx.amount, 2) * 100), # tde check amount or str * 100 ?
+ 'CURRENCY': tx.currency_id.name,
+ 'LANGUAGE': tx.partner_lang,
+ 'OPERATION': 'SAL',
+ 'ECI': 2, # Recurring (from MOTO)
+ 'ALIAS': tx.partner_reference,
+ 'RTIMEOUT': 30,
+ }
+ if custom_values.get('ogone_cvc'):
+ tx_data['CVC'] = custom_values.get('ogone_cvc')
+ if custom_values.pop('ogone_3ds', None):
+ tx_data.update({
+ 'FLAG3D': 'Y', # YEAH!!
+ })
+ if custom_values.get('ogone_complus'):
+ tx_data['COMPLUS'] = custom_values.get('ogone_complus')
+ if custom_values.get('ogone_accept_url'):
+ pass
+
+ shasign = self.pool['payment.acquirer']._ogone_generate_shasign(tx.acquirer_id, 'in', tx_data)
+ tx_data['SHASIGN'] = shasign
+ return tx_data
+
+ def ogone_s2s_feedback(self, cr, uid, data, context=None):
+ pass
+
+ def ogone_s2s_execute(self, cr, uid, id, values, context=None):
+ tx = self.browse(cr, uid, id, context=context)
+
+ tx_data = self.ogone_s2s_generate_values(cr, uid, id, values, context=context)
+ _logger.info('Generated Ogone s2s data %s', pformat(tx_data)) # debug
+
+ request = urllib2.Request(tx.acquirer_id.ogone_direct_order_url, urlencode(tx_data))
+ result = urllib2.urlopen(request).read()
+ _logger.info('Contacted Ogone direct order; result %s', result) # debug
+
+ tree = objectify.fromstring(result)
+ payid = tree.get('PAYID')
+
+ query_direct_data = dict(
+ PSPID=tx.acquirer_id.ogone_pspid,
+ USERID=tx.acquirer_id.ogone_userid,
+ PSWD=tx.acquirer_id.ogone_password,
+ ID=payid,
+ )
+ query_direct_url = 'https://secure.ogone.com/ncol/%s/querydirect.asp' % (tx.acquirer_id.env,)
+
+ tries = 2
+ tx_done = False
+ tx_status = False
+ while not tx_done or tries > 0:
+ try:
+ tree = objectify.fromstring(result)
+ except etree.XMLSyntaxError:
+ # invalid response from ogone
+ _logger.exception('Invalid xml response from ogone')
+ raise
+
+ # see https://secure.ogone.com/ncol/paymentinfos1.asp
+ VALID_TX = [5, 9]
+ WAIT_TX = [41, 50, 51, 52, 55, 56, 91, 92, 99]
+ PENDING_TX = [46] # 3DS HTML response
+ # other status are errors...
+
+ status = tree.get('STATUS')
+ if status == '':
+ status = None
+ else:
+ status = int(status)
+
+ if status in VALID_TX:
+ tx_status = True
+ tx_done = True
+
+ elif status in PENDING_TX:
+ html = str(tree.HTML_ANSWER)
+ tx_data.update(ogone_3ds_html=html.decode('base64'))
+ tx_status = False
+ tx_done = True
+
+ elif status in WAIT_TX:
+ time.sleep(1500)
+
+ request = urllib2.Request(query_direct_url, urlencode(query_direct_data))
+ result = urllib2.urlopen(request).read()
+ _logger.debug('Contacted Ogone query direct; result %s', result)
+
+ else:
+ error_code = tree.get('NCERROR')
+ if not ogone.retryable(error_code):
+ error_str = tree.get('NCERRORPLUS')
+ error_msg = ogone.OGONE_ERROR_MAP.get(error_code)
+ error = 'ERROR: %s\n\n%s: %s' % (error_str, error_code, error_msg)
+ _logger.info(error)
+ raise Exception(error)
+
+ tries = tries - 1
+
+ if not tx_done and tries == 0:
+ raise Exception('Cannot get transaction status...')
+
+ return tx_status
diff --git a/addons/payment_acquirer_ogone/static/description/icon.png b/addons/payment_acquirer_ogone/static/description/icon.png
new file mode 100644
index 00000000000..4151a633c79
Binary files /dev/null and b/addons/payment_acquirer_ogone/static/description/icon.png differ
diff --git a/addons/payment_acquirer_ogone/static/src/img/ogone_logo.png b/addons/payment_acquirer_ogone/static/src/img/ogone_logo.png
new file mode 100644
index 00000000000..89e0ef6816f
Binary files /dev/null and b/addons/payment_acquirer_ogone/static/src/img/ogone_logo.png differ
diff --git a/addons/payment_acquirer_ogone/static/src/img/ogone_logo_plain.gif b/addons/payment_acquirer_ogone/static/src/img/ogone_logo_plain.gif
new file mode 100644
index 00000000000..e7833ec3b6d
Binary files /dev/null and b/addons/payment_acquirer_ogone/static/src/img/ogone_logo_plain.gif differ
diff --git a/addons/payment_acquirer_ogone/tests/__init__.py b/addons/payment_acquirer_ogone/tests/__init__.py
new file mode 100644
index 00000000000..6607b8cf6cd
--- /dev/null
+++ b/addons/payment_acquirer_ogone/tests/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+
+from . import test_ogone
+
+checks = [
+ test_ogone,
+]
diff --git a/addons/payment_acquirer_ogone/tests/test_ogone.py b/addons/payment_acquirer_ogone/tests/test_ogone.py
new file mode 100644
index 00000000000..245cef73007
--- /dev/null
+++ b/addons/payment_acquirer_ogone/tests/test_ogone.py
@@ -0,0 +1,233 @@
+# -*- 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_ogone.controllers.main import OgoneController
+from openerp.tools import mute_logger
+
+from lxml import objectify
+# import requests
+import time
+import urlparse
+
+
+class OgonePayment(PaymentAcquirerCommon):
+
+ def setUp(self):
+ super(OgonePayment, 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.ogone_view_id = self.registry('ir.model.data').get_object_reference(cr, uid, 'payment_acquirer_ogone', 'ogone_acquirer_button')
+
+ # create a new ogone account
+ self.ogone_id = self.payment_acquirer.create(
+ cr, uid, {
+ 'name': 'ogone',
+ 'env': 'test',
+ 'view_template_id': self.ogone_view_id,
+ 'ogone_pspid': 'dummy',
+ 'ogone_userid': 'dummy',
+ 'ogone_password': 'dummy',
+ 'ogone_shakey_in': 'tINY4Yv14789gUix1130',
+ 'ogone_shakey_out': 'tINYj885Tfvd4P471464',
+ })
+ self.ogone_url = self.payment_acquirer._get_ogone_urls(cr, uid, [self.ogone_id], None, None)[self.ogone_id]['ogone_standard_order_url']
+
+ def test_00_ogone_acquirer(self):
+ ogone = self.payment_acquirer.browse(self.cr, self.uid, self.ogone_id, None)
+ self.assertEqual(ogone.env, 'test', 'test without test env')
+
+ def test_10_ogone_form_render(self):
+ cr, uid, context = self.cr, self.uid, {}
+
+ # ----------------------------------------
+ # Test: button direct rendering + shasign
+ # ----------------------------------------
+
+ form_values = {
+ 'PSPID': 'dummy',
+ 'ORDERID': 'test_ref0',
+ 'AMOUNT': '1',
+ 'CURRENCY': 'EUR',
+ 'LANGUAGE': 'en_US',
+ 'CN': 'Norbert Buyer',
+ 'EMAIL': 'norbert.buyer@example.com',
+ 'OWNERZIP': '1000',
+ 'OWNERADDRESS': 'Huge Street 2/543',
+ 'OWNERCTY': 'Belgium',
+ 'OWNERTOWN': 'Sin City',
+ 'OWNERTELNO': '0032 12 34 56 78',
+ 'SHASIGN': '815f67b8ff70d234ffcf437c13a9fa7f807044cc',
+ 'ACCEPTURL': '%s' % urlparse.urljoin(self.base_url, OgoneController._accept_url),
+ 'DECLINEURL': '%s' % urlparse.urljoin(self.base_url, OgoneController._decline_url),
+ 'EXCEPTIONURL': '%s' % urlparse.urljoin(self.base_url, OgoneController._exception_url),
+ 'CANCELURL': '%s' % urlparse.urljoin(self.base_url, OgoneController._cancel_url),
+ }
+
+ # render the button
+ res = self.payment_acquirer.render(
+ cr, uid, self.ogone_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://secure.ogone.com/ncol/test/orderstandard.asp', 'ogone: wrong form POST url')
+ for form_input in tree.input:
+ if form_input.get('name') in ['submit']:
+ continue
+ self.assertEqual(
+ form_input.get('value'),
+ form_values[form_input.get('name')],
+ 'ogone: wrong value for form: received %s instead of %s' % (form_input.get('value'), form_values[form_input.get('name')])
+ )
+
+ # ----------------------------------------
+ # Test2: button using tx + validation
+ # ----------------------------------------
+
+ # create a new draft tx
+ tx_id = self.payment_transaction.create(
+ cr, uid, {
+ 'amount': 0.01,
+ 'acquirer_id': self.ogone_id,
+ 'currency_id': self.currency_euro_id,
+ 'reference': 'test_ref0',
+ 'partner_id': self.buyer_id,
+ }, context=context
+ )
+ # render the button
+ res = self.payment_acquirer.render(
+ cr, uid, self.ogone_id,
+ 'should_be_erased', 0.01, self.currency_euro,
+ tx_id=tx_id,
+ partner_id=None,
+ partner_values=self.buyer_values,
+ context=context)
+
+ # check form result
+ tree = objectify.fromstring(res)
+ self.assertEqual(tree.get('action'), 'https://secure.ogone.com/ncol/test/orderstandard.asp', 'ogone: wrong form POST url')
+ for form_input in tree.input:
+ if form_input.get('name') in ['submit']:
+ continue
+ self.assertEqual(
+ form_input.get('value'),
+ form_values[form_input.get('name')],
+ 'ogone: wrong value for form 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_acquirer_ogone.models.ogone', 'ValidationError')
+ def test_20_ogone_form_management(self):
+ cr, uid, context = self.cr, self.uid, {}
+
+ # typical data posted by ogone after client has successfully paid
+ ogone_post_data = {
+ 'orderID': u'test_ref_2',
+ 'STATUS': u'9',
+ 'CARDNO': u'XXXXXXXXXXXX0002',
+ 'PAYID': u'25381582',
+ 'CN': u'Norbert Buyer',
+ 'NCERROR': u'0',
+ 'TRXDATE': u'11/15/13',
+ 'IP': u'85.201.233.72',
+ 'BRAND': u'VISA',
+ 'ACCEPTANCE': u'test123',
+ 'currency': u'EUR',
+ 'amount': u'1.95',
+ 'SHASIGN': u'7B7B0ED9CBC4A85543A9073374589033A62A05A5',
+ 'ED': u'0315',
+ 'PM': u'CreditCard'
+ }
+
+ # should raise error about unknown tx
+ with self.assertRaises(ValidationError):
+ self.payment_transaction.ogone_form_feedback(cr, uid, ogone_post_data, context=context)
+
+ # create tx
+ tx_id = self.payment_transaction.create(
+ cr, uid, {
+ 'amount': 1.95,
+ 'acquirer_id': self.ogone_id,
+ 'currency_id': self.currency_euro_id,
+ 'reference': 'test_ref_2',
+ 'partner_name': 'Norbert Buyer',
+ }, context=context
+ )
+ # validate it
+ self.payment_transaction.ogone_form_feedback(cr, uid, ogone_post_data, context=context)
+ # check state
+ tx = self.payment_transaction.browse(cr, uid, tx_id, context=context)
+ self.assertEqual(tx.state, 'done', 'ogone: validation did not put tx into done state')
+ self.assertEqual(tx.ogone_payid, ogone_post_data.get('PAYID'), 'ogone: validation did not update tx payid')
+
+ # reset tx
+ tx.write({'state': 'draft', 'date_validate': False, 'ogone_payid': False})
+
+ # now ogone post is ok: try to modify the SHASIGN
+ ogone_post_data['SHASIGN'] = 'a4c16bae286317b82edb49188d3399249a784691'
+ with self.assertRaises(ValidationError):
+ self.payment_transaction.ogone_form_feedback(cr, uid, ogone_post_data, context=context)
+
+ # simulate an error
+ ogone_post_data['STATUS'] = 2
+ ogone_post_data['SHASIGN'] = 'a4c16bae286317b82edb49188d3399249a784691'
+ self.payment_transaction.ogone_form_feedback(cr, uid, ogone_post_data, context=context)
+ # check state
+ tx = self.payment_transaction.browse(cr, uid, tx_id, context=context)
+ self.assertEqual(tx.state, 'error', 'ogone: erroneous validation did not put tx into error state')
+
+ def test_30_ogone_s2s(self):
+ test_ref = 'test_ref_%.15f' % time.time()
+ cr, uid, context = self.cr, self.uid, {}
+
+ # create a new draft tx
+ tx_id = self.payment_transaction.create(
+ cr, uid, {
+ 'amount': 0.01,
+ 'acquirer_id': self.ogone_id,
+ 'currency_id': self.currency_euro_id,
+ 'reference': test_ref,
+ 'partner_id': self.buyer_id,
+ 'type': 'server2server',
+ }, context=context
+ )
+
+ # create an alias
+ res = self.payment_transaction.ogone_s2s_create_alias(
+ cr, uid, tx_id, {
+ 'expiry_date_mm': '01',
+ 'expiry_date_yy': '2015',
+ 'holder_name': 'Norbert Poilu',
+ 'number': '4000000000000002',
+ 'brand': 'VISA',
+ }, context=context)
+
+ # check an alias is set, containing at least OPENERP
+ tx = self.payment_transaction.browse(cr, uid, tx_id, context=context)
+ self.assertIn('OPENERP', tx.partner_reference, 'ogone: wrong partner reference after creating an alias')
+
+ res = self.payment_transaction.ogone_s2s_execute(cr, uid, tx_id, {}, context=context)
+ # print res
+
+
+# {
+# 'orderID': u'reference',
+# 'STATUS': u'9',
+# 'CARDNO': u'XXXXXXXXXXXX0002',
+# 'PAYID': u'24998692',
+# 'CN': u'Norbert Poilu',
+# 'NCERROR': u'0',
+# 'TRXDATE': u'11/05/13',
+# 'IP': u'85.201.233.72',
+# 'BRAND': u'VISA',
+# 'ACCEPTANCE': u'test123',
+# 'currency': u'EUR',
+# 'amount': u'1.95',
+# 'SHASIGN': u'EFDC56879EF7DE72CCF4B397076B5C9A844CB0FA',
+# 'ED': u'0314',
+# 'PM': u'CreditCard'
+# }
diff --git a/addons/payment_acquirer_ogone/views/ogone.xml b/addons/payment_acquirer_ogone/views/ogone.xml
new file mode 100644
index 00000000000..f71f92197f0
--- /dev/null
+++ b/addons/payment_acquirer_ogone/views/ogone.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/payment_acquirer_ogone/views/payment_acquirer.xml b/addons/payment_acquirer_ogone/views/payment_acquirer.xml
new file mode 100644
index 00000000000..8ec958a5351
--- /dev/null
+++ b/addons/payment_acquirer_ogone/views/payment_acquirer.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+ acquirer.form.ogone
+ payment.acquirer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ acquirer.transaction.form.ogone
+ payment.transaction
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/payment_acquirer_paypal/__init__.py b/addons/payment_acquirer_paypal/__init__.py
new file mode 100644
index 00000000000..dde6f25c728
--- /dev/null
+++ b/addons/payment_acquirer_paypal/__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_paypal/__openerp__.py b/addons/payment_acquirer_paypal/__openerp__.py
new file mode 100644
index 00000000000..d7ba75d0a7b
--- /dev/null
+++ b/addons/payment_acquirer_paypal/__openerp__.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+
+{
+ 'name': 'Paypal Payment Acquirer',
+ 'category': 'Hidden',
+ 'summary': 'Payment Acquirer: Paypal Implementation',
+ 'version': '1.0',
+ 'description': """Paypal Payment Acquirer""",
+ 'author': 'OpenERP SA',
+ 'depends': ['payment_acquirer'],
+ 'data': [
+ 'views/paypal.xml',
+ 'views/payment_acquirer.xml',
+ 'data/paypal.xml',
+ ],
+ 'installable': True,
+}
diff --git a/addons/payment_acquirer_paypal/controllers/__init__.py b/addons/payment_acquirer_paypal/controllers/__init__.py
new file mode 100644
index 00000000000..bbd183e955b
--- /dev/null
+++ b/addons/payment_acquirer_paypal/controllers/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+import main
diff --git a/addons/payment_acquirer_paypal/controllers/main.py b/addons/payment_acquirer_paypal/controllers/main.py
new file mode 100644
index 00000000000..09321631db0
--- /dev/null
+++ b/addons/payment_acquirer_paypal/controllers/main.py
@@ -0,0 +1,110 @@
+# -*- 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 .
+#
+##############################################################################
+
+from openerp.addons.web import http
+from openerp.addons.web.http import request
+# from openerp.addons.payment_acquirer.models.payment_acquirer import ValidationError
+from openerp.addons.website.models import website
+
+import logging
+import requests
+from urllib import urlencode
+
+_logger = logging.getLogger(__name__)
+
+
+class PaypalController(http.Controller):
+ _notify_url = '/payment/paypal/ipn/'
+ _return_url = '/payment/paypal/dpn/'
+ _cancel_url = '/payment/paypal/cancel/'
+
+ @website.route([
+ '/payment/paypal/ipn/',
+ ], type='http', auth='public')
+ def paypal_ipn(self, **post):
+ print 'Entering paypal_ipn with post', post
+ # step 1: return an empty HTTP 200 response -> will be done at the end by returning ''
+
+ # step 2: POST the complete, unaltered message back to Paypal (preceded by cmd=_notify-validate), with same encoding
+ paypal_url = "https://www.sandbox.paypal.com/cgi-bin/webscr"
+ post_url = '%s?cmd=_notify-validate&%s' % (paypal_url, urlencode(post))
+ resp = requests.post(post_url)
+ print '\tReceived response', resp, resp.text
+
+ # step 3: paypal send either VERIFIED or INVALID (single word)
+ if resp.text == 'VERIFIED':
+ _logger.info('Paypal: received verified IPN')
+ cr, uid, context = request.cr, request.uid, request.context
+ payment_transaction = request.registry['payment.transaction']
+ res = payment_transaction.paypal_form_feedback(cr, uid, post, context=context)
+ print '\tValidation result', res
+ elif resp.text == 'INVALID':
+ _logger.warning('Paypal: received invalid IPN with post %s' % post)
+ else:
+ _logger.warning('Paypal: received unrecognized IPN with post %s' % post)
+
+ return ''
+
+ @website.route([
+ '/payment/paypal/dpn',
+ ], type='http', auth="public")
+ def paypal_dpn(self, **post):
+ """ TODO
+ """
+ cr, uid, context = request.cr, request.uid, request.context
+ print 'Entering paypal_dpn with post', post
+
+ # step 1: return an empty HTTP 200 response -> will be done at the end by returning ''
+
+ # step 2: POST the complete, unaltered message back to Paypal (preceded by cmd=_notify-validate), with same encoding
+ paypal_url = "https://www.sandbox.paypal.com/cgi-bin/webscr"
+ post_url = '%s?cmd=_notify-validate&%s' % (paypal_url, urlencode(post))
+ resp = requests.post(post_url)
+ print '\tReceived response', resp, resp.text
+
+ # step 3: paypal send either VERIFIED or INVALID (single word)
+ if resp.text == 'VERIFIED':
+ _logger.info('Paypal: received verified IPN')
+ cr, uid, context = request.cr, request.uid, request.context
+ payment_transaction = request.registry['payment.transaction']
+ res = payment_transaction.paypal_form_feedback(cr, uid, post, context=context)
+ print '\tValidation result', res
+ elif resp.text == 'INVALID':
+ _logger.warning('Paypal: received invalid IPN with post %s' % post)
+ else:
+ _logger.warning('Paypal: received unrecognized IPN with post %s' % post)
+
+ return_url = post.pop('return_url', '/')
+ print 'return_url', return_url
+ return request.redirect(return_url)
+
+ @website.route([
+ '/payment/paypal/cancel',
+ ], type='http', auth="public")
+ def paypal_cancel(self, **post):
+ """ TODO
+ """
+ cr, uid, context = request.cr, request.uid, request.context
+ print 'Entering paypal_cancel with post', post
+
+ return_url = post.pop('return_url', '/')
+ print 'return_url', return_url
+ return request.redirect(return_url)
diff --git a/addons/payment_acquirer_paypal/data/paypal.xml b/addons/payment_acquirer_paypal/data/paypal.xml
new file mode 100644
index 00000000000..eac0b78bb72
--- /dev/null
+++ b/addons/payment_acquirer_paypal/data/paypal.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ paypal
+
+ test
+ https://www.sandbox.paypal.com/cgi-bin/webscr
+ dummy
+ dummy
+
+
+
+
diff --git a/addons/payment_acquirer_paypal/doc/changelog.rst b/addons/payment_acquirer_paypal/doc/changelog.rst
new file mode 100644
index 00000000000..d0c24af384d
--- /dev/null
+++ b/addons/payment_acquirer_paypal/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_paypal/doc/index.rst b/addons/payment_acquirer_paypal/doc/index.rst
new file mode 100644
index 00000000000..85028ed0d3e
--- /dev/null
+++ b/addons/payment_acquirer_paypal/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_paypal/models/__init__.py b/addons/payment_acquirer_paypal/models/__init__.py
new file mode 100644
index 00000000000..a1fe670f56f
--- /dev/null
+++ b/addons/payment_acquirer_paypal/models/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+import paypal
diff --git a/addons/payment_acquirer_paypal/models/paypal.py b/addons/payment_acquirer_paypal/models/paypal.py
new file mode 100644
index 00000000000..a908ac34e2d
--- /dev/null
+++ b/addons/payment_acquirer_paypal/models/paypal.py
@@ -0,0 +1,191 @@
+# -*- coding: utf-'8' "-*-"
+
+from openerp.addons.payment_acquirer.models.payment_acquirer import ValidationError
+from openerp.addons.payment_acquirer_paypal.controllers.main import PaypalController
+from openerp.osv import osv, fields
+from openerp.tools.float_utils import float_compare
+
+import logging
+import urlparse
+
+_logger = logging.getLogger(__name__)
+
+
+class AcquirerPaypal(osv.Model):
+ _inherit = 'payment.acquirer'
+
+ _columns = {
+ 'paypal_email_id': fields.char('Email ID', required_if_provider='paypal'),
+ 'paypal_username': fields.char('Username', required_if_provider='paypal'),
+ 'paypal_password': fields.char('Password'),
+ 'paypal_signature': fields.char('Signature'),
+ 'paypal_tx_url': fields.char('Transaction URL', required_if_provider='paypal'),
+ 'paypal_use_dpn': fields.boolean('Use DPN'),
+ 'paypal_use_ipn': fields.boolean('Use IPN'),
+ }
+
+ _defaults = {
+ 'paypal_use_dpn': True,
+ 'paypal_use_ipn': True,
+ 'paypal_tx_url': 'https://www.sandbox.paypal.com/cgi-bin/webscr',
+ }
+
+ def paypal_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)
+ partner = None
+ if partner_id:
+ partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context)
+ tx_values = {
+ 'cmd': '_xclick',
+ 'business': acquirer.paypal_email_id,
+ 'item_name': reference,
+ 'item_number': reference,
+ 'amount': amount,
+ 'currency_code': currency and currency.name or 'EUR',
+ 'address1': partner and ' '.join((partner.street or '', partner.street2 or '')).strip() or ' '.join((partner_values.get('street', ''), partner_values.get('street2', ''))).strip(),
+ 'city': partner and partner.city or partner_values.get('city', ''),
+ 'country': partner and partner.country_id and partner.country_id.name or partner_values.get('country_name', ''),
+ 'email': partner and partner.email or partner_values.get('email', ''),
+ 'zip': partner and partner.zip or partner_values.get('zip', ''),
+ 'first_name': partner and partner.name or partner_values.get('name', '').split()[-1:],
+ 'last_name': partner and partner.name or partner_values.get('name', '').split()[:-1],
+ 'return': '%s' % urlparse.urljoin(base_url, PaypalController._return_url),
+ 'notify_url': '%s' % urlparse.urljoin(base_url, PaypalController._notify_url),
+ 'cancel_return': '%s' % urlparse.urljoin(base_url, PaypalController._cancel_url),
+ }
+ if tx_custom_values and tx_custom_values.get('return_url'):
+ tx_values['custom'] = 'return_url=%s' % tx_custom_values.pop('return_url')
+ if tx_custom_values:
+ tx_values.update(tx_custom_values)
+ return tx_values
+
+ def paypal_get_form_action_url(self, cr, uid, id, context=None):
+ acquirer = self.browse(cr, uid, id, context=context)
+ return acquirer.paypal_tx_url
+
+
+class TxPaypal(osv.Model):
+ _inherit = 'payment.transaction'
+
+ _columns = {
+ 'paypal_txn_id': fields.char('Transaction ID'),
+ 'paypal_txn_type': fields.char('Transaction type'),
+ }
+
+ # --------------------------------------------------
+ # FORM RELATED METHODS
+ # --------------------------------------------------
+
+ def paypal_form_generate_values(self, cr, uid, id, tx_custom_values=None, context=None):
+ tx = self.browse(cr, uid, id, context=context)
+
+ tx_data = {
+ 'item_name': tx.name,
+ 'first_name': tx.partner_name and tx.partner_name.split()[-1:],
+ 'last_name': tx.partner_name and tx.partner_name.split()[:-1],
+ 'email': tx.partner_email,
+ 'zip': tx.partner_zip,
+ 'address1': tx.partner_address,
+ 'city': tx.partner_city,
+ 'country': tx.partner_country_id and tx.partner_country_id.name or '',
+ }
+ 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 _paypal_get_tx_from_data(self, cr, uid, data, context=None):
+ reference, txn_id = data.get('item_number'), data.get('txn_id')
+ if not reference or not txn_id:
+ error_msg = 'Paypal: received data with missing reference (%s) or txn_id (%s)' % (reference, txn_id)
+ _logger.error(error_msg)
+ raise ValidationError(error_msg)
+
+ # find tx -> @TDENOTE use txn_id ?
+ tx_ids = self.pool['payment.transaction'].search(cr, uid, [('reference', '=', reference)], context=context)
+ if not tx_ids or len(tx_ids) > 1:
+ error_msg = 'Paypal: 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)
+
+ return tx
+
+ def paypal_form_feedback(self, cr, uid, data, context=None):
+ invalid_parameters = []
+
+ # get tx
+ tx = self._paypal_get_tx_from_data(cr, uid, data, context=context)
+
+ if data.get('notify_version')[0] != '2.6':
+ _logger.warning(
+ 'Received a notification from Paypal with version %s instead of 2.6. This could lead to issues when managing it.' %
+ data.get('notify_version')
+ )
+ if data.get('test_ipn'):
+ _logger.warning(
+ 'Received a notification from Paypal using sandbox'
+ ),
+ # check what is buyed
+ if float_compare(float(data.get('mc_gross', '0.0')), tx.amount, 2) != 0:
+ invalid_parameters.append(('mc_gross', tx.amount))
+ if data.get('mc_currency') != tx.currency_id.name:
+ invalid_parameters.append(('mc_currency', tx.currency_id.name))
+ # if parameters.get('payment_fee') != tx.payment_fee:
+ # invalid_parameters.append(('payment_fee', tx.payment_fee))
+ # if parameters.get('quantity') != tx.quantity:
+ # invalid_parameters.append(('mc_currency', tx.quantity))
+ # if parameters.get('shipping') != tx.shipping:
+ # invalid_parameters.append(('shipping', tx.shipping))
+ # check buyer
+ # if parameters.get('payer_id') != tx.payer_id:
+ # invalid_parameters.append(('mc_gross', tx.payer_id))
+ # if parameters.get('payer_email') != tx.payer_email:
+ # invalid_parameters.append(('payer_email', tx.payer_email))
+ # check seller
+ # if parameters.get('receiver_email') != tx.receiver_email:
+ # invalid_parameters.append(('receiver_email', tx.receiver_email))
+ # if parameters.get('receiver_id') != tx.receiver_id:
+ # invalid_parameters.append(('receiver_id', tx.receiver_id))
+
+ if invalid_parameters:
+ _warn_message = 'The following transaction parameters are incorrect:\n'
+ for item in invalid_parameters:
+ _warn_message += '\t%s: received %s instead of %s\n' % (item, data.get(item[0]), item[1])
+ _logger.warning(_warn_message)
+ return False
+
+ status = data.get('payment_status', 'Pending')
+ if status in ['Completed', 'Processed']:
+ tx.write({
+ 'state': 'done',
+ 'txn_id': data['txn_id'],
+ 'date_validate': data.get('payment_date', fields.datetime.now()),
+ 'paypal_txn_type': data.get('express_checkout')
+ })
+ return True
+ elif status in ['Pending', 'Expired']:
+ tx.write({
+ 'state': 'pending',
+ 'txn_id': data['txn_id'],
+ })
+ 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_paypal/static/description/icon.png b/addons/payment_acquirer_paypal/static/description/icon.png
new file mode 100644
index 00000000000..35528a10508
Binary files /dev/null and b/addons/payment_acquirer_paypal/static/description/icon.png differ
diff --git a/addons/payment_acquirer_paypal/static/src/img/paypal_logo.png b/addons/payment_acquirer_paypal/static/src/img/paypal_logo.png
new file mode 100644
index 00000000000..4803ff5d1ef
Binary files /dev/null and b/addons/payment_acquirer_paypal/static/src/img/paypal_logo.png differ
diff --git a/addons/payment_acquirer_paypal/tests/__init__.py b/addons/payment_acquirer_paypal/tests/__init__.py
new file mode 100644
index 00000000000..6a46df50be2
--- /dev/null
+++ b/addons/payment_acquirer_paypal/tests/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+
+from . import test_paypal
+
+checks = [
+ test_paypal,
+]
diff --git a/addons/payment_acquirer_paypal/tests/test_paypal.py b/addons/payment_acquirer_paypal/tests/test_paypal.py
new file mode 100644
index 00000000000..266c97d6b3b
--- /dev/null
+++ b/addons/payment_acquirer_paypal/tests/test_paypal.py
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Business Applications
+# Copyright (c) 2012-TODAY OpenERP S.A.
+#
+# 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 .
+#
+##############################################################################
+
+from openerp.addons.payment_acquirer.tests.common import PaymentAcquirerCommon
+from openerp.osv.orm import except_orm
+
+from lxml import objectify
+# import requests
+# import urlparse
+
+
+class BasicPayment(PaymentAcquirerCommon):
+
+ def test_10_paypal_basic(self):
+ pass
+
+ def test_11_paypal_form(self):
+ cr, uid = self.cr, self.uid
+ context = {}
+ base_url = self.registry('ir.config_parameter').get_param(cr, uid, 'web.base.url')
+ # ogone_url = self.payment_acquirer._get_ogone_urls(cr, uid, [ogone_id], None, None)[ogone_id]['ogone_standard_order_url']
+
+ model, paypal_view_id = self.registry('ir.model.data').get_object_reference(cr, uid, 'payment_acquirer_paypal', 'paypal_acquirer_button')
+
+ # forgot some mandatory fields: should crash
+ with self.assertRaises(except_orm):
+ paypal_id = self.payment_acquirer.create(
+ cr, uid, {
+ 'name': 'paypal',
+ 'env': 'test',
+ 'view_template_id': paypal_view_id,
+ 'paypal_email_id': 'tde+paypal-facilitator@openerp.com',
+ }, context=context
+ )
+ # tde+buyer@openerp.com
+
+ # create a new paypal account
+ paypal_id = self.payment_acquirer.create(
+ cr, uid, {
+ 'name': 'paypal',
+ 'env': 'test',
+ 'view_template_id': paypal_view_id,
+ 'paypal_email_id': 'dummy',
+ 'paypal_username': 'dummy',
+ }, context=context
+ )
+ # verify acquirer data
+ paypal = self.payment_acquirer.browse(cr, uid, paypal_id, context)
+ self.assertEqual(paypal.env, 'test', 'test without test env')
+
+ # render the button
+ res = self.payment_acquirer.render(
+ cr, uid, paypal_id,
+ 'test_ref0', 0.01, self.currency_euro,
+ partner_id=None,
+ partner_values=self.buyer_values,
+ context=context)
+ # print res
+
+ # # check some basic paypal methods
+ # res = self.payment_transaction.validate_paypal_notification(
+ # cr, uid,
+ # 'http://localhost/payment?mc_gross=19.95&protection_eligibility=Eligible&address_status=confirmed&payer_id=LPLWNMTBWMFAY&tax=0.00&address_street=1+Main+St&payment_date=20%3A12%3A59+Jan+13%2C+2009+PST&payment_status=Completed&charset=windows-1252&address_zip=95131&first_name=Test&mc_fee=0.88&address_country_code=US&address_name=Test+User¬ify_version=2.6&custom=&payer_status=verified&address_country=United+States&address_city=San+Jose&quantity=1&verify_sign=AtkOfCXbDm2hu0ZELryHFjY-Vb7PAUvS6nMXgysbElEn9v-1XcmSoGtf&payer_email=gpmac_1231902590_per%40paypal.com&txn_id=61E67681CH3238416&payment_type=instant&last_name=User&address_state=CA&receiver_email=gpmac_1231902686_biz%40paypal.com&payment_fee=0.88&receiver_id=S8XGHLYDW9T3S&txn_type=express_checkout&item_name=&mc_currency=USD&item_number=&residence_country=US&test_ipn=1&handling_amount=0.00&transaction_subject=&payment_gross=19.95&shipping=0.00')
+ # self.assertEqual(res, False, 'payment: paypal validation on a txn_id that does not exists should return False')
+
+ # txn_id = self.payment_transaction.create(
+ # cr, uid, {
+ # 'amount': 0.01,
+ # 'acquirer_id': paypal_id,
+ # 'currency_id': currency_id,
+ # 'reference': 'test_reference',
+ # 'paypal_txn_id': '61E67681CH3238416',
+ # }, context=context
+ # )
+ # res = self.payment_transaction.validate_paypal_notification(
+ # cr, uid,
+ # 'http://localhost/payment?mc_gross=19.95&protection_eligibility=Eligible&address_status=confirmed&payer_id=LPLWNMTBWMFAY&tax=0.00&address_street=1+Main+St&payment_date=20%3A12%3A59+Jan+13%2C+2009+PST&payment_status=Completed&charset=windows-1252&address_zip=95131&first_name=Test&mc_fee=0.88&address_country_code=US&address_name=Test+User¬ify_version=2.6&custom=&payer_status=verified&address_country=United+States&address_city=San+Jose&quantity=1&verify_sign=AtkOfCXbDm2hu0ZELryHFjY-Vb7PAUvS6nMXgysbElEn9v-1XcmSoGtf&payer_email=gpmac_1231902590_per%40paypal.com&txn_id=61E67681CH3238416&payment_type=instant&last_name=User&address_state=CA&receiver_email=gpmac_1231902686_biz%40paypal.com&payment_fee=0.88&receiver_id=S8XGHLYDW9T3S&txn_type=express_checkout&item_name=&mc_currency=USD&item_number=&residence_country=US&test_ipn=1&handling_amount=0.00&transaction_subject=&payment_gross=19.95&shipping=0.00')
+ # print res
+
+ # # user pays using Paypal
+ # resp = self.payment_transaction.create_paypal_command(
+ # cr, uid, cmd='_xclick', parameters={
+ # 'business': 'tdelavallee-facilitator@gmail.com',
+ # 'amount': 50,
+ # 'item_name': 'test_item',
+ # 'quantity': 1,
+ # 'currency_code': 'USD',
+ # 'return': 'http://www.example.com',
+ # })
+ # print resp
+ # print resp.url
+ # print resp.text
+
+ # self.payment_transaction.validate(cr, uid, [tx_id], context=context)
+
+# {
+# 'protection_eligibility': u'Ineligible',
+# 'last_name': u'Poilu',
+# 'txn_id': u'08D73520KX778924N',
+# 'receiver_email': u'tde+paypal-facilitator@openerp.com',
+# 'payment_status': u'Pending',
+# 'payment_gross': u'',
+# 'tax': u'0.00',
+# 'residence_country': u'FR',
+# 'address_state': u'Alsace',
+# 'payer_status': u'verified',
+# 'txn_type': u'web_accept',
+# 'address_street': u'Av. de la Pelouse, 87648672 Mayet',
+# 'handling_amount': u'0.00',
+# 'payment_date': u'03:21:19 Nov 18, 2013 PST',
+# 'first_name': u'Norbert',
+# 'item_name': u'test_ref_0',
+# 'address_country': u'France',
+# 'charset': u'windows-1252',
+# 'custom': u'',
+# 'notify_version': u'3.7',
+# 'address_name': u'Norbert Poilu',
+# 'pending_reason': u'multi_currency',
+# 'item_number': u'test_ref_0',
+# 'receiver_id': u'DEG7Z7MYGT6QA',
+# 'transaction_subject': u'',
+# 'business': u'tde+paypal-facilitator@openerp.com',
+# 'test_ipn': u'1',
+# 'payer_id': u'VTDKRZQSAHYPS',
+# 'verify_sign': u'An5ns1Kso7MWUdW4ErQKJJJ4qi4-AVoiUf-3478q3vrSmqh08IouiYpM',
+# 'address_zip': u'75002',
+# 'address_country_code': u'FR',
+# 'address_city': u'Paris',
+# 'address_status': u'unconfirmed',
+# 'mc_currency': u'EUR',
+# 'shipping': u'0.00',
+# 'payer_email': u'tde+buyer@openerp.com',
+# 'payment_type': u'instant',
+# 'mc_gross': u'1.95',
+# 'ipn_track_id': u'866df2ccd444b',
+# 'quantity': u'1'
+# }
diff --git a/addons/payment_acquirer_paypal/views/payment_acquirer.xml b/addons/payment_acquirer_paypal/views/payment_acquirer.xml
new file mode 100644
index 00000000000..ae89253545c
--- /dev/null
+++ b/addons/payment_acquirer_paypal/views/payment_acquirer.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+ acquirer.form.paypal
+ payment.acquirer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ acquirer.transaction.form.paypal
+ payment.transaction
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/payment_acquirer_paypal/views/paypal.xml b/addons/payment_acquirer_paypal/views/paypal.xml
new file mode 100644
index 00000000000..9515e0a12a2
--- /dev/null
+++ b/addons/payment_acquirer_paypal/views/paypal.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/payment_acquirer_transfer/__init__.py b/addons/payment_acquirer_transfer/__init__.py
new file mode 100644
index 00000000000..dde6f25c728
--- /dev/null
+++ b/addons/payment_acquirer_transfer/__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_transfer/__openerp__.py b/addons/payment_acquirer_transfer/__openerp__.py
new file mode 100644
index 00000000000..45293e4f84a
--- /dev/null
+++ b/addons/payment_acquirer_transfer/__openerp__.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+
+{
+ 'name': 'Transfer Payment Acquirer',
+ 'category': 'Hidden',
+ 'summary': 'Payment Acquirer: Transfer Implementation',
+ 'version': '1.0',
+ 'description': """Transfer Payment Acquirer""",
+ 'author': 'OpenERP SA',
+ 'depends': ['payment_acquirer'],
+ 'data': [
+ 'views/transfer.xml',
+ 'data/transfer.xml',
+ ],
+ 'installable': True,
+}
diff --git a/addons/payment_acquirer_transfer/controllers/__init__.py b/addons/payment_acquirer_transfer/controllers/__init__.py
new file mode 100644
index 00000000000..bbd183e955b
--- /dev/null
+++ b/addons/payment_acquirer_transfer/controllers/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+import main
diff --git a/addons/payment_acquirer_transfer/controllers/main.py b/addons/payment_acquirer_transfer/controllers/main.py
new file mode 100644
index 00000000000..6d0a7c1fa6f
--- /dev/null
+++ b/addons/payment_acquirer_transfer/controllers/main.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+import logging
+import pprint
+
+from openerp.addons.web import http
+from openerp.addons.web.http import request
+from openerp.addons.website.models import website
+
+_logger = logging.getLogger(__name__)
+
+
+class OgoneController(http.Controller):
+ _accept_url = '/payment/transfer/feedback'
+
+ @website.route([
+ '/payment/transfer/feedback',
+ ], type='http', auth='admin')
+ def transfer_form_feedback(self, **post):
+ cr, uid, context = request.cr, request.uid, request.context
+ _logger.info('Beginning form_feedback with post data %s', pprint.pformat(post)) # debug
+ request.registry['payment.transaction'].form_feedback(cr, uid, post, 'transfer', context)
+ return request.redirect(post.pop('return_url', '/'))
diff --git a/addons/payment_acquirer_transfer/data/transfer.xml b/addons/payment_acquirer_transfer/data/transfer.xml
new file mode 100644
index 00000000000..ce6e56be57d
--- /dev/null
+++ b/addons/payment_acquirer_transfer/data/transfer.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ transfer
+
+ test
+
+ Please use the account 001-002-003 to make the payment.
]]>
+
+
+
+
+
diff --git a/addons/payment_acquirer_transfer/doc/changelog.rst b/addons/payment_acquirer_transfer/doc/changelog.rst
new file mode 100644
index 00000000000..d0c24af384d
--- /dev/null
+++ b/addons/payment_acquirer_transfer/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_transfer/doc/index.rst b/addons/payment_acquirer_transfer/doc/index.rst
new file mode 100644
index 00000000000..85028ed0d3e
--- /dev/null
+++ b/addons/payment_acquirer_transfer/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_transfer/models/__init__.py b/addons/payment_acquirer_transfer/models/__init__.py
new file mode 100644
index 00000000000..da51c9c1854
--- /dev/null
+++ b/addons/payment_acquirer_transfer/models/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+import payment_acquirer
diff --git a/addons/payment_acquirer_transfer/models/payment_acquirer.py b/addons/payment_acquirer_transfer/models/payment_acquirer.py
new file mode 100644
index 00000000000..2f72214e4e6
--- /dev/null
+++ b/addons/payment_acquirer_transfer/models/payment_acquirer.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-'8' "-*-"
+
+from openerp.addons.payment_acquirer.models.payment_acquirer import ValidationError
+from openerp.osv import osv
+from openerp.tools.float_utils import float_compare
+
+import logging
+import pprint
+
+_logger = logging.getLogger(__name__)
+
+
+class TransferPaymentAcquirer(osv.Model):
+ _inherit = 'payment.acquirer'
+
+ def transfer_get_form_action_url(self, cr, uid, id, context=None):
+ return '/payment/transfer/feedback'
+
+
+class TransferPaymentTransaction(osv.Model):
+ _inherit = 'payment.transaction'
+
+ def _transfer_form_get_tx_from_data(self, cr, uid, data, context=None):
+ reference, amount, currency_name = data.get('reference'), data.get('amount'), data.get('currency_name')
+ tx_ids = self.search(
+ cr, uid, [
+ ('reference', '=', reference),
+ ], context=context)
+
+ if not tx_ids or len(tx_ids) > 1:
+ error_msg = 'received data for reference %s' % (pprint.pformat(reference))
+ if not tx_ids:
+ error_msg += '; no order found'
+ else:
+ error_msg += '; multiple order found'
+ _logger.error(error_msg)
+ raise ValidationError(error_msg)
+
+ return self.browse(cr, uid, tx_ids[0], context=context)
+
+ def _transfer_form_get_invalid_parameters(self, cr, uid, tx, data, context=None):
+ invalid_parameters = []
+
+ if float_compare(float(data.get('amount', '0.0')), tx.amount, 2) != 0:
+ invalid_parameters.append(('amount', data.get('amount'), '%.2f' % tx.amount))
+ if data.get('currency') != tx.currency_id.name:
+ invalid_parameters.append(('currency', data.get('currency'), tx.currency_id.name))
+
+ return invalid_parameters
+
+ def _transfer_form_validate(self, cr, uid, tx, data, context=None):
+ _logger.info('Validated transfer payment for tx %s: set as pending' % (tx.reference))
+ return tx.write({'state': 'pending'})
diff --git a/addons/payment_acquirer_transfer/static/description/icon.png b/addons/payment_acquirer_transfer/static/description/icon.png
new file mode 100644
index 00000000000..183937ed9ce
Binary files /dev/null and b/addons/payment_acquirer_transfer/static/description/icon.png differ
diff --git a/addons/payment_acquirer_transfer/views/transfer.xml b/addons/payment_acquirer_transfer/views/transfer.xml
new file mode 100644
index 00000000000..af5c6fcef0f
--- /dev/null
+++ b/addons/payment_acquirer_transfer/views/transfer.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/product/product.py b/addons/product/product.py
index 3220d695239..eb27befa6fa 100644
--- a/addons/product/product.py
+++ b/addons/product/product.py
@@ -642,6 +642,7 @@ class product_product(osv.osv):
_table = "product_product"
_inherits = {'product.template': 'product_tmpl_id'}
_inherit = ['mail.thread']
+ _inherit = ['mail.thread']
_order = 'default_code,name_template'
_columns = {
'qty_available': fields.function(_product_qty_available, type='float', string='Quantity On Hand'),
diff --git a/addons/website_payment/__init__.py b/addons/website_payment/__init__.py
new file mode 100644
index 00000000000..edccdf73182
--- /dev/null
+++ b/addons/website_payment/__init__.py
@@ -0,0 +1,22 @@
+# -*- 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 controllers
diff --git a/addons/website_payment/__openerp__.py b/addons/website_payment/__openerp__.py
new file mode 100644
index 00000000000..2a01d2f1aec
--- /dev/null
+++ b/addons/website_payment/__openerp__.py
@@ -0,0 +1,41 @@
+# -*- 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 .
+#
+##############################################################################
+
+{
+ 'name': 'Payment: Website Integration (Test Module)',
+ 'category': 'Website',
+ 'summary': 'Payment: Website Integration (Test Module)',
+ 'version': '1.0',
+ 'description': """Module installing all sub-payment modules and adding some
+ controllers and menu entries in order to test them.""",
+ 'author': 'OpenERP SA',
+ 'depends': [
+ 'website',
+ 'payment_acquirer',
+ 'payment_acquirer_ogone',
+ 'payment_acquirer_paypal',
+ 'payment_acquirer_transfer',
+ ],
+ 'data': [
+ 'views/website_payment_templates.xml',
+ ],
+ 'installable': True,
+}
diff --git a/addons/website_payment/controllers/__init__.py b/addons/website_payment/controllers/__init__.py
new file mode 100644
index 00000000000..8ee9bae18d9
--- /dev/null
+++ b/addons/website_payment/controllers/__init__.py
@@ -0,0 +1 @@
+import main
diff --git a/addons/website_payment/controllers/main.py b/addons/website_payment/controllers/main.py
new file mode 100644
index 00000000000..4a971975d97
--- /dev/null
+++ b/addons/website_payment/controllers/main.py
@@ -0,0 +1,95 @@
+# -*- 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 .
+#
+##############################################################################
+
+from openerp.addons.web import http
+from openerp.addons.web.http import request
+from openerp.addons.website.models import website
+
+
+class WebsitePayment(http.Controller):
+
+ @website.route([
+ '/payment/paypal/test',
+ ], type='http', auth="public")
+ def paypal_test(self, **post):
+ """ TODO
+ """
+ cr, uid, context = request.cr, request.uid, request.context
+ acquirer_obj = request.registry['payment.acquirer']
+ payment_obj = request.registry['payment.transaction']
+ currency_obj = request.registry['res.currency']
+
+ paypal_id = acquirer_obj.search(cr, uid, [('name', '=', 'paypal')], limit=1, context=context)[0]
+ currency_id = currency_obj.search(cr, uid, [('name', '=', 'EUR')], limit=1, context=context)[0]
+
+ nbr_tx = payment_obj.search(cr, uid, [], count=True, context=context)
+ tx_id = payment_obj.create(cr, uid, {
+ 'reference': 'test_ref_%s' % (nbr_tx),
+ 'amount': 1.95,
+ 'currency_id': currency_id,
+ 'acquirer_id': paypal_id,
+ 'partner_name': 'Norbert Buyer',
+ 'partner_email': 'norbert.buyer@example.com',
+ 'partner_lang': 'fr_FR',
+ }, context=context)
+
+ paypal_form = acquirer_obj.render(cr, uid, paypal_id, None, None, None, tx_id=tx_id, context=context)
+ paypal = acquirer_obj.browse(cr, uid, paypal_id, context=context)
+
+ values = {
+ 'acquirer': paypal,
+ 'acquirer_form': paypal_form,
+ }
+ return request.website.render("website_payment.index_paypal", values)
+
+ @website.route([
+ '/payment/ogone/test',
+ ], type='http', auth="public")
+ def ogone_test(self, **post):
+ """ TODO
+ """
+ cr, uid, context = request.cr, request.uid, request.context
+ acquirer_obj = request.registry['payment.acquirer']
+ payment_obj = request.registry['payment.transaction']
+ currency_obj = request.registry['res.currency']
+
+ ogone_id = acquirer_obj.search(cr, uid, [('name', '=', 'ogone')], limit=1, context=context)[0]
+ currency_id = currency_obj.search(cr, uid, [('name', '=', 'EUR')], limit=1, context=context)[0]
+
+ nbr_tx = payment_obj.search(cr, uid, [], count=True, context=context)
+ tx_id = payment_obj.create(cr, uid, {
+ 'reference': 'test_ref_%s' % (nbr_tx),
+ 'amount': 1.95,
+ 'currency_id': currency_id,
+ 'acquirer_id': ogone_id,
+ 'partner_name': 'Norbert Buyer',
+ 'partner_email': 'norbert.buyer@example.com',
+ 'partner_lang': 'fr_FR',
+ }, context=context)
+
+ ogone_form = acquirer_obj.render(cr, uid, ogone_id, None, None, None, tx_id=tx_id, context=context)
+ ogone = acquirer_obj.browse(cr, uid, ogone_id, context=context)
+
+ values = {
+ 'acquirer': ogone,
+ 'acquirer_form': ogone_form,
+ }
+ return request.website.render("website_payment.index_ogone", values)
diff --git a/addons/website_payment/doc/changelog.rst b/addons/website_payment/doc/changelog.rst
new file mode 100644
index 00000000000..8b6131b5806
--- /dev/null
+++ b/addons/website_payment/doc/changelog.rst
@@ -0,0 +1,9 @@
+.. _changelog:
+
+Changelog
+=========
+
+`trunk (saas-3)`
+----------------
+
+ - created ``website_blog`` menu, build on defunct document_page module.
diff --git a/addons/website_payment/doc/index.rst b/addons/website_payment/doc/index.rst
new file mode 100644
index 00000000000..953748fd48d
--- /dev/null
+++ b/addons/website_payment/doc/index.rst
@@ -0,0 +1,10 @@
+Blog Module documentation topics
+''''''''''''''''''''''''''''''''
+
+Changelog
+'''''''''
+
+.. toctree::
+ :maxdepth: 1
+
+ changelog.rst
diff --git a/addons/website_payment/static/description/icon.png b/addons/website_payment/static/description/icon.png
new file mode 100644
index 00000000000..51e33ac6ddf
Binary files /dev/null and b/addons/website_payment/static/description/icon.png differ
diff --git a/addons/website_payment/static/lib/jquery.payment/jquery.payment.js b/addons/website_payment/static/lib/jquery.payment/jquery.payment.js
new file mode 100644
index 00000000000..c81c6e00d81
--- /dev/null
+++ b/addons/website_payment/static/lib/jquery.payment/jquery.payment.js
@@ -0,0 +1,497 @@
+// Generated by CoffeeScript 1.4.0
+(function() {
+ var $, cardFromNumber, cardFromType, cards, defaultFormat, formatBackCardNumber, formatBackExpiry, formatCardNumber, formatExpiry, formatForwardExpiry, formatForwardSlash, hasTextSelected, luhnCheck, reFormatCardNumber, restrictCVC, restrictCardNumber, restrictExpiry, restrictNumeric, setCardType,
+ __slice = [].slice,
+ __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
+ _this = this;
+
+ $ = jQuery;
+
+ $.payment = {};
+
+ $.payment.fn = {};
+
+ $.fn.payment = function() {
+ var args, method;
+ method = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
+ return $.payment.fn[method].apply(this, args);
+ };
+
+ defaultFormat = /(\d{1,4})/g;
+
+ cards = [
+ {
+ type: 'maestro',
+ pattern: /^(5018|5020|5038|6304|6759|676[1-3])/,
+ format: defaultFormat,
+ length: [12, 13, 14, 15, 16, 17, 18, 19],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'dinersclub',
+ pattern: /^(36|38|30[0-5])/,
+ format: defaultFormat,
+ length: [14],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'laser',
+ pattern: /^(6706|6771|6709)/,
+ format: defaultFormat,
+ length: [16, 17, 18, 19],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'jcb',
+ pattern: /^35/,
+ format: defaultFormat,
+ length: [16],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'unionpay',
+ pattern: /^62/,
+ format: defaultFormat,
+ length: [16, 17, 18, 19],
+ cvcLength: [3],
+ luhn: false
+ }, {
+ type: 'discover',
+ pattern: /^(6011|65|64[4-9]|622)/,
+ format: defaultFormat,
+ length: [16],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'mastercard',
+ pattern: /^5[1-5]/,
+ format: defaultFormat,
+ length: [16],
+ cvcLength: [3],
+ luhn: true
+ }, {
+ type: 'amex',
+ pattern: /^3[47]/,
+ format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/,
+ length: [15],
+ cvcLength: [3, 4],
+ luhn: true
+ }, {
+ type: 'visa',
+ pattern: /^4/,
+ format: defaultFormat,
+ length: [13, 14, 15, 16],
+ cvcLength: [3],
+ luhn: true
+ }
+ ];
+
+ cardFromNumber = function(num) {
+ var card, _i, _len;
+ num = (num + '').replace(/\D/g, '');
+ for (_i = 0, _len = cards.length; _i < _len; _i++) {
+ card = cards[_i];
+ if (card.pattern.test(num)) {
+ return card;
+ }
+ }
+ };
+
+ cardFromType = function(type) {
+ var card, _i, _len;
+ for (_i = 0, _len = cards.length; _i < _len; _i++) {
+ card = cards[_i];
+ if (card.type === type) {
+ return card;
+ }
+ }
+ };
+
+ luhnCheck = function(num) {
+ var digit, digits, odd, sum, _i, _len;
+ odd = true;
+ sum = 0;
+ digits = (num + '').split('').reverse();
+ for (_i = 0, _len = digits.length; _i < _len; _i++) {
+ digit = digits[_i];
+ digit = parseInt(digit, 10);
+ if ((odd = !odd)) {
+ digit *= 2;
+ }
+ if (digit > 9) {
+ digit -= 9;
+ }
+ sum += digit;
+ }
+ return sum % 10 === 0;
+ };
+
+ hasTextSelected = function($target) {
+ var _ref;
+ if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== $target.prop('selectionEnd')) {
+ return true;
+ }
+ if (typeof document !== "undefined" && document !== null ? (_ref = document.selection) != null ? typeof _ref.createRange === "function" ? _ref.createRange().text : void 0 : void 0 : void 0) {
+ return true;
+ }
+ return false;
+ };
+
+ reFormatCardNumber = function(e) {
+ var _this = this;
+ return setTimeout(function() {
+ var $target, value;
+ $target = $(e.currentTarget);
+ value = $target.val();
+ value = $.payment.formatCardNumber(value);
+ return $target.val(value);
+ });
+ };
+
+ formatCardNumber = function(e) {
+ var $target, card, digit, length, re, upperLength, value;
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ $target = $(e.currentTarget);
+ value = $target.val();
+ card = cardFromNumber(value + digit);
+ length = (value.replace(/\D/g, '') + digit).length;
+ upperLength = 16;
+ if (card) {
+ upperLength = card.length[card.length.length - 1];
+ }
+ if (length >= upperLength) {
+ return;
+ }
+ if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
+ return;
+ }
+ if (card && card.type === 'amex') {
+ re = /^(\d{4}|\d{4}\s\d{6})$/;
+ } else {
+ re = /(?:^|\s)(\d{4})$/;
+ }
+ if (re.test(value)) {
+ e.preventDefault();
+ return $target.val(value + ' ' + digit);
+ } else if (re.test(value + digit)) {
+ e.preventDefault();
+ return $target.val(value + digit + ' ');
+ }
+ };
+
+ formatBackCardNumber = function(e) {
+ var $target, value;
+ $target = $(e.currentTarget);
+ value = $target.val();
+ if (e.meta) {
+ return;
+ }
+ if (e.which !== 8) {
+ return;
+ }
+ if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
+ return;
+ }
+ if (/\d\s$/.test(value)) {
+ e.preventDefault();
+ return $target.val(value.replace(/\d\s$/, ''));
+ } else if (/\s\d?$/.test(value)) {
+ e.preventDefault();
+ return $target.val(value.replace(/\s\d?$/, ''));
+ }
+ };
+
+ formatExpiry = function(e) {
+ var $target, digit, val;
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ $target = $(e.currentTarget);
+ val = $target.val() + digit;
+ if (/^\d$/.test(val) && (val !== '0' && val !== '1')) {
+ e.preventDefault();
+ return $target.val("0" + val + " / ");
+ } else if (/^\d\d$/.test(val)) {
+ e.preventDefault();
+ return $target.val("" + val + " / ");
+ }
+ };
+
+ formatForwardExpiry = function(e) {
+ var $target, digit, val;
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ $target = $(e.currentTarget);
+ val = $target.val();
+ if (/^\d\d$/.test(val)) {
+ return $target.val("" + val + " / ");
+ }
+ };
+
+ formatForwardSlash = function(e) {
+ var $target, slash, val;
+ slash = String.fromCharCode(e.which);
+ if (slash !== '/') {
+ return;
+ }
+ $target = $(e.currentTarget);
+ val = $target.val();
+ if (/^\d$/.test(val) && val !== '0') {
+ return $target.val("0" + val + " / ");
+ }
+ };
+
+ formatBackExpiry = function(e) {
+ var $target, value;
+ if (e.meta) {
+ return;
+ }
+ $target = $(e.currentTarget);
+ value = $target.val();
+ if (e.which !== 8) {
+ return;
+ }
+ if (($target.prop('selectionStart') != null) && $target.prop('selectionStart') !== value.length) {
+ return;
+ }
+ if (/\d(\s|\/)+$/.test(value)) {
+ e.preventDefault();
+ return $target.val(value.replace(/\d(\s|\/)*$/, ''));
+ } else if (/\s\/\s?\d?$/.test(value)) {
+ e.preventDefault();
+ return $target.val(value.replace(/\s\/\s?\d?$/, ''));
+ }
+ };
+
+ restrictNumeric = function(e) {
+ var input;
+ if (e.metaKey || e.ctrlKey) {
+ return true;
+ }
+ if (e.which === 32) {
+ return false;
+ }
+ if (e.which === 0) {
+ return true;
+ }
+ if (e.which < 33) {
+ return true;
+ }
+ input = String.fromCharCode(e.which);
+ return !!/[\d\s]/.test(input);
+ };
+
+ restrictCardNumber = function(e) {
+ var $target, card, digit, value;
+ $target = $(e.currentTarget);
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ if (hasTextSelected($target)) {
+ return;
+ }
+ value = ($target.val() + digit).replace(/\D/g, '');
+ card = cardFromNumber(value);
+ if (card) {
+ return value.length <= card.length[card.length.length - 1];
+ } else {
+ return value.length <= 16;
+ }
+ };
+
+ restrictExpiry = function(e) {
+ var $target, digit, value;
+ $target = $(e.currentTarget);
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ if (hasTextSelected($target)) {
+ return;
+ }
+ value = $target.val() + digit;
+ value = value.replace(/\D/g, '');
+ if (value.length > 6) {
+ return false;
+ }
+ };
+
+ restrictCVC = function(e) {
+ var $target, digit, val;
+ $target = $(e.currentTarget);
+ digit = String.fromCharCode(e.which);
+ if (!/^\d+$/.test(digit)) {
+ return;
+ }
+ val = $target.val() + digit;
+ return val.length <= 4;
+ };
+
+ setCardType = function(e) {
+ var $target, allTypes, card, cardType, val;
+ $target = $(e.currentTarget);
+ val = $target.val();
+ cardType = $.payment.cardType(val) || 'unknown';
+ if (!$target.hasClass(cardType)) {
+ allTypes = (function() {
+ var _i, _len, _results;
+ _results = [];
+ for (_i = 0, _len = cards.length; _i < _len; _i++) {
+ card = cards[_i];
+ _results.push(card.type);
+ }
+ return _results;
+ })();
+ $target.removeClass('unknown');
+ $target.removeClass(allTypes.join(' '));
+ $target.addClass(cardType);
+ $target.toggleClass('identified', cardType !== 'unknown');
+ return $target.trigger('payment.cardType', cardType);
+ }
+ };
+
+ $.payment.fn.formatCardCVC = function() {
+ this.payment('restrictNumeric');
+ this.on('keypress', restrictCVC);
+ return this;
+ };
+
+ $.payment.fn.formatCardExpiry = function() {
+ this.payment('restrictNumeric');
+ this.on('keypress', restrictExpiry);
+ this.on('keypress', formatExpiry);
+ this.on('keypress', formatForwardSlash);
+ this.on('keypress', formatForwardExpiry);
+ this.on('keydown', formatBackExpiry);
+ return this;
+ };
+
+ $.payment.fn.formatCardNumber = function() {
+ this.payment('restrictNumeric');
+ this.on('keypress', restrictCardNumber);
+ this.on('keypress', formatCardNumber);
+ this.on('keydown', formatBackCardNumber);
+ this.on('keyup', setCardType);
+ this.on('paste', reFormatCardNumber);
+ return this;
+ };
+
+ $.payment.fn.restrictNumeric = function() {
+ this.on('keypress', restrictNumeric);
+ return this;
+ };
+
+ $.payment.fn.cardExpiryVal = function() {
+ return $.payment.cardExpiryVal($(this).val());
+ };
+
+ $.payment.cardExpiryVal = function(value) {
+ var month, prefix, year, _ref;
+ value = value.replace(/\s/g, '');
+ _ref = value.split('/', 2), month = _ref[0], year = _ref[1];
+ if ((year != null ? year.length : void 0) === 2 && /^\d+$/.test(year)) {
+ prefix = (new Date).getFullYear();
+ prefix = prefix.toString().slice(0, 2);
+ year = prefix + year;
+ }
+ month = parseInt(month, 10);
+ year = parseInt(year, 10);
+ return {
+ month: month,
+ year: year
+ };
+ };
+
+ $.payment.validateCardNumber = function(num) {
+ var card, _ref;
+ num = (num + '').replace(/\s+|-/g, '');
+ if (!/^\d+$/.test(num)) {
+ return false;
+ }
+ card = cardFromNumber(num);
+ if (!card) {
+ return false;
+ }
+ return (_ref = num.length, __indexOf.call(card.length, _ref) >= 0) && (card.luhn === false || luhnCheck(num));
+ };
+
+ $.payment.validateCardExpiry = function(month, year) {
+ var currentTime, expiry, prefix, _ref;
+ if (typeof month === 'object' && 'month' in month) {
+ _ref = month, month = _ref.month, year = _ref.year;
+ }
+ if (!(month && year)) {
+ return false;
+ }
+ month = $.trim(month);
+ year = $.trim(year);
+ if (!/^\d+$/.test(month)) {
+ return false;
+ }
+ if (!/^\d+$/.test(year)) {
+ return false;
+ }
+ if (!(parseInt(month, 10) <= 12)) {
+ return false;
+ }
+ if (year.length === 2) {
+ prefix = (new Date).getFullYear();
+ prefix = prefix.toString().slice(0, 2);
+ year = prefix + year;
+ }
+ expiry = new Date(year, month);
+ currentTime = new Date;
+ expiry.setMonth(expiry.getMonth() - 1);
+ expiry.setMonth(expiry.getMonth() + 1, 1);
+ return expiry > currentTime;
+ };
+
+ $.payment.validateCardCVC = function(cvc, type) {
+ var _ref, _ref1;
+ cvc = $.trim(cvc);
+ if (!/^\d+$/.test(cvc)) {
+ return false;
+ }
+ if (type) {
+ return _ref = cvc.length, __indexOf.call((_ref1 = cardFromType(type)) != null ? _ref1.cvcLength : void 0, _ref) >= 0;
+ } else {
+ return cvc.length >= 3 && cvc.length <= 4;
+ }
+ };
+
+ $.payment.cardType = function(num) {
+ var _ref;
+ if (!num) {
+ return null;
+ }
+ return ((_ref = cardFromNumber(num)) != null ? _ref.type : void 0) || null;
+ };
+
+ $.payment.formatCardNumber = function(num) {
+ var card, groups, upperLength, _ref;
+ card = cardFromNumber(num);
+ if (!card) {
+ return num;
+ }
+ upperLength = card.length[card.length.length - 1];
+ num = num.replace(/\D/g, '');
+ num = num.slice(0, +upperLength + 1 || 9e9);
+ if (card.format.global) {
+ return (_ref = num.match(card.format)) != null ? _ref.join(' ') : void 0;
+ } else {
+ groups = card.format.exec(num);
+ if (groups != null) {
+ groups.shift();
+ }
+ return groups != null ? groups.join(' ') : void 0;
+ }
+ };
+
+}).call(this);
diff --git a/addons/website_payment/static/src/css/Makefile b/addons/website_payment/static/src/css/Makefile
new file mode 100644
index 00000000000..7f7f16d3616
--- /dev/null
+++ b/addons/website_payment/static/src/css/Makefile
@@ -0,0 +1,2 @@
+sass:
+ sass --trace -t expanded website_payment.sass:website_payment.css
diff --git a/addons/website_payment/static/src/css/website_payment.css b/addons/website_payment/static/src/css/website_payment.css
new file mode 100644
index 00000000000..196d6cf8956
--- /dev/null
+++ b/addons/website_payment/static/src/css/website_payment.css
@@ -0,0 +1,53 @@
+@charset "utf-8";
+input#cc_number {
+ background-image: url("/website_payment/static/src/img/grey-dots.png");
+ background-repeat: no-repeat;
+ background-position: 14px 14px;
+}
+
+div.card_placeholder {
+ background-image: url("/website_payment/static/src/img/placeholder.png");
+ background-repeat: no-repeat;
+ width: 32px;
+ height: 20px;
+ position: absolute;
+ top: 34px;
+ right: 20px;
+ -webkit-transition: 0.4s cubic-bezier(0.455, 0.03, 0.515, 0.955);
+ transition: 0.4s cubic-bezier(0.455, 0.03, 0.515, 0.955);
+ pointer-events: none;
+}
+
+div.amex {
+ background-image: url("/website_payment/static/src/img/amex.png");
+ background-repeat: no-repeat;
+}
+
+div.diners {
+ background-image: url("/website_payment/static/src/img/diners.png");
+ background-repeat: no-repeat;
+}
+
+div.discover {
+ background-image: url("/website_payment/static/src/img/discover.png");
+ background-repeat: no-repeat;
+}
+
+div.jcb {
+ background-image: url("/website_payment/static/src/img/jcb.png");
+ background-repeat: no-repeat;
+}
+
+div.mastercard {
+ background-image: url("/website_payment/static/src/img/mastercard.png");
+ background-repeat: no-repeat;
+}
+
+div.visa {
+ background-image: url("/website_payment/static/src/img/visa.png");
+ background-repeat: no-repeat;
+}
+
+input#cc_number:active, input#cc_number:focus {
+ background-image: none;
+}
diff --git a/addons/website_payment/static/src/css/website_payment.sass b/addons/website_payment/static/src/css/website_payment.sass
new file mode 100644
index 00000000000..b5d4d03a9bd
--- /dev/null
+++ b/addons/website_payment/static/src/css/website_payment.sass
@@ -0,0 +1,45 @@
+@charset "utf-8"
+
+input#cc_number
+ background-image: url('/website_payment/static/src/img/grey-dots.png')
+ background-repeat: no-repeat
+ background-position: 14px 14px
+
+div.card_placeholder
+ background-image: url('/website_payment/static/src/img/placeholder.png')
+ background-repeat: no-repeat
+ width: 32px
+ height: 20px
+ position: absolute
+ top: 34px
+ right: 20px
+ -webkit-transition: .4s cubic-bezier(0.455,0.03,0.515,0.955)
+ transition: .4s cubic-bezier(0.455,0.03,0.515,0.955)
+ pointer-events: none
+
+div.amex
+ background-image: url('/website_payment/static/src/img/amex.png')
+ background-repeat: no-repeat
+
+div.diners
+ background-image: url('/website_payment/static/src/img/diners.png')
+ background-repeat: no-repeat
+
+div.discover
+ background-image: url('/website_payment/static/src/img/discover.png')
+ background-repeat: no-repeat
+
+div.jcb
+ background-image: url('/website_payment/static/src/img/jcb.png')
+ background-repeat: no-repeat
+
+div.mastercard
+ background-image: url('/website_payment/static/src/img/mastercard.png')
+ background-repeat: no-repeat
+
+div.visa
+ background-image: url('/website_payment/static/src/img/visa.png')
+ background-repeat: no-repeat
+
+input#cc_number:active, input#cc_number:focus
+ background-image: none
diff --git a/addons/website_payment/static/src/img/amex.png b/addons/website_payment/static/src/img/amex.png
new file mode 100644
index 00000000000..7de472cdd9b
Binary files /dev/null and b/addons/website_payment/static/src/img/amex.png differ
diff --git a/addons/website_payment/static/src/img/diners.png b/addons/website_payment/static/src/img/diners.png
new file mode 100644
index 00000000000..93b909d1b33
Binary files /dev/null and b/addons/website_payment/static/src/img/diners.png differ
diff --git a/addons/website_payment/static/src/img/discover.png b/addons/website_payment/static/src/img/discover.png
new file mode 100644
index 00000000000..692bd76d403
Binary files /dev/null and b/addons/website_payment/static/src/img/discover.png differ
diff --git a/addons/website_payment/static/src/img/grey-dots.png b/addons/website_payment/static/src/img/grey-dots.png
new file mode 100644
index 00000000000..bf5391ef906
Binary files /dev/null and b/addons/website_payment/static/src/img/grey-dots.png differ
diff --git a/addons/website_payment/static/src/img/jcb.png b/addons/website_payment/static/src/img/jcb.png
new file mode 100644
index 00000000000..4160175eb5f
Binary files /dev/null and b/addons/website_payment/static/src/img/jcb.png differ
diff --git a/addons/website_payment/static/src/img/mastercard.png b/addons/website_payment/static/src/img/mastercard.png
new file mode 100644
index 00000000000..b51a920d156
Binary files /dev/null and b/addons/website_payment/static/src/img/mastercard.png differ
diff --git a/addons/website_payment/static/src/img/placeholder.png b/addons/website_payment/static/src/img/placeholder.png
new file mode 100644
index 00000000000..665f42efba8
Binary files /dev/null and b/addons/website_payment/static/src/img/placeholder.png differ
diff --git a/addons/website_payment/static/src/img/visa.png b/addons/website_payment/static/src/img/visa.png
new file mode 100644
index 00000000000..0358fbf00b6
Binary files /dev/null and b/addons/website_payment/static/src/img/visa.png differ
diff --git a/addons/website_payment/static/src/js/payment_acquirer.js b/addons/website_payment/static/src/js/payment_acquirer.js
new file mode 100644
index 00000000000..8955af0b7bc
--- /dev/null
+++ b/addons/website_payment/static/src/js/payment_acquirer.js
@@ -0,0 +1,58 @@
+$(document).ready(function () {
+
+ $('input#cc_number').payment('formatCardNumber');
+ $('input#cc_cvc').payment('formatCardCVC');
+ $('input#cc_expiry').payment('formatCardExpiry')
+
+ $('input#cc_number').on('focusout', function (e) {
+ var valid_value = $.payment.validateCardNumber(this.value);
+ var card_type = $.payment.cardType(this.value);
+ console.log('Validating card', this.value, 'is a', card_type, 'and valid:', valid_value);
+ if (card_type) {
+ $(this).parent('.form-group').children('.card_placeholder').removeClass().addClass('card_placeholder ' + card_type);
+ }
+ else {
+ $(this).parent('.form-group').children('.card_placeholder').removeClass().addClass('card_placeholder');
+ }
+ if (valid_value) {
+ $(this).parent('.form-group').addClass('has-success');
+ $(this).parent('.form-group').removeClass('has-error');
+ }
+ else {
+ $(this).parent('.form-group').addClass('has-error');
+ $(this).parent('.form-group').removeClass('has-success');
+ }
+ });
+
+ $('input#cc_cvc').on('focusout', function (e) {
+ var cc_nbr = $(this).parents('.oe_cc').find('#cc_number').val();
+ var card_type = $.payment.cardType(cc_nbr);
+ var valid_value = $.payment.validateCardCVC(this.value, card_type);
+ console.log('Validating CVC', this.value, 'for card', cc_nbr, 'of type', card_type, 'and is valid:', valid_value);
+ if (valid_value) {
+ $(this).parent('.form-group').addClass('has-success');
+ $(this).parent('.form-group').removeClass('has-error');
+ }
+ else {
+ $(this).parent('.form-group').addClass('has-error');
+ $(this).parent('.form-group').removeClass('has-success');
+ }
+ });
+
+ $('input#cc_expiry').on('focusout', function (e) {
+ var expiry_value = $.payment.cardExpiryVal(this.value);
+ var month = expiry_value.month || '';
+ var year = expiry_value.year || '';
+ var valid_value = $.payment.validateCardExpiry(month, year);
+ console.log('Validating expiry', this.value, 'month', month, 'year', year, 'and is valid:', valid_value);
+ if (valid_value) {
+ $(this).parent('.form-group').addClass('has-success');
+ $(this).parent('.form-group').removeClass('has-error');
+ }
+ else {
+ $(this).parent('.form-group').addClass('has-error');
+ $(this).parent('.form-group').removeClass('has-success');
+ }
+ });
+
+});
diff --git a/addons/website_payment/views/website_payment_templates.xml b/addons/website_payment/views/website_payment_templates.xml
new file mode 100644
index 00000000000..bf6b151326f
--- /dev/null
+++ b/addons/website_payment/views/website_payment_templates.xml
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Paypal payment: form based
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Transfer payment
+
+
+
+
+
+
+
+
+
diff --git a/addons/website_sale/__openerp__.py b/addons/website_sale/__openerp__.py
index 634078fe149..f3e370d989d 100644
--- a/addons/website_sale/__openerp__.py
+++ b/addons/website_sale/__openerp__.py
@@ -9,7 +9,7 @@ OpenERP E-Commerce
""",
'author': 'OpenERP SA',
- 'depends': ['website', 'sale', 'product'],
+ 'depends': ['website', 'sale', 'product', 'payment_acquirer'],
'data': [
'website_sale_data.xml',
'views/website_sale.xml',
diff --git a/addons/website_sale/controllers/__init__.py b/addons/website_sale/controllers/__init__.py
index e11f9ba81bb..ef613dca3d5 100644
--- a/addons/website_sale/controllers/__init__.py
+++ b/addons/website_sale/controllers/__init__.py
@@ -1,3 +1,2 @@
+import website
import main
-
-# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index 80dc60e111f..cb44d16ec98 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -1,63 +1,13 @@
# -*- coding: utf-8 -*-
import random
-import uuid
import simplejson
-
-import werkzeug.exceptions
+import urllib
from openerp import SUPERUSER_ID
-from openerp.osv import osv
from openerp.addons.web import http
from openerp.addons.web.http import request
from openerp.addons.website.models import website
-def get_order(order_id=None):
- order_obj = request.registry.get('sale.order')
- # check if order allready exists and have access
- if order_id:
- try:
- order = order_obj.browse(request.cr, request.uid, order_id, context=request.context)
- order.pricelist_id
- if order:
- return order
- except:
- return False
-
- fields = [k for k, v in order_obj._columns.items()]
- order_value = order_obj.default_get(request.cr, SUPERUSER_ID, fields, context=request.context)
- if request.httprequest.session.get('ecommerce_pricelist'):
- order_value['pricelist_id'] = request.httprequest.session['ecommerce_pricelist']
- order_value['partner_id'] = request.registry.get('res.users').browse(request.cr, SUPERUSER_ID, request.uid, context=request.context).partner_id.id
- order_value.update(order_obj.onchange_partner_id(request.cr, SUPERUSER_ID, [], order_value['partner_id'], context=request.context)['value'])
-
- # add website_session_id key for access rules
- if not request.httprequest.session.get('website_session_id'):
- request.httprequest.session['website_session_id'] = str(uuid.uuid4())
-
- order_value["website_session_id"] = request.httprequest.session['website_session_id']
- order_id = order_obj.create(request.cr, SUPERUSER_ID, order_value, context=request.context)
- order = order_obj.browse(request.cr, SUPERUSER_ID, order_id, context=request.context)
- request.httprequest.session['ecommerce_order_id'] = order.id
-
- return order_obj.browse(request.cr, request.uid, order_id,
- context=dict(request.context, pricelist=order.pricelist_id.id))
-
-def get_current_order():
- if request.httprequest.session.get('ecommerce_order_id'):
- order = get_order(request.httprequest.session.get('ecommerce_order_id'))
- if not order:
- request.httprequest.session['ecommerce_order_id'] = False
- return order
- else:
- return False
-
-class Website(osv.osv):
- _inherit = "website"
- def preprocess_request(self, cr, uid, ids, request, context=None):
- request.context.update({
- 'website_sale_order': get_current_order(),
- })
- return super(Website, self).preprocess_request(cr, uid, ids, request, context=None)
class CheckoutInfo:
mandatory_billing_fields = ["name", "phone", "email", "street", "city", "country_id", "zip"]
@@ -89,6 +39,7 @@ class CheckoutInfo:
def from_post(self, post):
return dict((field_name, post[field_name]) for field_name in self.all_fields() if post[field_name])
+
class Ecommerce(http.Controller):
_order = 'website_sequence desc, website_published desc'
@@ -438,7 +389,7 @@ class Ecommerce(http.Controller):
request.httprequest.session['ecommerce_pricelist'] = pricelist_id
- order = get_current_order()
+ order = request.registry['website'].get_current_order(request.cr, request.uid, context=request.context)
if order:
values = {'pricelist_id': pricelist_id}
values.update(order.onchange_pricelist_id(pricelist_id, None)['value'])
@@ -450,9 +401,9 @@ class Ecommerce(http.Controller):
order_line_obj = request.registry.get('sale.order.line')
order_obj = request.registry.get('sale.order')
- order = get_current_order()
+ order = request.registry.get('website').get_current_order(request.cr, request.uid, context=request.context)
if not order:
- order = get_order()
+ order = request.registry.get('website')._get_order(request.cr, request.uid, context=request.context)
request.context = dict(request.context, pricelist=self.get_pricelist())
@@ -502,9 +453,15 @@ class Ecommerce(http.Controller):
@website.route(['/shop/mycart/'], type='http', auth="public", multilang=True)
def mycart(self, **post):
- order = get_current_order()
+ cr, uid, context = request.cr, request.uid, request.context
prod_obj = request.registry.get('product.product')
+ # must have a draft sale order with lines at this point, otherwise reset
+ order = context.get('website_sale_order')
+ if order and order.state != 'draft':
+ request.registry['website'].sale_reset_order(cr, uid, context=context)
+ return request.redirect('/shop/')
+
if 'promo' in post:
self.change_pricelist(post.get('promo'))
@@ -516,7 +473,7 @@ class Ecommerce(http.Controller):
product_ids.append(line.product_id.id)
suggested_ids = list(set(suggested_ids) - set(product_ids))
if suggested_ids:
- suggested_ids = prod_obj.search(request.cr, request.uid, [('id', 'in', suggested_ids)], context=request.context)
+ suggested_ids = prod_obj.search(cr, uid, [('id', 'in', suggested_ids)], context=context)
# select 3 random products
suggested_products = []
@@ -527,7 +484,7 @@ class Ecommerce(http.Controller):
values = {
'int': int,
'get_categories': self.get_categories,
- 'suggested_products': prod_obj.browse(request.cr, request.uid, suggested_products, request.context),
+ 'suggested_products': prod_obj.browse(cr, uid, suggested_products, context),
}
return request.website.render("website_sale.mycart", values)
@@ -539,7 +496,7 @@ class Ecommerce(http.Controller):
@website.route(['/shop/add_cart_json/'], type='json', auth="public")
def add_cart_json(self, product_id=None, order_line_id=None, remove=None):
quantity = self.add_product_to_cart(product_id=product_id, order_line_id=order_line_id, number=(remove and -1 or 1))
- order = get_current_order()
+ order = self.registry['website'].get_current_order(request.cr, request.uid, context=request.context)
return [quantity, order.get_total_quantity(), order.amount_total, request.website.render("website_sale.total", {
'website_sale_order': order
}).strip()]
@@ -552,10 +509,15 @@ class Ecommerce(http.Controller):
def checkout(self, **post):
cr, uid, context, registry = request.cr, request.uid, request.context, request.registry
- order = get_current_order()
-
+ # must have a draft sale order with lines at this point, otherwise reset
+ order = context.get('website_sale_order')
if not order or order.state != 'draft' or not order.order_line:
- return self.mycart(**post)
+ request.registry['website'].sale_reset_order(cr, uid, context=context)
+ return request.redirect('/shop/')
+ # if transaction pending / done: redirect to confirmation
+ tx = context.get('website_sale_transaction')
+ if tx and tx.state != 'draft':
+ return request.redirect('/shop/payment/confirmation/%s' % order.id)
orm_partner = registry.get('res.partner')
orm_user = registry.get('res.users')
@@ -603,10 +565,15 @@ class Ecommerce(http.Controller):
def confirm_order(self, **post):
cr, uid, context, registry = request.cr, request.uid, request.context, request.registry
- order = get_current_order()
-
+ # must have a draft sale order with lines at this point, otherwise redirect to shop
+ order = request.registry['website'].get_current_order(request.cr, request.uid, context=request.context)
if not order or order.state != 'draft' or not order.order_line:
- return self.mycart(**post)
+ request.registry['website'].sale_reset_order(cr, uid, context=context)
+ return request.redirect('/shop/')
+ # if transaction pending / done: redirect to confirmation
+ tx = context.get('website_sale_transaction')
+ if tx and tx.state != 'draft':
+ return request.redirect('/shop/payment/confirmation/%s' % order.id)
orm_parter = registry.get('res.partner')
orm_user = registry.get('res.users')
@@ -646,7 +613,7 @@ class Ecommerce(http.Controller):
company_id = (company_ids and company_ids[0]) or orm_parter.create(cr, SUPERUSER_ID, {'name': company_name, 'is_company': True}, context)
billing_info = dict(checkout)
- billing_info['parent_id'] = company_id;
+ billing_info['parent_id'] = company_id
if not context['is_public_user']:
partner_id = orm_user.browse(cr, uid, uid, context=context).partner_id.id
@@ -662,6 +629,7 @@ class Ecommerce(http.Controller):
'street': post['shipping_street'],
'city': post['shipping_city'],
'name': post['shipping_name'],
+ 'email': post['email'],
'type': 'delivery',
'parent_id': partner_id,
'country_id': post['shipping_country_id'],
@@ -690,31 +658,183 @@ class Ecommerce(http.Controller):
@website.route(['/shop/payment/'], type='http', auth="public", multilang=True)
def payment(self, **post):
- order = get_current_order()
+ """ Payment step. This page proposes several payment means based on available
+ payment.acquirer. State at this point :
- if not order or not order.order_line:
- return self.mycart(**post)
+ - a draft sale order with lines; otherwise, clean context / session and
+ back to the shop
+ - no transaction in context / session, or only a draft one, if the customer
+ did go to a payment.acquirer website but closed the tab without
+ paying / canceling
+ """
+ cr, uid, context = request.cr, request.uid, request.context
+ payment_obj = request.registry.get('payment.acquirer')
+
+ # if no sale order at this stage: back to checkout beginning
+ order = context.get('website_sale_order')
+ if not order or not order.state == 'draft' or not order.order_line:
+ request.registry['website'].sale_reset_order(cr, uid, context=context)
+ return request.redirect("/shop/")
+ # alread a transaction: forward to confirmation
+ tx = context.get('website_sale_transaction')
+ if tx and not tx.state == 'draft':
+ print 'embetatn'
+ # return request.redirect('/shop/confirmation/%s' % order.id)
+
+ partner_id = False
+ shipping_partner_id = False
+ if order:
+ if order.partner_id.id:
+ partner_id = order.partner_id.id
+ shipping_partner_id = order.partner_id.id
+ if order.partner_shipping_id.id:
+ shipping_partner_id = order.partner_shipping_id.id
values = {
- 'partner': False,
- 'order': order,
+ 'partner': partner_id,
+ 'order': order
}
- values.update( request.registry.get('sale.order')._get_website_data(request.cr, request.uid, order, request.context) )
+ values.update(request.registry.get('sale.order')._get_website_data(cr, uid, order, context))
- payment_obj = request.registry.get('portal.payment.acquirer')
- payment_ids = payment_obj.search(request.cr, SUPERUSER_ID, [('visible', '=', True)], context=request.context)
- values['payments'] = payment_obj.browse(request.cr, SUPERUSER_ID, payment_ids, request.context)
- for payment in values['payments']:
- content = payment_obj.render(request.cr, SUPERUSER_ID, payment.id, order, order.name, order.pricelist_id.currency_id, order.amount_total)
- payment._content = content
+ # fetch all registered payment means
+ if tx:
+ payment_ids = [tx.acquirer_id.id]
+ else:
+ payment_ids = payment_obj.search(cr, SUPERUSER_ID, [('portal_published', '=', True)], context=context)
+ values['payments'] = payment_obj.browse(cr, uid, payment_ids, context=context)
+ for pay in values['payments']:
+ pay._content = payment_obj.render(
+ cr, uid, pay.id,
+ order.name,
+ order.amount_total,
+ order.pricelist_id.currency_id,
+ partner_id=shipping_partner_id,
+ tx_custom_values={
+ 'return_url': '/shop/payment/validate',
+ },
+ context=context)
return request.website.render("website_sale.payment", values)
- @website.route(['/shop/payment_validate/'], type='http', auth="public", multilang=True)
- def payment_validate(self, **post):
- request.httprequest.session['ecommerce_order_id'] = False
- request.httprequest.session['ecommerce_pricelist'] = False
- return request.redirect("/shop/")
+ @website.route(['/shop/payment/transaction/'],
+ type='http', methods=['POST'], auth="public")
+ def payment_transaction(self, acquirer_id, **post):
+ """ Hook method that creates a payment.transaction and redirect to the
+ acquirer, using post values to re-create the post action.
+
+ :param int acquirer_id: id of a payment.acquirer record. If not set the
+ user is redirected to the checkout page
+ :param dict post: should coutain all post data for the acquirer
+ """
+ # @TDEFIXME: don't know why we received those data, but should not be send to the acquirer
+ post.pop('submit.x', None)
+ post.pop('submit.y', None)
+ cr, uid, context = request.cr, request.uid, request.context
+ payment_obj = request.registry.get('payment.acquirer')
+ transaction_obj = request.registry.get('payment.transaction')
+ order = request.registry['website'].get_current_order(request.cr, request.uid, context=request.context)
+
+ if not order or not order.order_line or acquirer_id is None:
+ return request.redirect("/shop/checkout/")
+
+ # find an already existing transaction
+ tx = context.get('website_sale_transaction')
+ if not tx:
+ tx_id = transaction_obj.create(cr, uid, {
+ 'acquirer_id': acquirer_id,
+ 'type': 'form',
+ 'amount': order.amount_total,
+ 'currency_id': order.pricelist_id.currency_id.id,
+ 'partner_id': order.partner_id.id,
+ 'reference': order.name,
+ 'sale_order_id': order.id,
+ }, context=context)
+ request.httprequest.session['website_sale_transaction_id'] = tx_id
+ elif tx and tx.state == 'draft': # button cliked but no more info -> rewrite on tx or create a new one ?
+ tx.write({
+ 'acquirer_id': acquirer_id,
+ })
+
+ acquirer_form_post_url = payment_obj.get_form_action_url(cr, uid, acquirer_id, context=context)
+ acquirer_total_url = '%s?%s' % (acquirer_form_post_url, urllib.urlencode(post))
+ return request.redirect(acquirer_total_url)
+
+ @website.route('/shop/payment/get_status/', type='json', auth="public", multilang=True)
+ def payment_get_status(self, order, **post):
+ cr, uid, context = request.cr, request.uid, request.context
+ if not order:
+ return {
+ 'state': 'error',
+ }
+
+ tx_ids = request.registry['payment.transaction'].search(
+ cr, uid, [
+ '|', ('sale_order_id', '=', order.id), ('reference', '=', order.name)
+ ], context=context)
+ if not tx_ids:
+ return {
+ 'state': 'error'
+ }
+ tx = request.registry['payment.transaction'].browse(cr, uid, tx_ids[0], context=context)
+ return {
+ 'state': tx.state,
+ }
+
+ @website.route('/shop/payment/validate/', type='http', auth="public", multilang=True)
+ def payment_validate(self, transaction_id=None, sale_order_id=None, **post):
+ """ Method that should be called by the server when receiving an update
+ for a transaction. State at this point :
+
+ - UDPATE ME
+ """
+ cr, uid, context = request.cr, request.uid, request.context
+ email_act = None
+ sale_order_obj = request.registry['sale.order']
+
+ if transaction_id is None:
+ tx = context.get('website_sale_transaction')
+ else:
+ tx = request.registry['payment.transaction'].browse(cr, uid, transaction_id, context=context)
+
+ if sale_order_id is None:
+ order = context.get('website_sale_order')
+ else:
+ order = request.registry['sale.order'].browse(cr, uid, sale_order_id, context=context)
+
+ if tx.state == 'done':
+ # confirm the quotation
+ sale_order_obj.action_button_confirm(cr, SUPERUSER_ID, [order.id], context=request.context)
+ # send by email
+ email_act = sale_order_obj.action_quotation_send(cr, SUPERUSER_ID, [order.id], context=request.context)
+ elif tx.state == 'pending':
+ # send by email
+ email_act = sale_order_obj.action_quotation_send(cr, SUPERUSER_ID, [order.id], context=request.context)
+ elif tx.state == 'cancel':
+ # cancel the quotation
+ sale_order_obj.action_cancel(cr, SUPERUSER_ID, [order.id], context=request.context)
+
+ if email_act:
+ create_ctx = email_act.get('context', context)
+ compose_id = request.registry['mail.compose.message'].create(cr, uid, {}, context=create_ctx)
+ request.registry['mail.compose.message'].send_mail(cr, uid, [compose_id], context=create_ctx)
+
+ # clean context and session, then redirect to the confirmation page
+ request.registry['website'].sale_reset_order(cr, uid, context=context)
+
+ return request.redirect('/shop/confirmation/%s' % order.id)
+
+ @website.route(['/shop/confirmation/'], type='http', auth="public", multilang=True)
+ def payment_confirmation(self, order, **post):
+ """ End of checkout process controller. Confirmation is basically seing
+ the status of a sale.order. State at this point :
+
+ - should not have any context / session info: clean them
+ - take a sale.order id, because we request a sale.order and are not
+ session dependant anymore
+ """
+ cr, uid, context = request.cr, request.uid, request.context
+
+ return request.website.render("website_sale.confirmation", {'order': order})
@website.route(['/shop/change_sequence/'], type='json', auth="public")
def change_sequence(self, id, top):
diff --git a/addons/website_sale/controllers/website.py b/addons/website_sale/controllers/website.py
new file mode 100644
index 00000000000..949b2263617
--- /dev/null
+++ b/addons/website_sale/controllers/website.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+import uuid
+
+from openerp import SUPERUSER_ID
+from openerp.osv import osv
+from openerp.addons.web.http import request
+
+
+class Website(osv.Model):
+ _inherit = "website"
+
+ def _get_order(self, cr, uid, order_id=None, context=None):
+ order_obj = request.registry.get('sale.order')
+ # check if order allready exists and have access
+ if order_id:
+ if not order_id in order_obj.exists(cr, uid, [order_id], context=context):
+ return False
+ try:
+ order = order_obj.browse(cr, uid, order_id, context=context)
+ if order:
+ return order
+ except:
+ return False
+
+ fields = [k for k, v in order_obj._columns.items()]
+ order_value = order_obj.default_get(cr, SUPERUSER_ID, fields, context=context)
+ if request.httprequest.session.get('ecommerce_pricelist'):
+ order_value['pricelist_id'] = request.httprequest.session['ecommerce_pricelist']
+ order_value['partner_id'] = request.registry.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
+ order_value.update(order_obj.onchange_partner_id(cr, SUPERUSER_ID, [], order_value['partner_id'], context=context)['value'])
+
+ # add website_session_id key for access rules
+ if not request.httprequest.session.get('website_session_id'):
+ request.httprequest.session['website_session_id'] = str(uuid.uuid4())
+
+ order_value["website_session_id"] = request.httprequest.session['website_session_id']
+ order_id = order_obj.create(cr, SUPERUSER_ID, order_value, context=context)
+ order = order_obj.browse(cr, SUPERUSER_ID, order_id, context=context)
+ request.httprequest.session['ecommerce_order_id'] = order.id
+
+ return order_obj.browse(cr, uid, order_id,
+ context=dict(request.context, pricelist=order.pricelist_id.id))
+
+ def get_current_order(self, cr, uid, context=None):
+ if request.httprequest.session.get('ecommerce_order_id'):
+ order = self._get_order(cr, uid, order_id=request.httprequest.session['ecommerce_order_id'], context=context)
+ if not order:
+ request.httprequest.session['ecommerce_order_id'] = False
+ return order
+ return False
+
+ def _get_transaction(self, cr, uid, tx_id=None, context=None):
+ transaction_obj = request.registry['payment.transaction']
+ if tx_id:
+ tx_ids = transaction_obj.search(cr, uid, [('id', '=', tx_id), ('state', 'not in', ['cancel'])], context=context)
+ if tx_ids:
+ return transaction_obj.browse(cr, uid, tx_ids[0], context=context)
+ return False
+
+ def get_current_transaction(self, cr, uid, context=None):
+ if request.httprequest.session.get('website_sale_transaction_id'):
+ tx = self._get_transaction(cr, uid, tx_id=request.httprequest.session['website_sale_transaction_id'], context=context)
+ if not tx:
+ request.httprequest.session['website_sale_transaction_id'] = False
+ return tx
+ return False
+
+ def sale_reset_order(self, cr, uid, context=None):
+ request.httprequest.session.update({
+ 'ecommerce_order_id': False,
+ 'ecommerce_pricelist': False,
+ 'website_sale_transaction_id': False,
+ })
+ request.context.update({
+ 'website_sale_order': False,
+ 'website_sale_transaction': False,
+ })
+
+ def preprocess_request(self, cr, uid, ids, request, context=None):
+ request.context.update({
+ 'website_sale_order': self.get_current_order(cr, uid, context=context),
+ 'website_sale_transaction': self.get_current_transaction(cr, uid, context=context)
+ })
+ return super(Website, self).preprocess_request(cr, uid, ids, request, context=None)
diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py
index 83ede423d37..3193b41a4fd 100644
--- a/addons/website_sale/models/website.py
+++ b/addons/website_sale/models/website.py
@@ -2,6 +2,7 @@
from openerp.osv import orm, fields
from openerp.addons.web import http
+
class Website(orm.Model):
_inherit = 'website'
@@ -16,3 +17,12 @@ class Website(orm.Model):
'pricelist_id': fields.function(
_get_pricelist, type='many2one', obj='product.pricelist')
}
+
+
+class PaymentTransaction(orm.Model):
+ _inherit = 'payment.transaction'
+
+ _columns = {
+ # link with the sale order
+ 'sale_order_id': fields.many2one('sale.order', 'Sale Order'),
+ }
diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js
index 1c9dca65730..856c8bd1aa6 100644
--- a/addons/website_sale/static/src/js/website_sale.js
+++ b/addons/website_sale/static/src/js/website_sale.js
@@ -70,11 +70,11 @@ $(document).ready(function () {
$('form.js_add_cart_json label').on('mouseup', function (ev) {
ev.preventDefault();
var $label = $(ev.currentTarget);
- var $price = $label.parent("form").find(".oe_price");
+ var $price = $label.parent("form").find(".oe_price .oe_currency_value");
if (!$price.data("price")) {
- $price.data("price", parseFloat($price.html()));
+ $price.data("price", parseFloat($price.text()));
}
- $price.html($price.data("price")+parseFloat($label.find(".badge span").html() || 0));
+ $price.html($price.data("price")+parseFloat($label.find(".badge span").text() || 0));
});
diff --git a/addons/website_sale/static/src/js/website_sale_payment.js b/addons/website_sale/static/src/js/website_sale_payment.js
new file mode 100644
index 00000000000..3da753c03f9
--- /dev/null
+++ b/addons/website_sale/static/src/js/website_sale_payment.js
@@ -0,0 +1,14 @@
+$(document).ready(function () {
+
+ /* Hitting the payment button: payment transaction process begins
+ * We redirect the user to a custom shop page in oder to create the
+ * transaction. The form POST data will be used to perform the post
+ * query.
+ */
+ $('input#payment_submit').on('click', function (ev) { // TDEFIXME: change input#ID to input inside payment form, less strict
+ var acquirer_id = $(this).closest('form').closest('div.oe_payment_acquirer').data().id || 0;
+ var form_action = $(this).closest("form").attr('action');
+ console.log('cliking on submit for payment - redirecting from', form_action, 'to shop with acqurier_id', acquirer_id);
+ $(this).closest("form").attr("action", '/shop/payment/transaction/' + acquirer_id);
+ });
+});
diff --git a/addons/website_sale/static/src/js/website_sale_validate.js b/addons/website_sale/static/src/js/website_sale_validate.js
new file mode 100644
index 00000000000..2543d2e3c55
--- /dev/null
+++ b/addons/website_sale/static/src/js/website_sale_validate.js
@@ -0,0 +1,36 @@
+$(document).ready(function () {
+
+ var _poll_nbr = 0;
+
+ function payment_transaction_poll_status() {
+ var order_node = $('div.oe_website_sale_tx_status');
+ if (! order_node || order_node.data('orderId') === undefined) {
+ return;
+ }
+ var order_id = order_node.data('orderId');
+ return openerp.jsonRpc('/shop/payment/get_status/' + order_id, 'call', {
+ }).then(function (result) {
+ var tx_node = $('div.oe_website_sale_tx_status');
+ var txt = 'Your transaction is waiting confirmation.
';
+ _poll_nbr += 1;
+ if (result.state == 'pending' && _poll_nbr <= 5) {
+ txt = "Your transaction is waiting confirmation.
";
+ setTimeout(function () {
+ payment_transaction_poll_status();
+ }, 1000);
+ }
+ else if (result.state == 'done') {
+ txt = "Your payment has been received.
";
+ }
+ else if (result.state == 'pending') {
+ txt = "Your transaction is waiting confirmation. You may try to refresh this page.
";
+ }
+ else if (result.state == 'cancel') {
+ txt = "The payment seems to have been canceled.
";
+ }
+ tx_node.html(txt);
+ });
+ }
+
+ payment_transaction_poll_status();
+});
diff --git a/addons/website_sale/views/website_sale.xml b/addons/website_sale/views/website_sale.xml
index e9d014960b9..d30be42a3ca 100644
--- a/addons/website_sale/views/website_sale.xml
+++ b/addons/website_sale/views/website_sale.xml
@@ -207,7 +207,7 @@
-
+
@@ -274,7 +274,7 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Shop - Confirmed
+
+
+
+
+ - Review Order
+ - Shipping & Billing
+ - Payment
+ - Confirmation
+
+
Order Confirmed
+
+
+
Thank you for your order.
+
+
+
+
+
+
+
Bill To:
+
+
+
Ship To:
+
+
+
Amount:
+
+