[REF] point_of_sale: adapt code after jQuery update

Part of task 1896658

Co-authored-by: Aaron Bohy <aab@odoo.com>
Co-authored-by: Christophe Matthieu <chm@odoo.com>
Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
Co-authored-by: David Monjoie <dmo@odoo.com>
Co-authored-by: Martin Geubelle <mge@odoo.com>
Co-authored-by: svs-odoo <svs@odoo.com>
Co-authored-by: Vincent Schippefilt <vsc@odoo.com>
This commit is contained in:
Vincent Schippefilt
2019-03-06 20:07:17 +01:00
committed by Aaron Bohy
co-authored by Aaron Bohy Christophe Matthieu Mathieu Duckerts-Antoine David Monjoie Martin Geubelle svs-odoo
parent dab34f0aeb
commit 53285f1a76
6 changed files with 478 additions and 440 deletions
+39 -39
View File
@@ -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');
+167 -126
View File
@@ -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<string|Array>}
*/
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<string>} 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<Object>}
*/
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.
+35 -34
View File
@@ -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");
$("<a>",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] === '-' ) {
+223 -230
View File
@@ -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<string>}
*/
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({
+13 -10
View File
@@ -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');
});
+1 -1
View File
@@ -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();
});
};
}