diff --git a/addons/point_of_sale/static/src/js/chrome.js b/addons/point_of_sale/static/src/js/chrome.js index 026682d5135..ffc5d5cc860 100644 --- a/addons/point_of_sale/static/src/js/chrome.js +++ b/addons/point_of_sale/static/src/js/chrome.js @@ -453,34 +453,33 @@ var ClientScreenWidget = PosBaseWidget.extend({ var self = this; function loop() { if (self.pos.proxy.posbox_supports_display) { - var deffered = self.pos.proxy.test_ownership_of_client_screen(); - if (deffered) { - deffered.then( - function(data) { - if (typeof data === 'string') { - data = JSON.parse(data); - } - if (data.status === 'OWNER') { - self.change_status_display('success'); - } else { - self.change_status_display('warning'); - } - }, - - function(err) { - if (typeof err == "undefined") { - self.change_status_display('failure'); - } else { - self.change_status_display('not_found'); - self.pos.proxy.posbox_supports_display = false; - } - }) - - .always(function () { - setTimeout(loop,3000); - }); - } - } + self.pos.proxy.test_ownership_of_client_screen().then( + function (data) { + if (typeof data === 'string') { + data = JSON.parse(data); + } + if (data.status === 'OWNER') { + self.change_status_display('success'); + } else { + self.change_status_display('warning'); + } + setTimeout(loop, 3000); + }, + function (err) { + if (err.abort) { + // Stop the loop + return; + } + if (typeof err == "undefined") { + self.change_status_display('failure'); + } else { + self.change_status_display('not_found'); + self.pos.proxy.posbox_supports_display = false; + } + setTimeout(loop, 3000); + } + ); + } } loop(); }, @@ -563,12 +562,12 @@ var Chrome = PosBaseWidget.extend(AbstractAction.prototype, { this.logo_click_time = 0; this.logo_click_count = 0; - this.previous_touch_y_coordinate = -1; + this.previous_touch_y_coordinate = -1; this.widget = {}; // contains references to subwidgets instances this.cleanup_dom(); - this.pos.ready.done(function(){ + this.pos.ready.then(function(){ self.build_chrome(); self.build_widgets(); self.disable_rubberbanding(); @@ -577,7 +576,7 @@ var Chrome = PosBaseWidget.extend(AbstractAction.prototype, { self.loading_hide(); self.replace_crashmanager(); self.pos.push_order(); - }).fail(function(err){ // error when loading models data from the backend + }).guardedCatch(function (err) { // error when loading models data from the backend self.loading_error(err); }); }, @@ -859,6 +858,7 @@ var Chrome = PosBaseWidget.extend(AbstractAction.prototype, { // This method instantiates all the screens, widgets, etc. build_widgets: function() { + var self = this; this.load_widgets(this.widgets); this.screens = {}; @@ -874,15 +874,15 @@ var Chrome = PosBaseWidget.extend(AbstractAction.prototype, { } this.popups = {}; - for (i = 0; i < this.gui.popup_classes.length; i++) { - classe = this.gui.popup_classes[i]; - if (!classe.condition || classe.condition.call(this)) { - var popup = new classe.widget(this,{}); - popup.appendTo(this.$('.popups')); - this.popups[classe.name] = popup; - this.gui.add_popup(classe.name, popup); + _.forEach(this.gui.popup_classes, function (classe) { + if (!classe.condition || classe.condition.call(self)) { + var popup = new classe.widget(self,{}); + popup.appendTo(self.$('.popups')).then(function () { + self.popups[classe.name] = popup; + self.gui.add_popup(classe.name, popup); + }); } - } + }); this.gui.set_startup_screen('products'); this.gui.set_default_screen('products'); diff --git a/addons/point_of_sale/static/src/js/devices.js b/addons/point_of_sale/static/src/js/devices.js index c1db7f61884..683ca3f6853 100644 --- a/addons/point_of_sale/static/src/js/devices.js +++ b/addons/point_of_sale/static/src/js/devices.js @@ -11,7 +11,7 @@ var QWeb = core.qweb; var _t = core._t; // the JobQueue schedules a sequence of 'jobs'. each job is -// a function returning a deferred. the queue waits for each job to finish +// a function returning a promise. The queue waits for each job to finish // before launching the next. Each job can also be scheduled with a delay. // the is used to prevent parallel requests to the proxy. @@ -19,17 +19,19 @@ var JobQueue = function(){ var queue = []; var running = false; var scheduled_end_time = 0; - var end_of_queue = (new $.Deferred()).resolve(); + var end_of_queue = Promise.resolve(); var stoprepeat = false; - var run = function(){ - if(end_of_queue.state() === 'resolved'){ - end_of_queue = new $.Deferred(); - } - if(queue.length > 0){ + var run = function () { + var runNextJob = function () { + if (queue.length === 0) { + running = false; + scheduled_end_time = 0; + return Promise.resolve(); + } running = true; var job = queue[0]; - if(!job.opts.repeat || stoprepeat){ + if (!job.opts.repeat || stoprepeat) { queue.shift(); stoprepeat = false; } @@ -37,30 +39,37 @@ var JobQueue = function(){ // the time scheduled for this job scheduled_end_time = (new Date()).getTime() + (job.opts.duration || 0); - // we run the job and put in def when it finishes - var def = job.fun() || (new $.Deferred()).resolve(); + // we run the job and put in prom when it finishes + var prom = job.fun() || Promise.resolve(); - // we don't care if a job fails ... - def.always(function(){ + var always = function () { // we run the next job after the scheduled_end_time, even if it finishes before - setTimeout(function(){ - run(); - }, Math.max(0, scheduled_end_time - (new Date()).getTime()) ); - }); - }else{ - running = false; - scheduled_end_time = 0; - end_of_queue.resolve(); + return new Promise(function (resolve, reject) { + setTimeout( + resolve, + Math.max(0, scheduled_end_time - (new Date()).getTime()) + ); + }); + }; + // we don't care if a job fails ... + return prom.then(always, always).then(runNextJob); + }; + + if (!running) { + end_of_queue = runNextJob(); } }; - // adds a job to the schedule. - // opts : { - // duration : the job is guaranteed to finish no quicker than this (milisec) - // repeat : if true, the job will be endlessly repeated - // important : if true, the scheduled job cannot be canceled by a queue.clear() - // } - this.schedule = function(fun, opts){ + /** + * Adds a job to the schedule. + * + * @param {function} fun must return a promise + * @param {object} [opts] + * @param {number} [opts.duration] the job is guaranteed to finish no quicker than this (milisec) + * @param {boolean} [opts.repeat] if true, the job will be endlessly repeated + * @param {boolean} [opts.important] if true, the scheduled job cannot be canceled by a queue.clear() + */ + this.schedule = function (fun, opts) { queue.push({fun:fun, opts:opts || {}}); if(!running){ run(); @@ -77,10 +86,13 @@ var JobQueue = function(){ stoprepeat = true; }; - // returns a deferred that resolves when all scheduled - // jobs have been run. - // ( jobs added after the call to this method are considered as well ) - this.finished = function(){ + /** + * Returns a promise that resolves when all scheduled jobs have been run. + * (jobs added after the call to this method are considered as well) + * + * @returns {Promise} + */ + this.finished = function () { return end_of_queue; }; @@ -153,11 +165,16 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ } }, - // connects to the specified url + /** + * Connects to the specified url. + * + * @param {string} url + * @returns {Promise} + */ connect: function(url){ var self = this; this.connection = new Session(undefined,url, { use_cors: true}); - this.host = url; + this.host = url; this.set_connection_status('connecting',{}); return this.message('handshake').then(function(response){ @@ -175,82 +192,99 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ }); }, - // find a proxy and connects to it. for options see find_proxy - // - force_ip : only try to connect to the specified ip. - // - port: what port to listen to (default 8069) - // - progress(fac) : callback for search progress ( fac in [0,1] ) - autoconnect: function(options){ + /** + * Find a proxy and connects to it. + * + * @param {Object} [options] + * @param {string} [options.force_ip] only try to connect to the specified ip. + * @param {string} [options.port] @see find_proxy + * @param {function} [options.progress] @see find_proxy + * @returns {Promise} + */ + autoconnect: function (options) { var self = this; this.set_connection_status('connecting',{}); - var found_url = new $.Deferred(); - var success = new $.Deferred(); + var found_url = new Promise(function () {}); - if ( options.force_ip ){ + if (options.force_ip) { // if the ip is forced by server config, bailout on fail found_url = this.try_hard_to_connect(options.force_ip, options); - }else if( localStorage.hw_proxy_url ){ + } else if (localStorage.hw_proxy_url) { // try harder when we remember a good proxy url found_url = this.try_hard_to_connect(localStorage.hw_proxy_url, options) - .then(null,function(){ - if (window.location.protocol != 'https:'){ + .catch(function () { + if (window.location.protocol != 'https:') { return self.find_proxy(options); } }); - }else{ + } else { // just find something quick if (window.location.protocol != 'https:'){ found_url = this.find_proxy(options); } } - success = found_url.then(function(url){ - return self.connect(url); - }); + var successProm = found_url.then(function (url) { + return self.connect(url); + }); - success.fail(function(){ + successProm.catch(function () { self.set_connection_status('disconnected'); }); - return success; + return successProm; }, // starts a loop that updates the connection status - keepalive: function(){ + keepalive: function () { var self = this; function status(){ + var always = function () { + setTimeout(status, 5000); + }; self.connection.rpc('/hw_proxy/status_json',{},{shadow: true, timeout:2500}) - .then(function(driver_status){ + .then(function (driver_status) { self.set_connection_status('connected',driver_status); - },function(){ + }, function () { if(self.get('status').status !== 'connecting'){ self.set_connection_status('disconnected'); } - }).always(function(){ - setTimeout(status,5000); - }); + }).then(always, always); } - if(!this.keptalive){ + if (!this.keptalive) { this.keptalive = true; status(); } }, - message : function(name,params){ + /** + * @param {string} name + * @param {Object} [params] + * @returns {Promise} + */ + message : function (name, params) { var callbacks = this.notifications[name] || []; - for(var i = 0; i < callbacks.length; i++){ + for (var i = 0; i < callbacks.length; i++) { callbacks[i](params); } - if(this.get('status').status !== 'disconnected'){ + if (this.get('status').status !== 'disconnected') { return this.connection.rpc('/hw_proxy/' + name, params || {}, {shadow: true}); - }else{ - return (new $.Deferred()).reject(); + } else { + return Promise.reject(); } }, - // try several time to connect to a known proxy url - try_hard_to_connect: function(url,options){ + /** + * Tries several time to connect to a known proxy url. + * + * @param {*} url + * @param {Object} [options] + * @param {string} [options.port=8069] what port to listen to + * @returns {Promise} + */ + try_hard_to_connect: function (url, options) { options = options || {}; var protocol = window.location.protocol; var port = ( !options.port && protocol == "https:") ? ':443' : ':' + (options.port || '8069'); @@ -266,35 +300,36 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ } // try real hard to connect to url, with a 1sec timeout and up to 'retries' retries - function try_real_hard_to_connect(url, retries, done){ - - done = done || new $.Deferred(); - - $.ajax({ - url: url + '/hw_proxy/hello', - method: 'GET', - timeout: 1000, - }) - .done(function(){ - done.resolve(url); - }) - .fail(function(resp){ - if(retries > 0){ - try_real_hard_to_connect(url,retries-1,done); - }else{ - done.reject(resp.statusText, url); - } - }); - return done; + function try_real_hard_to_connect(url, retries) { + return Promise.resolve( + $.ajax({ + url: url + '/hw_proxy/hello', + method: 'GET', + timeout: 1000, + }) + .then(function () { + return Promise.resolve(url); + }, function (resp) { + if (retries > 0) { + return try_real_hard_to_connect(url, retries-1); + } else { + return Promise.reject([resp.statusText, url]); + } + }) + ); } - return try_real_hard_to_connect(url,3); + return try_real_hard_to_connect(url, 3); }, - // returns as a deferred a valid host url that can be used as proxy. - // options: - // - port: what port to listen to (default 8069) - // - progress(fac) : callback for search progress ( fac in [0,1] ) + /** + * Returns as a promise a valid host url that can be used as proxy. + * + * @param {Object} [options] + * @param {string} [options.port] what port to listen to (default 8069) + * @param {function} [options.progress] callback for search progress ( fac in [0,1] ) + * @returns {Promise} will be resolved with the proxy valid url + */ find_proxy: function(options){ options = options || {}; var self = this; @@ -302,7 +337,6 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ var urls = []; var found = false; var parallel = 8; - var done = new $.Deferred(); // will be resolved with the proxies valid urls var threads = []; var progress = 0; @@ -323,51 +357,47 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ } } - function thread(done){ + function thread () { var url = urls.shift(); - done = done || new $.Deferred(); - - if( !url || found || !self.searching_for_proxy ){ - done.resolve(); - return done; + if (!url || found || !self.searching_for_proxy) { + return Promise.resolve(); } - $.ajax({ + return Promise.resolve( + $.ajax({ url: url + '/hw_proxy/hello', method: 'GET', timeout: 400, - }).done(function(){ + }).then(function () { found = true; update_progress(); - done.resolve(url); - }) - .fail(function(){ + return Promise.resolve(url); + }, function () { update_progress(); - thread(done); - }); - - return done; + return thread(); + }) + ); } this.searching_for_proxy = true; - var len = Math.min(parallel,urls.length); + var len = Math.min(parallel, urls.length); for(i = 0; i < len; i++){ threads.push(thread()); } - $.when.apply($,threads).then(function(){ - var urls = []; - for(var i = 0; i < arguments.length; i++){ - if(arguments[i]){ - urls.push(arguments[i]); + return new Promise(function (resolve, reject) { + Promise.all(threads).then(function(results){ + var urls = []; + for(var i = 0; i < results.length; i++){ + if(results[i]){ + urls.push(results[i]); + } } - } - done.resolve(urls[0]); + resolve(urls[0]); + }); }); - - return done; }, stop_searching: function(){ @@ -384,20 +414,24 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ this.notifications[name].push(callback); }, - // returns the weight on the scale. - scale_read: function(){ + /** + * Returns the weight on the scale. + * + * @returns {Promise} + */ + scale_read: function () { var self = this; - var ret = new $.Deferred(); if (self.use_debug_weight) { - return (new $.Deferred()).resolve({weight:this.debug_weight, unit:'Kg', info:'ok'}); + return Promise.resolve({weight:this.debug_weight, unit:'Kg', info:'ok'}); } - this.message('scale_read',{}) - .then(function(weight){ - ret.resolve(weight); - }, function(){ //failed to read weight - ret.resolve({weight:0.0, unit:'Kg', info:'ok'}); + return new Promise(function (resolve, reject) { + self.message('scale_read',{}) + .then(function (weight) { + resolve(weight); + }, function () { //failed to read weight + resolve({weight:0.0, unit:'Kg', info:'ok'}); }); - return ret; + }); }, // sets a custom weight, ignoring the proxy returned value. @@ -476,15 +510,22 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{ } }, + /** + * @param {string} html + * @returns {Promise} + */ take_ownership_over_client_screen: function(html) { return this.message("take_control", { html: html }); }, + /** + * @returns {Promise} + */ test_ownership_of_client_screen: function() { if (this.connection) { return this.message("test_ownership", {}); } - return null; + return Promise.reject({abort: true}); }, // asks the proxy to log some information, as with the debug.log you can provide several arguments. diff --git a/addons/point_of_sale/static/src/js/gui.js b/addons/point_of_sale/static/src/js/gui.js index b1d99e2e199..0184b07a984 100644 --- a/addons/point_of_sale/static/src/js/gui.js +++ b/addons/point_of_sale/static/src/js/gui.js @@ -1,6 +1,6 @@ odoo.define('point_of_sale.gui', function (require) { "use strict"; -// this file contains the Gui, which is the pos 'controller'. +// this file contains the Gui, which is the pos 'controller'. // It contains high level methods to manipulate the interface // such as changing between screens, creating popups, etc. // @@ -24,7 +24,7 @@ var Gui = core.Class.extend({ this.default_screen = null; this.startup_screen = null; this.current_popup = null; - this.current_screen = null; + this.current_screen = null; this.chrome.ready.then(function(){ self.close_other_tabs(); @@ -55,7 +55,7 @@ var Gui = core.Class.extend({ // sets the screen that will be displayed // for new orders - set_default_screen: function(name){ + set_default_screen: function(name){ this.default_screen = name; }, @@ -73,8 +73,8 @@ var Gui = core.Class.extend({ options = options || {}; this.close_popup(); if (order) { - this.show_screen(order.get_screen_data('screen') || - options.default_screen || + this.show_screen(order.get_screen_data('screen') || + options.default_screen || this.default_screen, null,'refresh'); } else { @@ -82,7 +82,7 @@ var Gui = core.Class.extend({ } }, - // display a screen. + // display a screen. // If there is an order, the screen will be saved in the order // - params: used to load a screen with parameters, for // example loading a 'product_details' screen for a specific product. @@ -120,7 +120,7 @@ var Gui = core.Class.extend({ this.current_screen.show(refresh); } }, - + // returns the current screen. get_current_screen: function() { return this.pos.get_order() ? ( this.pos.get_order().get_screen_data('screen') || this.default_screen ) : this.startup_screen; @@ -213,7 +213,7 @@ var Gui = core.Class.extend({ msg.session == self.pos.pos_session.id && msg.window_uid != now) { - console.info('POS / Session opened in another window. EXITING POS') + console.info('POS / Session opened in another window. EXITING POS'); self._close(); } } @@ -224,28 +224,28 @@ var Gui = core.Class.extend({ /* ---- Gui: ACCESS CONTROL ---- */ // A Generic UI that allow to select a user from a list. - // It returns a deferred that resolves with the selected user + // It returns a promise that resolves with the selected user // upon success. Several options are available : // - security: passwords will be asked // - only_managers: restricts the list to managers - // - current_user: password will not be asked if this + // - current_user: password will not be asked if this // user is selected. // - title: The title of the employee selection list. - + // checks if the current user (or the user provided) has manager // access rights. If not, a popup is shown allowing the user to - // temporarily login as an administrator. - // This method returns a deferred, that succeeds with the + // temporarily login as an administrator. + // This method returns a promise, that succeeds with the // manager user when the login is successfull. sudo: function(user){ user = user || this.pos.get_cashier(); if (user.role === 'manager') { - return new $.Deferred().resolve(user); + return Promise.resolve(user); } else { return this.select_employee({ - security: true, + security: true, only_managers: true, title: _t('Login as a Manager'), }); @@ -261,14 +261,14 @@ var Gui = core.Class.extend({ if (!pending) { this._close(); } else { - this.pos.push_order().always(function() { + var always = function () { var pending = self.pos.db.get_orders().length; if (!pending) { self._close(); } else { - var reason = self.pos.get('failed') ? - 'configuration errors' : - 'internet connection issues'; + var reason = self.pos.get('failed') ? + 'configuration errors' : + 'internet connection issues'; self.show_popup('confirm', { 'title': _t('Offline Orders'), @@ -282,7 +282,8 @@ var Gui = core.Class.extend({ }, }); } - }); + }; + this.pos.push_order().then(always, always); } }, @@ -314,10 +315,10 @@ var Gui = core.Class.extend({ /* ---- Gui: FILE I/O ---- */ - // This will make the browser download 'contents' as a + // This will make the browser download 'contents' as a // file named 'name' // if 'contents' is not a string, it is converted into - // a JSON representation of the contents. + // a JSON representation of the contents. // TODO: remove me in master: deprecated in favor of prepare_download_link @@ -329,9 +330,9 @@ var Gui = core.Class.extend({ evt.initEvent("click"); $("",href_params).get(0).dispatchEvent(evt); - - }, - + + }, + prepare_download_link: function(contents, filename, src, target) { var href_params = this.prepare_file_blob(contents, filename); @@ -344,11 +345,11 @@ var Gui = core.Class.extend({ $(src).removeClass('oe_hidden'); $(this).addClass('oe_hidden'); }); - }, - + }, + prepare_file_blob: function(contents, name) { var URL = window.URL || window.webkitURL; - + if (typeof contents !== 'string') { contents = JSON.stringify(contents,null,2); } @@ -366,21 +367,21 @@ var Gui = core.Class.extend({ // prefilled. send_email: function(address, subject, body) { - window.open("mailto:" + address + + window.open("mailto:" + address + "?subject=" + (subject ? window.encodeURIComponent(subject) : '') + "&body=" + (body ? window.encodeURIComponent(body) : '')); }, /* ---- Gui: KEYBOARD INPUT ---- */ - // This is a helper to handle numpad keyboard input. + // This is a helper to handle numpad keyboard input. // - buffer: an empty or number string // - input: '[0-9],'+','-','.','CLEAR','BACKSPACE' // - options: 'firstinput' -> will clear buffer if // input is '[0-9]' or '.' // returns the new buffer containing the modifications - // (the original is not touched) - numpad_input: function(buffer, input, options) { + // (the original is not touched) + numpad_input: function(buffer, input, options) { var newbuf = buffer.slice(0); options = options || {}; var newbuf_float = newbuf === '-' ? newbuf : field_utils.parse.float(newbuf); @@ -395,8 +396,8 @@ var Gui = core.Class.extend({ newbuf = newbuf + decimal_point; } } else if (input === 'CLEAR') { - newbuf = ""; - } else if (input === 'BACKSPACE') { + newbuf = ""; + } else if (input === 'BACKSPACE') { newbuf = newbuf.substring(0,newbuf.length - 1); } else if (input === '+') { if ( newbuf[0] === '-' ) { diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 4cf483dfcde..2840b6b2866 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -23,12 +23,12 @@ var round_pr = utils.round_precision; var exports = {}; // The PosModel contains the Point Of Sale's representation of the backend. -// Since the PoS must work in standalone ( Without connection to the server ) -// it must contains a representation of the server's PoS backend. +// Since the PoS must work in standalone ( Without connection to the server ) +// it must contains a representation of the server's PoS backend. // (taxes, product list, configuration options, etc.) this representation -// is fetched and stored by the PosModel at the initialisation. -// this is done asynchronously, a ready deferred alows the GUI to wait interactively -// for the loading to be completed +// is fetched and stored by the PosModel at the initialisation. +// this is done asynchronously, a ready promise alows the GUI to wait interactively +// for the loading to be completed // There is a single instance of the PosModel for each Front-End instance, it is usually called // 'pos' and is available to all widgets extending PosWidget. @@ -116,48 +116,49 @@ exports.PosModel = Backbone.Model.extend({ this.barcode_reader.disconnect_from_proxy(); }, - connect_to_proxy: function(){ + connect_to_proxy: function () { var self = this; - var done = new $.Deferred(); - this.barcode_reader.disconnect_from_proxy(); - this.chrome.loading_message(_t('Connecting to the IoT Box'),0); - this.chrome.loading_skip(function(){ + return new Promise(function (resolve, reject) { + self.barcode_reader.disconnect_from_proxy(); + self.chrome.loading_message(_t('Connecting to the IoT Box'), 0); + self.chrome.loading_skip(function () { self.proxy.stop_searching(); }); - this.proxy.autoconnect({ + self.proxy.autoconnect({ force_ip: self.config.proxy_ip || undefined, progress: function(prog){ self.chrome.loading_progress(prog); }, }).then( - function(){ - if(self.config.iface_scan_via_proxy){ - self.barcode_reader.connect_to_proxy(); - } - done.resolve(); + function () { + if (self.config.iface_scan_via_proxy) { + self.barcode_reader.connect_to_proxy(); + } + resolve(); }, - function(statusText, url){ - if (statusText == 'error' && window.location.protocol == 'https:') { - var error = {message: 'TLSError', url: url}; - self.chrome.loading_error(error); - } else { - done.resolve(); - } - }); - return done; + function (statusText, url) { + if (statusText == 'error' && window.location.protocol == 'https:') { + var error = {message: 'TLSError', url: url}; + self.chrome.loading_error(error); + } else { + resolve(); + } + } + ); + }); }, // Server side model loaders. This is the list of the models that need to be loaded from // the server. The models are loaded one by one by this list's order. The 'loaded' callback // is used to store the data in the appropriate place once it has been loaded. This callback - // can return a deferred that will pause the loading of the next module. + // can return a promise that will pause the loading of the next module. // a shared temporary dictionary is available for loaders to communicate private variables // used during loading such as object ids, etc. models: [ { label: 'version', - loaded: function(self){ - return session.rpc('/web/webclient/version_info',{}).done(function(version) { + loaded: function (self) { + return session.rpc('/web/webclient/version_info',{}).then(function (version) { self.version = version; }); }, @@ -447,55 +448,51 @@ exports.PosModel = Backbone.Model.extend({ }, { label: 'fonts', loaded: function(){ - var fonts_loaded = new $.Deferred(); - // Waiting for fonts to be loaded to prevent receipt printing - // from printing empty receipt while loading Inconsolata - // ( The font used for the receipt ) - waitForWebfonts(['Lato','Inconsolata'], function(){ - fonts_loaded.resolve(); + return new Promise(function (resolve, reject) { + // Waiting for fonts to be loaded to prevent receipt printing + // from printing empty receipt while loading Inconsolata + // ( The font used for the receipt ) + waitForWebfonts(['Lato','Inconsolata'], function () { + resolve(); + }); + // The JS used to detect font loading is not 100% robust, so + // do not wait more than 5sec + setTimeout(resolve, 5000); }); - // The JS used to detect font loading is not 100% robust, so - // do not wait more than 5sec - setTimeout(function(){ - fonts_loaded.resolve(); - },5000); - - return fonts_loaded; }, },{ label: 'pictures', - loaded: function(self){ + loaded: function (self) { self.company_logo = new Image(); - var logo_loaded = new $.Deferred(); - self.company_logo.onload = function(){ - var img = self.company_logo; - var ratio = 1; - var targetwidth = 300; - var maxheight = 150; - if( img.width !== targetwidth ){ - ratio = targetwidth / img.width; - } - if( img.height * ratio > maxheight ){ - ratio = maxheight / img.height; - } - var width = Math.floor(img.width * ratio); - var height = Math.floor(img.height * ratio); - var c = document.createElement('canvas'); + return new Promise(function (resolve, reject) { + self.company_logo.onload = function () { + var img = self.company_logo; + var ratio = 1; + var targetwidth = 300; + var maxheight = 150; + if( img.width !== targetwidth ){ + ratio = targetwidth / img.width; + } + if( img.height * ratio > maxheight ){ + ratio = maxheight / img.height; + } + var width = Math.floor(img.width * ratio); + var height = Math.floor(img.height * ratio); + var c = document.createElement('canvas'); c.width = width; c.height = height; - var ctx = c.getContext('2d'); + var ctx = c.getContext('2d'); ctx.drawImage(self.company_logo,0,0, width, height); - self.company_logo_base64 = c.toDataURL(); - logo_loaded.resolve(); - }; - self.company_logo.onerror = function(){ - logo_loaded.reject(); - }; - self.company_logo.crossOrigin = "anonymous"; - self.company_logo.src = '/web/binary/company_logo' +'?dbname=' + session.db + '&_'+Math.random(); - - return logo_loaded; + self.company_logo_base64 = c.toDataURL(); + resolve(); + }; + self.company_logo.onerror = function () { + reject(); + }; + self.company_logo.crossOrigin = "anonymous"; + self.company_logo.src = '/web/binary/company_logo' + '?dbname=' + session.db + '&_' + Math.random(); + }); }, }, { label: 'barcodes', @@ -507,93 +504,94 @@ exports.PosModel = Backbone.Model.extend({ } ], - // loads all the needed data on the sever. returns a deferred indicating when all the data has loaded. + // loads all the needed data on the sever. returns a promise indicating when all the data has loaded. load_server_data: function(){ var self = this; - var loaded = new $.Deferred(); var progress = 0; var progress_step = 1.0 / self.models.length; var tmp = {}; // this is used to share a temporary state between models loaders - function load_model(index){ - if(index >= self.models.length){ - loaded.resolve(); - }else{ - var model = self.models[index]; - self.chrome.loading_message(_t('Loading')+' '+(model.label || model.model || ''), progress); + var loaded = new Promise(function (resolve, reject) { + function load_model(index) { + if (index >= self.models.length) { + resolve(); + } else { + var model = self.models[index]; + self.chrome.loading_message(_t('Loading')+' '+(model.label || model.model || ''), progress); - var cond = typeof model.condition === 'function' ? model.condition(self,tmp) : true; - if (!cond) { - load_model(index+1); - return; - } - - var fields = typeof model.fields === 'function' ? model.fields(self,tmp) : model.fields; - var domain = typeof model.domain === 'function' ? model.domain(self,tmp) : model.domain; - var context = typeof model.context === 'function' ? model.context(self,tmp) : model.context || {}; - var ids = typeof model.ids === 'function' ? model.ids(self,tmp) : model.ids; - var order = typeof model.order === 'function' ? model.order(self,tmp): model.order; - progress += progress_step; - - if( model.model ){ - var params = { - model: model.model, - context: _.extend(context, session.user_context || {}), - }; - - if (model.ids) { - params.method = 'read'; - params.args = [ids, fields]; - } else { - params.method = 'search_read'; - params.domain = domain; - params.fields = fields; - params.orderBy = order; + var cond = typeof model.condition === 'function' ? model.condition(self,tmp) : true; + if (!cond) { + load_model(index+1); + return; } - rpc.query(params).then(function(result){ - try{ // catching exceptions in model.loaded(...) - $.when(model.loaded(self,result,tmp)) - .then(function(){ load_model(index + 1); }, - function(err){ loaded.reject(err); }); - }catch(err){ - console.error(err.message, err.stack); - loaded.reject(err); + var fields = typeof model.fields === 'function' ? model.fields(self,tmp) : model.fields; + var domain = typeof model.domain === 'function' ? model.domain(self,tmp) : model.domain; + var context = typeof model.context === 'function' ? model.context(self,tmp) : model.context || {}; + var ids = typeof model.ids === 'function' ? model.ids(self,tmp) : model.ids; + var order = typeof model.order === 'function' ? model.order(self,tmp): model.order; + progress += progress_step; + + if( model.model ){ + var params = { + model: model.model, + context: _.extend(context, session.user_context || {}), + }; + + if (model.ids) { + params.method = 'read'; + params.args = [ids, fields]; + } else { + params.method = 'search_read'; + params.domain = domain; + params.fields = fields; + params.orderBy = order; } - },function(err){ - loaded.reject(err); - }); - }else if( model.loaded ){ - try{ // catching exceptions in model.loaded(...) - $.when(model.loaded(self,tmp)) - .then( function(){ load_model(index +1); }, - function(err){ loaded.reject(err); }); - }catch(err){ - loaded.reject(err); + + rpc.query(params).then(function (result) { + try { // catching exceptions in model.loaded(...) + Promise.resolve(model.loaded(self, result, tmp)) + .then(function () { load_model(index + 1); }, + function (err) { reject(err); }); + } catch (err) { + console.error(err.message, err.stack); + reject(err); + } + }, function (err) { + reject(err); + }); + } else if (model.loaded) { + try { // catching exceptions in model.loaded(...) + Promise.resolve(model.loaded(self, tmp)) + .then(function () { load_model(index +1); }, + function (err) { reject(err); }); + } catch (err) { + reject(err); + } + } else { + load_model(index + 1); } - }else{ - load_model(index + 1); } } - } - try{ - load_model(0); - }catch(err){ - loaded.reject(err); - } + try { + return load_model(0); + } catch (err) { + return Promise.reject(err); + } + }); return loaded; }, - // reload the list of partner, returns as a deferred that resolves if there were + // reload the list of partner, returns as a promise that resolves if there were // updated partners, and fails if not load_new_partners: function(){ var self = this; - var def = new $.Deferred(); - var fields = _.find(this.models,function(model){ return model.model === 'res.partner'; }).fields; - var domain = [['customer','=',true],['write_date','>',this.db.get_partner_write_date()]]; - rpc.query({ + return new Promise(function (resolve, reject) { + var fields = _.find(self.models, function(model){ return model.model === 'res.partner'; }).fields; + var domain = [['customer','=',true],['write_date','>',self.db.get_partner_write_date()]]; + rpc.query({ model: 'res.partner', method: 'search_read', args: [domain, fields], @@ -601,14 +599,14 @@ exports.PosModel = Backbone.Model.extend({ timeout: 3000, shadow: true, }) - .then(function(partners){ + .then(function (partners) { if (self.db.add_partners(partners)) { // check if the partners we got were real updates - def.resolve(); + resolve(); } else { - def.reject(); + reject(); } - }, function(type,err){ def.reject(); }); - return def; + }, function (type, err) { reject(); }); + }); }, // this is called when an order is removed from the order collection. It ensures that there is always an existing @@ -718,27 +716,26 @@ exports.PosModel = Backbone.Model.extend({ }, _convert_product_img_to_base64: function (product, url) { - var deferred = new $.Deferred(); - var img = new Image(); + return new Promise(function (resolve, reject) { + var img = new Image(); - img.onload = function () { - var canvas = document.createElement('CANVAS'); - var ctx = canvas.getContext('2d'); + img.onload = function () { + var canvas = document.createElement('CANVAS'); + var ctx = canvas.getContext('2d'); - canvas.height = this.height; - canvas.width = this.width; - ctx.drawImage(this,0,0); + canvas.height = this.height; + canvas.width = this.width; + ctx.drawImage(this,0,0); - var dataURL = canvas.toDataURL('image/jpeg'); - product.image_base64 = dataURL; - canvas = null; + var dataURL = canvas.toDataURL('image/jpeg'); + product.image_base64 = dataURL; + canvas = null; - deferred.resolve(); - }; - img.crossOrigin = 'use-credentials'; - img.src = url; - - return deferred; + resolve(); + }; + img.crossOrigin = 'use-credentials'; + img.src = url; + }); }, send_current_order_to_customer_facing_display: function() { @@ -748,6 +745,9 @@ exports.PosModel = Backbone.Model.extend({ }); }, + /** + * @returns {Promise} + */ render_html_for_customer_facing_display: function () { var self = this; var order = this.get_order(); @@ -758,7 +758,7 @@ exports.PosModel = Backbone.Model.extend({ // IoT Box is not logged in and thus doesn't have the access // rights to access product.product. So instead we'll base64 // encode it and embed it in the HTML. - var get_image_deferreds = []; + var get_image_promises = []; if (order) { order.get_orderlines().forEach(function (orderline) { @@ -767,13 +767,13 @@ exports.PosModel = Backbone.Model.extend({ // only download and convert image if we haven't done it before if (! product.image_base64) { - get_image_deferreds.push(self._convert_product_img_to_base64(product, image_url)); + get_image_promises.push(self._convert_product_img_to_base64(product, image_url)); } }); } // when all images are loaded in product.image_base64 - return $.when.apply($, get_image_deferreds).then(function () { + return Promise.all(get_image_promises).then(function () { var rendered_order_lines = ""; var rendered_payment_lines = ""; var order_total_with_tax = self.chrome.format_currency(0); @@ -813,87 +813,81 @@ exports.PosModel = Backbone.Model.extend({ }, // saves the order locally and try to send it to the backend. - // it returns a deferred that succeeds after having tried to send the order and all the other pending orders. - push_order: function(order, opts) { + // it returns a promise that succeeds after having tried to send the order and all the other pending orders. + push_order: function (order, opts) { opts = opts || {}; var self = this; - if(order){ + if (order) { this.db.add_order(order.export_as_JSON()); } - var pushed = new $.Deferred(); + return new Promise(function (resolve, reject) { + self.flush_mutex.exec(function () { + var flushed = self._flush_orders(self.db.get_orders(), opts); - this.flush_mutex.exec(function(){ - var flushed = self._flush_orders(self.db.get_orders(), opts); + flushed.then(resolve, resolve); - flushed.always(function(ids){ - pushed.resolve(); + return flushed; }); - - return flushed; }); - return pushed; }, // saves the order locally and try to send it to the backend and make an invoice - // returns a deferred that succeeds when the order has been posted and successfully generated + // returns a promise that succeeds when the order has been posted and successfully generated // an invoice. This method can fail in various ways: // error-no-client: the order must have an associated partner_id. You can retry to make an invoice once // this error is solved // error-transfer: there was a connection error during the transfer. You can retry to make the invoice once // the network connection is up - push_and_invoice_order: function(order){ + push_and_invoice_order: function (order) { var self = this; - var invoiced = new $.Deferred(); + var invoiced = new Promise(function (resolveInvoiced, rejectInvoiced) { + if(!order.get_client()){ + rejectInvoiced({code:400, message:'Missing Customer', data:{}}); + } + else { + var order_id = self.db.add_order(order.export_as_JSON()); - if(!order.get_client()){ - invoiced.reject({code:400, message:'Missing Customer', data:{}}); - return invoiced; - } + self.flush_mutex.exec(function () { + var done = new Promise(function (resolveDone, rejectDone) { + // send the order to the server + // we have a 30 seconds timeout on this push. + // FIXME: if the server takes more than 30 seconds to accept the order, + // the client will believe it wasn't successfully sent, and very bad + // things will happen as a duplicate will be sent next time + // so we must make sure the server detects and ignores duplicated orders - var order_id = this.db.add_order(order.export_as_JSON()); + var transfer = self._flush_orders([self.db.get_order(order_id)], {timeout:30000, to_invoice:true}); - this.flush_mutex.exec(function(){ - var done = new $.Deferred(); // holds the mutex + transfer.catch(function (error) { + rejectInvoiced(error); + rejectDone(); + }); - // send the order to the server - // we have a 30 seconds timeout on this push. - // FIXME: if the server takes more than 30 seconds to accept the order, - // the client will believe it wasn't successfully sent, and very bad - // things will happen as a duplicate will be sent next time - // so we must make sure the server detects and ignores duplicated orders - - var transfer = self._flush_orders([self.db.get_order(order_id)], {timeout:30000, to_invoice:true}); - - transfer.fail(function(error){ - invoiced.reject(error); - done.reject(); - }); - - // on success, get the order id generated by the server - transfer.pipe(function(order_server_id){ - - // generate the pdf and download it - if (order_server_id.length) { - self.chrome.do_action('point_of_sale.pos_invoice_report',{additional_context:{ - active_ids:order_server_id, - }}).done(function () { - invoiced.resolve(); - done.resolve(); + // on success, get the order id generated by the server + transfer.then(function(order_server_id){ + // generate the pdf and download it + if (order_server_id.length) { + self.chrome.do_action('point_of_sale.pos_invoice_report',{additional_context:{ + active_ids:order_server_id, + }}).then(function () { + resolveInvoiced(); + resolveDone(); + }); + } else { + // The order has been pushed separately in batch when + // the connection came back. + // The user has to go to the backend to print the invoice + rejectInvoiced({code:401, message:'Backend Invoice', data:{order: order}}); + rejectDone(); + } + }); + return done; }); - } else { - // The order has been pushed separately in batch when - // the connection came back. - // The user has to go to the backend to print the invoice - invoiced.reject({code:401, message:'Backend Invoice', data:{order: order}}); - done.reject(); - } - }); - - return done; - + }); + } }); return invoiced; @@ -904,7 +898,7 @@ exports.PosModel = Backbone.Model.extend({ var self = this; this.set('synch',{ state: 'connecting', pending: orders.length}); - return self._save_to_server(orders, options).done(function (server_ids) { + return self._save_to_server(orders, options).then(function (server_ids) { var pending = self.db.get_orders().length; self.set('synch', { @@ -913,7 +907,7 @@ exports.PosModel = Backbone.Model.extend({ }); return server_ids; - }).fail(function(error, event){ + }).catch(function(){ var pending = self.db.get_orders().length; if (self.get('failed')) { self.set('synch', { state: 'error', pending: pending }); @@ -926,13 +920,11 @@ exports.PosModel = Backbone.Model.extend({ // send an array of orders to the server // available options: // - timeout: timeout for the rpc call in ms - // returns a deferred that resolves with the list of + // returns a promise that resolves with the list of // server generated ids for the sent orders _save_to_server: function (orders, options) { if (!orders || !orders.length) { - var result = $.Deferred(); - result.resolve([]); - return result; + return Promise.resolve([]); } options = options || {}; @@ -966,7 +958,8 @@ exports.PosModel = Backbone.Model.extend({ }); self.set('failed',false); return server_ids; - }).fail(function (type, error){ + }).catch(function (reason){ + var error = reason.message; if(error.code === 200 ){ // Business Logic Error, not a connection problem //if warning do not need to display traceback!! if (error.data.exception_type == 'warning') { @@ -1152,7 +1145,7 @@ exports.load_fields = function(model_name, fields) { // Loads openerp models at the point of sale startup. // load_models take an array of model loader declarations. -// - The models will be loaded in the array order. +// - The models will be loaded in the array order. // - If no openerp model name is provided, no server data // will be loaded, but the system can be used to preprocess // data before load. @@ -1168,9 +1161,9 @@ exports.load_fields = function(model_name, fields) { // models: [{ // model: [string] the name of the openerp model to load. // label: [string] The label displayed during load. -// fields: [[string]|function] the list of fields to be loaded. +// fields: [[string]|function] the list of fields to be loaded. // Empty Array / Null loads all fields. -// order: [[string]|function] the models will be ordered by +// order: [[string]|function] the models will be ordered by // the provided fields // domain: [domain|function] the domain that determines what // models need to be loaded. Null loads everything @@ -1179,9 +1172,9 @@ exports.load_fields = function(model_name, fields) { // context: [Dict|function] the openerp context for the model read // condition: [function] do not load the models if it evaluates to // false. -// loaded: [function(self,model)] this function is called once the +// loaded: [function(self,model)] this function is called once the // models have been loaded, with the data as second argument -// if the function returns a deferred, the next model will +// if the function returns a promise, the next model will // wait until it resolves before loading. // }] // @@ -1311,7 +1304,7 @@ exports.Product = Backbone.Model.extend({ var orderline_id = 1; // An orderline represent one element of the content of a client's shopping cart. -// An orderline contains a product, its quantity, its price, discount. etc. +// An orderline contains a product, its quantity, its price, discount. etc. // An Order contains zero or more Orderlines. exports.Orderline = Backbone.Model.extend({ initialize: function(attr,options){ @@ -1980,8 +1973,8 @@ var PaymentlineCollection = Backbone.Collection.extend({ model: exports.Paymentline, }); -// An order more or less represents the content of a client's shopping cart (the OrderLines) -// plus the associated payment information (the Paymentlines) +// An order more or less represents the content of a client's shopping cart (the OrderLines) +// plus the associated payment information (the Paymentlines) // there is always an active ('selected') order in the Pos, a new one is created // automaticaly once an order is completed and sent to the server. exports.Order = Backbone.Model.extend({ diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 2d017af9934..56e460c4a85 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -1107,7 +1107,7 @@ var ClientListScreenWidget = ScreenWidget.extend({ this.display_client_details('show',this.old_client,0); } - this.$('.client-list-contents').delegate('.client-line','click',function(event){ + this.$('.client-list-contents').on('click', '.client-line', function(event){ self.line_select(event,$(this),parseInt($(this).data('id'))); }); @@ -1317,11 +1317,16 @@ var ClientListScreenWidget = ScreenWidget.extend({ contents.on('click','.button.save',function(){ self.save_client_details(partner); }); }); }, - + // what happens when we've just pushed modifications for a partner of id partner_id - saved_client_details: function(partner_id){ + saved_client_details: function (partner_id) { var self = this; - this.reload_partners().then(function(){ + var always = function () { + $(".client-details-contents").on('click', '.button.save', function () { + self.save_client_details(partner); + }); + }; + this.reload_partners().then( function() { var partner = self.pos.db.get_partner_by_id(partner_id); if (partner) { self.new_client = partner; @@ -1329,12 +1334,10 @@ var ClientListScreenWidget = ScreenWidget.extend({ self.display_client_details('show',partner); } else { // should never happen, because create_from_ui must return the id of the partner it - // has created, and reload_partner() must have loaded the newly created partner. + // has created, and reload_partner() must have loaded the newly created partner. self.display_client_details('hide'); } - }).always(function(){ - $(".client-details-contents").on('click','.button.save',function(){ self.save_client_details(partner); }); - }); + }).then(always, always); }, // resizes an image, keeping the aspect ratio intact, @@ -2081,7 +2084,7 @@ var PaymentScreenWidget = ScreenWidget.extend({ var invoiced = this.pos.push_and_invoice_order(order); this.invoicing = true; - invoiced.fail(function(error){ + invoiced.catch(function (error) { self.invoicing = false; order.finalized = false; if (error.message === 'Missing Customer') { @@ -2121,7 +2124,7 @@ var PaymentScreenWidget = ScreenWidget.extend({ } }); - invoiced.done(function(){ + invoiced.then(function () { self.invoicing = false; self.gui.show_screen('receipt'); }); diff --git a/addons/point_of_sale/static/src/js/tests.js b/addons/point_of_sale/static/src/js/tests.js index 59cddd0f352..027af38dcbb 100644 --- a/addons/point_of_sale/static/src/js/tests.js +++ b/addons/point_of_sale/static/src/js/tests.js @@ -39,7 +39,7 @@ odoo.define('point_of_sale.tour.pricelist', function (require) { var backend_price = backend_result[0].price; assert(frontend_price === backend_price, JSON.stringify(debug_info) + ' DOESN\'T MATCH -> ' + backend_price + ' (backend) != ' + frontend_price + ' (frontend)'); - return (new $.Deferred()).resolve(); + return Promise.resolve(); }); }; }