[ADD] payment_acquirer, transfer, ogone and paypal integration into ecommerce

[ADD] payment_acquirer module, holding :
- payment.acquirer model: models the acquirer with the various credentials (e.g. paypal, ogone). payment_acquirer only contains basic fields. Each acquirer can add fields through inheritance. See payment.acquirer model for more details.
- payment.transaction model: models the transaction itself. Inheritance is done by calling custom methods prefixed by the acquirer name. See payment.transaction model for more details.

[ADD] payment_acquirer_ogone, payment_acquirer_paypal and payment_acquirer_transfer modules. Those are specific implementations for ogone, paypal and basic money transfer. They contain dummy account data, to avoid committing real accounts. Will have to be cleaned before final merge. Those module contain tests but some tests will not work without real account creadentials. To be cleaned.

[ADD] website_payment: a test module for payment. Will have to be cleaned.

[IMP] website_sale: added support of payments in the checkout process. Improved checkout process. Confirmation is now basically a view on a sale.order, and a polling on a transaction to see its status. Context / Session are better managed, but still require some cleaning. Also fixed some links in the shop.

[TODO]
- some cleaning in paypal / ogone about form methods
- improve some tests
- clean checkout process after more tests
- cleaning in ogone server2server + tests

bzr revid: tde@openerp.com-20131121134140-3somglek7dk1o6t1
This commit is contained in:
Thibault Delavallée 2013-11-21 14:41:40 +01:00
commit 53252152bc
87 changed files with 4088 additions and 99 deletions

View File

@ -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)',

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import models

View File

@ -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,
}

View File

@ -0,0 +1,9 @@
.. _changelog:
Changelog
=========
`trunk (saas-3)`
----------------
- Module creation

View File

@ -0,0 +1,14 @@
Payment module documentation
============================
Payment documentation topics
''''''''''''''''''''''''''''
Changelog
'''''''''
.. toctree::
:maxdepth: 1
changelog.rst

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import payment_acquirer

View File

@ -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='<name>' 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:
- ``<name>_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.
- ``<name>_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="<name>"' attribute, then it
required if record.name is <name>. """
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:
- ``<name>_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:
- ``<name>_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:
- ``<name>_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

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 payment_acquirer_all payment.acquirer.all model_payment_acquirer 1 0 0 0
3 payment_acquirer_user payment.acquirer.user model_payment_acquirer base.group_user 1 1 1 0
4 payment_acquirer_system payment.acquirer.system model_payment_acquirer base.group_system 1 1 1 1
5 payment_transaction_all payment.transaction.all model_payment_transaction 1 1 1 0
6 payment_transaction_user payment.transaction.user model_payment_transaction base.group_user 1 1 1 0
7 payment_transaction_system payment.transaction.system model_payment_transaction base.group_system 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import common

View File

@ -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,
}
)

View File

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<menuitem
name='Payments'
id='root_payment_menu'
parent='base.menu_administration'/>
<record id="acquirer_form" model="ir.ui.view">
<field name="name">acquirer.form</field>
<field name="model">payment.acquirer</field>
<field name="arch" type="xml">
<form string="Payment Acquirer" version="7.0">
<group name="acquirer_base">
<field name="name"/>
<field name="portal_published"/>
<field name="env"/>
<field name="message"/>
<label for="view_template_id"/>
<div>
<field name="view_template_id" nolabel="1"/>
<div>
This is an HTML form template to submit a payment through this acquirer.
The template will be rendered with qWeb, so it may use qWeb expressions.
The qWeb evaluation context provides:
<ul>
<li>acquirer: payment.acquirer browse record</li>
<li>user: current user browse record</li>
<li>reference: the transaction reference number</li>
<li>currency: the transaction currency browse record</li>
<li>amount: the transaction amount, a float</li>
<li>partner: the buyer partner browse record, not necessarily set</li>
<li>partner_values: specific values about the buyer, for example coming from a shipping form</li>
<li>tx_values: specific transaction values</li>
<li>context: the current context dictionary</li>
</ul>
</div>
</div>
</group>
</form>
</field>
</record>
<record id="acquirer_list" model="ir.ui.view">
<field name="model">payment.acquirer</field>
<field name="arch" type="xml">
<tree string="Payment Acquirers">
<field name="name"/>
<field name="portal_published"/>
<field name="env"/>
</tree>
</field>
</record>
<record id="acquirer_search" model="ir.ui.view">
<field name="model">payment.acquirer</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
</search>
</field>
</record>
<record id="action_payment_acquirer" model="ir.actions.act_window">
<field name="name">Payment Acquirers</field>
<field name="res_model">payment.acquirer</field>
<field name='view_type'>form</field>
<field name='view_mode'>tree,form</field>
</record>
<menuitem
action='action_payment_acquirer'
id='payment_acquirer_menu'
parent='root_payment_menu'
sequence='10' />
<record id="transaction_form" model="ir.ui.view">
<field name="model">payment.transaction</field>
<field name="arch" type="xml">
<form string="Payment Transactions" version="7.0">
<sheet>
<group>
<group>
<field name="reference"/>
<field name="amount"/>
<field name="currency_id"/>
<field name="partner_id"/>
<field name="partner_name"/>
<field name="partner_email"/>
</group>
<group>
<field name="date_create"/>
<field name="date_validate"/>
<field name="acquirer_id"/>
<field name="state"/>
<field name="state_message"/>
</group>
</group>
<notebook>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/>
<field name="message_ids" widget="mail_thread" options='{"thread_level": 1}' placeholder="Send a message to the group"/>
</div>
</form>
</field>
</record>
<record id="transaction_list" model="ir.ui.view">
<field name="model">payment.transaction</field>
<field name="arch" type="xml">
<tree string="Payment Transactions">
<field name="reference"/>
<field name="acquirer_id"/>
<field name="partner_id"/>
<field name="partner_name"/>
<field name="state"/>
</tree>
</field>
</record>
<record id="transaction" model="ir.ui.view">
<field name="model">payment.transaction</field>
<field name="arch" type="xml">
<search>
<field name="reference"/>
<field name="acquirer_id"/>
<field name="partner_id"/>
<field name="partner_name"/>
</search>
</field>
</record>
<record id="action_payment_transaction" model="ir.actions.act_window">
<field name="name">Payment Transactions</field>
<field name="res_model">payment.transaction</field>
<field name='view_type'>form</field>
<field name='view_mode'>tree,form</field>
</record>
<menuitem
action='action_payment_transaction'
id='payment_transaction_menu'
parent='root_payment_menu'
sequence='20' />
</data>
</openerp>

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import models
import controllers

View File

@ -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,
}

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import main

View File

@ -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', '/'))

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import ogone

View File

@ -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': "payers account number format error",
'60001004': "payers account at different bank",
'60001005': "payees account at different bank",
'60001006': "payees account number format error",
'60001007': "payers account number blocked",
'60001008': "payers account number expired",
'60001009': "payees account number expired",
'60001010': "direct debit not possible",
'60001011': "creditor payment not possible",
'60001012': "payers account number unknown WKA-number",
'60001013': "payees 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': "payers account number not numeric",
'60001036': "payers account number not eleven-proof",
'60001037': "payers account number not issued",
'60001039': "payers account number of DNB/BGC/BLA",
'60001040': "payees account number not numeric",
'60001041': "payees account number not eleven-proof",
'60001042': "payees account number not issued",
'60001044': "payees account number unknown",
'60001050': "payees name missing",
'60001051': "indicate payees 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': "payers name missing",
'60001058': "payees account number missing",
'60001059': "restore not permitted",
'60001060': "banks 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': "payers 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 partys 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': "Payers 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
]

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<record id="payment_acquirer_ogone" model="payment.acquirer">
<field name="name">ogone</field>
<field name="view_template_id" ref="ogone_acquirer_button"/>
<field name="env">test</field>
<field name='ogone_pspid'>dummy</field>
<field name='ogone_userid'>dummy</field>
<field name='ogone_password'>dummy</field>
<field name="ogone_shakey_in">tINY4Yv14789gUix1130</field>
<field name="ogone_shakey_out">tINYj885Tfvd4P471464</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,9 @@
.. _changelog:
Changelog
=========
`trunk (saas-3)`
----------------
- Module creation

View File

@ -0,0 +1,14 @@
Payment module documentation
============================
Payment documentation topics
''''''''''''''''''''''''''''
Changelog
'''''''''
.. toctree::
:maxdepth: 1
changelog.rst

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import ogone

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
from . import test_ogone
checks = [
test_ogone,
]

View File

@ -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'
# }

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<template id="ogone_acquirer_button">
<form t-if="acquirer" t-att-action="acquirer.ogone_standard_order_url" method="post" target="_self">
<!-- seller -->
<input type='hidden' name='PSPID' t-att-value='tx_values["PSPID"]'/>
<input type='hidden' name='ORDERID' t-att-value='tx_values["ORDERID"]'/>
<!-- cart -->
<input type='hidden' name='AMOUNT' t-att-value='tx_values["AMOUNT"]'/>
<input type='hidden' name='CURRENCY' t-att-value='tx_values["CURRENCY"]'/>
<!-- buyer -->
<input type='hidden' name='LANGUAGE' t-att-value='tx_values["LANGUAGE"]'/>
<input type='hidden' name='CN' t-att-value='tx_values["CN"]'/>
<input type='hidden' name='EMAIL' t-att-value='tx_values["EMAIL"]'/>
<input type='hidden' name='OWNERZIP' t-att-value='tx_values["OWNERZIP"]'/>
<input type='hidden' name='OWNERADDRESS' t-att-value='tx_values["OWNERADDRESS"]'/>
<input type='hidden' name='OWNERCTY' t-att-value='tx_values["OWNERCTY"]'/>
<input type='hidden' name='OWNERTOWN' t-att-value='tx_values["OWNERTOWN"]'/>
<input type='hidden' name='OWNERTELNO' t-att-value='tx_values["OWNERTELNO"]'/>
<!-- before payment verification -->
<input type='hidden' name='SHASIGN' t-att-value='tx_values["SHASIGN"]'/>
<!-- look and print -->
<!-- <input type='hidden' name='TITLE' t-att-value='tx_dict["currency_name"]'/>
<input type='hidden' name='BGCOLOR' t-att-value='tx_dict["currency_name"]'/>
<input type='hidden' name='TXTCOLOR' t-att-value='tx_dict["currency_name"]'/>
<input type='hidden' name='TBLBGCOLOR' t-att-value='tx_dict["currency_name"]'/>
<input type='hidden' name='TBLTXTCOLOR' t-att-value='tx_dict["currency_name"]'/>
<input type='hidden' name='BUTTONBGCOLOR' t-att-value='tx_dict["currency_name"]'/>
<input type='hidden' name='BUTTONTXTCOLOR' t-att-value='tx_dict["currency_name"]'/>
<input type='hidden' name='LOGO' t-att-value='tx_dict["currency_name"]'/>
<input type='hidden' name='FONTTYPE' t-att-value='tx_dict["currency_name"]'/> -->
<!-- after payment parameters -->
<t t-if='tx_values.get("PARAMPLUS")'>
<input type='hidden' name="PARAMPLUS" t-att-value='tx_values["PARAMPLUS"]'/>
</t>
<!-- redirection -->
<input type='hidden' name='ACCEPTURL' t-att-value='tx_values["ACCEPTURL"]'/>
<input type='hidden' name='DECLINEURL' t-att-value='tx_values["DECLINEURL"]'/>
<input type='hidden' name='EXCEPTIONURL' t-att-value='tx_values["EXCEPTIONURL"]'/>
<input type='hidden' name='CANCELURL' t-att-value='tx_values["CANCELURL"]'/>
<input type="image" name="submit" id="payment_submit"
src="/payment_acquirer_ogone/static/src/img/ogone_logo_plain.gif"/>
</form>
</template>
</data>
</openerp>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="acquirer_form_ogone" model="ir.ui.view">
<field name="name">acquirer.form.ogone</field>
<field name="model">payment.acquirer</field>
<field name="inherit_id" ref="payment_acquirer.acquirer_form"/>
<field name="arch" type="xml">
<xpath expr='//group[@name="acquirer_base"]' position='after'>
<group string="Ogone Details"
attrs="{'invisible': [('name', '!=', 'ogone')]}">
<field name="ogone_pspid"/>
<field name="ogone_userid"/>
<field name="ogone_password"/>
<field name="ogone_shakey_in"/>
<field name="ogone_shakey_out"/>
</group>
</xpath>
</field>
</record>
<record id="transaction_form_ogone" model="ir.ui.view">
<field name="name">acquirer.transaction.form.ogone</field>
<field name="model">payment.transaction</field>
<field name="inherit_id" ref="payment_acquirer.transaction_form"/>
<field name="arch" type="xml">
<xpath expr='//notebook' position='inside'>
<page string="Ogone TX Details">
<group>
<field name="ogone_payid"/>
<!-- <field name="ogone_3ds"/>
<field name="ogone_3ds_html"/> -->
</group>
</page>
</xpath>
</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import models
import controllers

View File

@ -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,
}

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import main

View File

@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
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)

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<record id="payment_acquirer_paypal" model="payment.acquirer">
<field name="name">paypal</field>
<field name="view_template_id" ref="paypal_acquirer_button"/>
<field name="env">test</field>
<field name="paypal_tx_url">https://www.sandbox.paypal.com/cgi-bin/webscr</field>
<field name="paypal_email_id">dummy</field>
<field name="paypal_username">dummy</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,9 @@
.. _changelog:
Changelog
=========
`trunk (saas-3)`
----------------
- Module creation

View File

@ -0,0 +1,14 @@
Payment module documentation
============================
Payment documentation topics
''''''''''''''''''''''''''''
Changelog
'''''''''
.. toctree::
:maxdepth: 1
changelog.rst

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import paypal

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
from . import test_paypal
checks = [
test_paypal,
]

View File

@ -0,0 +1,155 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2012-TODAY OpenERP S.A. <http://openerp.com>
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
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&notify_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&notify_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'
# }

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="acquirer_form_paypal" model="ir.ui.view">
<field name="name">acquirer.form.paypal</field>
<field name="model">payment.acquirer</field>
<field name="inherit_id" ref="payment_acquirer.acquirer_form"/>
<field name="arch" type="xml">
<xpath expr='//group[@name="acquirer_base"]' position='after'>
<group string="Paypal Details"
attrs="{'invisible': [('name', '!=', 'paypal')]}">
<field name="paypal_email_id"/>
<field name="paypal_username"/>
<field name="paypal_password"/>
<field name="paypal_signature"/>
<field name="paypal_use_ipn"/>
</group>
</xpath>
</field>
</record>
<record id="transaction_form_paypal" model="ir.ui.view">
<field name="name">acquirer.transaction.form.paypal</field>
<field name="model">payment.transaction</field>
<field name="inherit_id" ref="payment_acquirer.transaction_form"/>
<field name="arch" type="xml">
<xpath expr='//notebook' position='inside'>
<page string="Paypal TX Details">
<group>
<field name="paypal_txn_id"/>
<field name="paypal_txn_type"/>
</group>
</page>
</xpath>
</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<template id="paypal_acquirer_button">
<form t-if="acquirer.paypal_email_id" t-att-action="acquirer.paypal_tx_url" method="post" target="_self">
<input type="hidden" name="cmd" t-att-value="tx_values['cmd']"/>
<input type="hidden" name="business" t-att-value="tx_values['business']"/>
<input type="hidden" name="item_name" t-att-value="tx_values['item_name']"/>
<input type="hidden" name="item_number" t-att-value="tx_values['item_number']"/>
<input type="hidden" name="amount" t-att-value="tx_values['amount']"/>
<input type="hidden" name="currency_code" t-att-value="tx_values['currency_code']"/>
<!-- partner / address data -->
<input type="hidden" name="address1" t-att-value="tx_values['address1']"/>
<input type="hidden" name="city" t-att-value="tx_values['city']"/>
<input type="hidden" name="country" t-att-value="tx_values['country']"/>
<input type="hidden" name="email" t-att-value="tx_values['email']"/>
<input type="hidden" name="first_name" t-att-value="tx_values['first_name']"/>
<input type="hidden" name="last_name" t-att-value="tx_values['last_name']"/>
<input type="hidden" name="zip" t-att-value="tx_values['zip']"/>
<!-- after payment parameters -->
<t t-if='tx_values.get("custom")'>
<input type='hidden' name="custom" t-att-value='tx_values["custom"]'/>
</t>
<!-- URLs -->
<input t-if="acquirer.paypal_use_dpn" type='hidden' name='return'
t-att-value="tx_values['return']"/>
<input t-if="acquirer.paypal_use_ipn" type='hidden' name='notify_url'
t-att-value="tx_values['notify_url']"/>
<input t-if="tx_values['cancel_return']" type="hidden" name="cancel_return"
t-att-value="tx_values['cancel_return']"/>
<!-- button -->
<input type="image" name="submit" id="payment_submit"
width="100px"
src="/payment_acquirer_paypal/static/src/img/paypal_logo.png"/>
</form>
</template>
</data>
</openerp>

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import models
import controllers

View File

@ -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,
}

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import main

View File

@ -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', '/'))

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="payment_acquirer_transfer" model="payment.acquirer">
<field name="name">transfer</field>
<field name="view_template_id" ref="transfer_acquirer_button"/>
<field name="env">test</field>
<field name="portal_published" eval="True"/>
<field name="message"><![CDATA[
<p>Please use the account 001-002-003 to make the payment.</p>]]>
</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,9 @@
.. _changelog:
Changelog
=========
`trunk (saas-3)`
----------------
- Module creation

View File

@ -0,0 +1,14 @@
Payment module documentation
============================
Payment documentation topics
''''''''''''''''''''''''''''
Changelog
'''''''''
.. toctree::
:maxdepth: 1
changelog.rst

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import payment_acquirer

View File

@ -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'})

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<template id="transfer_acquirer_button">
<div t-field="acquirer.message"/>
<form t-if="acquirer" action="/payment/transfer/feedback" method="post" target="_self">
<t t-if="tx_values.get('return_url')">
<input type='hidden' name='return_url' t-att-value='tx_values["return_url"]'/>
</t>
<input type='hidden' name='reference' t-att-value='reference'/>
<input type='hidden' name='amount' t-att-value='amount'/>
<input type='hidden' name='currency' t-att-value='currency.name'/>
<input type="submit" name="submit" id="payment_submit"
class="btn btn-primary" value="Confirmer"/>
</form>
</template>
</data>
</openerp>

View File

@ -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'),

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import controllers

View File

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'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,
}

View File

@ -0,0 +1 @@
import main

View File

@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
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)

View File

@ -0,0 +1,9 @@
.. _changelog:
Changelog
=========
`trunk (saas-3)`
----------------
- created ``website_blog`` menu, build on defunct document_page module.

View File

@ -0,0 +1,10 @@
Blog Module documentation topics
''''''''''''''''''''''''''''''''
Changelog
'''''''''
.. toctree::
:maxdepth: 1
changelog.rst

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -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);

View File

@ -0,0 +1,2 @@
sass:
sass --trace -t expanded website_payment.sass:website_payment.css

View File

@ -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;
}

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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');
}
});
});

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<!-- Add menus for testing -->
<data noupdate="0">
<record id="menu_paypal_test" model="website.menu">
<field name="name">Paypal (Test)</field>
<field name="url">/payment/paypal/test</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence" type="int">90</field>
</record>
<record id="menu_paypal_ogone" model="website.menu">
<field name="name">Ogone (Test)</field>
<field name="url">/payment/ogone/test</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence" type="int">91</field>
</record>
<record id="menu_transfer_test" model="website.menu">
<field name="name">Transfer (Test)</field>
<field name="url">/payment/transfer/test</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence" type="int">92</field>
</record>
</data>
<!-- Page -->
<data>
<template id="index_paypal" name="Paypal (Test)" page="True">
<t t-call="website.layout">
<t t-set="head">
<script type="text/javascript" src="/website_payment/static/src/js/payment_acquirer.js"></script>
<script type="text/javascript" src="/website_payment/static/lib/jquery.payment/jquery.payment.js"></script>
<link rel='stylesheet' href='/website_payment/static/src/css/website_payment.css'/>
</t>
<div id="wrap">
<div class="container mt16 js_website_blog">
<!-- <div class="row">
<h3>Paypal payment: server 2 server</h3>
<form class="form-horizontal col-sm-4 oe_cc" role="form">
<div class="form-group col-sm-8">
<label class="control-label" for="cc_number">Card number</label>
<input type="tel" id="cc_number" class="form-control"/>
<div class="card_placeholder"></div>
<div class="visa"></div>
</div>
<div class="form-group col-sm-4">
<label class="control-label" for="cc_cvc">Card code</label>
<input type="text" id="cc_cvc" class="form-control" maxlength="4" palceholder="CVC"/>
</div>
<div class="form-group col-sm-7">
<label class="control-label" for="cc_holder_name">Holder Name</label>
<input type="text" id="cc_hoder_name" class="form-control"/>
</div>
<div class="form-group col-sm-5">
<label class="control-label" for="cc_expires_mm">Expires</label>
<input type="text" id="cc_expiry" class="form-control" maxlength="7" placeholder="MM / YY"/>
</div>
</form>
</div> -->
<div>
<h3>Paypal payment: form based</h3>
<t t-raw="acquirer_form"/>
</div>
</div>
</div>
</t>
</template>
<template id="index_ogone" name="Ogone (Test)" page="True">
<t t-call="website.layout">
<div id="wrap">
<div class="container mt16 js_website_blog">
<div class="row">
Ogone payment
<t t-raw="acquirer_form"/>
</div>
</div>
</div>
</t>
</template>
<template id="index_transfer" name="Ogone (Test)" page="True">
<t t-call="website.layout">
<div id="wrap">
<div class="container mt16 js_website_blog">
<div class="row">
Transfer payment
<t t-raw="acquirer_form"/>
</div>
</div>
</div>
</t>
</template>
</data>
</openerp>

View File

@ -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',

View File

@ -1,3 +1,2 @@
import website
import main
# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -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/<int:acquirer_id>'],
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/<model("sale.order"):order>', 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/<model("sale.order"):order>'], 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):

View File

@ -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)

View File

@ -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'),
}

View File

@ -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));
});

View File

@ -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);
});
});

View File

@ -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 = '<h3>Your transaction is waiting confirmation.</h3>';
_poll_nbr += 1;
if (result.state == 'pending' && _poll_nbr <= 5) {
txt = "<h3>Your transaction is waiting confirmation.</h3>";
setTimeout(function () {
payment_transaction_poll_status();
}, 1000);
}
else if (result.state == 'done') {
txt = "<h3>Your payment has been received.</h3>";
}
else if (result.state == 'pending') {
txt = "<h3>Your transaction is waiting confirmation. You may try to refresh this page.</h3>";
}
else if (result.state == 'cancel') {
txt = "<h3>The payment seems to have been canceled.</h3>";
}
tx_node.html(txt);
});
}
payment_transaction_poll_status();
});

View File

@ -207,7 +207,7 @@
<template id="add_to_basket" inherit_option_id="website_sale.products_cart" name="Add to Cart">
<xpath expr="//div[@class='product_price']" position="inside">
<a t-href="./add_cart/?product_id=#{ product.id }" class="js_add_cart_json">
<a t-href="/shop/add_cart/?product_id=#{ product.id }" class="js_add_cart_json">
<span class="icon-shopping-cart"/>
</a>
</xpath>
@ -274,7 +274,7 @@
</div><div class="col-sm-5 col-md-5 col-lg-4 col-lg-offset-1">
<h1 t-field="product.name">Product Name</h1>
<form action="./add_cart/" class="js_add_cart_json">
<form action="/shop/add_cart/" class="js_add_cart_json">
<input type="hidden" t-if="len(product.product_variant_ids) == 1" name="product_id" t-att-value="product.product_variant_ids[0].id"/>
<t t-if="len(product.product_variant_ids) &gt; 1">
<label label-default="label-default" class="radio" t-foreach="product.product_variant_ids" t-as="variant_id">
@ -293,12 +293,20 @@
<div class="product_price mt16" t-if="product.product_variant_ids">
<h4>
<b><span class="oe_price" t-esc="product.product_variant_ids[0].price" /></b>
<t t-if="product.product_variant_ids[0].lst_price != product.product_variant_ids[0].price">
<span class="text-danger" style="text-decoration: line-through;">
<t t-esc="product.product_variant_ids[0].lst_price" />
</span>&amp;nbsp;
<span class="text-danger" style="text-decoration: line-through;"
t-field="product.product_variant_ids[0].lst_price"
t-field-options='{
"widget": "monetary",
"display_currency": "website.pricelist_id.currency_id"
}'/><br/>
</t>
<b class="oe_price"
t-field="product.product_variant_ids[0].price"
t-field-options='{
"widget": "monetary",
"display_currency": "website.pricelist_id.currency_id"
}'/>
</h4>
</div>
<button class="btn btn-primary btn-lg mt8">Add to Cart</button>
@ -782,6 +790,7 @@
<t t-call="website.layout">
<t t-set="head">
<script type="text/javascript" src="/website_sale/static/src/js/website_sale.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_payment.js"></script>
<link rel='stylesheet' href='/website_sale/static/src/css/website_sale.css'/>
<t t-raw="head or ''"/>
</t>
@ -877,19 +886,66 @@
</div>
</div>
<div class="js_payment mb64" id="js_payment">
<div class="js_payment mb64" t-if="not payment_acquirer_id and payments">
<p>Payment method:</p>
<div>
<t t-foreach="payments or []" t-as="payment">
<label>
<label t-if="payment._content">
<input t-att-value="payment.id" type="radio" name="payment_type"/> <span t-field="payment.name"/>
</label>
</t>
</div>
<t t-foreach="payments or []" t-as="payment">
<div t-att-data-id="payment.id" t-raw="payment._content" class="hidden"/>
<t t-foreach="payments" t-as="payment">
<div t-att-data-id="payment.id" t-raw="payment._content" class="oe_payment_acquirer hidden"/>
</t>
<a href="/shop/payment_validate/" class="btn btn-primary mt16">Validate &amp; Pay <span class="icon-long-arrow-right"/></a>
</div>
</div>
<div class="oe_structure"/>
</div>
</t>
</template>
<template id="confirmation">
<t t-call="website.layout">
<t t-set="head">
<link rel='stylesheet' href='/website_sale/static/src/css/website_sale.css'/>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_validate.js"></script>
<t t-raw="head or ''"/>
</t>
<t t-set="additional_title">Shop - Confirmed</t>
<div id="wrap">
<div class="container oe_website_sale">
<ul class="wizard pull-right">
<li class="text-muted">Review Order<span class="chevron"></span></li>
<li class="text-muted">Shipping &amp; Billing<span class="chevron"></span></li>
<li class="text-muted">Payment<span class="chevron"></span></li>
<li class="text-primary">Confirmation<span class="chevron"></span></li>
</ul>
<h1 class="mb32">Order Confirmed</h1>
<div class="row">
<div class="col-md-8 oe_mycart">
<h2>Thank you for your order.</h2>
<div class="oe_website_sale_tx_status" t-att-data-order-id="order.id">
</div>
<div class="clearfix"/>
<div class="oe_structure"/>
</div>
<div class="col-md-3 col-md-offset-1 text-muted" id="right_column">
<h4>Bill To:</h4>
<div t-field="order.partner_invoice_id"/>
<h4 class="mt32">Ship To:</h4>
<div t-field="order.partner_shipping_id"/>
<h4 class="mt32">Amount:</h4>
<!-- <div t-field="order.amount_total" t-field-options='{
"widget": "monetary",
"display_currency": "website.pricelist_id.currency_id"
}'/> -->
</div>
</div>
</div>