2 function openerp_pos_devices(instance,module){ //module is instance.point_of_sale
5 var _t = instance.web._t;
7 // the JobQueue schedules a sequence of 'jobs'. each job is
8 // a function returning a deferred. the queue waits for each job to finish
9 // before launching the next. Each job can also be scheduled with a delay.
10 // the is used to prevent parallel requests to the proxy.
12 module.JobQueue = function(){
15 var scheduled_end_time = 0;
16 var end_of_queue = (new $.Deferred()).resolve();
17 var stoprepeat = false;
20 if(end_of_queue.state() === 'resolved'){
21 end_of_queue = new $.Deferred();
26 if(!job.opts.repeat || stoprepeat){
31 // the time scheduled for this job
32 scheduled_end_time = (new Date()).getTime() + (job.opts.duration || 0);
34 // we run the job and put in def when it finishes
35 var def = job.fun() || (new $.Deferred()).resolve();
37 // we don't care if a job fails ...
38 def.always(function(){
39 // we run the next job after the scheduled_end_time, even if it finishes before
40 setTimeout(function(){
42 }, Math.max(0, scheduled_end_time - (new Date()).getTime()) );
46 scheduled_end_time = 0;
47 end_of_queue.resolve();
51 // adds a job to the schedule.
53 // duration : the job is guaranteed to finish no quicker than this (milisec)
54 // repeat : if true, the job will be endlessly repeated
55 // important : if true, the scheduled job cannot be canceled by a queue.clear()
57 this.schedule = function(fun, opts){
58 queue.push({fun:fun, opts:opts || {}});
64 // remove all jobs from the schedule (except the ones marked as important)
65 this.clear = function(){
66 queue = _.filter(queue,function(job){job.opts.important === true});
69 // end the repetition of the current job
70 this.stoprepeat = function(){
74 // returns a deferred that resolves when all scheduled
75 // jobs have been run.
76 // ( jobs added after the call to this method are considered as well )
77 this.finished = function(){
84 // this object interfaces with the local proxy to communicate to the various hardware devices
85 // connected to the Point of Sale. As the communication only goes from the POS to the proxy,
86 // methods are used both to signal an event, and to fetch information.
88 module.ProxyDevice = instance.web.Class.extend(openerp.PropertiesMixin,{
89 init: function(parent,options){
90 openerp.PropertiesMixin.init.call(this,parent);
92 options = options || {};
93 var url = options.url || 'http://localhost:8069';
97 this.weighting = false;
98 this.debug_weight = 0;
99 this.use_debug_weight = false;
102 this.default_payment_status = {
105 payment_method: undefined,
106 receipt_client: undefined,
107 receipt_shop: undefined,
109 this.custom_payment_status = this.default_payment_status;
111 this.receipt_queue = [];
113 this.notifications = {};
114 this.bypass_proxy = false;
116 this.connection = null;
118 this.keptalive = false;
120 this.set('status',{});
122 this.set_connection_status('disconnected');
124 this.on('change:status',this,function(eh,status){
125 status = status.newValue;
126 if(status.status === 'connected'){
127 self.print_receipt();
131 window.hw_proxy = this;
133 set_connection_status: function(status,drivers){
134 var oldstatus = this.get('status');
136 newstatus.status = status;
137 newstatus.drivers = status === 'disconnected' ? {} : oldstatus.drivers;
138 newstatus.drivers = drivers ? drivers : newstatus.drivers;
139 this.set('status',newstatus);
141 disconnect: function(){
142 if(this.get('status').status !== 'disconnected'){
143 this.connection.destroy();
144 this.set_connection_status('disconnected');
148 // connects to the specified url
149 connect: function(url){
151 this.connection = new instance.web.Session(undefined,url, { use_cors: true});
153 this.set_connection_status('connecting',{});
155 return this.message('handshake').then(function(response){
157 self.set_connection_status('connected');
158 localStorage['hw_proxy_url'] = url;
161 self.set_connection_status('disconnected');
162 console.error('Connection refused by the Proxy');
165 self.set_connection_status('disconnected');
166 console.error('Could not connect to the Proxy');
170 // find a proxy and connects to it. for options see find_proxy
171 // - force_ip : only try to connect to the specified ip.
172 // - port: what port to listen to (default 8069)
173 // - progress(fac) : callback for search progress ( fac in [0,1] )
174 autoconnect: function(options){
176 this.set_connection_status('connecting',{});
177 var found_url = new $.Deferred();
178 var success = new $.Deferred();
180 if ( options.force_ip ){
181 // if the ip is forced by server config, bailout on fail
182 found_url = this.try_hard_to_connect(options.force_ip, options)
183 }else if( localStorage['hw_proxy_url'] ){
184 // try harder when we remember a good proxy url
185 found_url = this.try_hard_to_connect(localStorage['hw_proxy_url'], options)
186 .then(null,function(){
187 return self.find_proxy(options);
190 // just find something quick
191 found_url = this.find_proxy(options);
194 success = found_url.then(function(url){
195 return self.connect(url);
198 success.fail(function(){
199 self.set_connection_status('disconnected');
205 // starts a loop that updates the connection status
206 keepalive: function(){
210 self.connection.rpc('/hw_proxy/status_json',{},{timeout:2500})
211 .then(function(driver_status){
212 self.set_connection_status('connected',driver_status);
214 if(self.get('status').status !== 'connecting'){
215 self.set_connection_status('disconnected');
217 }).always(function(){
218 setTimeout(status,5000);
223 this.keptalive = true;
228 message : function(name,params){
229 var callbacks = this.notifications[name] || [];
230 for(var i = 0; i < callbacks.length; i++){
231 callbacks[i](params);
233 if(this.get('status').status !== 'disconnected'){
234 return this.connection.rpc('/hw_proxy/' + name, params || {});
236 return (new $.Deferred()).reject();
240 // try several time to connect to a known proxy url
241 try_hard_to_connect: function(url,options){
242 options = options || {};
243 var port = ':' + (options.port || '8069');
245 this.set_connection_status('connecting');
247 if(url.indexOf('//') < 0){
251 if(url.indexOf(':',5) < 0){
255 // try real hard to connect to url, with a 1sec timeout and up to 'retries' retries
256 function try_real_hard_to_connect(url, retries, done){
258 done = done || new $.Deferred();
261 url: url + '/hw_proxy/hello',
270 try_real_hard_to_connect(url,retries-1,done);
278 return try_real_hard_to_connect(url,3);
281 // returns as a deferred a valid host url that can be used as proxy.
283 // - port: what port to listen to (default 8069)
284 // - progress(fac) : callback for search progress ( fac in [0,1] )
285 find_proxy: function(options){
286 options = options || {};
288 var port = ':' + (options.port || '8069');
292 var done = new $.Deferred(); // will be resolved with the proxies valid urls
297 urls.push('http://localhost'+port);
298 for(var i = 0; i < 256; i++){
299 urls.push('http://192.168.0.'+i+port);
300 urls.push('http://192.168.1.'+i+port);
301 urls.push('http://10.0.0.'+i+port);
304 var prog_inc = 1/urls.length;
306 function update_progress(){
307 progress = found ? 1 : progress + prog_inc;
308 if(options.progress){
309 options.progress(progress);
313 function thread(done){
314 var url = urls.shift();
316 done = done || new $.Deferred();
318 if( !url || found || !self.searching_for_proxy ){
324 url: url + '/hw_proxy/hello',
340 this.searching_for_proxy = true;
342 for(var i = 0, len = Math.min(parallel,urls.length); i < len; i++){
343 threads.push(thread());
346 $.when.apply($,threads).then(function(){
348 for(var i = 0; i < arguments.length; i++){
350 urls.push(arguments[i]);
353 done.resolve(urls[0]);
359 stop_searching: function(){
360 this.searching_for_proxy = false;
361 this.set_connection_status('disconnected');
364 // this allows the client to be notified when a proxy call is made. The notification
365 // callback will be executed with the same arguments as the proxy call
366 add_notification: function(name, callback){
367 if(!this.notifications[name]){
368 this.notifications[name] = [];
370 this.notifications[name].push(callback);
373 // returns the weight on the scale.
374 scale_read: function(){
376 var ret = new $.Deferred();
377 this.message('scale_read',{})
378 .then(function(weight){
379 ret.resolve(self.use_debug_weight ? self.debug_weight : weight);
380 }, function(){ //failed to read weight
381 ret.resolve(self.use_debug_weight ? self.debug_weight : {weight:0.0, unit:'Kg', info:'ok'});
386 // sets a custom weight, ignoring the proxy returned value.
387 debug_set_weight: function(kg){
388 this.use_debug_weight = true;
389 this.debug_weight = kg;
392 // resets the custom weight and re-enable listening to the proxy for weight values
393 debug_reset_weight: function(){
394 this.use_debug_weight = false;
395 this.debug_weight = 0;
398 // ask for the cashbox (the physical box where you store the cash) to be opened
399 open_cashbox: function(){
400 return this.message('open_cashbox');
404 * ask the printer to print a receipt
406 print_receipt: function(receipt){
409 this.receipt_queue.push(receipt);
412 function send_printing_job(){
413 if (self.receipt_queue.length > 0){
414 var r = self.receipt_queue.shift();
415 self.message('print_xml_receipt',{ receipt: r },{ timeout: 5000 })
420 self.pos.pos_widget.screen_selector.show_popup('error-traceback',{
421 'message': _t('Printing Error: ') + error.data.message,
422 'comment': error.data.debug,
426 self.receipt_queue.unshift(r)
433 // asks the proxy to log some information, as with the debug.log you can provide several arguments.
435 return this.message('log',{'arguments': _.toArray(arguments)});
440 // this module interfaces with the barcode reader. It assumes the barcode reader
441 // is set-up to act like a keyboard. Use connect() and disconnect() to activate
442 // and deactivate the barcode reader. Use set_action_callbacks to tell it
443 // what to do when it reads a barcode.
444 module.BarcodeReader = instance.web.Class.extend({
451 init: function(attributes){
452 this.pos = attributes.pos;
453 this.action_callback = {};
454 this.proxy = attributes.proxy;
455 this.remote_scanning = false;
456 this.remote_active = 0;
458 this.barcode_parser = attributes.barcode_parser;
460 this.action_callback_stack = [];
463 set_barcode_parser: function(barcode_parser) {
464 this.barcode_parser = barcode_parser;
467 save_callbacks: function(){
469 for(var name in this.action_callback){
470 callbacks[name] = this.action_callback[name];
472 this.action_callback_stack.push(callbacks);
475 restore_callbacks: function(){
476 if(this.action_callback_stack.length){
477 var callbacks = this.action_callback_stack.pop();
478 this.action_callback = callbacks;
482 // when a barcode is scanned and parsed, the callback corresponding
483 // to its type is called with the parsed_barcode as a parameter.
484 // (parsed_barcode is the result of parse_barcode(barcode))
486 // callbacks is a Map of 'actions' : callback(parsed_barcode)
487 // that sets the callback for each action. if a callback for the
488 // specified action already exists, it is replaced.
490 // possible actions include :
491 // 'product' | 'cashier' | 'client' | 'discount'
492 set_action_callback: function(action, callback){
493 if(arguments.length == 2){
494 this.action_callback[action] = callback;
496 var actions = arguments[0];
497 for(var action in actions){
498 this.set_action_callback(action,actions[action]);
503 //remove all action callbacks
504 reset_action_callbacks: function(){
505 for(var action in this.action_callback){
506 this.action_callback[action] = undefined;
510 scan: function(code){
511 var parsed_result = this.barcode_parser.parse_barcode(code);
513 if(parsed_result.type in {'product':'', 'weight':'', 'price':''}){ //barcode is associated to a product
514 if(this.action_callback['product']){
515 this.action_callback['product'](parsed_result);
518 else if (parsed_result.type in {'cashier':'', 'client':'', 'discount':''}){
519 if(this.action_callback[parsed_result.type]){
520 this.action_callback[parsed_result.type](parsed_result);
524 this.action_callback['error'](parsed_result);
528 // starts catching keyboard events and tries to interpret codebar
529 // calling the callbacks when needed.
537 this.handler = function(e){
539 if(e.which === 13){ //ignore returns
544 if(timeStamp + 50 < new Date().getTime()){
548 timeStamp = new Date().getTime();
549 clearTimeout(timeout);
551 code += String.fromCharCode(e.which);
553 // we wait for a while after the last input to be sure that we are not mistakingly
554 // returning a code which is a prefix of a bigger one :
555 // Internal Ref 5449 vs EAN13 5449000...
557 timeout = setTimeout(function(){
558 if(code.length >= 3){
565 $('body').on('keypress', this.handler);
568 // stops catching keyboard events
569 disconnect: function(){
570 $('body').off('keypress', this.handler)
573 // the barcode scanner will listen on the hw_proxy/scanner interface for
574 // scan events until disconnect_from_proxy is called
575 connect_to_proxy: function(){
577 this.remote_scanning = true;
578 if(this.remote_active >= 1){
581 this.remote_active = 1;
583 function waitforbarcode(){
584 return self.proxy.connection.rpc('/hw_proxy/scanner',{},{timeout:7500})
585 .then(function(barcode){
586 if(!self.remote_scanning){
587 self.remote_active = 0;
594 if(!self.remote_scanning){
595 self.remote_active = 0;
598 setTimeout(waitforbarcode,5000);
604 // the barcode scanner will stop listening on the hw_proxy/scanner remote interface
605 disconnect_from_proxy: function(){
606 this.remote_scanning = false;