[WIP] payment_acquirer

bzr revid: chm@openerp.com-20131018111530-tjyhp3cu1qlabhij
This commit is contained in:
Christophe Matthieu 2013-10-18 13:15:30 +02:00
parent 36675c3663
commit 666387a274
5 changed files with 313 additions and 70 deletions

View File

@ -31,7 +31,6 @@
'views/acquirer_view.xml',
'payment_acquirer_data.xml',
'security/ir.model.access.csv',
'security/ir.rule.xml',
],
'installable': True,
}

View File

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-'8' "-*-"
##############################################################################
#
# OpenERP, Open Source Management Solution
@ -28,73 +28,49 @@ import logging
_logger = logging.getLogger(__name__)
class type(osv.osv):
_name = 'payment.acquirer.type'
class Payment(osv.Model):
_name = 'payment.transaction'
_inherit = ['mail.thread']
_order = 'id desc'
_columns = {
'name': fields.char('Name', required=True),
'create_date': fields.datetime('Creation Date', readonly=True, required=True),
'partner_id': fields.related('creditcard_id', 'partner_id', type='many2one', relation='res.partner', readonly=True),
'amount': fields.integer('Amount', required=True, help='in cents'),
'currency_id': fields.many2one('res.currency', 'Currency', required=True),
'reference': fields.char('Order Reference'),
'acquirer_ref': fields.char('Payment Acquirer Ref'),
'state': fields.selection([("pending", "Pending"),("validated", "Validated"),("refused", "Refused")], 'Status', required=True),
'res_model': fields.char('Object Model'),
'res_id': fields.char('Object Id'),
}
def validate_payement(self, cr, uid, id, object, reference, currency, amount, context=None):
"""
return (payment, retry_time)
payment: "validated" or "refused" or "pending"
retry_time = False (don't retry validation) or int (seconds for retry validation)
"""
if isinstance(id, list):
id = id[0]
pay_type = self.browse(cr, uid, id, context=context)
method = getattr(self, '_validate_payement_%s' % pay_type.name)
return method(object, reference, currency, amount, context=context)
def _validate_payement_virement(self, object, reference, currency, amount, context=None):
return ("pending", False)
class type_paypal(osv.osv):
_inherit = "payment.acquirer.type"
def _validate_payement_paypal(self, object, reference, currency, amount, context=None):
parameters = {}
parameters.update(
cmd='_notify-validate',
business=object.company_id.paypal_account,
item_name="%s %s" % (object.company_id.name, reference),
item_number=reference,
amount=amount,
currency_code=currency.name
)
paypal_url = "https://www.paypal.com/cgi-bin/webscr"
paypal_url = "https://www.sandbox.paypal.com/cgi-bin/webscr"
response = urlparse.parse_qsl(requests.post(paypal_url, data=parameters))
# transaction's unique id
# response["txn_id"]
if response["payment_status"] == "Voided":
raise "Paypal authorization has been voided."
elif response["payment_status"] in ("Completed", "Processed") and response["item_number"] == reference and response["mc_gross"] == amount:
return ("validated", False)
elif response["payment_status"] == "Expired":
_logger.warn("Paypal Validate Payement status: Expired")
return ("pending", 5)
elif response["payment_status"] == "Pending":
_logger.warn("Paypal Validate Payement status: Pending, reason: %s" % response["pending_reason"])
return ("pending", 5)
# Canceled_Reversal, Denied, Failed, Refunded, Reversed
return ("refused", False)
class acquirer(osv.osv):
class acquirer(osv.Model):
_name = 'payment.acquirer'
_description = 'Online Payment Acquirer'
def list_acquirers(self, cr, uid, context=None):
return [("virement", "Virement")]
_columns = {
'name': fields.char('Name', required=True),
'type_id': fields.many2one('payment.acquirer.type', required=True),
'acquirer': fields.selection(lambda self, *a, **k: self.list_acquirers(*a, **k), 'Acquirer', required=True),
'form_template_id': fields.many2one('ir.ui.view', required=True),
'visible': fields.boolean('Visible', help="Make this payment acquirer available (Customer invoices, etc.)"),
}
def _check_required_if_acquirer(self, cr, uid, ids, context=None):
for this 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_acquirer', None) == this.acquirer and not this[c]):
return False
return True
_constraints = [
(_check_required_if_acquirer, 'Required fields not filled', ['required for this payment acquirer']),
]
_defaults = {
'visible': True,
}
@ -107,6 +83,9 @@ class acquirer(osv.osv):
if not context:
context = {}
if isinstance(id, list):
id = id[0]
qweb_context = {}
qweb_context.update(
object=object,
@ -126,11 +105,282 @@ class acquirer(osv.osv):
def validate_payement(self, cr, uid, id, object, reference, currency, amount, context=None):
"""
return (payment, retry_time)
payment: "validated" or "refused" or "pending"
return (status, retry_time, log)
status: "validated" or "refused" or "pending"
retry_time = False (don't retry validation) or int (seconds for retry validation)
log = str
"""
if isinstance(id, list):
id = id[0]
type_id = self.browse(cr, uid, id, context=context).type_id
return type_id.validate_payement(object, reference, currency, amount, context=context)
pay = self.browse(cr, uid, id, context=context)
method = getattr(self, '_validate_payement_%s' % pay.acquirer)
status, retry_time, log = method(object, reference, currency, amount, context=context)
# log transaction and payment
if getattr(object, 'message_post'):
object.message_post(cr, uid, False,
body=log or "",
subject="%s%s" % (status, retry_time and ": %s" % retry_time or ""),
type='notification',
context=context)
if status == "validated":
_logger.info("Payment Validate for %s:%s" % (object._name, reference) )
elif status == "pending":
_logger.debug("Payment Pending for %s:%s. Reason: %s" % (object._name, reference, log) )
else:
_logger.error("Payment Refused for %s:%s. Reason: %s" % (object._name, reference, log) )
return (status, retry_time, log)
def _validate_payement_virement(self, object, reference, currency, amount, context=None):
return ("pending", False, "")
def transaction_feedback(self, cr, uid, acquirer, context=None, **values):
method = getattr(self, '_transaction_feedback_%s' % acquirer)
return method(**values)
# paypal
class acquirer_paypal(osv.osv):
_inherit = 'payment.acquirer'
def list_acquirers(self, cr, uid, context=None):
l = super(acquirer_paypal, self).list_acquirers(cr, uid, context)
l.append(('paypal', 'Paypal'))
return l
def _validate_payement_paypal(self, object, reference, currency, amount, context=None):
parameters = {}
parameters.update(
cmd='_notify-validate',
business=object.company_id.paypal_account,
item_name="%s %s" % (object.company_id.name, reference),
item_number=reference,
amount=amount,
currency_code=currency.name
)
paypal_url = "https://www.paypal.com/cgi-bin/webscr"
paypal_url = "https://www.sandbox.paypal.com/cgi-bin/webscr"
response = urlparse.parse_qsl(requests.post(paypal_url, data=parameters))
# transaction's unique id
# response["txn_id"]
# "Failed", "Reversed", "Refunded", "Canceled_Reversal", "Denied"
status = "refused"
retry_time = False
if response["payment_status"] == "Voided":
status = "refused"
elif response["payment_status"] in ("Completed", "Processed") and response["item_number"] == reference and response["mc_gross"] == amount:
status = "validated"
elif response["payment_status"] in ("Expired", "Pending"):
status = "pending"
retry_time = 60
return (status, retry_time, "payment_status=%s&pending_reason=%s&reason_code=%s" % (
response["payment_status"],
response.get("pending_reason"),
response.get("reason_code")))
def _transaction_feedback_paypal(self, **values):
print values
return True
# ogone
class acquirer_ogone(osv.Model):
_name = 'payment.payment'
_columns = {
'ogone_3ds': fields.dummy('3ds activated'),
'ogone_3ds_html': fields.text(),
'ogone_feedback_model': fields.char(),
'ogone_feedback_eval': fields.char(),
# just for info
'ogone_accepturl': fields.dummy(),
'ogone_declineurl': fields.dummy(),
'ogone_exceptionurl': fields.dummy(),
'ogone_complus': fields.dummy(),
}
def _create_ogone(self, cr, uid, creditcard, values):
currency = self.pool['res.currency'].browse(cr, uid, values['currency_id'])
orderid = values.get('order_ref') or 'OE-ORDER-%s' % (time.time(),)
account = creditcard.provider_account_id
_logger.debug("Values %s", pformat(values))
data = {
'PSPID': account.ogone_pspid,
'USERID': account.ogone_userid,
'PSWD': account.ogone_password,
'OrderID': orderid,
'amount': values['amount'],
'CURRENCY': currency.name,
'OPERATION': 'SAL',
'ECI': 2, # Recurring (from MOTO)
'ALIAS': creditcard.provider_ref,
'RTIMEOUT': 30,
}
if creditcard.cvc:
data['CVC'] = creditcard.cvc
if values.pop('ogone_3ds', None):
data.update({
'FLAG3D': 'Y', # YEAH!!
'LANGUAGE': creditcard.partner_id.lang or 'en_US',
})
complus = values.get('ogone_complus')
if complus:
data['COMPLUS'] = complus
for url in 'accept decline exception'.split():
key = 'ogone_{0}url'.format(url)
val = values.pop(key, None)
if val:
key = '{0}URL'.format(url).upper()
data[key] = val
_logger.debug("data %s", pformat(data))
data['SHASIGN'] = _generate_ogone_shasign(account, 'in', data)
direct_order_url = 'https://secure.ogone.com/ncol/%s/orderdirect.asp' % (account.ogone_env,)
request = urllib2.Request(direct_order_url, urlencode(data))
result = urllib2.urlopen(request).read()
_logger.debug('result = %s', result)
try:
tree = objectify.fromstring(result)
except etree.XMLSyntaxError:
# invalid response from ogone
_logger.exception('Invalid xml response from ogone')
raise
payid = tree.get('PAYID')
query_direct_data = dict(
PSPID=account.ogone_pspid,
USERID=account.ogone_userid,
PSWD=account.ogone_password,
ID=payid,
)
query_direct_url = 'https://secure.ogone.com/ncol/%s/querydirect.asp' % (account.ogone_env,)
def check_status(tree, tries=2):
# 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:
return True, (orderid, payid)
if status in PENDING_TX:
html = str(tree.HTML_ANSWER)
values.update(ogone_3ds_html=html.decode('base64'))
return False, (orderid, payid)
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('result = %s', result)
try:
tree = objectify.fromstring(result)
except etree.XMLSyntaxError:
# invalid response from ogone
pass # retry...
if tries == 0:
raise Exception('Cannot get transaction status...')
return check_status(tree, tries - 1)
else:
error_code = tree.get('NCERROR')
if tries and retryable(error_code):
return check_status(tree, tries - 1)
error_str = tree.get('NCERRORPLUS')
error_msg = 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)
return check_status(tree)
def _ogone_3ds_action(self, cr, uid, ids, context=None):
assert len(ids) == 1
p = self.browse(cr, uid, ids[0], context=context)
return {
'type': 'ir.actions.client',
'tag': 'ogone_3ds',
'params': {
'payment_id': p.id,
}
}
def _check_sha_sign_out(self, cr, uid, data, context=None):
"""Verify the SHA OUT signature of a ogone request.
return the linked payment (which must be in pending mode)
"""
payid = data['PAYID']
orderid = data['orderID']
p_ids = self.search(cr, uid, [('provider_ref', '=', payid), ('order_ref', '=', orderid)], context=context)
if len(p_ids) != 1:
raise ValidationError('Unknow order')
payment = self.browse(cr, uid, p_ids[0], context=context)
# if payment.state != 'pending':
# raise ValidationError('Invalid order')
shasign = data['SHASIGN'].upper()
if shasign != _generate_ogone_shasign(payment.creditcard_id.provider_account_id, 'out', data).upper():
raise ValidationError('SHASIGN validation error')
return payment
def _ogone_transaction_feedback(self, cr, uid, data, context=None):
payment = self._check_sha_sign_out(cr, uid, data, context)
status = int(data.get('STATUS') or '0')
if status in [5, 9]:
payment.write(dict(state='done'))
if payment.ogone_feedback_model and payment.ogone_feedback_eval:
model = self.pool.get(payment.ogone_feedback_model)
if model:
locals_ = {'cr': cr, 'uid': uid, 'model': model}
safe_eval(payment.ogone_feedback_eval, locals_)
return True
else:
error_code = data.get('NCERROR')
error_str = data.get('NCERRORPLUS')
error_msg = OGONE_ERROR_MAP.get(error_code)
error = 'ERROR: %s\n\n%s: %s' % (error_str, error_code, error_msg)
_logger.info(error)
payment.write({'state': 'error', 'error': error})
return False

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="0">
<record id="paypal" model="payment.acquirer.type">
<field name="name">paypal</field>
</record>
<!-- Paypal -->
@ -22,17 +19,13 @@
</template>
<record id="paypal_acquirer" model="payment.acquirer">
<field name="name">Paypal</field>
<field name="acquirer">paypal</field>
<field name="form_template_id" ref="paypal_acquirer_view"/>
<field name="type_id" ref="paypal"/>
</record>
<!-- Virement -->
<record id="virement" model="payment.acquirer.type">
<field name="name">virement</field>
</record>
<template id="virement_acquirer_view">
<div>
<table>
@ -44,7 +37,7 @@
</template>
<record id="virement_acquirer" model="payment.acquirer">
<field name="name">Virement</field>
<field name="acquirer">virement</field>
<field name="form_template_id" ref="virement_acquirer_view"/>
<field name="type_id" ref="virement"/>
</record>

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

@ -230,7 +230,7 @@
<div class="col-sm-5">
<ol class="breadcrumb">
<li><a t-href="/shop">Products</a></li>
<li t-if="search.get('category')"><a t-att-href="'/shop/" t-keep-query="category,search,facettes"><span t-field="category.name"/></a></li>
<li t-if="search.get('category')"><a t-href="/shop/" t-keep-query="category,search,facettes"><span t-field="category.name"/></a></li>
<li class="active"><span t-field="product.name"></span></li>
</ol>
</div><div class="col-sm-3">