+
+
diff --git a/addons/payment/data/payment_icon_data.xml b/addons/payment/data/payment_icon_data.xml
new file mode 100644
index 00000000000..4dc62d67e39
--- /dev/null
+++ b/addons/payment/data/payment_icon_data.xml
@@ -0,0 +1,57 @@
+
+
+
+ VISA
+
+
+
+
+ American Express
+
+
+
+
+ Cirrus
+
+
+
+
+ Diners Club International
+
+
+
+
+ Diners Club International
+
+
+
+
+ Discover
+
+
+
+
+ JCB
+
+
+
+
+ Maestro
+
+
+
+
+ MasterCard
+
+
+
+
+ UnionPay
+
+
+
+
+ Bancontact
+
+
+
\ No newline at end of file
diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py
index 2e36d891344..d225dd8c801 100644
--- a/addons/payment/models/payment_acquirer.py
+++ b/addons/payment/models/payment_acquirer.py
@@ -5,7 +5,7 @@ import logging
import datetime
from odoo import api, exceptions, fields, models, _
-from odoo.tools import consteq, float_round, image_resize_images, ustr
+from odoo.tools import consteq, float_round, image_resize_images, image_resize_image, ustr
from odoo.addons.base.module import module
from odoo.exceptions import ValidationError
@@ -117,8 +117,8 @@ class PaymentAcquirer(models.Model):
help='Message displayed, if error is occur during the payment process.')
save_token = fields.Selection([
('none', 'Never'),
- ('ask', 'Let the customer decide (recommended for eCommerce)'),
- ('always', 'Always (recommended for Subscriptions)')],
+ ('ask', 'Let the customer decide'),
+ ('always', 'Always')],
string='Save Cards', default='none',
help="This option allows customers to save their credit card as a payment token and to reuse it for a later purchase."
"If you manage subscriptions (recurring invoicing), you need it to automatically charge the customer when you "
@@ -150,6 +150,12 @@ class PaymentAcquirer(models.Model):
"resized as a 64x64px image, with aspect ratio preserved. "
"Use this field anywhere a small image is required.")
+ payment_icon_ids = fields.Many2many('payment.icon', string='Supported Payment Icons')
+ payment_flow = fields.Selection(selection=[('s2s','The customer encode his payment details on the website.'),
+ ('form', 'The customer is redirected to the website of the acquirer.')],
+ default='form', required=True, string='Payment flow',
+ help="""Note: Subscriptions does not take this field in account, it uses server to server by default.""")
+
def _search_is_tokenized(self, operator, value):
tokenized = self._get_feature_support()['tokenize']
if (operator, value) in [('=', True), ('!=', False)]:
@@ -344,16 +350,11 @@ class PaymentAcquirer(models.Model):
return self.view_template_id.render(values, engine='ir.qweb')
- @api.multi
- def _registration_render(self, partner_id, qweb_context=None):
- if qweb_context is None:
- qweb_context = {}
- qweb_context.update(id=self.ids[0], partner_id=partner_id)
- method_name = '_%s_registration_form_generate_values' % (self.provider,)
- if hasattr(self, method_name):
- method = getattr(self, method_name)
- qweb_context.update(method(qweb_context))
- return self.registration_view_template_id.render(qweb_context, engine='ir.qweb')
+ def get_s2s_form_xml_id(self):
+ if self.registration_view_template_id:
+ model_data = self.env['ir.model.data'].search([('model', '=', 'ir.ui.view'), ('res_id', '=', self.registration_view_template_id.id)])
+ return ('%s.%s') % (model_data.module, model_data.name)
+ return False
@api.multi
def s2s_process(self, data):
@@ -394,6 +395,32 @@ class PaymentAcquirer(models.Model):
'context': context,
}
+class PaymentIcon(models.Model):
+ _name = 'payment.icon'
+ _description = 'Payment Icon'
+
+ name = fields.Char(string='Name')
+ acquirer_ids = fields.Many2many('payment.acquirer', string="Acquirers", help="List of Acquirers supporting this payment icon.")
+ image = fields.Binary(
+ "Image", attachment=True,
+ help="This field holds the image used for this payment icon, limited to 1024x1024px")
+
+ image_payment_form = fields.Binary(
+ "Image displayed on the payment form", attachment=True)
+
+ @api.model
+ def create(self, vals):
+ if 'image' in vals:
+ vals['image_payment_form'] = image_resize_image(vals['image'], size=(45,30))
+ vals['image'] = image_resize_image(vals['image'], size=(64,64))
+ return super(PaymentIcon, self).create(vals)
+
+ @api.multi
+ def write(self, vals):
+ if 'image' in vals:
+ vals['image_payment_form'] = image_resize_image(vals['image'], size=(45,30))
+ vals['image'] = image_resize_image(vals['image'], size=(64,64))
+ return super(PaymentIcon, self).write(vals)
class PaymentTransaction(models.Model):
""" Transaction Model. Each specific acquirer can extend the model by adding
diff --git a/addons/payment/security/ir.model.access.csv b/addons/payment/security/ir.model.access.csv
index 2bfa46839a0..2982f1ab0c0 100644
--- a/addons/payment/security/ir.model.access.csv
+++ b/addons/payment/security/ir.model.access.csv
@@ -9,3 +9,6 @@ payment_method_all,payment.token.all,model_payment_token,,1,0,0,0
payment_method_user,payment.token.user,model_payment_token,base.group_user,1,1,1,1
payment_method_portal,payment.token.portal,model_payment_token,base.group_portal,1,1,1,1
payment_method_system,payment.token.system,model_payment_token,base.group_system,1,1,1,1
+payment_icon_all,payment.icon.all,model_payment_icon,,1,0,0,0
+payment_icon_user,payment.icon.user,model_payment_icon,base.group_user,1,1,1,0
+payment_icon_system,payment.icon.system,model_payment_icon,base.group_system,1,1,1,1
\ No newline at end of file
diff --git a/addons/payment/static/img/american_express.png b/addons/payment/static/img/american_express.png
new file mode 100644
index 00000000000..9f31d21ef20
Binary files /dev/null and b/addons/payment/static/img/american_express.png differ
diff --git a/addons/payment/static/img/bancontact.png b/addons/payment/static/img/bancontact.png
new file mode 100644
index 00000000000..7941a69cf64
Binary files /dev/null and b/addons/payment/static/img/bancontact.png differ
diff --git a/addons/payment/static/img/cirrus.png b/addons/payment/static/img/cirrus.png
new file mode 100644
index 00000000000..eb4b2aa81f6
Binary files /dev/null and b/addons/payment/static/img/cirrus.png differ
diff --git a/addons/payment/static/img/diners_club_intl.png b/addons/payment/static/img/diners_club_intl.png
new file mode 100644
index 00000000000..888860a140e
Binary files /dev/null and b/addons/payment/static/img/diners_club_intl.png differ
diff --git a/addons/payment/static/img/discover.png b/addons/payment/static/img/discover.png
new file mode 100644
index 00000000000..d9222265651
Binary files /dev/null and b/addons/payment/static/img/discover.png differ
diff --git a/addons/payment/static/img/jcb.png b/addons/payment/static/img/jcb.png
new file mode 100644
index 00000000000..f3e3c5bfb06
Binary files /dev/null and b/addons/payment/static/img/jcb.png differ
diff --git a/addons/payment/static/img/maestro.png b/addons/payment/static/img/maestro.png
new file mode 100644
index 00000000000..a0a6b328380
Binary files /dev/null and b/addons/payment/static/img/maestro.png differ
diff --git a/addons/payment/static/img/mastercard.png b/addons/payment/static/img/mastercard.png
new file mode 100644
index 00000000000..2572a33b373
Binary files /dev/null and b/addons/payment/static/img/mastercard.png differ
diff --git a/addons/payment/static/img/unionpay.png b/addons/payment/static/img/unionpay.png
new file mode 100644
index 00000000000..8d830c5f944
Binary files /dev/null and b/addons/payment/static/img/unionpay.png differ
diff --git a/addons/payment/static/img/visa.png b/addons/payment/static/img/visa.png
new file mode 100644
index 00000000000..58cfab08725
Binary files /dev/null and b/addons/payment/static/img/visa.png differ
diff --git a/addons/payment/static/src/js/payment_form.js b/addons/payment/static/src/js/payment_form.js
index ae2d44c1cb7..491cafcf00c 100644
--- a/addons/payment/static/src/js/payment_form.js
+++ b/addons/payment/static/src/js/payment_form.js
@@ -18,7 +18,6 @@ odoo.define('payment.payment_form', function (require){
},
payEvent: function(ev) {
- ev.stopPropagation();
ev.preventDefault();
var form = this.el;
var checked_radio = this.$('input[type="radio"]:checked');
@@ -33,8 +32,28 @@ odoo.define('payment.payment_form', function (require){
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 inputs_form = $('input', acquirer_form);
+ var form_data = this.getFormData(inputs_form);
var ds = $('input[name="data_set"]', acquirer_form)[0];
+ var empty_inputs = false;
+
+ inputs_form.toArray().forEach(function(element) {
+ if(element.dataset.isRequired) {
+ if (element.value.length === 0) {
+ $(element).closest('div.form-group').addClass('has-error');
+ }
+ else {
+ $(element).closest('div.form-group').removeClass('has-error');
+ }
+ empty_inputs = true;
+ }
+ });
+
+ if(empty_inputs) {
+ this.error(_t('Missing values'), _t('
Please fill all the inputs required.
'));
+ return;
+ }
+
$(button).attr('disabled', true);
$(button).prepend('');
@@ -59,7 +78,7 @@ odoo.define('payment.payment_form', function (require){
}
// if the server has returned false, we display an error
else {
- self.error(_t('Error'),
+ self.error(_t('Server Error'),
_t("
We are not able to add your payment method at the moment.
"));
}
// here we remove the 'processing' icon from the 'add a new payment' button
@@ -70,10 +89,62 @@ odoo.define('payment.payment_form', function (require){
$(button).attr('disabled', false);
$(button).find('span.o_loader').remove();
- self.error(_t('Error'),
+ self.error(_t('Server Error'),
_t("
We are not able to add your payment method at the moment.
") + (core.debug ? data.data.message : ''));
});
}
+ // if the user is going to pay with a form payment, then
+ else if(this.isFormPaymentRadio(checked_radio))
+ {
+ var self = this;
+ // we retrieve the acquirer id and its form
+ var acquirer_id = this.getAcquirerIdFromRadio(checked_radio);
+ var acquirer_form = this.$('#o_payment_form_acq_' + acquirer_id);
+ var inputs_form = $('input', acquirer_form);
+ var ds = $('input[name="data_set"]', acquirer_form)[0];
+ var form_save_token = false;
+ var $tx_url = this.$el.find('input[name="prepare_tx_url"]');
+
+ // if neither the dataset or the action url is defined, then we just stop the execution of the function
+ if(!ds || !ds.dataset.actionUrl)
+ return;
+
+ // if there's a prepare tx url set
+ if($tx_url.length == 1)
+ {
+ // if the user wants to save his credit card info
+ form_save_token = $('input[name="o_payment_form_save_token"]').checked === true;
+ // then we call the route to prepare the transaction
+ ajax.jsonRpc($tx_url[0].value, 'call', {
+ 'acquirer_id': parseInt(acquirer_id),
+ 'save_token': form_save_token
+ }).then(function(result){
+ if(result) {
+ // if the server sent us the html form, we create a form element
+ var newForm = document.createElement('form');
+ newForm.setAttribute("method", "post"); // set it to post
+ newForm.setAttribute("action", ds.dataset.actionUrl); // set the action url
+ newForm.hidden = true; // hide it
+ newForm.innerHTML = result; // put the html sent by the server inside the form
+ document.getElementsByTagName('body')[0].appendChild(newForm); // append the form to the body
+ $(newForm).find('input[data-remove-me]').remove(); // remove all the input that should be removed
+ newForm.submit(); // and finally submit the form
+ }
+ else {
+ self.error(_t('Server Error'),
+ _t("
We are not able to redirect you to the payment form.
We are not able to redirect you to the payment form.
") + (core.debug ? data.data.message : ''));
+ });
+ }
+ else
+ {
+ // we append the form to the body and send it.
+ this.error(_t("Cannot set-up the payment"), _t("
We're unable to process your payment.
"));
+ }
+ }
// if the user is using an old payment
else {
// then we just submit the form
@@ -81,8 +152,8 @@ odoo.define('payment.payment_form', function (require){
}
}
else {
- this.error(_t('Error'),
- _t('
'));
}
},
// event handler when clicking on the button to add a new payment method
@@ -96,16 +167,35 @@ odoo.define('payment.payment_form', function (require){
// 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('');
// 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 inputs_form = $('input', acquirer_form);
+ var form_data = this.getFormData(inputs_form);
var ds = $('input[name="data_set"]', acquirer_form)[0];
+ var empty_inputs = false;
+
+ inputs_form.toArray().forEach(function(element) {
+ if(element.dataset.isRequired) {
+ if (element.value.length === 0) {
+ $(element).closest('div.form-group').addClass('has-error');
+ }
+ else {
+ $(element).closest('div.form-group').removeClass('has-error');
+ }
+ empty_inputs = true;
+ }
+ });
+
+ if(empty_inputs) {
+ this.error(_t('Missing values'), _t('
Please fill all the inputs required.
'));
+ return;
+ }
+ // We add a 'processing' icon into the 'add a new payment' button
+ $(button).attr('disabled', true);
+ $(button).prepend('');
// we force the check when adding a card trough here
form_data.verify_validity = true;
@@ -132,7 +222,7 @@ odoo.define('payment.payment_form', function (require){
}
// if the server has returned false, we display an error
else {
- self.error(_t('Error'),
+ self.error(_t('Server Error'),
_t("
We are not able to add your payment method at the moment.
"));
}
// here we remove the 'processing' icon from the 'add a new payment' button
@@ -143,13 +233,13 @@ odoo.define('payment.payment_form', function (require){
$(button).attr('disabled', false);
$(button).find('span.o_loader').remove();
- self.error(_t('Error'),
+ self.error(_t('Server error'),
_t("
We are not able to add your payment method at the moment.
+
diff --git a/addons/payment/views/payment_templates.xml b/addons/payment/views/payment_templates.xml
index ae9a1501632..81e73aa464b 100644
--- a/addons/payment/views/payment_templates.xml
+++ b/addons/payment/views/payment_templates.xml
@@ -17,36 +17,100 @@
- '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
+ - 's2s_acquirers' the list of the server2server acquirers
+ - 'form_acquirers' the list of the payment acquirers to pay with forms
- 'verify_validity' if we need to verify if the payment method is valid when adding a new one
+ - 'prepare_tx_url' the url of the route which will handle the creation of a transaction for a form base payment (handles if the transaction is form or form_save)
-->
-
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index 98e271dfde1..15ebd112268 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -708,14 +708,22 @@ class WebsiteSale(http.Controller):
partner=order.partner_id.id,
order=order,
payment_action_id=request.env.ref('payment.action_payment_acquirer').id,
+ return_url= '/shop/payment/validate',
+ bootstrap_formatting= True
)
acquirers = request.env['payment.acquirer'].search(
[('website_published', '=', True), ('company_id', '=', order.company_id.id)]
)
- values['acquirers'] = []
- for acquirer in acquirers:
- acquirer_button = acquirer.with_context(submit_class='btn btn-primary', submit_txt=_('Pay Now')).sudo().render(
+
+ values['form_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 'form' and acq.view_template_id]
+ values['s2s_acquirers'] = [acq for acq in acquirers if acq.payment_flow == 's2s' and acq.registration_view_template_id]
+ values['tokens'] = request.env['payment.token'].search(
+ [('partner_id', '=', order.partner_id.id),
+ ('acquirer_id', 'in', [acq.id for acq in values['s2s_acquirers']])])
+
+ for acq in values['form_acquirers']:
+ acq.form = acq.with_context(submit_class='btn btn-primary', submit_txt=_('Pay Now')).sudo().render(
'/',
order.amount_total,
order.pricelist_id.currency_id.id,
@@ -725,9 +733,6 @@ class WebsiteSale(http.Controller):
'billing_partner_id': order.partner_invoice_id.id,
}
)
- acquirer.button = acquirer_button
- values['acquirers'].append(acquirer)
- values['tokens'] = request.env['payment.token'].search([('partner_id', '=', order.partner_id.id), ('acquirer_id', 'in', acquirers.ids)])
return values
@@ -776,8 +781,10 @@ class WebsiteSale(http.Controller):
else:
return request.redirect("/shop/payment?error=no_token_or_missmatch_tx")
- @http.route(['/shop/payment/transaction/'], type='json', auth="public", website=True)
- def payment_transaction(self, acquirer_id, tx_type='form', token=None, **kwargs):
+ @http.route(['/shop/payment/transaction/',
+ '/shop/payment/transaction/',
+ '/shop/payment/transaction//'], type='json', auth="public", website=True)
+ def payment_transaction(self, acquirer_id, save_token=False, so_id=None, access_token=None, token=None, **kwargs):
""" Json method that creates a payment.transaction, used to create a
transaction when the user clicks on 'pay now' button. After having
created the transaction, the event continues and the user is redirected
@@ -786,10 +793,11 @@ class WebsiteSale(http.Controller):
:param int acquirer_id: id of a payment.acquirer record. If not set the
user is redirected to the checkout page
"""
+ tx_type = 'form'
+ if save_token:
+ tx_type = 'form_save'
# In case the route is called directly from the JS (as done in Stripe payment method)
- so_id = kwargs.get('so_id')
- access_token = kwargs.get('access_token')
if so_id and access_token:
order = request.env['sale.order'].sudo().search([('id', '=', so_id), ('access_token', '=', access_token)])
elif so_id:
@@ -797,7 +805,7 @@ class WebsiteSale(http.Controller):
else:
order = request.website.sale_get_order()
if not order or not order.order_line or acquirer_id is None:
- return request.redirect("/shop/checkout")
+ return False
assert order.partner_id.id != request.website.partner_id.id
@@ -808,11 +816,44 @@ class WebsiteSale(http.Controller):
tx = tx.check_or_create_sale_tx(order, acquirer, payment_token=payment_token, tx_type=tx_type)
request.session['sale_transaction_id'] = tx.id
- if token:
- return request.env.ref('website_sale.payment_token_form').render(dict(tx=tx), engine='ir.qweb')
-
return tx.render_sale_button(order, '/shop/payment/validate')
+ @http.route('/shop/payment/token', type='http', auth='public', website=True)
+ def payment_token(self, pm_id=None, **kwargs):
+ """ Method that handles payment using saved tokens
+
+ :param int pm_id: id of the payment.token that we want to use to pay.
+ """
+ order = request.website.sale_get_order()
+ # do not crash if the user has already paid and try to pay again
+ if not order:
+ return request.redirect('/shop/?error=no_order')
+
+ assert order.partner_id.id != request.website.partner_id.id
+
+ try:
+ pm_id = int(pm_id)
+ except ValueError:
+ return request.redirect('/shop/?error=invalid_token_id')
+
+ # We retrieve the token the user want to use to pay
+ token = request.env['payment.token'].browse(pm_id)
+ if not token:
+ return request.redirect('/shop/?error=token_not_found')
+
+ # we retrieve an existing transaction (if it exists obviously)
+ tx = request.website.sale_get_transaction() or request.env['payment.transaction'].sudo()
+ # we check if the transaction is Ok, if not then we create it
+ tx = tx.check_or_create_sale_tx(order, token.acquirer_id, payment_token=token, tx_type='server2server')
+ # we set the transaction id into the session (so `sale_get_transaction` can retrieve it )
+ request.session['sale_transaction_id'] = tx.id
+ # we proceed the s2s payment
+ res = tx.confirm_sale_token()
+ # we then redirect to the page that validates the payment by giving it error if there's one
+ if res is not True:
+ return request.redirect('/shop/payment/validate?success=False&error=%s' % res)
+ return request.redirect('/shop/payment/validate?success=True')
+
@http.route('/shop/payment/get_status/', type='json', auth="public", website=True)
def payment_get_status(self, sale_order_id, **post):
order = request.env['sale.order'].sudo().browse(sale_order_id).exists()
diff --git a/addons/website_sale/static/src/js/website_sale_tour_buy.js b/addons/website_sale/static/src/js/website_sale_tour_buy.js
index e3878a588f4..ade80828c12 100644
--- a/addons/website_sale/static/src/js/website_sale_tour_buy.js
+++ b/addons/website_sale/static/src/js/website_sale_tour_buy.js
@@ -71,12 +71,13 @@ tour.register('shop_buy_product', {
},
{
content: "select payment",
- trigger: '#payment_method label:contains("Wire Transfer") input',
+ trigger: '#payment_method label:contains("Wire Transfer")',
},
{
content: "Pay Now",
- extra_trigger: '#payment_method label:contains("Wire Transfer") input:checked',
- trigger: '.o_payment_acquirer_button .btn[type="submit"]:visible',
+ //Either there are multiple payment methods, and one is checked, either there is only one, and therefore there are no radio inputs
+ extra_trigger: '#payment_method label:contains("Wire Transfer") input:checked,#payment_method:not(:has("input:radio:visible"))',
+ trigger: 'button[id="o_payment_form_pay"]:visible',
},
{
content: "finish",
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 58eb670c901..1c113d8f759 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -1324,71 +1324,26 @@
-
-
-
Choose a payment acquirer
-
-
-
-
-
-
-
-
-
-
-
- No payment gateway is available for your country
-