1b4202676de4efeb0b14fe315af89cf32bd794ea
[odoo/odoo.git] / addons / point_of_sale / static / src / js / devices.js
1
2 function openerp_pos_devices(instance,module){ //module is instance.point_of_sale
3     "use strict";
4
5         var _t = instance.web._t;
6
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.
11
12     module.JobQueue = function(){
13         var queue = [];
14         var running = false;
15         var scheduled_end_time = 0;
16         var end_of_queue = (new $.Deferred()).resolve();
17         var stoprepeat = false;
18
19         var run = function(){
20             if(end_of_queue.state() === 'resolved'){
21                 end_of_queue =  new $.Deferred();
22             }
23             if(queue.length > 0){
24                 running = true;
25                 var job = queue[0];
26                 if(!job.opts.repeat || stoprepeat){
27                     queue.shift();
28                     stoprepeat = false;
29                 }
30
31                 // the time scheduled for this job
32                 scheduled_end_time = (new Date()).getTime() + (job.opts.duration || 0);
33
34                 // we run the job and put in def when it finishes
35                 var def = job.fun() || (new $.Deferred()).resolve();
36                 
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(){
41                         run();
42                     }, Math.max(0, scheduled_end_time - (new Date()).getTime()) ); 
43                 });
44             }else{
45                 running = false;
46                 scheduled_end_time = 0;
47                 end_of_queue.resolve();
48             }
49         };
50         
51         // adds a job to the schedule.
52         // opts : {
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()
56         // }
57         this.schedule  = function(fun, opts){
58             queue.push({fun:fun, opts:opts || {}});
59             if(!running){
60                 run();
61             }
62         }
63
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}); 
67         };
68
69         // end the repetition of the current job
70         this.stoprepeat = function(){
71             stoprepeat = true;
72         };
73         
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(){
78             return end_of_queue;
79         }
80
81     };
82
83
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. 
87
88     module.ProxyDevice  = instance.web.Class.extend(openerp.PropertiesMixin,{
89         init: function(parent,options){
90             openerp.PropertiesMixin.init.call(this,parent);
91             var self = this;
92             options = options || {};
93             var url = options.url || 'http://localhost:8069';
94
95             this.pos = parent;
96             
97             this.weighting = false;
98             this.debug_weight = 0;
99             this.use_debug_weight = false;
100
101             this.paying = false;
102             this.default_payment_status = {
103                 status: 'waiting',
104                 message: '',
105                 payment_method: undefined,
106                 receipt_client: undefined,
107                 receipt_shop:   undefined,
108             };    
109             this.custom_payment_status = this.default_payment_status;
110
111             this.receipt_queue = [];
112
113             this.notifications = {};
114             this.bypass_proxy = false;
115
116             this.connection = null; 
117             this.host       = '';
118             this.keptalive  = false;
119
120             this.set('status',{});
121
122             this.set_connection_status('disconnected');
123
124             this.on('change:status',this,function(eh,status){
125                 status = status.newValue;
126                 if(status.status === 'connected'){
127                     self.print_receipt();
128                 }
129             });
130
131             window.hw_proxy = this;
132         },
133         set_connection_status: function(status,drivers){
134             var oldstatus = this.get('status');
135             var newstatus = {};
136             newstatus.status = status;
137             newstatus.drivers = status === 'disconnected' ? {} : oldstatus.drivers;
138             newstatus.drivers = drivers ? drivers : newstatus.drivers;
139             this.set('status',newstatus);
140         },
141         disconnect: function(){
142             if(this.get('status').status !== 'disconnected'){
143                 this.connection.destroy();
144                 this.set_connection_status('disconnected');
145             }
146         },
147
148         // connects to the specified url
149         connect: function(url){
150             var self = this;
151             this.connection = new instance.web.Session(undefined,url, { use_cors: true});
152             this.host   = url;
153             this.set_connection_status('connecting',{});
154
155             return this.message('handshake').then(function(response){
156                     if(response){
157                         self.set_connection_status('connected');
158                         localStorage['hw_proxy_url'] = url;
159                         self.keepalive();
160                     }else{
161                         self.set_connection_status('disconnected');
162                         console.error('Connection refused by the Proxy');
163                     }
164                 },function(){
165                     self.set_connection_status('disconnected');
166                     console.error('Could not connect to the Proxy');
167                 });
168         },
169
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){
175             var self = this;
176             this.set_connection_status('connecting',{});
177             var found_url = new $.Deferred();
178             var success = new $.Deferred();
179
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);
188                     });
189             }else{
190                 // just find something quick
191                 found_url = this.find_proxy(options);
192             }
193
194             success = found_url.then(function(url){
195                     return self.connect(url);
196                 });
197
198             success.fail(function(){
199                 self.set_connection_status('disconnected');
200             });
201
202             return success;
203         },
204
205         // starts a loop that updates the connection status
206         keepalive: function(){
207             var self = this;
208
209             function status(){
210                 self.connection.rpc('/hw_proxy/status_json',{},{timeout:2500})       
211                     .then(function(driver_status){
212                         self.set_connection_status('connected',driver_status);
213                     },function(){
214                         if(self.get('status').status !== 'connecting'){
215                             self.set_connection_status('disconnected');
216                         }
217                     }).always(function(){
218                         setTimeout(status,5000);
219                     });
220             }
221
222             if(!this.keptalive){
223                 this.keptalive = true;
224                 status();
225             };
226         },
227
228         message : function(name,params){
229             var callbacks = this.notifications[name] || [];
230             for(var i = 0; i < callbacks.length; i++){
231                 callbacks[i](params);
232             }
233             if(this.get('status').status !== 'disconnected'){
234                 return this.connection.rpc('/hw_proxy/' + name, params || {});       
235             }else{
236                 return (new $.Deferred()).reject();
237             }
238         },
239
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');
244
245             this.set_connection_status('connecting');
246
247             if(url.indexOf('//') < 0){
248                 url = 'http://'+url;
249             }
250
251             if(url.indexOf(':',5) < 0){
252                 url = url+port;
253             }
254
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){
257
258                 done = done || new $.Deferred();
259
260                 var c = $.ajax({
261                     url: url + '/hw_proxy/hello',
262                     method: 'GET',
263                     timeout: 1000,
264                 })
265                 .done(function(){
266                     done.resolve(url);
267                 })
268                 .fail(function(){
269                     if(retries > 0){
270                         try_real_hard_to_connect(url,retries-1,done);
271                     }else{
272                         done.reject();
273                     }
274                 });
275                 return done;
276             }
277
278             return try_real_hard_to_connect(url,3);
279         },
280
281         // returns as a deferred a valid host url that can be used as proxy.
282         // options:
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 || {};
287             var self  = this;
288             var port  = ':' + (options.port || '8069');
289             var urls  = [];
290             var found = false;
291             var parallel = 8;
292             var done = new $.Deferred(); // will be resolved with the proxies valid urls
293             var threads  = [];
294             var progress = 0;
295
296
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);
302             }
303
304             var prog_inc = 1/urls.length; 
305
306             function update_progress(){
307                 progress = found ? 1 : progress + prog_inc;
308                 if(options.progress){
309                     options.progress(progress);
310                 }
311             }
312
313             function thread(done){
314                 var url = urls.shift();
315
316                 done = done || new $.Deferred();
317
318                 if( !url || found || !self.searching_for_proxy ){ 
319                     done.resolve();
320                     return done;
321                 }
322
323                 var c = $.ajax({
324                         url: url + '/hw_proxy/hello',
325                         method: 'GET',
326                         timeout: 400, 
327                     }).done(function(){
328                         found = true;
329                         update_progress();
330                         done.resolve(url);
331                     })
332                     .fail(function(){
333                         update_progress();
334                         thread(done);
335                     });
336
337                 return done;
338             }
339
340             this.searching_for_proxy = true;
341
342             for(var i = 0, len = Math.min(parallel,urls.length); i < len; i++){
343                 threads.push(thread());
344             }
345             
346             $.when.apply($,threads).then(function(){
347                 var urls = [];
348                 for(var i = 0; i < arguments.length; i++){
349                     if(arguments[i]){
350                         urls.push(arguments[i]);
351                     }
352                 }
353                 done.resolve(urls[0]);
354             });
355
356             return done;
357         },
358
359         stop_searching: function(){
360             this.searching_for_proxy = false;
361             this.set_connection_status('disconnected');
362         },
363
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] = [];
369             }
370             this.notifications[name].push(callback);
371         },
372         
373         // returns the weight on the scale. 
374         scale_read: function(){
375             var self = this;
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'});
382                 });
383             return ret;
384         },
385
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;
390         },
391
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;
396         },
397
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');
401         },
402
403         /* 
404          * ask the printer to print a receipt
405          */
406         print_receipt: function(receipt){
407             var self = this;
408             if(receipt){
409                 this.receipt_queue.push(receipt);
410             }
411             var aborted = false;
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 })
416                         .then(function(){
417                             send_printing_job();
418                         },function(error){
419                             if (error) {
420                                 self.pos.pos_widget.screen_selector.show_popup('error-traceback',{
421                                     'message': _t('Printing Error: ') + error.data.message,
422                                     'comment': error.data.debug,
423                                 });
424                                 return;
425                             }
426                             self.receipt_queue.unshift(r)
427                         });
428                 }
429             }
430             send_printing_job();
431         },
432
433         // asks the proxy to log some information, as with the debug.log you can provide several arguments.
434         log: function(){
435             return this.message('log',{'arguments': _.toArray(arguments)});
436         },
437
438     });
439
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({
445         actions:[
446             'product',
447             'cashier',
448             'client',
449         ],
450
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;
457
458             this.barcode_parser = attributes.barcode_parser;
459
460             this.action_callback_stack = [];
461         },
462
463         set_barcode_parser: function(barcode_parser) {
464             this.barcode_parser = barcode_parser;
465         },
466
467         save_callbacks: function(){
468             var callbacks = {};
469             for(var name in this.action_callback){
470                 callbacks[name] = this.action_callback[name];
471             }
472             this.action_callback_stack.push(callbacks);
473         },
474
475         restore_callbacks: function(){
476             if(this.action_callback_stack.length){
477                 var callbacks = this.action_callback_stack.pop();
478                 this.action_callback = callbacks;
479             }
480         },
481        
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)) 
485         // 
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. 
489         // 
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;
495             }else{
496                 var actions = arguments[0];
497                 for(var action in actions){
498                     this.set_action_callback(action,actions[action]);
499                 }
500             }
501         },
502
503         //remove all action callbacks 
504         reset_action_callbacks: function(){
505             for(var action in this.action_callback){
506                 this.action_callback[action] = undefined;
507             }
508         },
509
510         scan: function(code){
511             var parsed_result = this.barcode_parser.parse_barcode(code);
512             
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);
516                 }
517             }
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);
521                 }
522             }
523             else{
524                 this.action_callback['error'](parsed_result);
525             }
526         },
527
528         // starts catching keyboard events and tries to interpret codebar 
529         // calling the callbacks when needed.
530         connect: function(){
531
532             var self = this;
533             var code = "";
534             var timeStamp  = 0;
535             var timeout = null;
536
537             this.handler = function(e){
538
539                 if(e.which === 13){ //ignore returns
540                     e.preventDefault();
541                     return;
542                 }
543
544                 if(timeStamp + 50 < new Date().getTime()){
545                     code = "";
546                 }
547
548                 timeStamp = new Date().getTime();
549                 clearTimeout(timeout);
550
551                 code += String.fromCharCode(e.which);
552
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...
556
557                 timeout = setTimeout(function(){
558                     if(code.length >= 3){
559                         self.scan(code);
560                     }
561                     code = "";
562                 },100);
563             };
564
565             $('body').on('keypress', this.handler);
566         },
567
568         // stops catching keyboard events 
569         disconnect: function(){
570             $('body').off('keypress', this.handler)
571         },
572
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(){ 
576             var self = this;
577             this.remote_scanning = true;
578             if(this.remote_active >= 1){
579                 return;
580             }
581             this.remote_active = 1;
582
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;
588                             return; 
589                         }
590                         self.scan(barcode);
591                         waitforbarcode();
592                     },
593                     function(){
594                         if(!self.remote_scanning){
595                             self.remote_active = 0;
596                             return;
597                         }
598                         setTimeout(waitforbarcode,5000);
599                     });
600             }
601             waitforbarcode();
602         },
603
604         // the barcode scanner will stop listening on the hw_proxy/scanner remote interface
605         disconnect_from_proxy: function(){
606             this.remote_scanning = false;
607         },
608     });
609
610 }