[IMP] payment, website_payment: generic mechanism to verify payment tokens on registration
When registering a payment token, validating it using a payment of a small amount followed by a refund allows ensuring that the method is valid (i.e. checksumming the card number simple ensure the number is valid but not that the card exists). This commit introduces a generic approach that must be implemented for each acquirer that has tokenization support. This commit also introduces a generic payment token registration/usage template that can be adapted according to one's need. payment_ogone: add support for tokens validation
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
from odoo import api, exceptions, fields, models, _
|
||||
from odoo.tools import consteq, float_round, image_resize_images, ustr, pycompat
|
||||
@@ -428,6 +429,7 @@ class PaymentTransaction(models.Model):
|
||||
acquirer_id = fields.Many2one('payment.acquirer', 'Acquirer', required=True)
|
||||
provider = fields.Selection(string='Provider', related='acquirer_id.provider')
|
||||
type = fields.Selection([
|
||||
('validation', 'Validation of the bank card'),
|
||||
('server2server', 'Server To Server'),
|
||||
('form', 'Form'),
|
||||
('form_save', 'Form with tokenization')], 'Type',
|
||||
@@ -437,6 +439,8 @@ class PaymentTransaction(models.Model):
|
||||
('pending', 'Pending'),
|
||||
('authorized', 'Authorized'),
|
||||
('done', 'Done'),
|
||||
('refunding', 'Refunding'),
|
||||
('refunded', 'Refunded'),
|
||||
('error', 'Error'),
|
||||
('cancel', 'Canceled')], 'Status',
|
||||
copy=False, default='draft', required=True, track_visibility='onchange')
|
||||
@@ -643,6 +647,12 @@ class PaymentTransaction(models.Model):
|
||||
if hasattr(self, custom_method_name):
|
||||
return getattr(self, custom_method_name)(**kwargs)
|
||||
|
||||
@api.multi
|
||||
def s2s_do_refund(self, **kwargs):
|
||||
custom_method_name = '%s_s2s_do_refund' % self.acquirer_id.provider
|
||||
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
|
||||
@@ -711,6 +721,7 @@ class PaymentToken(models.Model):
|
||||
acquirer_ref = fields.Char('Acquirer Ref.', required=True)
|
||||
active = fields.Boolean('Active', default=True)
|
||||
payment_ids = fields.One2many('payment.transaction', 'payment_token_id', 'Payment Transactions')
|
||||
verified = fields.Boolean(string='Verified', default=False)
|
||||
|
||||
@api.model
|
||||
def create(self, values):
|
||||
@@ -726,9 +737,85 @@ class PaymentToken(models.Model):
|
||||
fields_wl = set(self._fields) & set(values)
|
||||
values = {field: values[field] for field in fields_wl}
|
||||
return super(PaymentToken, self).create(values)
|
||||
"""
|
||||
@TBE: stolen shamelessly from there https://www.paypal.com/us/selfhelp/article/why-is-there-a-$1.95-charge-on-my-card-statement-faq554
|
||||
Most of them are ~1.50€s
|
||||
TODO: See this with @AL & @DBO
|
||||
"""
|
||||
VALIDATION_AMOUNTS = {
|
||||
'CAD': 2.45,
|
||||
'EUR': 1.50,
|
||||
'GBP': 1.00,
|
||||
'JPY': 200,
|
||||
'AUD': 2.00,
|
||||
'NZD': 3.00,
|
||||
'CHF': 3.00,
|
||||
'HKD': 15.00,
|
||||
'SEK': 15.00,
|
||||
'DKK': 12.50,
|
||||
'PLN': 6.50,
|
||||
'NOK': 15.00,
|
||||
'HUF': 400.00,
|
||||
'CZK': 50.00,
|
||||
'BRL': 4.00,
|
||||
'MYR': 10.00,
|
||||
'MXN': 20.00,
|
||||
'ILS': 8.00,
|
||||
'PHP': 100.00,
|
||||
'TWD': 70.00,
|
||||
'THB': 70.00
|
||||
}
|
||||
|
||||
@api.model
|
||||
def validate(self, **kwargs):
|
||||
"""
|
||||
This method allow to verify if this payment method is valid or not.
|
||||
It does this by withdrawing a certain amount and then refund it right after.
|
||||
"""
|
||||
currency = self.partner_id.currency_id
|
||||
|
||||
if self.VALIDATION_AMOUNTS.get(currency.name):
|
||||
amount = self.VALIDATION_AMOUNTS.get(currency.name)
|
||||
else:
|
||||
# If we don't find the user's currency, then we set the currency to EUR and the amount to 1€50.
|
||||
currency = self.env['res.currency'].search([('name', '=', 'EUR')])
|
||||
amount = 1.5
|
||||
|
||||
if len(currency) != 1:
|
||||
_logger.error("Error 'EUR' currency not found for payment method validation!")
|
||||
return False
|
||||
|
||||
reference = "VALIDATION-%s-%s" % (self.id, datetime.datetime.now().strftime('%y%m%d_%H%M%S'))
|
||||
tx = self.env['payment.transaction'].create({
|
||||
'amount': amount,
|
||||
'acquirer_id': self.acquirer_id.id,
|
||||
'type': 'validation',
|
||||
'currency_id': currency.id,
|
||||
'reference': reference,
|
||||
'payment_token_id': self.id,
|
||||
'partner_id': self.partner_id.id,
|
||||
'partner_country_id': self.partner_id.country_id.id,
|
||||
})
|
||||
|
||||
try:
|
||||
kwargs.update({'3d_secure': True})
|
||||
tx.s2s_do_transaction(**kwargs)
|
||||
# if 3D secure is called, then we do not refund right now
|
||||
if tx.html_3ds:
|
||||
return tx
|
||||
except:
|
||||
_logger.error('Error while validating a payment method')
|
||||
finally:
|
||||
tx.s2s_do_refund()
|
||||
return tx
|
||||
|
||||
@api.multi
|
||||
@api.depends('name')
|
||||
def _compute_short_name(self):
|
||||
for token in self:
|
||||
token.short_name = token.name.replace('XXXXXXXXXXXX', '***')
|
||||
|
||||
@api.multi
|
||||
def _get_linked_records(self):
|
||||
""" Returns the list of records currently using the records in self as payment token. """
|
||||
return {}
|
||||
@@ -5,4 +5,63 @@
|
||||
<link rel="stylesheet" href="/payment/static/src/less/payment_acquirer.less"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="payment_tokens_list" name="Payment Tokens list">
|
||||
<!--
|
||||
Variables description:
|
||||
- 'payment' set this variable when you're doing a payment, this will allow to display the radio buttons for each tokens and show the submit button
|
||||
- 'show_delete' when you want to display the delete button for each token
|
||||
- 'show_radio' when you want to display the radio button for each token
|
||||
- 'show_submit' when you want to display the submit button at the bottom of the form
|
||||
- 'submit_txt' the text displayed inside the submit button
|
||||
- 'submit_class' the css classes to style the submit button
|
||||
- 'form_action' the URI to the page that will handle the form values
|
||||
- 'pms' the tokens
|
||||
- 'checked_pm_id' the payment token that should be checked (for radio buttons)
|
||||
-->
|
||||
<form id="payment_tokens_list" method="post" t-att-action="form_action if form_action else '#'">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<table class="table table-hover table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Payment Methods</th>
|
||||
<th t-if="show_verification_status">Status</th>
|
||||
<th t-if="show_delete"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="pms" t-as="pm">
|
||||
<tr>
|
||||
<td>
|
||||
<t t-if="show_radio or payment">
|
||||
<div class="radio">
|
||||
<label><input type="radio" name="pm_id" t-att-value="pm.id" t-att-checked="checked_pm_id == pm.id"/><t t-esc="pm.short_name"/></label>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="1">
|
||||
<p><t t-esc="pm.short_name"/></p>
|
||||
</t>
|
||||
</td>
|
||||
<td t-if="show_verification_status"><t t-esc="'Verified' if pm.verified else 'Not Verified'"/></td>
|
||||
<td t-if="show_delete"><button type="submit" name="delete_pm_id" t-att-value="pm.id" class="btn btn-xs btn-primary pull-right"><i class="fa fa-trash"> Delete</i></button></td>
|
||||
</tr>
|
||||
</t>
|
||||
<tr t-if="(payment or show_submit) and not delete">
|
||||
<th t-if="show_verification_status"></th>
|
||||
<th t-if="show_delete"></th>
|
||||
<th>
|
||||
<button type="submit" t-att-class="submit_class if submit_class else 'btn btn-primary btn-sm mb8 mt8 pull-right'">
|
||||
<t t-if="submit_txt">
|
||||
<t t-raw="submit_txt"/>
|
||||
</t>
|
||||
<t t-else="1">
|
||||
<span class="fa fa-lock"></span> Pay
|
||||
</t>
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
@@ -38,12 +38,34 @@ class OgoneController(http.Controller):
|
||||
error = ''
|
||||
acq = request.env['payment.acquirer'].browse(int(post.get('acquirer_id')))
|
||||
try:
|
||||
acq.s2s_process(post)
|
||||
token = acq.s2s_process(post)
|
||||
except Exception as e:
|
||||
# synthax error: 'CHECK ERROR: |Not a valid date\n\n50001111: None'
|
||||
error = str(e).splitlines()[0].split('|')[-1] or ''
|
||||
|
||||
if token and post.get('verify_validity'):
|
||||
baseurl = request.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
params = {
|
||||
'accept_url': baseurl + '/payment/ogone/validate/accept',
|
||||
'decline_url': baseurl + '/payment/ogone/validate/decline',
|
||||
'exception_url': baseurl + '/payment/ogone/validate/exception',
|
||||
'return_url': post.get('return_url', baseurl)
|
||||
}
|
||||
tx = token.validate(**params)
|
||||
if tx and tx.html_3ds:
|
||||
return tx.html_3ds
|
||||
return werkzeug.utils.redirect(post.get('return_url', '/') + (error and '#error=%s' % werkzeug.url_quote(error) or ''))
|
||||
|
||||
@http.route([
|
||||
'/payment/ogone/validate/accept',
|
||||
'/payment/ogone/validate/decline',
|
||||
'/payment/ogone/validate/exception',
|
||||
], type='http', auth='none')
|
||||
def ogone_validation_form_feedback(self, **post):
|
||||
""" Feedback from 3d secure for a bank card validation """
|
||||
request.env['payment.transaction'].sudo().form_feedback(post, 'ogone')
|
||||
return werkzeug.utils.redirect(werkzeug.url_unquote(post.pop('return_url', '/')))
|
||||
|
||||
@http.route(['/payment/ogone/s2s/feedback'], auth='none', csrf=False)
|
||||
def feedback(self, **kwargs):
|
||||
try:
|
||||
|
||||
@@ -8,7 +8,7 @@ from unicodedata import normalize
|
||||
|
||||
import requests
|
||||
from lxml import etree, objectify
|
||||
from werkzeug import urls
|
||||
from werkzeug import urls, url_encode
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.addons.payment.models.payment_acquirer import ValidationError
|
||||
@@ -280,7 +280,7 @@ class PaymentTxOgone(models.Model):
|
||||
return invalid_parameters
|
||||
|
||||
def _ogone_form_validate(self, data):
|
||||
if self.state == 'done':
|
||||
if self.state in ['done', 'refunded']:
|
||||
_logger.info('Ogone: trying to validate an already validated tx (ref %s)', self.reference)
|
||||
return True
|
||||
|
||||
@@ -302,7 +302,12 @@ class PaymentTxOgone(models.Model):
|
||||
})
|
||||
vals.update(payment_token_id=pm.id)
|
||||
self.write(vals)
|
||||
self.payment_token_id.verified = True
|
||||
self.execute_callback()
|
||||
# if this transaction is a validation one, then we refund the money we just withdrawn
|
||||
if self.type == 'verification':
|
||||
self.s2s_do_refund()
|
||||
|
||||
return True
|
||||
elif status in self._ogone_cancel_tx_status:
|
||||
self.write({
|
||||
@@ -331,12 +336,15 @@ class PaymentTxOgone(models.Model):
|
||||
# --------------------------------------------------
|
||||
# S2S RELATED METHODS
|
||||
# --------------------------------------------------
|
||||
|
||||
def ogone_s2s_do_transaction(self, **kwargs):
|
||||
# TODO: create tx with s2s type
|
||||
account = self.acquirer_id
|
||||
reference = self.reference or "ODOO-%s-%s" % (datetime.datetime.now().strftime('%y%m%d_%H%M%S'), self.partner_id.id)
|
||||
|
||||
param_plus = {
|
||||
'return_url': kwargs.get('return_url', False)
|
||||
}
|
||||
|
||||
data = {
|
||||
'PSPID': account.ogone_pspid,
|
||||
'USERID': account.ogone_userid,
|
||||
@@ -348,6 +356,7 @@ class PaymentTxOgone(models.Model):
|
||||
'ECI': 2, # Recurring (from MOTO)
|
||||
'ALIAS': self.payment_token_id.acquirer_ref,
|
||||
'RTIMEOUT': 30,
|
||||
'PARAMPLUS' : url_encode(param_plus)
|
||||
}
|
||||
|
||||
if kwargs.get('3d_secure'):
|
||||
@@ -380,19 +389,58 @@ class PaymentTxOgone(models.Model):
|
||||
|
||||
return self._ogone_s2s_validate_tree(tree)
|
||||
|
||||
def ogone_s2s_do_refund(self, **kwargs):
|
||||
|
||||
# we refund only if this transaction hasn't been already refunded and was paid.
|
||||
if self.state != 'done':
|
||||
return False
|
||||
|
||||
self.state = 'refunding'
|
||||
account = self.acquirer_id
|
||||
reference = self.reference or "ODOO-%s-%s" % (datetime.datetime.now().strftime('%y%m%d_%H%M%S'), self.partner_id.id)
|
||||
|
||||
data = {
|
||||
'PSPID': account.ogone_pspid,
|
||||
'USERID': account.ogone_userid,
|
||||
'PSWD': account.ogone_password,
|
||||
'ORDERID': reference,
|
||||
'AMOUNT': int(self.amount * 100),
|
||||
'CURRENCY': self.currency_id.name,
|
||||
'OPERATION': 'RFD',
|
||||
'ECI': 2, # Recurring (from MOTO)
|
||||
'ALIAS': self.payment_token_id.acquirer_ref,
|
||||
'RTIMEOUT': 30,
|
||||
}
|
||||
|
||||
direct_order_url = 'https://secure.ogone.com/ncol/%s/orderdirect.asp' % (self.acquirer_id.environment)
|
||||
|
||||
_logger.debug("Ogone data %s", pformat(data))
|
||||
result = requests.post(direct_order_url, data=data).content
|
||||
_logger.debug('Ogone response = %s', result)
|
||||
|
||||
try:
|
||||
tree = objectify.fromstring(result)
|
||||
except etree.XMLSyntaxError:
|
||||
# invalid response from ogone
|
||||
_logger.exception('Invalid xml response from ogone')
|
||||
raise
|
||||
|
||||
return self._ogone_s2s_validate_tree(tree)
|
||||
|
||||
def _ogone_s2s_validate(self):
|
||||
tree = self._ogone_s2s_get_tx_status()
|
||||
return self._ogone_s2s_validate_tree(tree)
|
||||
|
||||
def _ogone_s2s_validate_tree(self, tree, tries=2):
|
||||
if self.state not in ('draft', 'pending'):
|
||||
if self.state not in ('draft', 'pending', 'refunding'):
|
||||
_logger.info('Ogone: trying to validate an already validated tx (ref %s)', self.reference)
|
||||
return True
|
||||
|
||||
status = int(tree.get('STATUS') or 0)
|
||||
if status in self._ogone_valid_tx_status:
|
||||
new_state = 'refunded' if self.state == 'refunding' else 'done'
|
||||
self.write({
|
||||
'state': 'done',
|
||||
'state': new_state,
|
||||
'date_validate': datetime.date.today().strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||
'acquirer_reference': tree.get('PAYID'),
|
||||
})
|
||||
@@ -406,6 +454,7 @@ class PaymentTxOgone(models.Model):
|
||||
'name': tree.get('CARDNO'),
|
||||
})
|
||||
self.write({'payment_token_id': pm.id})
|
||||
self.payment_token_id.verified = True
|
||||
self.execute_callback()
|
||||
return True
|
||||
elif status in self._ogone_cancel_tx_status:
|
||||
@@ -414,8 +463,9 @@ class PaymentTxOgone(models.Model):
|
||||
'acquirer_reference': tree.get('PAYID'),
|
||||
})
|
||||
elif status in self._ogone_pending_tx_status:
|
||||
new_state = 'refunding' if self.type == 'validation' else 'pending'
|
||||
self.write({
|
||||
'state': 'pending',
|
||||
'state': new_state,
|
||||
'acquirer_reference': tree.get('PAYID'),
|
||||
'html_3ds': str(tree.HTML_ANSWER).decode('base64')
|
||||
})
|
||||
@@ -522,4 +572,4 @@ class PaymentToken(models.Model):
|
||||
'acquirer_ref': alias,
|
||||
'name': 'XXXXXXXXXXXX%s - %s' % (values['cc_number'][-4:], values['cc_holder_name'])
|
||||
}
|
||||
return {}
|
||||
return {}
|
||||
@@ -56,6 +56,7 @@
|
||||
<template id="ogone_s2s_form">
|
||||
<form method="post" t-att-action="'/payment/ogone/s2s/create' if not json else '/payment/ogone/s2s/create_json'">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<input t-if="verify_validity==True" type="hidden" name="verify_validity" value="1"/>
|
||||
<div t-att-class="'row' if bootstrap_formatting else None">
|
||||
<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>
|
||||
|
||||
@@ -18,7 +18,7 @@ class WebsitePayment(http.Controller):
|
||||
}
|
||||
return_url = request.params.get('redirect', '/my/payment_method')
|
||||
for acquirer in acquirers:
|
||||
acquirer.form = acquirer.sudo()._registration_render(request.env.user.partner_id.id, {'error': {}, 'error_message': [], 'return_url': return_url, 'json': False, 'bootstrap_formatting': True})
|
||||
acquirer.form = acquirer.sudo()._registration_render(request.env.user.partner_id.id, {'error': {}, 'error_message': [], 'return_url': return_url, 'json': False, 'bootstrap_formatting': True, 'verify_validity': True})
|
||||
return request.render("website_payment.pay_methods", values)
|
||||
|
||||
@http.route(['/website_payment/delete/'], methods=['POST'], type='http', auth="user", website=True)
|
||||
@@ -90,3 +90,25 @@ class WebsitePayment(http.Controller):
|
||||
return request.render('website_payment.confirm', {'tx': tx, 'status': status, 'message': message})
|
||||
else:
|
||||
return request.redirect('/my/home')
|
||||
|
||||
@http.route(['/website_payment/get_linked_records'], type='json', auth='user', website=True)
|
||||
def get_linked_records(self, payment_token_id):
|
||||
payment_token_id = int(payment_token_id)
|
||||
|
||||
res = []
|
||||
token = request.env['payment.token'].search([('id','=',payment_token_id)])
|
||||
# if the payment token is valid
|
||||
if token:
|
||||
# then we retrieve the description for the model sale.subscription
|
||||
subscription_description = request.env['sale.subscription']._description
|
||||
# we retrieve all the linked records for the token
|
||||
records = token._get_linked_records()
|
||||
# transform the list of tuple into a dict so we can search easily
|
||||
records = dict(records[payment_token_id])
|
||||
# if there's subscriptions linked to that token
|
||||
if records.get(subscription_description):
|
||||
subscriptions = records[subscription_description]
|
||||
# we create our json with all the info needed
|
||||
for r in subscriptions:
|
||||
res.append({'id': r.id, 'name': r.name})
|
||||
return res
|
||||
@@ -60,3 +60,58 @@ $(document).ready(function () {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
odoo.define('website_payment.payment_acquirer', function (require) {
|
||||
"use strict";
|
||||
|
||||
var website = require('website.website');
|
||||
var ajax = require('web.ajax');
|
||||
var Dialog = require('web.Dialog');
|
||||
|
||||
var payment_methods = $('#payment_tokens_list');
|
||||
|
||||
payment_methods.find('button[name="delete_pm_id"]').on('click', function(ev){
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
var self = this;
|
||||
|
||||
// we retrieve the selected payment method id
|
||||
var payment_method_id = ev.target.value;
|
||||
|
||||
var Delete = function()
|
||||
{
|
||||
payment_methods.find('button[name="delete_pm_id"]').off("click");
|
||||
ev.target.click();
|
||||
};
|
||||
|
||||
ajax.jsonRpc('/website_payment/get_linked_records', 'call', {
|
||||
'payment_token_id': payment_method_id,
|
||||
}).done(function (response) {
|
||||
// if there's records linked to the payment method we're trying to delete
|
||||
if(response.length > 0)
|
||||
{
|
||||
var content = '';
|
||||
|
||||
response.forEach(function(sub) {
|
||||
content += '<p><a href="/my/subscription/' + sub.id + '">' + sub.name + '</a><p/>';
|
||||
});
|
||||
|
||||
content = $('<div>').html('<p>This card is currently linked to the following records:<p/>' + content);
|
||||
// Then we display the list of the records and ask the user if he really want to remove the ppayment method.
|
||||
new Dialog(self, {
|
||||
title: 'Warning!',
|
||||
size: 'medium',
|
||||
$content: content,
|
||||
buttons: [
|
||||
{text: 'Confirm Deletion', classes: 'btn-primary', close: true, click: Delete},
|
||||
{text: 'Cancel', close: true}]}).open();
|
||||
}
|
||||
// otherwise if the user has no records linked to this payment method, then we just delete it.
|
||||
else
|
||||
{
|
||||
Delete();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,11 +40,11 @@
|
||||
<h1>Payment Acquirers</h1>
|
||||
<h4 t-if="acquirers">Add a new payment acquirer:</h4>
|
||||
<div t-if="acquirers" class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="col-sm-8">
|
||||
<div t-if="error_message" class="alert alert-danger">
|
||||
<t t-foreach="error_message" t-as="err"><t t-esc="err"/><br /></t>
|
||||
</div>
|
||||
<div t-if='len(acquirers)>1'>
|
||||
<div t-if='len(acquirers)>1'>
|
||||
<label class="control-label" for="pm_acquirer_id">Acquirer</label>
|
||||
<select name="pm_acquirer_id" class="form-control">
|
||||
<t t-foreach="acquirers" t-as="acquirer">
|
||||
@@ -57,22 +57,12 @@
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<h4>Delete a payment acquirer:</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form method="post" action="/website_payment/delete/">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<div class="form-group">
|
||||
<select name="delete_pm_id" class="form-control" >
|
||||
<t t-foreach="pms" t-as="pm">
|
||||
<option t-att-value="pm.id" t-esc="pm.name"/>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<button class="btn btn-primary">Delete <span class="fa fa-long-arrow-right"></span></button>
|
||||
</form>
|
||||
<div t-if="pms" class="clearfix">
|
||||
<h4>Your payment Methods</h4>
|
||||
<t t-set="show_delete" t-value="True"/>
|
||||
<t t-set="form_action" t-value="'/website_payment/delete/'"/>
|
||||
<div class="col-sm-8">
|
||||
<t t-call="payment.payment_tokens_list"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user