diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 43ef948a626..40098f5f16e 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -88,7 +88,8 @@ class StockMove(models.Model): @api.multi def action_cancel(self): if any(move.quantity_done for move in self): #TODO: either put in stock, or check there is a production order related to it - raise exceptions.UserError(_('You cannot cancel a stock move having already consumed material')) + raise exceptions.UserError(_('You cannot cancel a manufacturing order if you have already consumed material.\ + If you want to cancel this MO, please change the consumed quantities to 0.')) return super(StockMove, self).action_cancel() @api.multi diff --git a/addons/payment/controllers/portal.py b/addons/payment/controllers/portal.py index 93d33151d0a..5b170662f0e 100644 --- a/addons/payment/controllers/portal.py +++ b/addons/payment/controllers/portal.py @@ -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("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('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 \ No newline at end of file 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/css/portal_payment.css b/addons/payment/static/src/css/portal_payment.css index 1892700fdd0..e9db1fd8d28 100644 --- a/addons/payment/static/src/css/portal_payment.css +++ b/addons/payment/static/src/css/portal_payment.css @@ -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); diff --git a/addons/payment/static/src/css/portal_payment.sass b/addons/payment/static/src/css/portal_payment.sass index a396bae99c1..e7de8c55cd5 100644 --- a/addons/payment/static/src/css/portal_payment.sass +++ b/addons/payment/static/src/css/portal_payment.sass @@ -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) 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 = $('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/js/payment_portal.js b/addons/payment/static/src/js/payment_portal.js index 74b4cb95282..d7397f1b04d 100644 --- a/addons/payment/static/src/js/payment_portal.js +++ b/addons/payment/static/src/js/payment_portal.js @@ -60,64 +60,3 @@ $(document).ready(function () { }); }); - - -odoo.define('payment.payment_acquirer', function (require) { -"use strict"; - -var ajax = require('web.ajax'); -var Dialog = require('web.Dialog'); - -$(document).ready(function () { - - if( !$('#payment_tokens_list').length ) { - return $.Deferred().reject("DOM doesn't contain '.payment_tokens_list'"); - } - - 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 += ''; - }); - - content = $('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: '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(); - } - }); - }); -}); -}); 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/assets.xml b/addons/payment/views/assets.xml index da3d93ee378..2c1c6e5a072 100644 --- a/addons/payment/views/assets.xml +++ b/addons/payment/views/assets.xml @@ -7,6 +7,8 @@ + + - \ No newline at end of file + diff --git a/addons/payment/views/payment_portal_templates.xml b/addons/payment/views/payment_portal_templates.xml index c898a7374a3..3c8a5e3f5d3 100644 --- a/addons/payment/views/payment_portal_templates.xml +++ b/addons/payment/views/payment_portal_templates.xml @@ -1,45 +1,28 @@