From c089d39a668fa6ab1f5d7867747da19d0b75f035 Mon Sep 17 00:00:00 2001 From: amoyaux Date: Wed, 9 Aug 2017 16:42:41 +0200 Subject: [PATCH 01/10] [FIX] stock : traceback filter 'Available Products' It happens because the function still use the old fields utilized before refactoring introduced by commit b3180c841101510081ee8ef9c52d205497efdd4f This commit adapt the function by using quantity instead of qty. --- addons/stock/models/product.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index dcd964a5df5..43cbe1e2e4d 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -267,9 +267,9 @@ class Product(models.Model): domain_quant.append(('owner_id', '=', owner_id)) if package_id: domain_quant.append(('package_id', '=', package_id)) - quants_groupby = self.env['stock.quant'].read_group(domain_quant, ['product_id', 'qty'], ['product_id']) + quants_groupby = self.env['stock.quant'].read_group(domain_quant, ['product_id', 'quantity'], ['product_id']) for quant in quants_groupby: - if OPERATORS[operator](quant['qty'], value): + if OPERATORS[operator](quant['quantity'], value): product_ids.add(quant['product_id'][0]) return list(product_ids) From 2df9c22d80be82b4ed7ffcd79f858e46c34e6479 Mon Sep 17 00:00:00 2001 From: tbe-odoo Date: Fri, 28 Jul 2017 15:52:34 +0200 Subject: [PATCH 02/10] [IMP] Payments & subscriptions: Improved Payments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- addons/payment/models/payment_acquirer.py | 19 +- addons/payment/static/src/js/payment_form.js | 261 ++++++++++++++++++ .../payment/static/src/less/payment_form.less | 32 +++ addons/payment/views/payment_assets.xml | 2 + addons/payment/views/payment_templates.xml | 102 ++++--- addons/payment_authorize/controllers/main.py | 26 +- .../models/authorize_request.py | 27 ++ addons/payment_authorize/models/payment.py | 39 ++- .../views/payment_authorize_templates.xml | 54 ++-- addons/payment_ogone/controllers/main.py | 34 +++ addons/payment_ogone/models/payment.py | 10 +- .../views/payment_ogone_templates.xml | 53 ++-- addons/payment_stripe/controllers/main.py | 26 +- addons/payment_stripe/models/payment.py | 47 +++- .../views/payment_stripe_templates.xml | 53 ++-- addons/website_payment/controllers/main.py | 31 +-- .../static/src/css/website_payment.css | 2 +- .../static/src/css/website_payment.sass | 2 +- .../static/src/js/payment_acquirer.js | 55 ---- .../views/website_payment_templates.xml | 43 +-- 20 files changed, 639 insertions(+), 279 deletions(-) create mode 100644 addons/payment/static/src/js/payment_form.js create mode 100644 addons/payment/static/src/less/payment_form.less diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index 80c84094c30..623676c7a9c 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -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 {} \ No newline at end of file + 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} diff --git a/addons/payment/static/src/js/payment_form.js b/addons/payment/static/src/js/payment_form.js new file mode 100644 index 00000000000..ae2d44c1cb7 --- /dev/null +++ b/addons/payment/static/src/js/payment_form.js @@ -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(' '); + + 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("

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 + $(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("

We are not able to add your payment method at the moment.

") + (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('

Please select a payment method

')); + } + }, + // 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(' '); + // 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("

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 + $(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("

We are not able to add your payment method at the moment.

") + (core.debug ? data.data.message : '')); + }); + } + else { + this.error(_t('Error'), + _t('

Please select the option to add a new payment method

')); + } + }, + // 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("

We are not able to delete your payment method at the moment.

")); + }); + }; + + 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 += '

' + _.str.escapeHTML(sub.name) + '

'; + }); + + content = $('

').html(_t('

This card is currently linked to the following records:

') + 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("

We are not able to delete your payment method at the moment.

") + (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')); + }); +}); diff --git a/addons/payment/static/src/less/payment_form.less b/addons/payment/static/src/less/payment_form.less new file mode 100644 index 00000000000..ce6b2aeab6e --- /dev/null +++ b/addons/payment/static/src/less/payment_form.less @@ -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; + } +} \ No newline at end of file diff --git a/addons/payment/views/payment_assets.xml b/addons/payment/views/payment_assets.xml index 47d4717ef97..76f32ae1ed1 100644 --- a/addons/payment/views/payment_assets.xml +++ b/addons/payment/views/payment_assets.xml @@ -3,6 +3,8 @@ \ No newline at end of file diff --git a/addons/payment/views/payment_templates.xml b/addons/payment/views/payment_templates.xml index abf18e0cf25..ae9a1501632 100644 --- a/addons/payment/views/payment_templates.xml +++ b/addons/payment/views/payment_templates.xml @@ -9,59 +9,71 @@ diff --git a/addons/payment_authorize/controllers/main.py b/addons/payment_authorize/controllers/main.py index b42efe817bb..6b0aae9bf6a 100644 --- a/addons/payment_authorize/controllers/main.py +++ b/addons/payment_authorize/controllers/main.py @@ -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): diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py index d8191dfc34c..fcfab1e3ac2 100644 --- a/addons/payment_authorize/models/authorize_request.py +++ b/addons/payment_authorize/models/authorize_request.py @@ -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. diff --git a/addons/payment_authorize/models/payment.py b/addons/payment_authorize/models/payment.py index 184f07cdc58..bfc5bfd569a 100644 --- a/addons/payment_authorize/models/payment.py +++ b/addons/payment_authorize/models/payment.py @@ -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 diff --git a/addons/payment_authorize/views/payment_authorize_templates.xml b/addons/payment_authorize/views/payment_authorize_templates.xml index abbbc80b93a..c4b91605a00 100644 --- a/addons/payment_authorize/views/payment_authorize_templates.xml +++ b/addons/payment_authorize/views/payment_authorize_templates.xml @@ -54,38 +54,28 @@ +
+ +
+
+ +
+
+ +
+
+ + + + + diff --git a/addons/payment_ogone/controllers/main.py b/addons/payment_ogone/controllers/main.py index 503f1de1419..9c81761a70e 100644 --- a/addons/payment_ogone/controllers/main.py +++ b/addons/payment_ogone/controllers/main.py @@ -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 = '' diff --git a/addons/payment_ogone/models/payment.py b/addons/payment_ogone/models/payment.py index 0702d38c8bb..9ac90f9478f 100644 --- a/addons/payment_ogone/models/payment.py +++ b/addons/payment_ogone/models/payment.py @@ -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'), diff --git a/addons/payment_ogone/views/payment_ogone_templates.xml b/addons/payment_ogone/views/payment_ogone_templates.xml index 63b4af07142..c70d461d0a7 100644 --- a/addons/payment_ogone/views/payment_ogone_templates.xml +++ b/addons/payment_ogone/views/payment_ogone_templates.xml @@ -54,40 +54,27 @@ - diff --git a/addons/payment_stripe/controllers/main.py b/addons/payment_stripe/controllers/main.py index b4627157a9e..55cad84fd9d 100644 --- a/addons/payment_stripe/controllers/main.py +++ b/addons/payment_stripe/controllers/main.py @@ -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 diff --git a/addons/payment_stripe/models/payment.py b/addons/payment_stripe/models/payment.py index e43e0cbd71f..1bb4ea9aad8 100644 --- a/addons/payment_stripe/models/payment.py +++ b/addons/payment_stripe/models/payment.py @@ -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'] diff --git a/addons/payment_stripe/views/payment_stripe_templates.xml b/addons/payment_stripe/views/payment_stripe_templates.xml index 9e8e28c4d07..8f7f8edd0eb 100644 --- a/addons/payment_stripe/views/payment_stripe_templates.xml +++ b/addons/payment_stripe/views/payment_stripe_templates.xml @@ -32,38 +32,29 @@