[IMP] point_of_sale: big reworking of the scale screens:

- no welcome scale in cashier mode, as it was an unnecessary delay for people who use it all day
- no more parallel calls to the proxy which could reorder or make the driver crash
- auto throttling of the proxy calls -> max 20 reads per seconds, but can go slower as well.
- faster and cleaner update of the weight in the scale screen

bzr revid: fva@openerp.com-20130911154155-rs600cixvftvkhoi
This commit is contained in:
Frédéric van der Essen 2013-09-11 17:41:55 +02:00
parent 44967200e4
commit b38c271031
4 changed files with 179 additions and 102 deletions

View File

@ -3,12 +3,17 @@ import logging
import simplejson import simplejson
import os import os
import openerp import openerp
import time
import random
from openerp.addons.web import http from openerp.addons.web import http
from openerp.addons.web.http import request from openerp.addons.web.http import request
from openerp.addons.web.controllers.main import manifest_list, module_boot, html_template from openerp.addons.web.controllers.main import manifest_list, module_boot, html_template
class PointOfSaleController(http.Controller): class PointOfSaleController(http.Controller):
def __init__(self):
self.scale = 'closed'
self.scale_weight = 0.0
@http.route('/pos/app', type='http', auth='admin') @http.route('/pos/app', type='http', auth='admin')
def app(self): def app(self):
@ -64,6 +69,10 @@ class PointOfSaleController(http.Controller):
return m return m
@http.route('/pos/test_connection', type='json', auth='admin')
def test_connection(self):
return
@http.route('/pos/scan_item_success', type='json', auth='admin') @http.route('/pos/scan_item_success', type='json', auth='admin')
def scan_item_success(self, ean): def scan_item_success(self, ean):
""" """
@ -98,18 +107,38 @@ class PointOfSaleController(http.Controller):
@http.route('/pos/weighting_start', type='json', auth='admin') @http.route('/pos/weighting_start', type='json', auth='admin')
def weighting_start(self): def weighting_start(self):
print "weighting_start" if self.scale == 'closed':
print "Opening (Fake) Connection to Scale..."
self.scale = 'open'
self.scale_weight = 0.0
time.sleep(0.1)
print "... Scale Open."
else:
print "WARNING: Scale already Connected !!!"
return return
@http.route('/pos/weighting_read_kg', type='json', auth='admin') @http.route('/pos/weighting_read_kg', type='json', auth='admin')
def weighting_read_kg(self): def weighting_read_kg(self):
print "weighting_read_kg" if self.scale == 'open':
return 3.14 print "Reading Scale..."
time.sleep(0.025)
self.scale_weight += 0.01
print "... Done."
return self.scale_weight
else:
print "WARNING: Reading closed scale !!!"
return 0.0
@http.route('/pos/weighting_end', type='json', auth='admin') @http.route('/pos/weighting_end', type='json', auth='admin')
def weighting_end(self): def weighting_end(self):
print "weighting_end" if self.scale == 'open':
return print "Closing Connection to Scale ..."
self.scale = 'closed'
self.scale_weight = 0.0
time.sleep(0.1)
print "... Scale Closed."
else:
print "WARNING: Scale already Closed !!!"
@http.route('/pos/payment_request', type='json', auth='admin') @http.route('/pos/payment_request', type='json', auth='admin')
def payment_request(self, price): def payment_request(self, price):

View File

@ -1,6 +1,81 @@
function openerp_pos_devices(instance,module){ //module is instance.point_of_sale function openerp_pos_devices(instance,module){ //module is instance.point_of_sale
// the JobQueue schedules a sequence of 'jobs'. each job is
// a function returning a deferred. 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.
module.JobQueue = function(){
var queue = [];
var running = false;
var scheduled_end_time = 0;
var end_of_queue = (new $.Deferred()).resolve();
var stoprepeat = false;
var run = function(){
if(end_of_queue.state() === 'resolved'){
end_of_queue = new $.Deferred();
}
if(queue.length > 0){
running = true;
var job = queue[0];
if(!job.opts.repeat || stoprepeat){
queue.shift();
stoprepeat = false;
}
// 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 don't care if a job fails ...
def.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;
end_of_queue.resolve();
}
};
// 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){
queue.push({fun:fun, opts:opts || {}});
if(!running){
run();
}
}
// remove all jobs from the schedule (except the ones marked as important)
this.clear = function(){
queue = _.filter(queue,function(job){job.opts.important === true});
};
// end the repetition of the current job
this.stoprepeat = 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(){
return end_of_queue;
}
};
// this object interfaces with the local proxy to communicate to the various hardware devices // this object interfaces with the local proxy to communicate to the various hardware devices
// connected to the Point of Sale. As the communication only goes from the POS to the proxy, // connected to the Point of Sale. As the communication only goes from the POS to the proxy,
// methods are used both to signal an event, and to fetch information. // methods are used both to signal an event, and to fetch information.
@ -10,7 +85,6 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
options = options || {}; options = options || {};
url = options.url || 'http://localhost:8069'; url = options.url || 'http://localhost:8069';
this.weight = 0;
this.weighting = false; this.weighting = false;
this.debug_weight = 0; this.debug_weight = 0;
this.use_debug_weight = false; this.use_debug_weight = false;
@ -35,18 +109,11 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
this.connection.destroy(); this.connection.destroy();
}, },
message : function(name,params){ message : function(name,params){
var ret = new $.Deferred();
var callbacks = this.notifications[name] || []; var callbacks = this.notifications[name] || [];
for(var i = 0; i < callbacks.length; i++){ for(var i = 0; i < callbacks.length; i++){
callbacks[i](params); callbacks[i](params);
} }
return this.connection.rpc('/pos/' + name, params || {});
this.connection.rpc('/pos/' + name, params || {}).done(function(result) {
ret.resolve(result);
}).fail(function(error) {
ret.reject(error);
});
return ret;
}, },
// this allows the client to be notified when a proxy call is made. The notification // this allows the client to be notified when a proxy call is made. The notification
@ -82,14 +149,32 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
//the client is starting to weight //the client is starting to weight
weighting_start: function(){ weighting_start: function(){
var ret = new $.Deferred();
if(!this.weighting){ if(!this.weighting){
this.weighting = true; this.weighting = true;
this.weight = 0; this.message('weighting_start').always(function(){
return this.message('weighting_start'); ret.resolve();
});
}else{ }else{
console.error('Weighting already started!!!'); console.error('Weighting already started!!!');
this.weight = 0; ret.resolve();
} }
return ret;
},
// the client has finished weighting products
weighting_end: function(){
var ret = new $.Deferred();
if(this.weighting){
this.weighting = false;
this.message('weighting_end').always(function(){
ret.resolve();
});
}else{
console.error('Weighting already ended !!!');
ret.resolve();
}
return ret;
}, },
//returns the weight on the scale. //returns the weight on the scale.
@ -97,20 +182,14 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
// and a weighting_end() // and a weighting_end()
weighting_read_kg: function(){ weighting_read_kg: function(){
var self = this; var self = this;
if(!this.weighting){ var ret = new $.Deferred();
console.error('Weighting while not started!!!');
}
this.message('weighting_read_kg',{}) this.message('weighting_read_kg',{})
.done(function(weight){ .then(function(weight){
if(self.weighting){ ret.resolve(self.use_debug_weight ? self.debug_weight : weight);
if(self.use_debug_weight){ }, function(){ //failed to read weight
self.weight = self.debug_weight; ret.resolve(self.use_debug_weight ? self.debug_weight : 0.0);
}else{
self.weight = weight;
}
}
}); });
return this.weight; return ret;
}, },
// sets a custom weight, ignoring the proxy returned value. // sets a custom weight, ignoring the proxy returned value.
@ -125,17 +204,6 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
this.debug_weight = 0; this.debug_weight = 0;
}, },
// the client has finished weighting products
weighting_end: function(){
if(this.weighting){
this.weight = 0;
this.weighting = false;
this.message('weighting_end');
}else{
console.error('Weighting already ended !!!');
this.weight = 0;
}
},
// the pos asks the client to pay 'price' units // the pos asks the client to pay 'price' units
payment_request: function(price){ payment_request: function(price){

View File

@ -24,6 +24,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
this.barcode_reader = new module.BarcodeReader({'pos': this}); // used to read barcodes this.barcode_reader = new module.BarcodeReader({'pos': this}); // used to read barcodes
this.proxy = new module.ProxyDevice(); // used to communicate to the hardware devices via a local proxy this.proxy = new module.ProxyDevice(); // used to communicate to the hardware devices via a local proxy
this.proxy_queue = new module.JobQueue(); // used to prevent parallels communications to the proxy
this.db = new module.PosLS(); // a database used to store the products and categories this.db = new module.PosLS(); // a database used to store the products and categories
this.db.clear('products','categories'); this.db.clear('products','categories');
this.debug = jQuery.deparam(jQuery.param.querystring()).debug !== undefined; //debug mode this.debug = jQuery.deparam(jQuery.param.querystring()).debug !== undefined; //debug mode

View File

@ -441,7 +441,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
module.ErrorInvoiceTransferPopupWidget = module.ErrorPopupWidget.extend({ module.ErrorInvoiceTransferPopupWidget = module.ErrorPopupWidget.extend({
template: 'ErrorInvoiceTransferPopupWidget', template: 'ErrorInvoiceTransferPopupWidget',
}); });
module.ScaleInviteScreenWidget = module.ScreenWidget.extend({ module.ScaleInviteScreenWidget = module.ScreenWidget.extend({
template:'ScaleInviteScreenWidget', template:'ScaleInviteScreenWidget',
@ -451,30 +451,35 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
show: function(){ show: function(){
this._super(); this._super();
var self = this; var self = this;
var queue = this.pos.proxy_queue;
self.pos.proxy.weighting_start(); queue.schedule(function(){
return self.pos.proxy.weighting_start();
this.intervalID = setInterval(function(){ },{ unclearable: true });
var weight = self.pos.proxy.weighting_read_kg();
if(weight > 0.001){ queue.schedule(function(){
clearInterval(this.intervalID); return self.pos.proxy.weighting_read_kg().then(function(weight){
self.pos_widget.screen_selector.set_current_screen(self.next_screen); if(weight > 0.001){
} self.pos_widget.screen_selector.set_current_screen(self.next_screen);
},50); }
});
},{duration: 100, repeat: true});
this.add_action_button({ this.add_action_button({
label: _t('Back'), label: _t('Back'),
icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png', icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
click: function(){ click: function(){
clearInterval(this.intervalID);
self.pos_widget.screen_selector.set_current_screen(self.previous_screen); self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
} }
}); });
}, },
close: function(){ close: function(){
this._super(); this._super();
clearInterval(this.intervalID); var self = this;
this.pos.proxy.weighting_end(); this.pos.proxy_queue.clear();
this.pos.proxy_queue.schedule(function(){
return self.pos.proxy.weighting_end();
},{ unclearable: true });
}, },
}); });
@ -486,9 +491,11 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
show: function(){ show: function(){
this._super(); this._super();
this.renderElement();
var self = this; var self = this;
var queue = this.pos.proxy_queue;
this.set_weight(0);
this.renderElement();
this.add_action_button({ this.add_action_button({
label: _t('Back'), label: _t('Back'),
@ -507,14 +514,16 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
}, },
}); });
this.pos.proxy.weighting_start(); queue.schedule(function(){
this.intervalID = setInterval(function(){ return self.pos.proxy.weighting_start()
var weight = self.pos.proxy.weighting_read_kg(); },{ unclearable: true });
if(weight != self.weight){
self.weight = weight; queue.schedule(function(){
self.$('.js-weight').text(self.get_product_weight_string()); return self.pos.proxy.weighting_read_kg().then(function(weight){
} self.set_weight(weight);
},50); });
},{duration:50, repeat: true});
}, },
renderElement: function(){ renderElement: function(){
var self = this; var self = this;
@ -544,54 +553,24 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
var product = this.get_product(); var product = this.get_product();
return (product ? product.get('price') : 0) || 0; return (product ? product.get('price') : 0) || 0;
}, },
set_weight: function(weight){
this.weight = weight;
this.$('.js-weight').text(this.get_product_weight_string());
},
get_product_weight_string: function(){ get_product_weight_string: function(){
return (this.weight || 0).toFixed(3) + ' Kg'; return (this.weight || 0).toFixed(3) + ' Kg';
}, },
close: function(){ close: function(){
var self = this;
this._super(); this._super();
clearInterval(this.intervalID);
this.pos.proxy.weighting_end(); this.pos.proxy_queue.clear();
this.pos.proxy_queue.schedule(function(){
self.pos.proxy.weighting_end();
},{ unclearable: true });
}, },
}); });
// the JobQueue schedules a sequence of 'jobs'. each job is
// a function returning a deferred. the queue waits for each job to finish
// before launching the next. Each job can also be scheduled with a delay.
// the queue jobqueue is used to prevent parallel requests to the payment terminal.
module.JobQueue = function(){
var queue = [];
var running = false;
var run = function(){
if(queue.length > 0){
running = true;
var job = queue.shift();
setTimeout(function(){
var def = job.fun();
if(def){
def.done(run);
}else{
run();
}
},job.delay || 0);
}else{
running = false;
}
};
// adds a job to the schedule.
this.schedule = function(fun, delay){
queue.push({fun:fun, delay:delay});
if(!running){
run();
}
}
// remove all jobs from the schedule
this.clear = function(){
queue = [];
};
};
module.ClientPaymentScreenWidget = module.ScreenWidget.extend({ module.ClientPaymentScreenWidget = module.ScreenWidget.extend({
template:'ClientPaymentScreenWidget', template:'ClientPaymentScreenWidget',