[IMP] Payments & subscriptions: Improved Payments
- When registering a payment token, validating it using a payment of a small amount (~1.50€) followed by a refund allows ensuring
that the payment 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.
- Introducing a new payment form that handles payment, deletion and adding payment method (only for server2server for the moment).
- On /my/payment_method, changed strings 'Payment Acquirers' to 'Payment Methods' which is more clear.
- Stripe can now be used to pay subscriptions.
This commit is contained in:
@@ -786,7 +786,7 @@ class PaymentToken(models.Model):
|
||||
return False
|
||||
|
||||
reference = "VALIDATION-%s-%s" % (self.id, datetime.datetime.now().strftime('%y%m%d_%H%M%S'))
|
||||
tx = self.env['payment.transaction'].create({
|
||||
tx = self.env['payment.transaction'].sudo().create({
|
||||
'amount': amount,
|
||||
'acquirer_id': self.acquirer_id.id,
|
||||
'type': 'validation',
|
||||
@@ -816,6 +816,17 @@ class PaymentToken(models.Model):
|
||||
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 {}
|
||||
def get_linked_records(self):
|
||||
""" This method returns a dict containing all the records linked to the payment.token (e.g Subscriptions),
|
||||
the key is the id of the payment.token and the value is an array that must follow the scheme below.
|
||||
|
||||
{
|
||||
token_id: [
|
||||
'description': The model description (e.g 'Sale Subscription'),
|
||||
'id': The id of the record,
|
||||
'name': The name of the record,
|
||||
'url': The url to access to this record.
|
||||
]
|
||||
}
|
||||
"""
|
||||
return {r.id:[] for r in self}
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
odoo.define('payment.payment_form', function (require){
|
||||
"use strict";
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
var Dialog = require("web.Dialog");
|
||||
var Widget = require("web.Widget");
|
||||
var rpc = require("web.rpc");
|
||||
var _t = core._t;
|
||||
|
||||
var PaymentForm = Widget.extend({
|
||||
|
||||
events: {
|
||||
'click #o_payment_form_pay': 'payEvent',
|
||||
'click #o_payment_form_add_pm': 'addPmEvent',
|
||||
'click button[name="delete_pm"]': 'deletePmEvent',
|
||||
'click input[type="radio"]': 'radioClickEvent'
|
||||
},
|
||||
|
||||
payEvent: function(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
var form = this.el;
|
||||
var checked_radio = this.$('input[type="radio"]:checked');
|
||||
var self = this;
|
||||
var button = ev.target;
|
||||
|
||||
// first we check that the user has selected a payment method
|
||||
if(checked_radio.length == 1) {
|
||||
checked_radio = checked_radio[0];
|
||||
// if the user is adding a new payment
|
||||
if(this.isNewPaymentRadio(checked_radio)) {
|
||||
var acquirer_id = this.getAcquirerIdFromRadio(checked_radio);
|
||||
var acquirer_form = this.$('#o_payment_add_token_acq_' + acquirer_id);
|
||||
// we retrieve all the input inside the acquirer form and 'serialize' them to an indexed array
|
||||
var form_data = this.getFormData($('input', acquirer_form));
|
||||
var ds = $('input[name="data_set"]', acquirer_form)[0];
|
||||
$(button).attr('disabled', true);
|
||||
$(button).prepend('<span class="o_loader"><i class="fa fa-refresh fa-spin"></i> </span>');
|
||||
|
||||
var verify_validity = this.$el.find('input[name="verify_validity"]');
|
||||
|
||||
if(verify_validity.length>0)
|
||||
form_data.verify_validity = verify_validity[0].value === "1";
|
||||
|
||||
// do the call to the route stored in the 'data_set' input of the acquirer form, the data must be called 'create-route'
|
||||
ajax.jsonRpc(ds.dataset.createRoute, 'call', form_data).then(function(data) {
|
||||
// if the server has returned true
|
||||
if(data.result) {
|
||||
// and it need a 3DS authentication
|
||||
if(data['3d_secure'] !== false) {
|
||||
// then we display the 3DS page to the user
|
||||
$("body").html(data['3d_secure']);
|
||||
}
|
||||
else {
|
||||
checked_radio.value = data.id; // set the radio value to the new card id
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
// if the server has returned false, we display an error
|
||||
else {
|
||||
self.error(_t('Error'),
|
||||
_t("<p>We are not able to add your payment method at the moment.</p>"));
|
||||
}
|
||||
// here we remove the 'processing' icon from the 'add a new payment' button
|
||||
$(button).attr('disabled', false);
|
||||
$(button).find('span.o_loader').remove();
|
||||
}).fail(function(message, data) {
|
||||
// if the rpc fails, pretty obvious
|
||||
$(button).attr('disabled', false);
|
||||
$(button).find('span.o_loader').remove();
|
||||
|
||||
self.error(_t('Error'),
|
||||
_t("<p>We are not able to add your payment method at the moment.</p>") + (core.debug ? data.data.message : ''));
|
||||
});
|
||||
}
|
||||
// if the user is using an old payment
|
||||
else {
|
||||
// then we just submit the form
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.error(_t('Error'),
|
||||
_t('<p>Please select a payment method</p>'));
|
||||
}
|
||||
},
|
||||
// event handler when clicking on the button to add a new payment method
|
||||
addPmEvent: function(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
var form = this.el;
|
||||
var checked_radio = this.$('input[type="radio"]:checked');
|
||||
var self = this;
|
||||
var button = ev.target;
|
||||
|
||||
// we check if the user has selected a 'add a new payment' option
|
||||
if(checked_radio.length == 1 && this.isNewPaymentRadio(checked_radio[0])) {
|
||||
// then we add a 'processing' icon into the 'add a new payment' button
|
||||
$(button).attr('disabled', true);
|
||||
$(button).prepend('<span class="o_loader"><i class="fa fa-refresh fa-spin"></i> </span>');
|
||||
// we retrieve which acquirer is used
|
||||
checked_radio = checked_radio[0];
|
||||
var acquirer_id = this.getAcquirerIdFromRadio(checked_radio);
|
||||
var acquirer_form = this.$('#o_payment_add_token_acq_' + acquirer_id);
|
||||
// we retrieve all the input inside the acquirer form and 'serialize' them to an indexed array
|
||||
var form_data = this.getFormData($('input', acquirer_form));
|
||||
var ds = $('input[name="data_set"]', acquirer_form)[0];
|
||||
|
||||
// we force the check when adding a card trough here
|
||||
form_data.verify_validity = true;
|
||||
|
||||
// do the call to the route stored in the 'data_set' input of the acquirer form, the data must be called 'create-route'
|
||||
ajax.jsonRpc(ds.dataset.createRoute, 'call', form_data).then(function(data) {
|
||||
// if the server has returned true
|
||||
if(data.result) {
|
||||
// and it need a 3DS authentication
|
||||
if(data['3d_secure'] !== false) {
|
||||
// then we display the 3DS page to the user
|
||||
$("body").html(data['3d_secure']);
|
||||
}
|
||||
// if it doesn't require 3DS
|
||||
else {
|
||||
// we just go to the return_url or reload the page
|
||||
if(form_data.return_url) {
|
||||
window.location = form_data.return_url;
|
||||
}
|
||||
else {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
// if the server has returned false, we display an error
|
||||
else {
|
||||
self.error(_t('Error'),
|
||||
_t("<p>We are not able to add your payment method at the moment.</p>"));
|
||||
}
|
||||
// here we remove the 'processing' icon from the 'add a new payment' button
|
||||
$(button).attr('disabled', false);
|
||||
$(button).find('span.o_loader').remove();
|
||||
}).fail(function(message, data) {
|
||||
// if the rpc fails, pretty obvious
|
||||
$(button).attr('disabled', false);
|
||||
$(button).find('span.o_loader').remove();
|
||||
|
||||
self.error(_t('Error'),
|
||||
_t("<p>We are not able to add your payment method at the moment.</p>") + (core.debug ? data.data.message : ''));
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.error(_t('Error'),
|
||||
_t('<p>Please select the option to add a new payment method</p>'));
|
||||
}
|
||||
},
|
||||
// event handler when clicking on a button to delete a payment method
|
||||
deletePmEvent: function (ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
var self = this;
|
||||
var pm_id = parseInt(ev.target.value);
|
||||
|
||||
var Delete = function() {
|
||||
rpc.query({
|
||||
model: 'payment.token',
|
||||
method: 'unlink',
|
||||
args: [pm_id],
|
||||
})
|
||||
.then(function(result){
|
||||
if(result === true) {
|
||||
ev.target.closest('div').remove();
|
||||
}
|
||||
},function(type,err){
|
||||
self.error(_t('Error'),
|
||||
_t("<p>We are not able to delete your payment method at the moment.</p>"));
|
||||
});
|
||||
};
|
||||
|
||||
rpc.query({
|
||||
model: 'payment.token',
|
||||
method: 'get_linked_records',
|
||||
args: [pm_id],
|
||||
}).then(function(result){
|
||||
if(result[pm_id].length > 0) {
|
||||
// if there's records linked to this payment method
|
||||
var content = '';
|
||||
result[pm_id].forEach(function(sub) {
|
||||
content += '<p><a href="' + sub.url + '" title="' + _.str.escapeHTML(sub.description) + '">' + _.str.escapeHTML(sub.name) + '</a><p/>';
|
||||
});
|
||||
|
||||
content = $('<div>').html(_t('<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: _t('Warning!'),
|
||||
size: 'medium',
|
||||
$content: content,
|
||||
buttons: [
|
||||
{text: _t('Confirm Deletion'), classes: 'btn-primary', close: true, click: Delete},
|
||||
{text: _t('Cancel'), close: true}]}).open();
|
||||
}
|
||||
else {
|
||||
// if there's no records linked to this payment method, then we delete it
|
||||
Delete();
|
||||
}
|
||||
}, function(type, err){
|
||||
self.error(_t('Error'),
|
||||
_t("<p>We are not able to delete your payment method at the moment.</p>") + (core.debug ? err.data.message : ''));
|
||||
});
|
||||
},
|
||||
// event handler when clicking on a radio button
|
||||
radioClickEvent: function(ev) {
|
||||
this.updateNewPaymentDisplayStatus();
|
||||
},
|
||||
updateNewPaymentDisplayStatus: function()
|
||||
{
|
||||
var checked_radio = this.$('input[type="radio"]:checked');
|
||||
// we hide all the acquirers form
|
||||
this.$('[id*="o_payment_add_token_acq_"]').addClass('hidden');
|
||||
// if we clicked on an add new payment radio
|
||||
if(checked_radio.length == 1 && this.isNewPaymentRadio(checked_radio[0]))
|
||||
{
|
||||
// then we retrieve the acquirer name
|
||||
var acquirer_id = this.getAcquirerIdFromRadio(checked_radio[0]);
|
||||
// and display its form
|
||||
this.$('#o_payment_add_token_acq_' + acquirer_id).removeClass('hidden');
|
||||
}
|
||||
},
|
||||
isNewPaymentRadio: function(element) {
|
||||
return $(element).data('acquirer-id') !== undefined;
|
||||
},
|
||||
getAcquirerIdFromRadio: function(element) {
|
||||
return $(element).data('acquirer-id');
|
||||
},
|
||||
error: function(title, message, url) {
|
||||
return new Dialog(null, {
|
||||
title: title || "",
|
||||
$content: $(core.qweb.render('website.error_dialog', {
|
||||
message: message || "",
|
||||
})),
|
||||
}).open();
|
||||
},
|
||||
getFormData: function($form){
|
||||
var unindexed_array = $form.serializeArray();
|
||||
var indexed_array = {};
|
||||
|
||||
$.map(unindexed_array, function(n, i){
|
||||
indexed_array[n.name] = n.value;
|
||||
});
|
||||
return indexed_array;
|
||||
},
|
||||
start: function() {
|
||||
this.updateNewPaymentDisplayStatus();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
$(document).ready(function (){
|
||||
var form = new PaymentForm();
|
||||
form.attachTo($('.o_payment_form'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
.o_payment_form {
|
||||
label > input[type="radio"]{
|
||||
vertical-align: bottom;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
font-weight: normal !important;
|
||||
font-family: Helvetica Neue, sans-serif;
|
||||
line-height: 1.3em;
|
||||
color: #4d4d4d;
|
||||
}
|
||||
|
||||
.panel-body:first-child {
|
||||
border-top: 0px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.panel-footer:last-child {
|
||||
border-bottom-right-radius: 10px !important;
|
||||
border-bottom-left-radius: 10px !important;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
border-top: 1px solid #ddd;
|
||||
padding: 1.14em !important;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
<template id="assets_frontend" inherit_id="web.assets_frontend" name="Payment Tx Assets">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/payment/static/src/js/payment_transaction.js"></script>
|
||||
<script type="text/javascript" src="/payment/static/src/js/payment_form.js"></script>
|
||||
<link rel="stylesheet" href="/payment/static/src/less/payment_form.less"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -9,59 +9,71 @@
|
||||
<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
|
||||
- 'icon_class' font awesome class (e.g. 'fa-trash', 'fa-lock')
|
||||
- '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)
|
||||
- 'mode' can take two values, either 'payment' or 'manage'. 'manage' displays the add a new card and delete buttons. 'payment'
|
||||
display a form that is used to pay and send the information to the form action url.
|
||||
- 'acquirers' the list of the acquirers
|
||||
- 'verify_validity' if we need to verify if the payment method is valid when adding a new one
|
||||
-->
|
||||
<form id="payment_tokens_list" method="post" t-att-action="form_action if form_action else '#'">
|
||||
<form method="post" t-att-action="form_action if form_action else '#'" class="o_payment_form">
|
||||
<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>
|
||||
<input type="hidden" t-if="verify_validity==True" name="verify_validity" value="1"/>
|
||||
<div class="panel panel-default">
|
||||
<t t-foreach="acquirers" t-as="acq">
|
||||
<div class="panel-body">
|
||||
<label>
|
||||
<t t-if="mode == 'manage'">
|
||||
<input type="radio" t-att-data-acquirer-id="acq.id" name="pm_id" t-attf-value="new_{{acq.id}}" t-att-class="'hidden' if (len(acquirers)==1) else ''" t-att-checked="len(acquirers) == 1"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<input type="radio" t-att-data-acquirer-id="acq.id" name="pm_id" t-attf-value="new_{{acq.id}}" t-att-class="'hidden' if (len(acquirers)==1 and len(pms)==0) else ''" t-att-checked="len(acquirers)==1 and len(pms)==0"/>
|
||||
</t>
|
||||
<t t-esc="acq.name"/>
|
||||
</label>
|
||||
</div>
|
||||
<div t-attf-id="o_payment_add_token_acq_{{acq.id}}" t-attf-class="panel-footer {{'hidden' if (len(acquirers)>1 and len(pms)==0 and acquirers[0]!=acq) else 'hidden' if len(pms)>0 else ''}}">
|
||||
<div class="clearfix">
|
||||
<t t-raw="acq.form" t-att-data-acquirer-id="acquirer.id"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-foreach="pms" t-as="pm">
|
||||
<div class="panel-body">
|
||||
<label>
|
||||
<input t-if="mode == 'payment'" type="radio" name="pm_id" t-att-value="pm.id" t-att-checked="checked_pm_id == pm.id"/>
|
||||
<t t-esc="pm.name"/>
|
||||
<t t-if="pm.verified">
|
||||
<i class="fa fa-check text-success" title="This payment method is verified by our system."></i>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<i class="fa fa-close text-danger" title="This payment method has not been verified by our system."></i>
|
||||
</t>
|
||||
</label>
|
||||
<button t-if="mode == 'manage'" name="delete_pm" t-att-value="pm.id" class="btn btn-primary btn-xs pull-right">
|
||||
<i class="fa fa-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<button t-if="mode == 'payment'" id="o_payment_form_pay" type="submit" t-att-class="submit_class if submit_class else 'btn btn-primary btn-lg mb8 mt8'">
|
||||
<t t-if="submit_txt">
|
||||
<i t-if="icon_class" t-attf-class="fa {{icon_class}}"/>
|
||||
<t t-esc="submit_txt"/>
|
||||
</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>
|
||||
<t t-else="">
|
||||
<i class="fa fa-lock"> Pay</i>
|
||||
</t>
|
||||
</button>
|
||||
<button t-if="mode == 'manage'" id="o_payment_form_add_pm" class="btn btn-primary btn-lg mb8 mt8">
|
||||
<i class="fa fa-plus-circle"> Add new card</i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
@@ -35,7 +35,31 @@ class AuthorizeController(http.Controller):
|
||||
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)
|
||||
return acquirer.s2s_process(kwargs).id
|
||||
|
||||
@http.route(['/payment/authorize/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
|
||||
def authorize_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
|
||||
token = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
|
||||
|
||||
if not token:
|
||||
res = {
|
||||
'result': False,
|
||||
}
|
||||
return res
|
||||
|
||||
res = {
|
||||
'result': True,
|
||||
'id': token.id,
|
||||
'short_name': token.short_name,
|
||||
'3d_secure': False,
|
||||
'verified': False,
|
||||
}
|
||||
|
||||
if verify_validity != False:
|
||||
token.validate()
|
||||
res['verified'] = token.verified
|
||||
|
||||
return res
|
||||
|
||||
@http.route(['/payment/authorize/s2s/create'], type='http', auth='public')
|
||||
def authorize_s2s_create(self, **post):
|
||||
|
||||
@@ -163,6 +163,33 @@ class AuthorizeAPI():
|
||||
res['name'] = response_profile.find('paymentProfile/payment/creditCard/cardNumber').text
|
||||
return res
|
||||
|
||||
def credit(self, token, amount, transaction_id):
|
||||
""" Refund a payment for the given amount.
|
||||
|
||||
:param record token: the payment.token record that must be refunded.
|
||||
:param str amount: transaction amount
|
||||
:param str transaction_id: the reference of the transacation that is going to be refunded.
|
||||
|
||||
: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 = "refundTransaction"
|
||||
etree.SubElement(tx, "amount").text = str(amount)
|
||||
payment = etree.SubElement(tx, "payment")
|
||||
credit_card = etree.SubElement(payment, "creditCard")
|
||||
idx = token.name.find(' - ')
|
||||
etree.SubElement(credit_card, "cardNumber").text = token.name[idx-4:idx] # shitty hack, but that's the only way to get the 4 last digits
|
||||
etree.SubElement(credit_card, "expirationDate").text = "XXXX"
|
||||
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'] = transaction_id
|
||||
res['x_type'] = 'refund'
|
||||
return res
|
||||
|
||||
# Transaction management
|
||||
def auth_and_capture(self, token, amount, reference):
|
||||
"""Authorize and capture a payment for the given amount.
|
||||
|
||||
@@ -116,7 +116,7 @@ class PaymentAcquirerAuthorize(models.Model):
|
||||
'partner_id': int(data.get('partner_id'))
|
||||
}
|
||||
PaymentMethod = self.env['payment.token'].sudo().create(values)
|
||||
return PaymentMethod.id
|
||||
return PaymentMethod
|
||||
|
||||
@api.multi
|
||||
def authorize_s2s_form_validate(self, data):
|
||||
@@ -191,7 +191,7 @@ class TxAuthorize(models.Model):
|
||||
|
||||
@api.multi
|
||||
def _authorize_form_validate(self, data):
|
||||
if self.state == 'done':
|
||||
if self.state in ['done', 'refunded']:
|
||||
_logger.warning('Authorize: trying to validate an already validated tx (ref %s)' % self.reference)
|
||||
return True
|
||||
status_code = int(data.get('x_response_code', '0'))
|
||||
@@ -219,6 +219,9 @@ class TxAuthorize(models.Model):
|
||||
'partner_id': self.partner_id.id,
|
||||
})
|
||||
self.payment_token_id = token_id
|
||||
|
||||
if self.payment_token_id:
|
||||
self.payment_token_id.verified = True
|
||||
return True
|
||||
elif status_code == self._authorize_pending_tx_status:
|
||||
self.write({
|
||||
@@ -252,6 +255,17 @@ class TxAuthorize(models.Model):
|
||||
res = transaction.authorize(self.payment_token_id, self.amount, self.reference)
|
||||
return self._authorize_s2s_validate_tree(res)
|
||||
|
||||
@api.multi
|
||||
def authorize_s2s_do_refund(self):
|
||||
self.ensure_one()
|
||||
transaction = AuthorizeAPI(self.acquirer_id)
|
||||
self.state = 'refunding'
|
||||
if self.type == 'validation':
|
||||
res = transaction.void(self.acquirer_reference)
|
||||
else:
|
||||
res = transaction.credit(self.payment_token_id, self.amount, self.acquirer_reference)
|
||||
return self._authorize_s2s_validate_tree(res)
|
||||
|
||||
@api.multi
|
||||
def authorize_s2s_capture_transaction(self):
|
||||
self.ensure_one()
|
||||
@@ -272,8 +286,7 @@ class TxAuthorize(models.Model):
|
||||
|
||||
@api.multi
|
||||
def _authorize_s2s_validate(self, tree):
|
||||
self.ensure_one()
|
||||
if self.state == 'done':
|
||||
if self.state in ['done', 'refunded']:
|
||||
_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'))
|
||||
@@ -287,6 +300,10 @@ class TxAuthorize(models.Model):
|
||||
})
|
||||
if init_state != 'authorized':
|
||||
self.execute_callback()
|
||||
|
||||
if self.payment_token_id:
|
||||
self.payment_token_id.verified = True
|
||||
|
||||
if tree.get('x_type').lower() == 'auth_only':
|
||||
self.write({
|
||||
'state': 'authorized',
|
||||
@@ -294,13 +311,19 @@ class TxAuthorize(models.Model):
|
||||
})
|
||||
self.execute_callback()
|
||||
if tree.get('x_type').lower() == 'void':
|
||||
self.write({
|
||||
'state': 'cancel',
|
||||
})
|
||||
if self.type == 'validation' and self.state == 'refunding':
|
||||
self.write({
|
||||
'state': 'refunded',
|
||||
})
|
||||
else:
|
||||
self.write({
|
||||
'state': 'cancel',
|
||||
})
|
||||
return True
|
||||
elif status_code == self._authorize_pending_tx_status:
|
||||
new_state = 'refunding' if self.state == 'refunding' else 'pending'
|
||||
self.write({
|
||||
'state': 'pending',
|
||||
'state': new_state,
|
||||
'acquirer_reference': tree.get('x_trans_id'),
|
||||
})
|
||||
return True
|
||||
|
||||
@@ -54,38 +54,28 @@
|
||||
</template>
|
||||
|
||||
<template id="authorize_s2s_form">
|
||||
<form method="post" t-att-action="'/payment/authorize/s2s/create' if not json else '/payment/authorize/s2s/create_json'">
|
||||
<div t-att-class="'row' if bootstrap_formatting else ''">
|
||||
<div t-att-class="'form-group col-md-8' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_number">Card number</label>
|
||||
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="•••• •••• •••• •••• "/>
|
||||
<div class="card_placeholder"></div>
|
||||
<div class="visa"></div>
|
||||
<input type="hidden" name="cc_brand" value=""/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-4' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_cvc">CVC</label>
|
||||
<input type="text" name="cc_cvc" id="cc_cvc" class="form-control" maxlength="4" placeholder="CVC"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-7' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_holder_name">Name</label>
|
||||
<input type="text" name="cc_holder_name" id="cc_holder_name" class="form-control"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-5' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_expires_mm">Expires</label>
|
||||
<input type="text" name="cc_expiry" id="cc_expiry" class="form-control" maxlength="7" placeholder="MM / YY"/>
|
||||
</div>
|
||||
<input type="hidden" name="acquirer_id" t-att-value="id"/>
|
||||
<input class="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<input t-if="return_url" type="hidden" name="return_url" t-att-value="return_url"/>
|
||||
<input t-if="partner_id" type="hidden" name="partner_id" t-att-value="partner_id"/>
|
||||
<div class="clearfix"/>
|
||||
<input type="hidden" name="data_set" data-create-route="/payment/authorize/s2s/create_json_3ds"/>
|
||||
<div class="mt8">
|
||||
<div t-att-class="'form-group col-md-12' if bootstrap_formatting else 'form-group'">
|
||||
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="Card number"/>
|
||||
<div class="card_placeholder"></div>
|
||||
<div class="visa"></div>
|
||||
<input type="hidden" name="cc_brand" value=""/>
|
||||
</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>
|
||||
<div t-att-class="'form-group col-md-5' if bootstrap_formatting else 'form-group'">
|
||||
<input type="text" name="cc_holder_name" id="cc_holder_name" class="form-control" placeholder="Cardholder name"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-3' if bootstrap_formatting else 'form-group'">
|
||||
<input type="text" name="cc_expiry" id="cc_expiry" class="form-control" maxlength="7" placeholder="Expires (MM / YY)"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-4' if bootstrap_formatting else 'form-group'">
|
||||
<input type="text" name="cc_cvc" id="cc_cvc" class="form-control" maxlength="4" placeholder="CVC"/>
|
||||
</div>
|
||||
</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"/>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -33,6 +33,40 @@ class OgoneController(http.Controller):
|
||||
new_id = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
|
||||
return new_id.id
|
||||
|
||||
@http.route(['/payment/ogone/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
|
||||
def ogone_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
|
||||
token = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
|
||||
|
||||
if not token:
|
||||
res = {
|
||||
'result': False,
|
||||
}
|
||||
return res
|
||||
|
||||
res = {
|
||||
'result': True,
|
||||
'id': token.id,
|
||||
'short_name': token.short_name,
|
||||
'3d_secure': False,
|
||||
'verified': False,
|
||||
}
|
||||
|
||||
if verify_validity != False:
|
||||
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': kwargs.get('return_url', baseurl)
|
||||
}
|
||||
tx = token.validate(**params)
|
||||
res['verified'] = token.verified
|
||||
|
||||
if tx and tx.html_3ds:
|
||||
res['3d_secure'] = tx.html_3ds
|
||||
|
||||
return res
|
||||
|
||||
@http.route(['/payment/ogone/s2s/create'], type='http', auth='public', methods=["POST"], csrf=False)
|
||||
def ogone_s2s_create(self, **post):
|
||||
error = ''
|
||||
|
||||
@@ -302,10 +302,11 @@ class PaymentTxOgone(models.Model):
|
||||
})
|
||||
vals.update(payment_token_id=pm.id)
|
||||
self.write(vals)
|
||||
self.payment_token_id.verified = True
|
||||
if self.payment_token_id:
|
||||
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':
|
||||
if self.type == 'validation':
|
||||
self.s2s_do_refund()
|
||||
|
||||
return True
|
||||
@@ -454,7 +455,8 @@ class PaymentTxOgone(models.Model):
|
||||
'name': tree.get('CARDNO'),
|
||||
})
|
||||
self.write({'payment_token_id': pm.id})
|
||||
self.payment_token_id.verified = True
|
||||
if self.payment_token_id:
|
||||
self.payment_token_id.verified = True
|
||||
self.execute_callback()
|
||||
return True
|
||||
elif status in self._ogone_cancel_tx_status:
|
||||
@@ -463,7 +465,7 @@ 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'
|
||||
new_state = 'refunding' if self.state == 'refunding' else 'pending'
|
||||
self.write({
|
||||
'state': new_state,
|
||||
'acquirer_reference': tree.get('PAYID'),
|
||||
|
||||
@@ -54,40 +54,27 @@
|
||||
</template>
|
||||
|
||||
<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>
|
||||
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="•••• •••• •••• •••• "/>
|
||||
<div class="card_placeholder"></div>
|
||||
<div class="visa"></div>
|
||||
<input type="hidden" name="cc_brand" value=""/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-4' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_cvc">CVC</label>
|
||||
<input type="text" name="cc_cvc" id="cc_cvc" class="form-control" maxlength="4" placeholder="CVC"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-7' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_holder_name">Name</label>
|
||||
<input type="text" name="cc_holder_name" id="cc_holder_name" class="form-control"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-5' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_expires_mm">Expires</label>
|
||||
<input type="text" name="cc_expiry" id="cc_expiry" class="form-control" maxlength="7" placeholder="MM / YY"/>
|
||||
</div>
|
||||
<input type="hidden" name="acquirer_id" t-att-value="id"/>
|
||||
<input 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"/>
|
||||
<input type="hidden" name="data_set" data-create-route="/payment/ogone/s2s/create_json_3ds"/>
|
||||
<input type="hidden" name="acquirer_id" t-att-value="id"/>
|
||||
<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="mt8">
|
||||
<div t-att-class="'form-group col-md-12' if bootstrap_formatting else 'form-group'">
|
||||
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="Card number"/>
|
||||
<div class="card_placeholder"></div>
|
||||
<div class="visa"></div>
|
||||
<input type="hidden" name="cc_brand" value=""/>
|
||||
</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>
|
||||
<div t-att-class="'form-group col-md-5' if bootstrap_formatting else 'form-group'">
|
||||
<input type="text" name="cc_holder_name" id="cc_holder_name" class="form-control" placeholder="Cardholder name"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-3' if bootstrap_formatting else 'form-group'">
|
||||
<input type="text" name="cc_expiry" id="cc_expiry" class="form-control" maxlength="7" placeholder="Expires (MM / YY)"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-4' if bootstrap_formatting else 'form-group'">
|
||||
<input type="text" name="cc_cvc" id="cc_cvc" class="form-control" maxlength="4" placeholder="CVC"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -15,7 +15,7 @@ class StripeController(http.Controller):
|
||||
def stripe_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)
|
||||
return acquirer.s2s_process(kwargs).id
|
||||
|
||||
@http.route(['/payment/stripe/s2s/create'], type='http', auth='public')
|
||||
def stripe_s2s_create(self, **post):
|
||||
@@ -24,6 +24,30 @@ class StripeController(http.Controller):
|
||||
acquirer.s2s_process(post)
|
||||
return werkzeug.utils.redirect(post.get('return_url', '/'))
|
||||
|
||||
@http.route(['/payment/stripe/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
|
||||
def stripe_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
|
||||
token = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
|
||||
|
||||
if not token:
|
||||
res = {
|
||||
'result': False,
|
||||
}
|
||||
return res
|
||||
|
||||
res = {
|
||||
'result': True,
|
||||
'id': token.id,
|
||||
'short_name': token.short_name,
|
||||
'3d_secure': False,
|
||||
'verified': False,
|
||||
}
|
||||
|
||||
if verify_validity != False:
|
||||
token.validate()
|
||||
res['verified'] = token.verified
|
||||
|
||||
return res
|
||||
|
||||
@http.route(['/payment/stripe/create_charge'], type='json', auth='public')
|
||||
def stripe_create_charge(self, **post):
|
||||
""" Create a payment transaction
|
||||
|
||||
@@ -71,7 +71,7 @@ class PaymentAcquirerStripe(models.Model):
|
||||
'acquirer_id': int(data['acquirer_id']),
|
||||
'partner_id': int(data['partner_id'])
|
||||
})
|
||||
return payment_token.id
|
||||
return payment_token
|
||||
|
||||
@api.multi
|
||||
def stripe_s2s_form_validate(self, data):
|
||||
@@ -83,6 +83,21 @@ class PaymentAcquirerStripe(models.Model):
|
||||
return False
|
||||
return True
|
||||
|
||||
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(PaymentAcquirerStripe, self)._get_feature_support()
|
||||
res['tokenize'].append('stripe')
|
||||
return res
|
||||
|
||||
|
||||
class PaymentTransactionStripe(models.Model):
|
||||
_inherit = 'payment.transaction'
|
||||
@@ -112,6 +127,29 @@ class PaymentTransactionStripe(models.Model):
|
||||
result = self._create_stripe_charge(acquirer_ref=self.payment_token_id.acquirer_ref)
|
||||
return self._stripe_s2s_validate_tree(result)
|
||||
|
||||
|
||||
def _create_stripe_refund(self):
|
||||
api_url_refund = 'https://%s/refunds' % (self.acquirer_id._get_stripe_api_url())
|
||||
|
||||
refund_params = {
|
||||
'charge': self.acquirer_reference,
|
||||
'amount': int(self.amount*100), # by default, stripe refund the full amount (we don't really need to specify the value)
|
||||
'metadata[reference]': self.reference,
|
||||
}
|
||||
|
||||
r = requests.post(api_url_refund,
|
||||
auth=(self.acquirer_id.stripe_secret_key, ''),
|
||||
params=refund_params,
|
||||
headers=STRIPE_HEADERS)
|
||||
return r.json()
|
||||
|
||||
@api.multi
|
||||
def stripe_s2s_do_refund(self, **kwargs):
|
||||
self.ensure_one()
|
||||
self.state = 'refunding'
|
||||
result = self._create_stripe_refund()
|
||||
return self._stripe_s2s_validate_tree(result)
|
||||
|
||||
@api.model
|
||||
def _stripe_form_get_tx_from_data(self, data):
|
||||
""" Given a data dict coming from stripe, verify it and find the related
|
||||
@@ -135,18 +173,21 @@ class PaymentTransactionStripe(models.Model):
|
||||
@api.multi
|
||||
def _stripe_s2s_validate_tree(self, tree):
|
||||
self.ensure_one()
|
||||
if self.state not in ('draft', 'pending'):
|
||||
if self.state not in ('draft', 'pending', 'refunding'):
|
||||
_logger.info('Stripe: trying to validate an already validated tx (ref %s)', self.reference)
|
||||
return True
|
||||
|
||||
status = tree.get('status')
|
||||
if status == 'succeeded':
|
||||
new_state = 'refunded' if self.state == 'refunding' else 'done'
|
||||
self.write({
|
||||
'state': 'done',
|
||||
'state': new_state,
|
||||
'date_validate': fields.datetime.now(),
|
||||
'acquirer_reference': tree.get('id'),
|
||||
})
|
||||
self.execute_callback()
|
||||
if self.payment_token_id:
|
||||
self.payment_token_id.verified = True
|
||||
return True
|
||||
else:
|
||||
error = tree['error']['message']
|
||||
|
||||
@@ -32,38 +32,29 @@
|
||||
</template>
|
||||
|
||||
<template id="stripe_s2s_form">
|
||||
<form method="post" t-att-action="'/payment/stripe/s2s/create' if not json else '/payment/stripe/s2s/create_json'">
|
||||
<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>
|
||||
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="•••• •••• •••• •••• "/>
|
||||
<div class="card_placeholder"></div>
|
||||
<div class="visa"></div>
|
||||
<input type="hidden" name="cc_brand" value=""/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-4' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cvc">CVC</label>
|
||||
<input type="text" name="cvc" id="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">Card Holder 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">Expiration Date</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 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"/>
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<div class="clearfix"/>
|
||||
<input type="hidden" name="data_set" data-create-route="/payment/stripe/s2s/create_json_3ds"/>
|
||||
<div class="mt8">
|
||||
<div t-att-class="'form-group col-md-12' if bootstrap_formatting else 'form-group'">
|
||||
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="Card number"/>
|
||||
<div class="card_placeholder"></div>
|
||||
<div class="visa"></div>
|
||||
<input type="hidden" name="cc_brand" value=""/>
|
||||
</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>
|
||||
<div t-att-class="'form-group col-md-5' if bootstrap_formatting else 'form-group'">
|
||||
<input type="text" name="cc_holder_name" id="cc_holder_name" class="form-control" placeholder="Cardholder name"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-3' if bootstrap_formatting else 'form-group'">
|
||||
<input type="text" name="cc_expiry" id="cc_expiry" class="form-control" maxlength="7" placeholder="Expires (MM / YY)"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-4' if bootstrap_formatting else 'form-group'">
|
||||
<input type="text" name="cvc" id="cvc" class="form-control" maxlength="4" placeholder="CVC"/>
|
||||
</div>
|
||||
<input type="hidden" name="acquirer_id" t-att-value="id"/>
|
||||
<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"/>
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<div class="clearfix"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="payment_stripe_redirect" name="Payment Stripe">
|
||||
|
||||
@@ -18,16 +18,9 @@ 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, 'verify_validity': 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})
|
||||
return request.render("website_payment.pay_methods", values)
|
||||
|
||||
@http.route(['/website_payment/delete/'], methods=['POST'], type='http', auth="user", website=True)
|
||||
def delete(self, delete_pm_id=None):
|
||||
if delete_pm_id:
|
||||
pay_meth = request.env['payment.token'].browse(int(delete_pm_id))
|
||||
pay_meth.unlink()
|
||||
return request.redirect('/my/payment_method')
|
||||
|
||||
@http.route(['/website_payment/pay'], type='http', auth='public', website=True)
|
||||
def pay(self, reference='', amount=False, currency_id=None, acquirer_id=None, **kw):
|
||||
env = request.env
|
||||
@@ -90,25 +83,3 @@ 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
|
||||
@@ -8,7 +8,7 @@ div.card_placeholder {
|
||||
width: 32px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
top: 34px;
|
||||
top: 8px;
|
||||
right: 20px;
|
||||
-webkit-transition: 0.4s cubic-bezier(0.455, 0.03, 0.515, 0.955);
|
||||
transition: 0.4s cubic-bezier(0.455, 0.03, 0.515, 0.955);
|
||||
|
||||
@@ -10,7 +10,7 @@ div.card_placeholder
|
||||
width: 32px
|
||||
height: 20px
|
||||
position: absolute
|
||||
top: 34px
|
||||
top: 8px
|
||||
right: 20px
|
||||
-webkit-transition: .4s cubic-bezier(0.455,0.03,0.515,0.955)
|
||||
transition: .4s cubic-bezier(0.455,0.03,0.515,0.955)
|
||||
|
||||
@@ -60,58 +60,3 @@ $(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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="header_footer_custom_payment" inherit_id="website.footer_default" customize_show="True" active="False" name="Payment Acquirers">
|
||||
<template id="header_footer_custom_payment" inherit_id="website.footer_default" customize_show="True" active="False" name="Payment Methods">
|
||||
<xpath expr="//div[@id='info']" position="inside">
|
||||
<h4 class="payment_method_title">Payment Acquirer</h4>
|
||||
<ul class="payment_method_list list-inline">
|
||||
@@ -24,45 +24,28 @@
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="pay_methods" name="Payment Acquirers">
|
||||
<template id="pay_methods" name="Payment Methods">
|
||||
<t t-call="website.layout">
|
||||
<t t-set="additional_title">Payment Acquirers</t>
|
||||
<t t-set="additional_title">Payment Methods</t>
|
||||
<div class="wrap">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb mt8">
|
||||
<li><a href="/my/home"><i class="fa fa-home"/></a></li>
|
||||
<li>Payment Acquirers</li>
|
||||
<li>Payment Methods</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<h1>Payment Acquirers</h1>
|
||||
<h4 t-if="acquirers">Add a new payment acquirer:</h4>
|
||||
<div t-if="acquirers" class="row">
|
||||
<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 class="clearfix">
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<h3>Manage your Payment Methods</h3>
|
||||
<t t-call="payment.payment_tokens_list">
|
||||
<t t-set="mode" t-value="'manage'"/>
|
||||
<t t-set="verify_validity" t-value="True"/>
|
||||
</t>
|
||||
</div>
|
||||
<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">
|
||||
<option t-att-value="acquirer.id" t-esc="acquirer.name"/>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<t t-foreach="acquirers" t-as="acquirer">
|
||||
<div t-raw="acquirer.form" t-att-data-acquirer-id="acquirer.id" t-attf-class="acquirer #{'hidden' if acquirer!=acquirers[0] else ''}"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
@@ -73,7 +56,7 @@
|
||||
<template id="pay_meth_link" inherit_id="website_portal.portal_layout" customize_show="True">
|
||||
<xpath expr="//div[@class='o_my_details']" position="inside">
|
||||
<t t-if="request.env['payment.acquirer'].search([('website_published', '=', True), ('registration_view_template_id', '!=', False)])">
|
||||
<div class='manage_payment_method mt16'><a href="/my/payment_method">Manage your payment acquirers</a></div>
|
||||
<div class='manage_payment_method mt16'><a href="/my/payment_method">Manage your Payment Methods</a></div>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user