[MERGE] payment: add tokenization/authorization support
* Tokenization from ecommerce checkout * Authorization/capture flow * Advanced feature support for payment providers * Server2server and auth_only tx for authorize.net * Adapt Ogone s2s flow to new payment flow Impacted modules: * payment * payment_ogone * payment_authorize * website_sale * website_portal_sale * webste_quote
This commit is contained in:
@@ -44,7 +44,7 @@
|
||||
</p>
|
||||
<ul class="list-inline">
|
||||
<li><i class="fa fa-check"/>eCommerce</li>
|
||||
<li><i class="fa fa-check"/>Subscription</li>
|
||||
<li><i class="fa fa-check"/>Cards storage</li>
|
||||
<li><i class="fa fa-check"/>Pay button in emails</li>
|
||||
</ul>
|
||||
</field>
|
||||
@@ -74,8 +74,10 @@
|
||||
<p>
|
||||
A payment gateway to accept online payments via credit cards and e-checks.
|
||||
</p>
|
||||
<ul>
|
||||
<ul class="list-inline">
|
||||
<li><i class="fa fa-check"/>eCommerce</li>
|
||||
<li><i class="fa fa-check"/>Cards storage</li>
|
||||
<li><i class="fa fa-check"/>Authorize & Capture</li>
|
||||
</ul>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -10,7 +10,8 @@ class AccountPayment(models.Model):
|
||||
_inherit = 'account.payment'
|
||||
|
||||
payment_transaction_id = fields.Many2one('payment.transaction', string="Payment Transaction")
|
||||
payment_token_id = fields.Many2one('payment.token', string="Saved payment token")
|
||||
payment_token_id = fields.Many2one('payment.token', string="Saved payment token", domain=[('acquirer_id.auto_confirm', '!=', 'authorize')],
|
||||
help="Note that tokens from acquirers set to only authorize transactions (instead of capturing the amount) are not available.")
|
||||
payment_type = fields.Selection(selection_add=[('electronic', 'Electronically receive money')])
|
||||
payment_method_id_code = fields.Char(related='payment_method_id.code')
|
||||
|
||||
@@ -18,14 +19,14 @@ class AccountPayment(models.Model):
|
||||
def _onchange_partner_id(self):
|
||||
res = {}
|
||||
if self.partner_id:
|
||||
res['domain'] = {'payment_token_id': [('partner_id', '=', self.partner_id.id)]}
|
||||
res['domain'] = {'payment_token_id': [('partner_id', '=', self.partner_id.id), ('acquirer_id.auto_confirm', '!=', 'authorize')]}
|
||||
|
||||
return res
|
||||
|
||||
@api.onchange('payment_method_id', 'journal_id')
|
||||
def _onchange_payment_method(self):
|
||||
if self.payment_method_id.code == 'electronic':
|
||||
self.payment_token_id = self.env['payment.token'].search([('partner_id', '=', self.partner_id.id)], limit=1)
|
||||
self.payment_token_id = self.env['payment.token'].search([('partner_id', '=', self.partner_id.id), ('acquirer_id.auto_confirm', '!=', 'authorize')], limit=1)
|
||||
else:
|
||||
self.payment_token_id = False
|
||||
|
||||
@@ -38,6 +39,9 @@ class AccountPayment(models.Model):
|
||||
return account_payment
|
||||
|
||||
def _do_payment(self):
|
||||
if self.payment_token_id.acquirer_id.auto_confirm == 'authorize':
|
||||
raise ValidationError('This feature is not available for payment acquirers set to the "Authorize" mode.\n'
|
||||
'Please use a token from another provider than %s.' % self.payment_token_id.acquirer_id.name)
|
||||
reference = "PAYMENT-%s-%s" % (self.id, datetime.datetime.now().strftime('%y%m%d_%H%M%S'))
|
||||
tx = self.env['payment.transaction'].create({
|
||||
'amount': self.amount,
|
||||
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
from odoo import api, exceptions, fields, models, _
|
||||
from odoo.tools import float_round, image_resize_images
|
||||
from odoo.addons.base.module import module
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -16,11 +17,6 @@ def _partner_split_name(partner_name):
|
||||
return [' '.join(partner_name.split()[:-1]), ' '.join(partner_name.split()[-1:])]
|
||||
|
||||
|
||||
class ValidationError(ValueError):
|
||||
""" Used for value error when validating transaction data coming from acquirers. """
|
||||
pass
|
||||
|
||||
|
||||
class PaymentAcquirer(models.Model):
|
||||
""" Acquirer Model. Each specific acquirer can extend the model by adding
|
||||
its own fields, using the acquirer_name as a prefix for the new fields.
|
||||
@@ -75,6 +71,7 @@ class PaymentAcquirer(models.Model):
|
||||
help="Make this payment acquirer available (Customer invoices, etc.)")
|
||||
auto_confirm = fields.Selection([
|
||||
('none', 'No automatic confirmation'),
|
||||
('authorize', 'Authorize the amount and confirm the SO on acquirer confirmation'),
|
||||
('confirm_so', 'Confirm the SO on acquirer confirmation'),
|
||||
('generate_and_pay_invoice', 'On acquirer confirmation, confirm the SO, generate the invoice and pay it')],
|
||||
string='Order Confirmation', default='confirm_so', required=True)
|
||||
@@ -104,8 +101,18 @@ class PaymentAcquirer(models.Model):
|
||||
'Error Message', translate=True,
|
||||
default='<i>Error,</i> Please be aware that an error occurred during the transaction. The order has been confirmed but will not be paid. Do not hesitate to contact us if you have any questions on the status of your order.',
|
||||
help='Message displayed, if error is occur during the payment process.')
|
||||
save_token = fields.Selection([
|
||||
('none', 'Never'),
|
||||
('ask', 'Let the customer decide'),
|
||||
('always', 'Always')],
|
||||
string='Store Card Data', default='none',
|
||||
help="Determine if card data is saved as a token automatically or not. "
|
||||
"Payment tokens allow your customer to reuse their cards in the e-commerce "
|
||||
"or allow you to charge an invoice directly on a credit card. If set to "
|
||||
"'let the customer decide', ecommerce customers will have a checkbox displayed on the payment page.")
|
||||
token_implemented = fields.Boolean('Saving Card Data supported', compute='_compute_feature_support')
|
||||
|
||||
fees_implemented = fields.Boolean('Fees Computation Supported', compute='_compute_fees_implemented')
|
||||
fees_implemented = fields.Boolean('Fees Computation Supported', compute='_compute_feature_support')
|
||||
fees_active = fields.Boolean('Add Extra Fees')
|
||||
fees_dom_fixed = fields.Float('Fixed domestic fees')
|
||||
fees_dom_var = fields.Float('Variable domestic fees (in percents)')
|
||||
@@ -130,9 +137,12 @@ class PaymentAcquirer(models.Model):
|
||||
"resized as a 64x64px image, with aspect ratio preserved. "
|
||||
"Use this field anywhere a small image is required.")
|
||||
|
||||
@api.one
|
||||
def _compute_fees_implemented(self):
|
||||
self.fees_implemented = hasattr(self, '%s_compute_fees' % self.provider)
|
||||
@api.multi
|
||||
def _compute_feature_support(self):
|
||||
feature_support = self._get_feature_support()
|
||||
for acquirer in self:
|
||||
acquirer.fees_implemented = acquirer.provider in feature_support['fees']
|
||||
acquirer.token_implemented = acquirer.provider in feature_support['tokenize']
|
||||
|
||||
@api.multi
|
||||
def _check_required_if_provider(self):
|
||||
@@ -143,10 +153,30 @@ class PaymentAcquirer(models.Model):
|
||||
return False
|
||||
return True
|
||||
|
||||
@api.constrains('auto_confirm')
|
||||
def _check_authorization_support(self):
|
||||
for acquirer in self:
|
||||
if acquirer.auto_confirm == 'authorize' and acquirer.provider not in self._get_feature_support()['authorize']:
|
||||
raise ValidationError('Transaction Authorization is not supported by this payment provider.')
|
||||
return True
|
||||
|
||||
_constraints = [
|
||||
(_check_required_if_provider, 'Required fields not filled', []),
|
||||
]
|
||||
|
||||
def _get_feature_support(self):
|
||||
"""Get advanced feature support by provider.
|
||||
|
||||
Each provider should add its technical in the corresponding
|
||||
key for the following features:
|
||||
* fees: support payment fees computations
|
||||
* authorize: support authorizing payment (separates
|
||||
authorization and capture)
|
||||
* tokenize: support saving payment data in a payment.tokenize
|
||||
object
|
||||
"""
|
||||
return dict(authorize=[], tokenize=[], fees=[])
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
image_resize_images(vals)
|
||||
@@ -373,11 +403,12 @@ class PaymentTransaction(models.Model):
|
||||
type = fields.Selection([
|
||||
('server2server', 'Server To Server'),
|
||||
('form', 'Form'),
|
||||
('form_save', 'Form with credentials storage')], 'Type',
|
||||
('form_save', 'Form with tokenization')], 'Type',
|
||||
default='form', required=True)
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('pending', 'Pending'),
|
||||
('authorized', 'Authorized'),
|
||||
('done', 'Done'),
|
||||
('error', 'Error'),
|
||||
('cancel', 'Canceled')], 'Status',
|
||||
@@ -562,15 +593,40 @@ class PaymentTransaction(models.Model):
|
||||
if hasattr(self, custom_method_name):
|
||||
return getattr(self, custom_method_name)(**kwargs)
|
||||
|
||||
@api.multi
|
||||
def s2s_capture_transaction(self, **kwargs):
|
||||
custom_method_name = '%s_s2s_capture_transaction' % self.acquirer_id.provider
|
||||
if hasattr(self, custom_method_name):
|
||||
return getattr(self, custom_method_name)(**kwargs)
|
||||
|
||||
@api.multi
|
||||
def s2s_void_transaction(self, **kwargs):
|
||||
custom_method_name = '%s_s2s_void_transaction' % self.acquirer_id.provider
|
||||
if hasattr(self, custom_method_name):
|
||||
return getattr(self, custom_method_name)(**kwargs)
|
||||
|
||||
@api.multi
|
||||
def s2s_get_tx_status(self):
|
||||
""" Get the tx status. """
|
||||
invalid_param_method_name = '_%s_s2s_get_tx_status' % self.acquirer_id.provider
|
||||
if hasattr(self, invalid_param_method_name):
|
||||
return getattr(self, invalid_param_method_name)()
|
||||
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def action_capture(self):
|
||||
if any(self.mapped(lambda tx: tx.state != 'authorized')):
|
||||
raise ValidationError('Only transactions in the Authorized status can be captured.')
|
||||
for tx in self:
|
||||
tx.s2s_capture_transaction()
|
||||
|
||||
@api.multi
|
||||
def action_void(self):
|
||||
if any(self.mapped(lambda tx: tx.state != 'authorized')):
|
||||
raise ValidationError('Only transactions in the Authorized status can be voided.')
|
||||
for tx in self:
|
||||
tx.s2s_void_transaction()
|
||||
|
||||
|
||||
class PaymentToken(models.Model):
|
||||
_name = 'payment.token'
|
||||
@@ -594,8 +650,10 @@ class PaymentToken(models.Model):
|
||||
custom_method_name = '%s_create' % acquirer.provider
|
||||
if hasattr(self, custom_method_name):
|
||||
values.update(getattr(self, custom_method_name)(values))
|
||||
|
||||
return super(PaymentToken, self).create(values)
|
||||
# remove all non-model fields used by (provider)_create method to avoid warning
|
||||
fields_wl = set(self._fields.keys()) & set(values.keys())
|
||||
clean_vals = {field: values[field] for field in fields_wl}
|
||||
return super(PaymentToken, self).create(clean_vals)
|
||||
|
||||
@api.multi
|
||||
@api.depends('name')
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<form string="Payment Acquirer">
|
||||
<field name="environment" invisible='1'/>
|
||||
<field name="fees_implemented" invisible='1'/>
|
||||
<field name="token_implemented" invisible='1'/>
|
||||
<sheet>
|
||||
<field name="module_id" invisible="1"/>
|
||||
<field name="module_state" invisible="1"/>
|
||||
@@ -79,6 +80,7 @@
|
||||
<page string="Configuration">
|
||||
<group name="acquirer_config">
|
||||
<field name="auto_confirm" widget="radio"/>
|
||||
<field name="save_token" widget="radio" attrs="{'invisible': [('token_implemented', '=', False)]}"/>
|
||||
<field name="journal_id" attrs="{'required': [('auto_confirm', '=', 'generate_and_pay_invoice')], 'invisible': [('auto_confirm', '!=', 'generate_and_pay_invoice')]}"/>
|
||||
<label for="view_template_id" groups="base.group_no_one"/>
|
||||
<div groups="base.group_no_one">
|
||||
@@ -208,6 +210,8 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Payment Transactions">
|
||||
<header>
|
||||
<button type="object" name="action_capture" states="authorized" string="Capture Transaction" class="oe_highlight"/>
|
||||
<button type="object" name="action_void" states="authorized" string="Void Transaction"/>
|
||||
<field name="state" widget="statusbar" clickable="True"/>
|
||||
</header>
|
||||
<sheet>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import pprint
|
||||
import logging
|
||||
import urlparse
|
||||
import werkzeug
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
@@ -30,3 +31,16 @@ class AuthorizeController(http.Controller):
|
||||
return request.render('payment_authorize.payment_authorize_redirect', {
|
||||
'return_url': '%s' % urlparse.urljoin(base_url, return_url)
|
||||
})
|
||||
|
||||
@http.route(['/payment/authorize/s2s/create_json'], type='json', auth='public')
|
||||
def authorize_s2s_create_json(self, **kwargs):
|
||||
acquirer_id = int(kwargs.get('acquirer_id'))
|
||||
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
|
||||
return acquirer.s2s_process(kwargs)
|
||||
|
||||
@http.route(['/payment/authorize/s2s/create'], type='http', auth='public')
|
||||
def authorize_s2s_create(self, **post):
|
||||
acquirer_id = int(post.get('acquirer_id'))
|
||||
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
|
||||
acquirer.s2s_process(post)
|
||||
return werkzeug.utils.redirect(post.get('return_url', '/'))
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<field name="provider">authorize</field>
|
||||
<field name="company_id" ref="base.main_company"/>
|
||||
<field name="view_template_id" ref="authorize_acquirer_button"/>
|
||||
<field name="registration_view_template_id" ref="authorize_s2s_form"/>
|
||||
<field name="environment">test</field>
|
||||
<field name="pre_msg"><![CDATA[
|
||||
<p>You will be redirected to the Authorize website after clicking on the payment button.</p>]]></field>
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import payment
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from lxml import etree, objectify
|
||||
from urllib2 import urlopen, Request
|
||||
from StringIO import StringIO
|
||||
import xml.etree.ElementTree as ET
|
||||
from uuid import uuid4
|
||||
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
|
||||
XMLNS = 'AnetApi/xml/v1/schema/AnetApiSchema.xsd'
|
||||
|
||||
|
||||
def strip_ns(xml, ns):
|
||||
"""Strip the provided name from tag names.
|
||||
|
||||
:param str xml: xml document
|
||||
:param str ns: namespace to strip
|
||||
|
||||
:rtype: etree._Element
|
||||
:return: the parsed xml string with the namespace prefix removed
|
||||
"""
|
||||
it = ET.iterparse(StringIO(xml))
|
||||
ns_prefix = '{%s}' % XMLNS
|
||||
for _, el in it:
|
||||
if el.tag.startswith(ns_prefix):
|
||||
el.tag = el.tag[len(ns_prefix):] # strip all Auth.net namespaces
|
||||
return it.root
|
||||
|
||||
|
||||
class AuthorizeAPI():
|
||||
"""Authorize.net Gateway API integration.
|
||||
|
||||
This class allows contacting the Authorize.net API with simple operation
|
||||
requests. It implements a *very limited* subset of the complete API
|
||||
(http://developer.authorize.net/api/reference); namely:
|
||||
- Customer Profile/Payment Profile creation
|
||||
- Transaction authorization/capture/voiding
|
||||
"""
|
||||
def __init__(self, acquirer):
|
||||
"""Initiate the environment with the acquirer data.
|
||||
|
||||
:param record acquirer: payment.acquirer account that will be contacted
|
||||
"""
|
||||
if acquirer.environment == 'test':
|
||||
self.url = 'https://apitest.authorize.net/xml/v1/request.api'
|
||||
else:
|
||||
self.url = 'https://api.authorize.net/xml/v1/request.api'
|
||||
self.name = acquirer.authorize_login
|
||||
self.transaction_key = acquirer.authorize_transaction_key
|
||||
|
||||
def _authorize_request(self, data):
|
||||
"""Encode, send and process the request to the Authorize.net API.
|
||||
|
||||
Encodes the xml data and process the response. Note that only a basic
|
||||
processing is done at this level (namespace cleanup, basic error management).
|
||||
|
||||
:param etree._Element data: etree data to process
|
||||
"""
|
||||
data = etree.tostring(data, xml_declaration=True, encoding='utf-8')
|
||||
request = Request(self.url, data)
|
||||
request.add_header('Content-Type', 'text/xml')
|
||||
response = urlopen(request).read()
|
||||
response = strip_ns(response, XMLNS)
|
||||
if response.find('messages/resultCode').text == 'Error':
|
||||
messages = map(lambda m: m.text, response.findall('messages/message/text'))
|
||||
raise ValidationError('Authorize.net Error Message(s):\n %s' % '\n'.join(messages))
|
||||
return response
|
||||
|
||||
def _base_tree(self, requestType):
|
||||
"""Create a basic tree containing authentication information.
|
||||
|
||||
Create a etree Element of type requestType and appends the Authorize.net
|
||||
credentials (they are always required).
|
||||
:param str requestType: the type of request to send to Authorize.net
|
||||
See http://developer.authorize.net/api/reference
|
||||
for available types.
|
||||
:return: basic etree Element of the requested type
|
||||
containing credentials information
|
||||
:rtype: etree._Element
|
||||
"""
|
||||
root = etree.Element(requestType, xmlns=XMLNS)
|
||||
auth = etree.SubElement(root, "merchantAuthentication")
|
||||
etree.SubElement(auth, "name").text = self.name
|
||||
etree.SubElement(auth, "transactionKey").text = self.transaction_key
|
||||
return root
|
||||
|
||||
# Customer profiles
|
||||
def create_customer_profile(self, partner, cardnumber, expiration_date, card_code):
|
||||
"""Create a payment and customer profile in the Authorize.net backend.
|
||||
|
||||
Creates a customer profile for the partner/credit card combination and links
|
||||
a corresponding payment profile to it. Note that a single partner in the Odoo
|
||||
database can have multiple customer profiles in Authorize.net (i.e. a customer
|
||||
profile is created for every res.partner/payment.token couple).
|
||||
|
||||
:param record partner: the res.partner record of the customer
|
||||
:param str cardnumber: cardnumber in string format (numbers only, no separator)
|
||||
:param str expiration_date: expiration date in 'YYYY-MM' string format
|
||||
:param str card_code: three- or four-digit verification number
|
||||
|
||||
:return: a dict containing the profile_id and payment_profile_id of the
|
||||
newly created customer profile and payment profile
|
||||
:rtype: dict
|
||||
"""
|
||||
root = self._base_tree('createCustomerProfileRequest')
|
||||
profile = etree.SubElement(root, "profile")
|
||||
etree.SubElement(profile, "merchantCustomerId").text = 'ODOO-%s-%s' % (partner.id, uuid4().hex[:8])
|
||||
etree.SubElement(profile, "email").text = partner.email
|
||||
payment_profile = etree.SubElement(profile, "paymentProfiles")
|
||||
etree.SubElement(payment_profile, "customerType").text = 'business' if partner.is_company else 'individual'
|
||||
billTo = etree.SubElement(payment_profile, "billTo")
|
||||
etree.SubElement(billTo, "address").text = (partner.street + (partner.street2 if partner.street2 else '')) or None
|
||||
etree.SubElement(billTo, "city").text = partner.city
|
||||
etree.SubElement(billTo, "state").text = partner.state_id.name or None
|
||||
etree.SubElement(billTo, "zip").text = partner.zip
|
||||
etree.SubElement(billTo, "country").text = partner.country_id.name or None
|
||||
payment = etree.SubElement(payment_profile, "payment")
|
||||
creditCard = etree.SubElement(payment, "creditCard")
|
||||
etree.SubElement(creditCard, "cardNumber").text = cardnumber
|
||||
etree.SubElement(creditCard, "expirationDate").text = expiration_date
|
||||
etree.SubElement(creditCard, "cardCode").text = card_code
|
||||
etree.SubElement(root, "validationMode").text = 'liveMode'
|
||||
response = self._authorize_request(root)
|
||||
res = dict()
|
||||
res['profile_id'] = response.find('customerProfileId').text
|
||||
res['payment_profile_id'] = response.find('customerPaymentProfileIdList/numericString').text
|
||||
return res
|
||||
|
||||
def create_customer_profile_from_tx(self, partner, transaction_id):
|
||||
"""Create an Auth.net payment/customer profile from an existing transaction.
|
||||
|
||||
Creates a customer profile for the partner/credit card combination and links
|
||||
a corresponding payment profile to it. Note that a single partner in the Odoo
|
||||
database can have multiple customer profiles in Authorize.net (i.e. a customer
|
||||
profile is created for every res.partner/payment.token couple).
|
||||
|
||||
Note that this function makes 2 calls to the authorize api, since we need to
|
||||
obtain a partial cardnumber to generate a meaningful payment.token name.
|
||||
|
||||
:param record partner: the res.partner record of the customer
|
||||
:param str transaction_id: id of the authorized transaction in the
|
||||
Authorize.net backend
|
||||
|
||||
:return: a dict containing the profile_id and payment_profile_id of the
|
||||
newly created customer profile and payment profile as well as the
|
||||
last digits of the card number
|
||||
:rtype: dict
|
||||
"""
|
||||
root = self._base_tree('createCustomerProfileFromTransactionRequest')
|
||||
etree.SubElement(root, "transId").text = transaction_id
|
||||
customer = etree.SubElement(root, "customer")
|
||||
etree.SubElement(customer, "merchantCustomerId").text = 'ODOO-%s-%s' % (partner.id, uuid4().hex[:8])
|
||||
etree.SubElement(customer, "email").text = partner.email
|
||||
response = self._authorize_request(root)
|
||||
res = dict()
|
||||
res['profile_id'] = response.find('customerProfileId').text
|
||||
res['payment_profile_id'] = response.find('customerPaymentProfileIdList/numericString').text
|
||||
root_profile = self._base_tree('getCustomerPaymentProfileRequest')
|
||||
etree.SubElement(root_profile, "customerProfileId").text = res['profile_id']
|
||||
etree.SubElement(root_profile, "customerPaymentProfileId").text = res['payment_profile_id']
|
||||
response_profile = self._authorize_request(root_profile)
|
||||
res['name'] = response_profile.find('paymentProfile/payment/creditCard/cardNumber').text
|
||||
return res
|
||||
|
||||
# Transaction management
|
||||
def auth_and_capture(self, token, amount, reference):
|
||||
"""Authorize and capture a payment for the given amount.
|
||||
|
||||
Authorize and immediately capture a payment for the given payment.token
|
||||
record for the specified amount with reference as communication.
|
||||
|
||||
:param record token: the payment.token record that must be charged
|
||||
:param str amount: transaction amount (up to 15 digits with decimal point)
|
||||
:param str reference: used as "invoiceNumber" in the Authorize.net backend
|
||||
|
||||
:return: a dict containing the response code, transaction id and transaction type
|
||||
:rtype: dict
|
||||
"""
|
||||
root = self._base_tree('createTransactionRequest')
|
||||
tx = etree.SubElement(root, "transactionRequest")
|
||||
etree.SubElement(tx, "transactionType").text = "authCaptureTransaction"
|
||||
etree.SubElement(tx, "amount").text = str(amount)
|
||||
profile = etree.SubElement(tx, "profile")
|
||||
etree.SubElement(profile, "customerProfileId").text = token.authorize_profile
|
||||
payment_profile = etree.SubElement(profile, "paymentProfile")
|
||||
etree.SubElement(payment_profile, "paymentProfileId").text = token.acquirer_ref
|
||||
order = etree.SubElement(tx, "order")
|
||||
etree.SubElement(order, "invoiceNumber").text = reference
|
||||
response = self._authorize_request(root)
|
||||
res = dict()
|
||||
res['x_response_code'] = response.find('transactionResponse/responseCode').text
|
||||
res['x_trans_id'] = response.find('transactionResponse/transId').text
|
||||
res['x_type'] = 'auth_capture'
|
||||
return res
|
||||
|
||||
def authorize(self, token, amount, reference):
|
||||
"""Authorize a payment for the given amount.
|
||||
|
||||
Authorize (without capture) a payment for the given payment.token
|
||||
record for the specified amount with reference as communication.
|
||||
|
||||
:param record token: the payment.token record that must be charged
|
||||
:param str amount: transaction amount (up to 15 digits with decimal point)
|
||||
:param str reference: used as "invoiceNumber" in the Authorize.net backend
|
||||
|
||||
:return: a dict containing the response code, transaction id and transaction type
|
||||
:rtype: dict
|
||||
"""
|
||||
root = self._base_tree('createTransactionRequest')
|
||||
tx = etree.SubElement(root, "transactionRequest")
|
||||
etree.SubElement(tx, "transactionType").text = "authOnlyTransaction"
|
||||
etree.SubElement(tx, "amount").text = str(amount)
|
||||
profile = etree.SubElement(tx, "profile")
|
||||
etree.SubElement(profile, "customerProfileId").text = token.authorize_profile
|
||||
payment_profile = etree.SubElement(profile, "paymentProfile")
|
||||
etree.SubElement(payment_profile, "paymentProfileId").text = token.acquirer_ref
|
||||
order = etree.SubElement(tx, "order")
|
||||
etree.SubElement(order, "invoiceNumber").text = reference
|
||||
response = self._authorize_request(root)
|
||||
res = dict()
|
||||
res['x_response_code'] = response.find('transactionResponse/responseCode').text
|
||||
res['x_trans_id'] = response.find('transactionResponse/transId').text
|
||||
res['x_type'] = 'auth_only'
|
||||
return res
|
||||
|
||||
def capture(self, transaction_id, amount):
|
||||
"""Capture a previously authorized payment for the given amount.
|
||||
|
||||
Capture a previsouly authorized payment. Note that the amount is required
|
||||
even though we do not support partial capture.
|
||||
|
||||
:param str transaction_id: id of the authorized transaction in the
|
||||
Authorize.net backend
|
||||
:param str amount: transaction amount (up to 15 digits with decimal point)
|
||||
|
||||
:return: a dict containing the response code, transaction id and transaction type
|
||||
:rtype: dict
|
||||
"""
|
||||
root = self._base_tree('createTransactionRequest')
|
||||
tx = etree.SubElement(root, "transactionRequest")
|
||||
etree.SubElement(tx, "transactionType").text = "priorAuthCaptureTransaction"
|
||||
etree.SubElement(tx, "amount").text = str(amount)
|
||||
etree.SubElement(tx, "refTransId").text = transaction_id
|
||||
response = self._authorize_request(root)
|
||||
res = dict()
|
||||
res['x_response_code'] = response.find('transactionResponse/responseCode').text
|
||||
res['x_trans_id'] = response.find('transactionResponse/transId').text
|
||||
res['x_type'] = 'prior_auth_capture'
|
||||
return res
|
||||
|
||||
def void(self, transaction_id):
|
||||
"""Void a previously authorized payment.
|
||||
|
||||
:param str transaction_id: the id of the authorized transaction in the
|
||||
Authorize.net backend
|
||||
|
||||
:return: a dict containing the response code, transaction id and transaction type
|
||||
:rtype: dict
|
||||
"""
|
||||
root = self._base_tree('createTransactionRequest')
|
||||
tx = etree.SubElement(root, "transactionRequest")
|
||||
etree.SubElement(tx, "transactionType").text = "voidTransaction"
|
||||
etree.SubElement(tx, "refTransId").text = transaction_id
|
||||
response = self._authorize_request(root)
|
||||
res = dict()
|
||||
res['x_response_code'] = response.find('transactionResponse/responseCode').text
|
||||
res['x_trans_id'] = response.find('transactionResponse/transId').text
|
||||
res['x_type'] = 'void'
|
||||
return res
|
||||
|
||||
# Test
|
||||
def test_authenticate(self):
|
||||
"""Test Authorize.net communication with a simple credentials check.
|
||||
|
||||
:return: True if authentication was successful, else False (or throws an error)
|
||||
:rtype: bool
|
||||
"""
|
||||
test_auth = self._base_tree('authenticateTestRequest')
|
||||
response = self._authorize_request(test_auth)
|
||||
root = objectify.fromstring(response)
|
||||
if root.find('{ns}messages/{ns}resultCode'.format(ns='{%s}' % XMLNS)) == 'Ok':
|
||||
return True
|
||||
return False
|
||||
@@ -1,5 +1,7 @@
|
||||
# coding: utf-8
|
||||
|
||||
from authorize_request import AuthorizeAPI
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
import hmac
|
||||
import logging
|
||||
@@ -10,6 +12,7 @@ from odoo import api, fields, models
|
||||
from odoo.addons.payment.models.payment_acquirer import ValidationError
|
||||
from odoo.addons.payment_authorize.controllers.main import AuthorizeController
|
||||
from odoo.tools.float_utils import float_compare
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -21,6 +24,22 @@ class PaymentAcquirerAuthorize(models.Model):
|
||||
authorize_login = fields.Char(string='API Login Id', required_if_provider='authorize')
|
||||
authorize_transaction_key = fields.Char(string='API Transaction Key', required_if_provider='authorize')
|
||||
|
||||
def _get_feature_support(self):
|
||||
"""Get advanced feature support by provider.
|
||||
|
||||
Each provider should add its technical in the corresponding
|
||||
key for the following features:
|
||||
* fees: support payment fees computations
|
||||
* authorize: support authorizing payment (separates
|
||||
authorization and capture)
|
||||
* tokenize: support saving payment data in a payment.tokenize
|
||||
object
|
||||
"""
|
||||
res = super(PaymentAcquirerAuthorize, self)._get_feature_support()
|
||||
res['authorize'].append('authorize')
|
||||
res['tokenize'].append('authorize')
|
||||
return res
|
||||
|
||||
def _get_authorize_urls(self, environment):
|
||||
""" Authorize URLs """
|
||||
if environment == 'prod':
|
||||
@@ -47,7 +66,7 @@ class PaymentAcquirerAuthorize(models.Model):
|
||||
'x_trans_key': self.authorize_transaction_key,
|
||||
'x_amount': str(values['amount']),
|
||||
'x_show_form': 'PAYMENT_FORM',
|
||||
'x_type': 'AUTH_CAPTURE',
|
||||
'x_type': 'AUTH_CAPTURE' if self.auto_confirm != 'authorize' else 'AUTH_ONLY',
|
||||
'x_method': 'CC',
|
||||
'x_fp_sequence': '%s%s' % (self.id, int(time.time())),
|
||||
'x_version': '3.1',
|
||||
@@ -85,6 +104,37 @@ class PaymentAcquirerAuthorize(models.Model):
|
||||
self.ensure_one()
|
||||
return self._get_authorize_urls(self.environment)['authorize_form_url']
|
||||
|
||||
@api.model
|
||||
def authorize_s2s_form_process(self, data):
|
||||
values = {
|
||||
'cc_number': data.get('cc_number'),
|
||||
'cc_holder_name': data.get('cc_holder_name'),
|
||||
'cc_expiry': data.get('cc_expiry'),
|
||||
'cc_cvc': data.get('cc_cvc'),
|
||||
'cc_brand': data.get('cc_brand'),
|
||||
'acquirer_id': int(data.get('acquirer_id')),
|
||||
'partner_id': int(data.get('partner_id'))
|
||||
}
|
||||
PaymentMethod = self.env['payment.token'].sudo().create(values)
|
||||
return PaymentMethod.id
|
||||
|
||||
@api.multi
|
||||
def authorize_s2s_form_validate(self, data):
|
||||
error = dict()
|
||||
mandatory_fields = ["cc_number", "cc_cvc", "cc_holder_name", "cc_expiry", "cc_brand"]
|
||||
# Validation
|
||||
for field_name in mandatory_fields:
|
||||
if not data.get(field_name):
|
||||
error[field_name] = 'missing'
|
||||
if data['cc_expiry'] and datetime.now().strftime('%y%M') > datetime.strptime(data['cc_expiry'], '%M / %y').strftime('%y%M'):
|
||||
return False
|
||||
return False if error else True
|
||||
|
||||
@api.multi
|
||||
def authorize_test_credentials(self):
|
||||
self.ensure_one()
|
||||
transaction = AuthorizeAPI(self.acquirer_id)
|
||||
return transaction.test_authenticate()
|
||||
|
||||
class TxAuthorize(models.Model):
|
||||
_inherit = 'payment.transaction'
|
||||
@@ -104,7 +154,7 @@ class TxAuthorize(models.Model):
|
||||
reference, trans_id, fingerprint = data.get('x_invoice_num'), data.get('x_trans_id'), data.get('x_MD5_Hash')
|
||||
if not reference or not trans_id or not fingerprint:
|
||||
error_msg = 'Authorize: received data with missing reference (%s) or trans_id (%s) or fingerprint (%s)' % (reference, trans_id, fingerprint)
|
||||
_logger.error(error_msg)
|
||||
_logger.info(error_msg)
|
||||
raise ValidationError(error_msg)
|
||||
tx = self.search([('reference', '=', reference)])
|
||||
if not tx or len(tx) > 1:
|
||||
@@ -113,7 +163,7 @@ class TxAuthorize(models.Model):
|
||||
error_msg += '; no order found'
|
||||
else:
|
||||
error_msg += '; multiple order found'
|
||||
_logger.error(error_msg)
|
||||
_logger.info(error_msg)
|
||||
raise ValidationError(error_msg)
|
||||
return tx[0]
|
||||
|
||||
@@ -135,10 +185,29 @@ class TxAuthorize(models.Model):
|
||||
return True
|
||||
status_code = int(data.get('x_response_code', '0'))
|
||||
if status_code == self._authorize_valid_tx_status:
|
||||
self.write({
|
||||
'state': 'done',
|
||||
'acquirer_reference': data.get('x_trans_id'),
|
||||
})
|
||||
if data.get('x_type').lower() in ['auth_capture', 'prior_auth_capture']:
|
||||
self.write({
|
||||
'state': 'done',
|
||||
'acquirer_reference': data.get('x_trans_id'),
|
||||
'date_validate': fields.Datetime.now(),
|
||||
})
|
||||
elif data.get('x_type').lower() in ['auth_only']:
|
||||
self.write({
|
||||
'state': 'authorized',
|
||||
'acquirer_reference': data.get('x_trans_id'),
|
||||
})
|
||||
if self.partner_id and not self.payment_token_id and \
|
||||
(self.type == 'form_save' or self.acquirer_id.save_token == 'always'):
|
||||
transaction = AuthorizeAPI(self.acquirer_id)
|
||||
res = transaction.create_customer_profile_from_tx(self.partner_id, self.acquirer_reference)
|
||||
token_id = self.env['payment.token'].create({
|
||||
'authorize_profile': res.get('profile_id'),
|
||||
'name': res.get('name'),
|
||||
'acquirer_ref': res.get('payment_profile_id'),
|
||||
'acquirer_id': self.acquirer_id.id,
|
||||
'partner_id': self.partner_id.id,
|
||||
})
|
||||
self.payment_token_id = token_id
|
||||
return True
|
||||
elif status_code == self._authorize_pending_tx_status:
|
||||
self.write({
|
||||
@@ -161,3 +230,108 @@ class TxAuthorize(models.Model):
|
||||
'acquirer_reference': data.get('x_trans_id'),
|
||||
})
|
||||
return False
|
||||
|
||||
@api.multi
|
||||
def authorize_s2s_do_transaction(self, **data):
|
||||
self.ensure_one()
|
||||
transaction = AuthorizeAPI(self.acquirer_id)
|
||||
if self.acquirer_id.auto_confirm != "authorize":
|
||||
res = transaction.auth_and_capture(self.payment_token_id, self.amount, self.reference)
|
||||
else:
|
||||
res = transaction.authorize(self.payment_token_id, self.amount, self.reference)
|
||||
return self._authorize_s2s_validate_tree(res)
|
||||
|
||||
@api.multi
|
||||
def authorize_s2s_capture_transaction(self):
|
||||
self.ensure_one()
|
||||
transaction = AuthorizeAPI(self.acquirer_id)
|
||||
tree = transaction.capture(self.acquirer_reference, self.amount)
|
||||
return self._authorize_s2s_validate_tree(tree)
|
||||
|
||||
@api.multi
|
||||
def authorize_s2s_void_transaction(self):
|
||||
self.ensure_one()
|
||||
transaction = AuthorizeAPI(self.acquirer_id)
|
||||
tree = transaction.void(self.acquirer_reference)
|
||||
return self._authorize_s2s_validate_tree(tree)
|
||||
|
||||
@api.multi
|
||||
def _authorize_s2s_validate_tree(self, tree):
|
||||
return self._authorize_s2s_validate(tree)
|
||||
|
||||
@api.multi
|
||||
def _authorize_s2s_validate(self, tree):
|
||||
self.ensure_one()
|
||||
if self.state == 'done':
|
||||
_logger.warning('Authorize: trying to validate an already validated tx (ref %s)' % self.reference)
|
||||
return True
|
||||
status_code = int(tree.get('x_response_code', '0'))
|
||||
if status_code == self._authorize_valid_tx_status:
|
||||
if tree.get('x_type').lower() in ['auth_capture', 'prior_auth_capture']:
|
||||
if self.callback_eval and self.state != 'authorized':
|
||||
safe_eval(self.callback_eval, {'self': self})
|
||||
self.write({
|
||||
'state': 'done',
|
||||
'acquirer_reference': tree.get('x_trans_id'),
|
||||
'date_validate': fields.Datetime.now(),
|
||||
})
|
||||
if tree.get('x_type').lower() == 'auth_only':
|
||||
if self.callback_eval:
|
||||
safe_eval(self.callback_eval, {'self': self})
|
||||
self.write({
|
||||
'state': 'authorized',
|
||||
'acquirer_reference': tree.get('x_trans_id'),
|
||||
})
|
||||
if tree.get('x_type').lower() == 'void':
|
||||
self.write({
|
||||
'state': 'cancel',
|
||||
})
|
||||
return True
|
||||
elif status_code == self._authorize_pending_tx_status:
|
||||
self.write({
|
||||
'state': 'pending',
|
||||
'acquirer_reference': tree.get('x_trans_id'),
|
||||
})
|
||||
return True
|
||||
elif status_code == self._authorize_cancel_tx_status:
|
||||
self.write({
|
||||
'state': 'cancel',
|
||||
'acquirer_reference': tree.get('x_trans_id'),
|
||||
})
|
||||
return True
|
||||
else:
|
||||
error = tree.get('x_response_reason_text')
|
||||
_logger.info(error)
|
||||
self.write({
|
||||
'state': 'error',
|
||||
'state_message': error,
|
||||
'acquirer_reference': tree.get('x_trans_id'),
|
||||
})
|
||||
return False
|
||||
|
||||
|
||||
class PaymentToken(models.Model):
|
||||
_inherit = 'payment.token'
|
||||
|
||||
authorize_profile = fields.Char(string='Authorize.net Profile ID', help='This contains the unique reference '
|
||||
'for this partner/payment token combination in the Authorize.net backend')
|
||||
|
||||
@api.model
|
||||
def authorize_create(self, values):
|
||||
if values.get('cc_number'):
|
||||
values['cc_number'] = values['cc_number'].replace(' ', '')
|
||||
acquirer = self.env['payment.acquirer'].browse(values['acquirer_id'])
|
||||
expiry = str(values['cc_expiry'][:2]) + str(values['cc_expiry'][-2:])
|
||||
partner = self.env['res.partner'].browse(values['partner_id'])
|
||||
transaction = AuthorizeAPI(acquirer)
|
||||
res = transaction.create_customer_profile(partner, values['cc_number'], expiry, values['cc_cvc'])
|
||||
if res.get('profile_id') and res.get('payment_profile_id'):
|
||||
return {
|
||||
'authorize_profile': res.get('profile_id'),
|
||||
'name': 'XXXXXXXXXXXX%s - %s' % (values['cc_number'][-4:], values['cc_holder_name']),
|
||||
'acquirer_ref': res.get('payment_profile_id'),
|
||||
}
|
||||
else:
|
||||
raise ValidationError('The Customer Profile creation in Authorize.NET failed.')
|
||||
else:
|
||||
return values
|
||||
|
||||
@@ -4,6 +4,7 @@ import hashlib
|
||||
import hmac
|
||||
import time
|
||||
import urlparse
|
||||
import unittest
|
||||
from lxml import objectify
|
||||
|
||||
import odoo
|
||||
@@ -23,6 +24,8 @@ class AuthorizeCommon(PaymentAcquirerCommon):
|
||||
self.currency_usd = self.env['res.currency'].search([('name', '=', 'USD')], limit=1)[0]
|
||||
# get the authorize account
|
||||
self.authorize = self.env.ref('payment.payment_acquirer_authorize')
|
||||
# Be sure to be in 'capture' mode
|
||||
self.authorize.auto_confirm = 'confirm_so'
|
||||
|
||||
|
||||
@odoo.tests.common.at_install(True)
|
||||
@@ -97,7 +100,7 @@ class AuthorizeForm(AuthorizeCommon):
|
||||
'Authorize: wrong value for input %s: received %s instead of %s' % (values[1], values[2], form_values[values[1]])
|
||||
)
|
||||
|
||||
@mute_logger('openerp.addons.payment_authorize.models.payment', 'ValidationError')
|
||||
@mute_logger('odoo.addons.payment_authorize.models.payment', 'ValidationError')
|
||||
def test_20_authorize_form_management(self):
|
||||
# be sure not to do stupid thing
|
||||
self.assertEqual(self.authorize.environment, 'test', 'test without test environment')
|
||||
@@ -120,7 +123,7 @@ class AuthorizeForm(AuthorizeCommon):
|
||||
'x_cvv2_resp_code': u'',
|
||||
'x_description': u'',
|
||||
'x_duty': u'0.00',
|
||||
'x_email': u'norbert.buyer@exampl',
|
||||
'x_email': u'norbert.buyer@example.com',
|
||||
'x_fax': u'',
|
||||
'x_first_name': u'Norbert',
|
||||
'x_freight': u'0.00',
|
||||
@@ -174,3 +177,77 @@ class AuthorizeForm(AuthorizeCommon):
|
||||
self.env['payment.transaction'].form_feedback(authorize_post_data, 'authorize')
|
||||
# check state
|
||||
self.assertEqual(tx.state, 'error', 'Authorize: erroneous validation did not put tx into error state')
|
||||
|
||||
@unittest.skip("Authorize s2s test disabled: We do not want to overload Authorize.net with runbot's requests")
|
||||
def test_30_authorize_s2s(self):
|
||||
# be sure not to do stupid thing
|
||||
authorize = self.authorize
|
||||
self.assertEqual(authorize.environment, 'test', 'test without test environment')
|
||||
|
||||
# add credential
|
||||
# FIXME: put this test in master-nightly on odoo/odoo + create sandbox account
|
||||
authorize.write({
|
||||
'authorize_transaction_key': '',
|
||||
'authorize_login': '',
|
||||
})
|
||||
self.assertTrue(authorize.authorize_test_credentials, 'Authorize.net: s2s authentication failed')
|
||||
|
||||
# create payment meethod
|
||||
payment_token = self.env['payment.token'].create({
|
||||
'acquirer_id': authorize.id,
|
||||
'partner_id': self.buyer_id,
|
||||
'cc_number': '4111 1111 1111 1111',
|
||||
'cc_expiry': '02 / 26',
|
||||
'cc_brand': 'visa',
|
||||
'cc_cvc': '111',
|
||||
'cc_holder_name': 'test',
|
||||
})
|
||||
|
||||
# create normal s2s transaction
|
||||
transaction = self.env['payment.transaction'].create({
|
||||
'amount': 500,
|
||||
'acquirer_id': authorize.id,
|
||||
'type': 'server2server',
|
||||
'currency_id': self.currency_usd.id,
|
||||
'reference': 'test_ref_%s' % odoo.fields.Date.today(),
|
||||
'payment_token_id': payment_token.id,
|
||||
'partner_id': self.buyer_id,
|
||||
|
||||
})
|
||||
transaction.authorize_s2s_do_transaction()
|
||||
self.assertEqual(transaction.state, 'done',)
|
||||
|
||||
# switch to 'authorize only'
|
||||
# create authorize only s2s transaction & capture it
|
||||
self.authorize.auto_confirm = 'authorize'
|
||||
transaction = self.env['payment.transaction'].create({
|
||||
'amount': 500,
|
||||
'acquirer_id': authorize.id,
|
||||
'type': 'server2server',
|
||||
'currency_id': self.currency_usd.id,
|
||||
'reference': 'test_%s' % int(time.time()),
|
||||
'payment_token_id': payment_token.id,
|
||||
'partner_id': self.buyer_id,
|
||||
|
||||
})
|
||||
transaction.authorize_s2s_do_transaction()
|
||||
self.assertEqual(transaction.state, 'authorized')
|
||||
transaction.action_capture()
|
||||
self.assertEqual(transaction.state, 'done')
|
||||
|
||||
# create authorize only s2s transaction & void it
|
||||
self.authorize.auto_confirm = 'authorize'
|
||||
transaction = self.env['payment.transaction'].create({
|
||||
'amount': 500,
|
||||
'acquirer_id': authorize.id,
|
||||
'type': 'server2server',
|
||||
'currency_id': self.currency_usd.id,
|
||||
'reference': 'test_%s' % int(time.time()),
|
||||
'payment_token_id': payment_token.id,
|
||||
'partner_id': self.buyer_id,
|
||||
|
||||
})
|
||||
transaction.authorize_s2s_do_transaction()
|
||||
self.assertEqual(transaction.state, 'authorized')
|
||||
transaction.action_void()
|
||||
self.assertEqual(transaction.state, 'cancel')
|
||||
|
||||
@@ -52,5 +52,40 @@
|
||||
window.location.href = '<t t-esc="return_url"/>';
|
||||
</script>
|
||||
</template>
|
||||
|
||||
<template id="authorize_s2s_form">
|
||||
<form method="post" t-att-action="'/payment/authorize/s2s/create' if not json else '/payment/authorize/s2s/create_json'">
|
||||
<div t-att-class="'row' if bootstrap_formatting else ''">
|
||||
<div t-att-class="'form-group col-md-8' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_number">Card number</label>
|
||||
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="•••• •••• •••• •••• "/>
|
||||
<div class="card_placeholder"></div>
|
||||
<div class="visa"></div>
|
||||
<input type="hidden" name="cc_brand" value=""/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-4' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_cvc">CVC</label>
|
||||
<input type="text" name="cc_cvc" id="cc_cvc" class="form-control" maxlength="4" placeholder="CVC"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-7' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_holder_name">Name</label>
|
||||
<input type="text" name="cc_holder_name" id="cc_holder_name" class="form-control"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-5' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_expires_mm">Expires</label>
|
||||
<input type="text" name="cc_expiry" id="cc_expiry" class="form-control" maxlength="7" placeholder="MM / YY"/>
|
||||
</div>
|
||||
<input type="hidden" name="acquirer_id" t-att-value="id"/>
|
||||
<input class="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<input t-if="return_url" type="hidden" name="return_url" t-att-value="return_url"/>
|
||||
<input t-if="partner_id" type="hidden" name="partner_id" t-att-value="partner_id"/>
|
||||
<div class="clearfix"/>
|
||||
</div>
|
||||
<button t-att-class="submit_class" class="btn btn-primary" type="submit">
|
||||
<span t-if="not submit_txt">Confirm <span class="fa fa-long-arrow-right"></span></span>
|
||||
<span t-if="submit_txt"><t t-esc="submit_txt"/></span>
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -32,10 +32,25 @@ class PaymentAcquirerOgone(models.Model):
|
||||
ogone_password = fields.Char('API User 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')
|
||||
ogone_alias_usage = fields.Char('Alias Usage', help="""If you want to use Ogone Aliases,
|
||||
this default Alias Usage will be presented to
|
||||
the customer as the reason you want to
|
||||
keep his payment data""")
|
||||
ogone_alias_usage = fields.Char('Alias Usage', default="Allow saving my payment data",
|
||||
help="If you want to use Ogone Aliases, this default "
|
||||
"Alias Usage will be presented to the customer as the "
|
||||
"reason you want to keep his payment data")
|
||||
|
||||
def _get_feature_support(self):
|
||||
"""Get advanced feature support by provider.
|
||||
|
||||
Each provider should add its technical in the corresponding
|
||||
key for the following features:
|
||||
* fees: support payment fees computations
|
||||
* authorize: support authorizing payment (separates
|
||||
authorization and capture)
|
||||
* tokenize: support saving payment data in a payment.tokenize
|
||||
object
|
||||
"""
|
||||
res = super(PaymentAcquirerOgone, self)._get_feature_support()
|
||||
res['tokenize'].append('ogone')
|
||||
return res
|
||||
|
||||
def _get_ogone_urls(self, environment):
|
||||
""" Ogone URLS:
|
||||
@@ -156,7 +171,7 @@ class PaymentAcquirerOgone(models.Model):
|
||||
'CANCELURL': '%s' % urlparse.urljoin(base_url, OgoneController._cancel_url),
|
||||
'PARAMPLUS': 'return_url=%s' % ogone_tx_values.pop('return_url') if ogone_tx_values.get('return_url') else False,
|
||||
}
|
||||
if values.get('type') == 'form_save':
|
||||
if self.save_token in ['ask', 'always']:
|
||||
temp_ogone_tx_values.update({
|
||||
'ALIAS': 'ODOO-NEW-ALIAS-%s' % time.time(), # something unique,
|
||||
'ALIASUSAGE': values.get('alias_usage') or self.ogone_alias_usage,
|
||||
@@ -238,17 +253,17 @@ class PaymentTxOgone(models.Model):
|
||||
tx.acquirer_reference = pay_id
|
||||
|
||||
# alias was created on ogone server, store it
|
||||
if alias:
|
||||
Method = self.env['payment.token']
|
||||
if alias and tx.type == 'form_save':
|
||||
Token = self.env['payment.token']
|
||||
domain = [('acquirer_ref', '=', alias)]
|
||||
cardholder = data.get('CN')
|
||||
if not Method.search_count(domain):
|
||||
if not Token.search_count(domain):
|
||||
_logger.info('Ogone: saving alias %s for partner %s' % (data.get('CARDNO'), tx.partner_id))
|
||||
ref = Method.create({'name': data.get('CARDNO') + (' - ' + cardholder if cardholder else ''),
|
||||
'partner_id': tx.partner_id.id,
|
||||
'acquirer_id': tx.acquirer_id.id,
|
||||
'acquirer_ref': alias})
|
||||
tx.write({'payment_token_id': ref})
|
||||
ref = Token.create({'name': data.get('CARDNO') + (' - ' + cardholder if cardholder else ''),
|
||||
'partner_id': tx.partner_id.id,
|
||||
'acquirer_id': tx.acquirer_id.id,
|
||||
'acquirer_ref': alias})
|
||||
tx.write({'payment_token_id': ref.id})
|
||||
|
||||
return tx
|
||||
|
||||
@@ -278,7 +293,9 @@ class PaymentTxOgone(models.Model):
|
||||
'date_validate': datetime.datetime.strptime(data['TRXDATE'], '%m/%d/%y').strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||
'acquirer_reference': data['PAYID'],
|
||||
}
|
||||
if data.get('ALIAS') and self.partner_id and self.type == 'form_save' and not self.payment_token_id:
|
||||
if data.get('ALIAS') and self.partner_id and \
|
||||
(self.type == 'form_save' or self.acquirer_id.save_token == 'always')\
|
||||
and not self.payment_token_id:
|
||||
pm = self.env['payment.token'].create({
|
||||
'partner_id': self.partner_id.id,
|
||||
'acquirer_id': self.acquirer_id.id,
|
||||
@@ -383,7 +400,9 @@ class PaymentTxOgone(models.Model):
|
||||
'date_validate': datetime.date.today().strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||
'acquirer_reference': tree.get('PAYID'),
|
||||
})
|
||||
if tree.get('ALIAS') and self.partner_id and self.type == 'form_save' and not self.payment_token_id:
|
||||
if tree.get('ALIAS') and self.partner_id and \
|
||||
(self.type == 'form_save' or self.acquirer_id.save_token == 'always')\
|
||||
and not self.payment_token_id:
|
||||
pm = self.env['payment.token'].create({
|
||||
'partner_id': self.partner_id.id,
|
||||
'acquirer_id': self.acquirer_id.id,
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<input type='hidden' name='OWNERCTY' t-att-value='OWNERCTY'/>
|
||||
<input type='hidden' name='OWNERTOWN' t-att-value='OWNERTOWN'/>
|
||||
<input type='hidden' name='OWNERTELNO' t-att-value='OWNERTELNO'/>
|
||||
<t t-if="type == 'form_save'">
|
||||
<t t-if="acquirer.save_token in ['ask', 'always']">
|
||||
<input type='hidden' name='ALIAS' t-att-value='ALIAS'/>
|
||||
<input type='hidden' name='ALIASUSAGE' t-att-value='ALIASUSAGE'/>
|
||||
</t>
|
||||
|
||||
@@ -34,6 +34,21 @@ class AcquirerPaypal(models.Model):
|
||||
fees_int_fixed = fields.Float(default=0.35)
|
||||
fees_int_var = fields.Float(default=3.9)
|
||||
|
||||
def _get_feature_support(self):
|
||||
"""Get advanced feature support by provider.
|
||||
|
||||
Each provider should add its technical in the corresponding
|
||||
key for the following features:
|
||||
* fees: support payment fees computations
|
||||
* authorize: support authorizing payment (separates
|
||||
authorization and capture)
|
||||
* tokenize: support saving payment data in a payment.tokenize
|
||||
object
|
||||
"""
|
||||
res = super(AcquirerPaypal, self)._get_feature_support()
|
||||
res['fees'].append('paypal')
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _get_paypal_urls(self, environment):
|
||||
""" Paypal URLS """
|
||||
|
||||
@@ -52,6 +52,9 @@ class PaymentTransaction(models.Model):
|
||||
# verify SO/TX match, excluding tx.fees which are currently not included in SO
|
||||
amount_matches = float_compare(tx.amount, tx.sale_order_id.amount_total, 2) == 0
|
||||
if amount_matches:
|
||||
if tx.state == 'authorized' and tx.acquirer_id.auto_confirm == 'authorize':
|
||||
_logger.info('<%s> transaction authorized, auto-confirming order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
|
||||
tx.sale_order_id.with_context(send_email=True).action_confirm()
|
||||
if tx.state == 'done' and tx.acquirer_id.auto_confirm in ['confirm_so', 'generate_and_pay_invoice']:
|
||||
_logger.info('<%s> transaction completed, auto-confirming order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
|
||||
tx.sale_order_id.with_context(send_email=True).action_confirm()
|
||||
|
||||
@@ -271,6 +271,11 @@
|
||||
Your transaction is waiting confirmation.
|
||||
<span t-if='tx_post_msg' t-raw="tx_post_msg"/>
|
||||
</div>
|
||||
<div class="alert alert-warning alert-dismissable" t-if="tx_id and tx_state == 'authorized'">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
|
||||
Your payment has been authorized.
|
||||
<span t-if='tx_post_msg' t-raw="tx_post_msg"/>
|
||||
</div>
|
||||
<div class="alert alert-warning alert-dismissable" t-if="message==4 and quotation.state != 'sent'">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
|
||||
This order
|
||||
|
||||
@@ -748,7 +748,8 @@ class WebsiteSale(http.Controller):
|
||||
tx.partner_id == tx.sale_order_id.partner_id):
|
||||
try:
|
||||
s2s_result = tx.s2s_do_transaction()
|
||||
if not s2s_result or tx.state != 'done':
|
||||
valid_state = 'authorized' if tx.acquirer_id.auto_confirm == 'authorize' else 'done'
|
||||
if not s2s_result or tx.state != valid_state:
|
||||
return dict(success=False, error=_("Payment transaction failed (%s)") % tx.state_message)
|
||||
return dict(success=True, url='/shop/payment/validate')
|
||||
except Exception, e:
|
||||
@@ -768,7 +769,7 @@ class WebsiteSale(http.Controller):
|
||||
return request.redirect("/shop/payment?error=no_token_or_missmatch_tx")
|
||||
|
||||
@http.route(['/shop/payment/transaction/<int:acquirer_id>'], type='json', auth="public", website=True)
|
||||
def payment_transaction(self, acquirer_id, token=None):
|
||||
def payment_transaction(self, acquirer_id, tx_type='form', token=None):
|
||||
""" Json method that creates a payment.transaction, used to create a
|
||||
transaction when the user clicks on 'pay now' button. After having
|
||||
created the transaction, the event continues and the user is redirected
|
||||
@@ -779,7 +780,6 @@ class WebsiteSale(http.Controller):
|
||||
"""
|
||||
Transaction = request.env['payment.transaction'].sudo()
|
||||
order = request.website.sale_get_order()
|
||||
|
||||
if not order or not order.order_line or acquirer_id is None:
|
||||
return request.redirect("/shop/checkout")
|
||||
|
||||
@@ -794,11 +794,11 @@ class WebsiteSale(http.Controller):
|
||||
# new or distinct token
|
||||
tx = False
|
||||
elif tx.state == 'draft': # button cliked but no more info -> rewrite on tx or create a new one ?
|
||||
tx.write(dict(Transaction.on_change_partner_id(order.partner_id.id).get('value', {}), amount=order.amount_total))
|
||||
tx.write(dict(Transaction.on_change_partner_id(order.partner_id.id).get('value', {}), amount=order.amount_total, type=tx_type))
|
||||
if not tx:
|
||||
tx_values = {
|
||||
'acquirer_id': acquirer_id,
|
||||
'type': 'form',
|
||||
'type': tx_type,
|
||||
'amount': order.amount_total,
|
||||
'currency_id': order.pricelist_id.currency_id.id,
|
||||
'partner_id': order.partner_id.id,
|
||||
@@ -884,7 +884,7 @@ class WebsiteSale(http.Controller):
|
||||
if not order or (order.amount_total and not tx):
|
||||
return request.redirect('/shop')
|
||||
|
||||
if (not order.amount_total and not tx) or tx.state in ['pending', 'done']:
|
||||
if (not order.amount_total and not tx) or tx.state in ['pending', 'done', 'authorized']:
|
||||
if (not order.amount_total and not tx):
|
||||
# Orders are confirmed by payment transactions, but there is none for free orders,
|
||||
# (e.g. free events), so confirm immediately
|
||||
|
||||
@@ -24,13 +24,13 @@ $(document).ready(function () {
|
||||
$("div.js_payment a.list-group-item").removeClass("list-group-item-info");
|
||||
$('span.js_radio').switchClass(ico_on, ico_off, 0);
|
||||
if (token) {
|
||||
$("div.oe_sale_acquirer_button div.pre_msg").hide();
|
||||
$("div.oe_sale_acquirer_button div.token_hide").hide();
|
||||
$(ev.currentTarget).find('span.js_radio').switchClass(ico_off, ico_on, 0);
|
||||
$(ev.currentTarget).parents('li').find('input').prop("checked", true);
|
||||
$(ev.currentTarget).addClass("list-group-item-info");
|
||||
}
|
||||
else{
|
||||
$("div.oe_sale_acquirer_button div.pre_msg").show();
|
||||
$("div.oe_sale_acquirer_button div.token_hide").show();
|
||||
}
|
||||
$("div.oe_sale_acquirer_button[data-id]", $payment).addClass("hidden");
|
||||
$("div.oe_sale_acquirer_button[data-id='"+payment_id+"']", $payment).removeClass("hidden");
|
||||
@@ -46,7 +46,7 @@ $(document).ready(function () {
|
||||
var acquirer = $(ev.currentTarget).parents('div.oe_sale_acquirer_button').first();
|
||||
var acquirer_id = acquirer.data('id');
|
||||
var acquirer_token = acquirer.attr('data-token'); // !=data
|
||||
var params = {};
|
||||
var params = {'tx_type': acquirer.find('input[name="odoo_save_token"]').is(':checked')?'form_save':'form'};
|
||||
if (! acquirer_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='amount_total']" position="after">
|
||||
<field name="payment_acquirer_id" readonly="1" attrs="{'invisible': [('payment_acquirer_id', '=', False)]}" options='{"no_open":True}'/>
|
||||
<field name="payment_tx_id" readonly="1" attrs="{'invisible': [('payment_acquirer_id', '=', False)]}" groups="base.group_no_one"/>
|
||||
<field name="payment_tx_id" readonly="1" attrs="{'invisible': [('payment_acquirer_id', '=', False)]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -1350,9 +1350,6 @@
|
||||
<span t-if="token.acquirer_id.fees_active">(processing fees apply)</span>
|
||||
</a>
|
||||
</t>
|
||||
<a href="/my/payment_method?redirect=/shop/payment" class="list-group-item" >
|
||||
<span class="fa fa-plus-square"/> <b>New card</b>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -1361,7 +1358,13 @@
|
||||
<t t-foreach="acquirers or []" t-as="acquirer" id="acquirers_list">
|
||||
<div t-att-data-id="acquirer.id" class="oe_sale_acquirer_button hidden pull-right">
|
||||
<div t-raw="acquirer.button" />
|
||||
<div class="pre_msg" t-field="acquirer.pre_msg" />
|
||||
<div class="token_hide">
|
||||
<div t-if="acquirer.save_token == 'ask'">
|
||||
<input type="checkbox" name="odoo_save_token"/>
|
||||
<label for="odoo_save_token">Save my payment data</label>
|
||||
</div>
|
||||
<div class="pre_msg" t-field="acquirer.pre_msg" />
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
@@ -1535,6 +1538,9 @@
|
||||
<t t-if="tx_ids and state == 'done'">
|
||||
<p>Your payment has been received.</p>
|
||||
</t>
|
||||
<t t-if="tx_ids and state == 'authorized'">
|
||||
<p>Your payment has been authorized.</p>
|
||||
</t>
|
||||
<t t-if="tx_ids and state == 'cancel'">
|
||||
<p>The payment seems to have been canceled.</p>
|
||||
</t>
|
||||
@@ -1547,7 +1553,7 @@
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="tx_ids and state not in ['done', 'cancel', 'pending']">
|
||||
<t t-if="tx_ids and state not in ['done', 'cancel', 'pending', 'authorized']">
|
||||
<p>Your transaction is waiting confirmation.</p>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user