[IMP] payment_authorize: add server2server support

This commit add s2s communication to the Authorize.net payment
provider, allowing the user to:
- Create a payment token
- Charge a payment token
- Authorize/capture transactions
This commit is contained in:
Damien Bouvy
2016-09-01 15:58:41 +02:00
parent ba87ff438d
commit a2b9b3a6bb
7 changed files with 518 additions and 7 deletions
@@ -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,247 @@
# -*- 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
# 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: the 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
+142 -4
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__)
@@ -47,7 +50,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 +88,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 +138,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 +147,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]
@@ -136,8 +170,9 @@ class TxAuthorize(models.Model):
status_code = int(data.get('x_response_code', '0'))
if status_code == self._authorize_valid_tx_status:
self.write({
'state': 'done',
'state': 'done' if data.get('x_type').lower() in ['auth_capture', 'prior_auth_capture'] else 'authorized',
'acquirer_reference': data.get('x_trans_id'),
'date_validate': fields.Datetime.now() if data.get('x_type').lower() in ['auth_capture', 'prior_auth_capture'] else False,
})
return True
elif status_code == self._authorize_pending_tx_status:
@@ -161,3 +196,106 @@ 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.')
@@ -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>