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 (Test) + /payment/paypal/test + + 90 + + + + Ogone (Test) + /payment/ogone/test + + 91 + + + + Transfer (Test) + /payment/transfer/test + + 92 + + + + + + + + + + + + + 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 @@ + +