[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:
Damien Bouvy
2016-09-01 16:00:00 +02:00
20 changed files with 758 additions and 59 deletions
@@ -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 &amp; Capture</li>
</ul>
</field>
</record>
+7 -3
View File
@@ -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,
+71 -13
View File
@@ -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')
+4
View File
@@ -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
+181 -7
View File
@@ -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="&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;"/>
<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>
+34 -15
View File
@@ -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>
+15
View File
@@ -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">&amp;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">&amp;times;</button>
This order
+6 -6
View File
@@ -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;
}
+1 -1
View File
@@ -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>
+11 -5
View File
@@ -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>