flectra/addons/point_of_sale/static/src/js/devices.js

624 lines
20 KiB
JavaScript

flectra.define('point_of_sale.devices', function (require) {
"use strict";
var core = require('web.core');
var mixins = require('web.mixins');
var rpc = require('web.rpc');
var Session = require('web.Session');
var PosBaseWidget = require('point_of_sale.BaseWidget');
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
// before launching the next. Each job can also be scheduled with a delay.
// the is used to prevent parallel requests to the proxy.
var 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;
scheduled_end_time = 0;
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){return 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
// 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.
var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
init: function(parent,options){
mixins.PropertiesMixin.init.call(this);
var self = this;
this.setParent(parent);
options = options || {};
this.pos = parent;
this.weighing = false;
this.debug_weight = 0;
this.use_debug_weight = false;
this.paying = false;
this.default_payment_status = {
status: 'waiting',
message: '',
payment_method: undefined,
receipt_client: undefined,
receipt_shop: undefined,
};
this.custom_payment_status = this.default_payment_status;
this.receipt_queue = [];
this.notifications = {};
this.bypass_proxy = false;
this.connection = null;
this.host = '';
this.keptalive = false;
this.set('status',{});
this.set_connection_status('disconnected');
this.on('change:status',this,function(eh,status){
status = status.newValue;
if(status.status === 'connected'){
self.print_receipt();
}
});
this.posbox_supports_display = true;
window.hw_proxy = this;
},
set_connection_status: function(status,drivers){
var oldstatus = this.get('status');
var newstatus = {};
newstatus.status = status;
newstatus.drivers = status === 'disconnected' ? {} : oldstatus.drivers;
newstatus.drivers = drivers ? drivers : newstatus.drivers;
this.set('status',newstatus);
},
disconnect: function(){
if(this.get('status').status !== 'disconnected'){
this.connection.destroy();
this.set_connection_status('disconnected');
}
},
// connects to the specified url
connect: function(url){
var self = this;
this.connection = new Session(undefined,url, { use_cors: true});
this.host = url;
this.set_connection_status('connecting',{});
return this.message('handshake').then(function(response){
if(response){
self.set_connection_status('connected');
localStorage.hw_proxy_url = url;
self.keepalive();
}else{
self.set_connection_status('disconnected');
console.error('Connection refused by the Proxy');
}
},function(){
self.set_connection_status('disconnected');
console.error('Could not connect to the Proxy');
});
},
// 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 7073)
// - progress(fac) : callback for search progress ( fac in [0,1] )
autoconnect: function(options){
var self = this;
this.set_connection_status('connecting',{});
var found_url = new $.Deferred();
var success = new $.Deferred();
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 ){
// try harder when we remember a good proxy url
found_url = this.try_hard_to_connect(localStorage.hw_proxy_url, options)
.then(null,function(){
return self.find_proxy(options);
});
}else{
// just find something quick
found_url = this.find_proxy(options);
}
success = found_url.then(function(url){
return self.connect(url);
});
success.fail(function(){
self.set_connection_status('disconnected');
});
return success;
},
// starts a loop that updates the connection status
keepalive: function(){
var self = this;
function status(){
self.connection.rpc('/hw_proxy/status_json',{},{shadow: true, timeout:2500})
.then(function(driver_status){
self.set_connection_status('connected',driver_status);
},function(){
if(self.get('status').status !== 'connecting'){
self.set_connection_status('disconnected');
}
}).always(function(){
setTimeout(status,5000);
});
}
if(!this.keptalive){
this.keptalive = true;
status();
}
},
message : function(name,params){
var callbacks = this.notifications[name] || [];
for(var i = 0; i < callbacks.length; i++){
callbacks[i](params);
}
if(this.get('status').status !== 'disconnected'){
return this.connection.rpc('/hw_proxy/' + name, params || {}, {shadow: true});
}else{
return (new $.Deferred()).reject();
}
},
// try several time to connect to a known proxy url
try_hard_to_connect: function(url,options){
options = options || {};
var port = ':' + (options.port || '7073');
this.set_connection_status('connecting');
if(url.indexOf('//') < 0){
url = 'http://'+url;
}
if(url.indexOf(':',5) < 0){
url = url+port;
}
// 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(){
if(retries > 0){
try_real_hard_to_connect(url,retries-1,done);
}else{
done.reject();
}
});
return done;
}
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 7073)
// - progress(fac) : callback for search progress ( fac in [0,1] )
find_proxy: function(options){
options = options || {};
var self = this;
var port = ':' + (options.port || '7073');
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;
urls.push('http://localhost'+port);
for(var i = 0; i < 256; i++){
urls.push('http://192.168.0.'+i+port);
urls.push('http://192.168.1.'+i+port);
urls.push('http://10.0.0.'+i+port);
}
var prog_inc = 1/urls.length;
function update_progress(){
progress = found ? 1 : progress + prog_inc;
if(options.progress){
options.progress(progress);
}
}
function thread(done){
var url = urls.shift();
done = done || new $.Deferred();
if( !url || found || !self.searching_for_proxy ){
done.resolve();
return done;
}
$.ajax({
url: url + '/hw_proxy/hello',
method: 'GET',
timeout: 400,
}).done(function(){
found = true;
update_progress();
done.resolve(url);
})
.fail(function(){
update_progress();
thread(done);
});
return done;
}
this.searching_for_proxy = true;
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]);
}
}
done.resolve(urls[0]);
});
return done;
},
stop_searching: function(){
this.searching_for_proxy = false;
this.set_connection_status('disconnected');
},
// this allows the client to be notified when a proxy call is made. The notification
// callback will be executed with the same arguments as the proxy call
add_notification: function(name, callback){
if(!this.notifications[name]){
this.notifications[name] = [];
}
this.notifications[name].push(callback);
},
// returns the weight on the scale.
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'});
}
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 ret;
},
// sets a custom weight, ignoring the proxy returned value.
debug_set_weight: function(kg){
this.use_debug_weight = true;
this.debug_weight = kg;
},
// resets the custom weight and re-enable listening to the proxy for weight values
debug_reset_weight: function(){
this.use_debug_weight = false;
this.debug_weight = 0;
},
// ask for the cashbox (the physical box where you store the cash) to be opened
open_cashbox: function(){
return this.message('open_cashbox');
},
/*
* ask the printer to print a receipt
*/
print_receipt: function(receipt){
var self = this;
if(receipt){
this.receipt_queue.push(receipt);
}
function send_printing_job(){
if (self.receipt_queue.length > 0){
var r = self.receipt_queue.shift();
self.message('print_xml_receipt',{ receipt: r },{ timeout: 5000 })
.then(function(){
send_printing_job();
},function(error){
if (error) {
self.pos.gui.show_popup('error-traceback',{
'title': _t('Printing Error: ') + error.data.message,
'body': error.data.debug,
});
return;
}
self.receipt_queue.unshift(r);
});
}
}
send_printing_job();
},
print_sale_details: function() {
var self = this;
rpc.query({
model: 'report.point_of_sale.report_saledetails',
method: 'get_sale_details',
})
.then(function(result){
var env = {
widget: new PosBaseWidget(self),
company: self.pos.company,
pos: self.pos,
products: result.products,
payments: result.payments,
taxes: result.taxes,
total_paid: result.total_paid,
date: (new Date()).toLocaleString(),
};
var report = QWeb.render('SaleDetailsReport', env);
self.print_receipt(report);
});
},
update_customer_facing_display: function(html) {
if (this.posbox_supports_display) {
return this.message('customer_facing_display',
{ html: html },
{ timeout: 5000 });
}
},
take_ownership_over_client_screen: function(html) {
return this.message("take_control", { html: html });
},
test_ownership_of_client_screen: function() {
if (this.connection) {
return this.message("test_ownership", {});
}
return null;
},
// asks the proxy to log some information, as with the debug.log you can provide several arguments.
log: function(){
return this.message('log',{'arguments': _.toArray(arguments)});
},
});
// this module interfaces with the barcode reader. It assumes the barcode reader
// is set-up to act like a keyboard. Use connect() and disconnect() to activate
// and deactivate the barcode reader. Use set_action_callbacks to tell it
// what to do when it reads a barcode.
var BarcodeReader = core.Class.extend({
actions:[
'product',
'cashier',
'client',
],
init: function(attributes){
this.pos = attributes.pos;
this.action_callback = {};
this.proxy = attributes.proxy;
this.remote_scanning = false;
this.remote_active = 0;
this.barcode_parser = attributes.barcode_parser;
this.action_callback_stack = [];
core.bus.on('barcode_scanned', this, function (barcode) {
this.scan(barcode);
});
},
set_barcode_parser: function(barcode_parser) {
this.barcode_parser = barcode_parser;
},
save_callbacks: function(){
var callbacks = {};
for(var name in this.action_callback){
callbacks[name] = this.action_callback[name];
}
this.action_callback_stack.push(callbacks);
},
restore_callbacks: function(){
if(this.action_callback_stack.length){
var callbacks = this.action_callback_stack.pop();
this.action_callback = callbacks;
}
},
// when a barcode is scanned and parsed, the callback corresponding
// to its type is called with the parsed_barcode as a parameter.
// (parsed_barcode is the result of parse_barcode(barcode))
//
// callbacks is a Map of 'actions' : callback(parsed_barcode)
// that sets the callback for each action. if a callback for the
// specified action already exists, it is replaced.
//
// possible actions include :
// 'product' | 'cashier' | 'client' | 'discount'
set_action_callback: function(action, callback){
if(arguments.length == 2){
this.action_callback[action] = callback;
}else{
var actions = arguments[0];
for(var action in actions){
this.set_action_callback(action,actions[action]);
}
}
},
//remove all action callbacks
reset_action_callbacks: function(){
for(var action in this.action_callback){
this.action_callback[action] = undefined;
}
},
scan: function(code){
if (!code) {
return;
}
var parsed_result = this.barcode_parser.parse_barcode(code);
if (this.action_callback[parsed_result.type]) {
this.action_callback[parsed_result.type](parsed_result);
} else if (this.action_callback.error) {
this.action_callback.error(parsed_result);
} else {
console.warn("Ignored Barcode Scan:", parsed_result);
}
},
// the barcode scanner will listen on the hw_proxy/scanner interface for
// scan events until disconnect_from_proxy is called
connect_to_proxy: function(){
var self = this;
this.remote_scanning = true;
if(this.remote_active >= 1){
return;
}
this.remote_active = 1;
function waitforbarcode(){
return self.proxy.connection.rpc('/hw_proxy/scanner',{},{shadow: true, timeout:7500})
.then(function(barcode){
if(!self.remote_scanning){
self.remote_active = 0;
return;
}
self.scan(barcode);
waitforbarcode();
},
function(){
if(!self.remote_scanning){
self.remote_active = 0;
return;
}
setTimeout(waitforbarcode,5000);
});
}
waitforbarcode();
},
// the barcode scanner will stop listening on the hw_proxy/scanner remote interface
disconnect_from_proxy: function(){
this.remote_scanning = false;
},
});
return {
JobQueue: JobQueue,
ProxyDevice: ProxyDevice,
BarcodeReader: BarcodeReader,
};
});