[IMP] point_of_sale: put the POS js files include function into the openerp namespace
[odoo/odoo.git] / addons / point_of_sale / static / src / js / devices.js
1 openerp.point_of_sale.load_devices = function load_devices(instance,module){ //module is instance.point_of_sale
2     "use strict";
3
4         var _t = instance.web._t;
5
6     // the JobQueue schedules a sequence of 'jobs'. each job is
7     // a function returning a deferred. the queue waits for each job to finish 
8     // before launching the next. Each job can also be scheduled with a delay. 
9     // the  is used to prevent parallel requests to the proxy.
10
11     module.JobQueue = function(){
12         var queue = [];
13         var running = false;
14         var scheduled_end_time = 0;
15         var end_of_queue = (new $.Deferred()).resolve();
16         var stoprepeat = false;
17
18         var run = function(){
19             if(end_of_queue.state() === 'resolved'){
20                 end_of_queue =  new $.Deferred();
21             }
22             if(queue.length > 0){
23                 running = true;
24                 var job = queue[0];
25                 if(!job.opts.repeat || stoprepeat){
26                     queue.shift();
27                     stoprepeat = false;
28                 }
29
30                 // the time scheduled for this job
31                 scheduled_end_time = (new Date()).getTime() + (job.opts.duration || 0);
32
33                 // we run the job and put in def when it finishes
34                 var def = job.fun() || (new $.Deferred()).resolve();
35                 
36                 // we don't care if a job fails ... 
37                 def.always(function(){
38                     // we run the next job after the scheduled_end_time, even if it finishes before
39                     setTimeout(function(){
40                         run();
41                     }, Math.max(0, scheduled_end_time - (new Date()).getTime()) ); 
42                 });
43             }else{
44                 running = false;
45                 scheduled_end_time = 0;
46                 end_of_queue.resolve();
47             }
48         };
49         
50         // adds a job to the schedule.
51         // opts : {
52         //    duration    : the job is guaranteed to finish no quicker than this (milisec)
53         //    repeat      : if true, the job will be endlessly repeated
54         //    important   : if true, the scheduled job cannot be canceled by a queue.clear()
55         // }
56         this.schedule  = function(fun, opts){
57             queue.push({fun:fun, opts:opts || {}});
58             if(!running){
59                 run();
60             }
61         }
62
63         // remove all jobs from the schedule (except the ones marked as important)
64         this.clear = function(){
65             queue = _.filter(queue,function(job){job.opts.important === true}); 
66         };
67
68         // end the repetition of the current job
69         this.stoprepeat = function(){
70             stoprepeat = true;
71         };
72         
73         // returns a deferred that resolves when all scheduled 
74         // jobs have been run.
75         // ( jobs added after the call to this method are considered as well )
76         this.finished = function(){
77             return end_of_queue;
78         }
79
80     };
81
82
83     // this object interfaces with the local proxy to communicate to the various hardware devices
84     // connected to the Point of Sale. As the communication only goes from the POS to the proxy,
85     // methods are used both to signal an event, and to fetch information. 
86
87     module.ProxyDevice  = instance.web.Class.extend(openerp.PropertiesMixin,{
88         init: function(parent,options){
89             openerp.PropertiesMixin.init.call(this,parent);
90             var self = this;
91             options = options || {};
92             var url = options.url || 'http://localhost:8069';
93
94             this.pos = parent;
95             
96             this.weighting = false;
97             this.debug_weight = 0;
98             this.use_debug_weight = false;
99
100             this.paying = false;
101             this.default_payment_status = {
102                 status: 'waiting',
103                 message: '',
104                 payment_method: undefined,
105                 receipt_client: undefined,
106                 receipt_shop:   undefined,
107             };    
108             this.custom_payment_status = this.default_payment_status;
109
110             this.receipt_queue = [];
111
112             this.notifications = {};
113             this.bypass_proxy = false;
114
115             this.connection = null; 
116             this.host       = '';
117             this.keptalive  = false;
118
119             this.set('status',{});
120
121             this.set_connection_status('disconnected');
122
123             this.on('change:status',this,function(eh,status){
124                 status = status.newValue;
125                 if(status.status === 'connected'){
126                     self.print_receipt();
127                 }
128             });
129
130             window.hw_proxy = this;
131         },
132         set_connection_status: function(status,drivers){
133             var oldstatus = this.get('status');
134             var newstatus = {};
135             newstatus.status = status;
136             newstatus.drivers = status === 'disconnected' ? {} : oldstatus.drivers;
137             newstatus.drivers = drivers ? drivers : newstatus.drivers;
138             this.set('status',newstatus);
139         },
140         disconnect: function(){
141             if(this.get('status').status !== 'disconnected'){
142                 this.connection.destroy();
143                 this.set_connection_status('disconnected');
144             }
145         },
146
147         // connects to the specified url
148         connect: function(url){
149             var self = this;
150             this.connection = new instance.web.Session(undefined,url, { use_cors: true});
151             this.host   = url;
152             this.set_connection_status('connecting',{});
153
154             return this.message('handshake').then(function(response){
155                     if(response){
156                         self.set_connection_status('connected');
157                         localStorage['hw_proxy_url'] = url;
158                         self.keepalive();
159                     }else{
160                         self.set_connection_status('disconnected');
161                         console.error('Connection refused by the Proxy');
162                     }
163                 },function(){
164                     self.set_connection_status('disconnected');
165                     console.error('Could not connect to the Proxy');
166                 });
167         },
168
169         // find a proxy and connects to it. for options see find_proxy
170         //   - force_ip : only try to connect to the specified ip. 
171         //   - port: what port to listen to (default 8069)
172         //   - progress(fac) : callback for search progress ( fac in [0,1] ) 
173         autoconnect: function(options){
174             var self = this;
175             this.set_connection_status('connecting',{});
176             var found_url = new $.Deferred();
177             var success = new $.Deferred();
178
179             if ( options.force_ip ){
180                 // if the ip is forced by server config, bailout on fail
181                 found_url = this.try_hard_to_connect(options.force_ip, options)
182             }else if( localStorage['hw_proxy_url'] ){
183                 // try harder when we remember a good proxy url
184                 found_url = this.try_hard_to_connect(localStorage['hw_proxy_url'], options)
185                     .then(null,function(){
186                         return self.find_proxy(options);
187                     });
188             }else{
189                 // just find something quick
190                 found_url = this.find_proxy(options);
191             }
192
193             success = found_url.then(function(url){
194                     return self.connect(url);
195                 });
196
197             success.fail(function(){
198                 self.set_connection_status('disconnected');
199             });
200
201             return success;
202         },
203
204         // starts a loop that updates the connection status
205         keepalive: function(){
206             var self = this;
207
208             function status(){
209                 self.connection.rpc('/hw_proxy/status_json',{},{timeout:2500})       
210                     .then(function(driver_status){
211                         self.set_connection_status('connected',driver_status);
212                     },function(){
213                         if(self.get('status').status !== 'connecting'){
214                             self.set_connection_status('disconnected');
215                         }
216                     }).always(function(){
217                         setTimeout(status,5000);
218                     });
219             }
220
221             if(!this.keptalive){
222                 this.keptalive = true;
223                 status();
224             };
225         },
226
227         message : function(name,params){
228             var callbacks = this.notifications[name] || [];
229             for(var i = 0; i < callbacks.length; i++){
230                 callbacks[i](params);
231             }
232             if(this.get('status').status !== 'disconnected'){
233                 return this.connection.rpc('/hw_proxy/' + name, params || {});       
234             }else{
235                 return (new $.Deferred()).reject();
236             }
237         },
238
239         // try several time to connect to a known proxy url
240         try_hard_to_connect: function(url,options){
241             options   = options || {};
242             var port  = ':' + (options.port || '8069');
243
244             this.set_connection_status('connecting');
245
246             if(url.indexOf('//') < 0){
247                 url = 'http://'+url;
248             }
249
250             if(url.indexOf(':',5) < 0){
251                 url = url+port;
252             }
253
254             // try real hard to connect to url, with a 1sec timeout and up to 'retries' retries
255             function try_real_hard_to_connect(url, retries, done){
256
257                 done = done || new $.Deferred();
258
259                 var c = $.ajax({
260                     url: url + '/hw_proxy/hello',
261                     method: 'GET',
262                     timeout: 1000,
263                 })
264                 .done(function(){
265                     done.resolve(url);
266                 })
267                 .fail(function(){
268                     if(retries > 0){
269                         try_real_hard_to_connect(url,retries-1,done);
270                     }else{
271                         done.reject();
272                     }
273                 });
274                 return done;
275             }
276
277             return try_real_hard_to_connect(url,3);
278         },
279
280         // returns as a deferred a valid host url that can be used as proxy.
281         // options:
282         //   - port: what port to listen to (default 8069)
283         //   - progress(fac) : callback for search progress ( fac in [0,1] ) 
284         find_proxy: function(options){
285             options = options || {};
286             var self  = this;
287             var port  = ':' + (options.port || '8069');
288             var urls  = [];
289             var found = false;
290             var parallel = 8;
291             var done = new $.Deferred(); // will be resolved with the proxies valid urls
292             var threads  = [];
293             var progress = 0;
294
295
296             urls.push('http://localhost'+port);
297             for(var i = 0; i < 256; i++){
298                 urls.push('http://192.168.0.'+i+port);
299                 urls.push('http://192.168.1.'+i+port);
300                 urls.push('http://10.0.0.'+i+port);
301             }
302
303             var prog_inc = 1/urls.length; 
304
305             function update_progress(){
306                 progress = found ? 1 : progress + prog_inc;
307                 if(options.progress){
308                     options.progress(progress);
309                 }
310             }
311
312             function thread(done){
313                 var url = urls.shift();
314
315                 done = done || new $.Deferred();
316
317                 if( !url || found || !self.searching_for_proxy ){ 
318                     done.resolve();
319                     return done;
320                 }
321
322                 var c = $.ajax({
323                         url: url + '/hw_proxy/hello',
324                         method: 'GET',
325                         timeout: 400, 
326                     }).done(function(){
327                         found = true;
328                         update_progress();
329                         done.resolve(url);
330                     })
331                     .fail(function(){
332                         update_progress();
333                         thread(done);
334                     });
335
336                 return done;
337             }
338
339             this.searching_for_proxy = true;
340
341             for(var i = 0, len = Math.min(parallel,urls.length); i < len; i++){
342                 threads.push(thread());
343             }
344             
345             $.when.apply($,threads).then(function(){
346                 var urls = [];
347                 for(var i = 0; i < arguments.length; i++){
348                     if(arguments[i]){
349                         urls.push(arguments[i]);
350                     }
351                 }
352                 done.resolve(urls[0]);
353             });
354
355             return done;
356         },
357
358         stop_searching: function(){
359             this.searching_for_proxy = false;
360             this.set_connection_status('disconnected');
361         },
362
363         // this allows the client to be notified when a proxy call is made. The notification 
364         // callback will be executed with the same arguments as the proxy call
365         add_notification: function(name, callback){
366             if(!this.notifications[name]){
367                 this.notifications[name] = [];
368             }
369             this.notifications[name].push(callback);
370         },
371         
372         // returns the weight on the scale. 
373         scale_read: function(){
374             var self = this;
375             var ret = new $.Deferred();
376             this.message('scale_read',{})
377                 .then(function(weight){
378                     ret.resolve(self.use_debug_weight ? self.debug_weight : weight);
379                 }, function(){ //failed to read weight
380                     ret.resolve(self.use_debug_weight ? self.debug_weight : {weight:0.0, unit:'Kg', info:'ok'});
381                 });
382             return ret;
383         },
384
385         // sets a custom weight, ignoring the proxy returned value. 
386         debug_set_weight: function(kg){
387             this.use_debug_weight = true;
388             this.debug_weight = kg;
389         },
390
391         // resets the custom weight and re-enable listening to the proxy for weight values
392         debug_reset_weight: function(){
393             this.use_debug_weight = false;
394             this.debug_weight = 0;
395         },
396
397         // ask for the cashbox (the physical box where you store the cash) to be opened
398         open_cashbox: function(){
399             return this.message('open_cashbox');
400         },
401
402         /* 
403          * ask the printer to print a receipt
404          */
405         print_receipt: function(receipt){
406             var self = this;
407             if(receipt){
408                 this.receipt_queue.push(receipt);
409             }
410             var aborted = false;
411             function send_printing_job(){
412                 if (self.receipt_queue.length > 0){
413                     var r = self.receipt_queue.shift();
414                     self.message('print_xml_receipt',{ receipt: r },{ timeout: 5000 })
415                         .then(function(){
416                             send_printing_job();
417                         },function(error){
418                             if (error) {
419                                 self.pos.pos_widget.screen_selector.show_popup('error-traceback',{
420                                     'message': _t('Printing Error: ') + error.data.message,
421                                     'comment': error.data.debug,
422                                 });
423                                 return;
424                             }
425                             self.receipt_queue.unshift(r)
426                         });
427                 }
428             }
429             send_printing_job();
430         },
431
432         // asks the proxy to log some information, as with the debug.log you can provide several arguments.
433         log: function(){
434             return this.message('log',{'arguments': _.toArray(arguments)});
435         },
436
437     });
438
439     // this module interfaces with the barcode reader. It assumes the barcode reader
440     // is set-up to act like  a keyboard. Use connect() and disconnect() to activate 
441     // and deactivate the barcode reader. Use set_action_callbacks to tell it
442     // what to do when it reads a barcode.
443     module.BarcodeReader = instance.web.Class.extend({
444         actions:[
445             'product',
446             'cashier',
447             'client',
448         ],
449
450         init: function(attributes){
451             this.pos = attributes.pos;
452             this.action_callback = {};
453             this.proxy = attributes.proxy;
454             this.remote_scanning = false;
455             this.remote_active = 0;
456
457             this.barcode_parser = attributes.barcode_parser;
458
459             this.action_callback_stack = [];
460         },
461
462         set_barcode_parser: function(barcode_parser) {
463             this.barcode_parser = barcode_parser;
464         },
465
466         save_callbacks: function(){
467             var callbacks = {};
468             for(var name in this.action_callback){
469                 callbacks[name] = this.action_callback[name];
470             }
471             this.action_callback_stack.push(callbacks);
472         },
473
474         restore_callbacks: function(){
475             if(this.action_callback_stack.length){
476                 var callbacks = this.action_callback_stack.pop();
477                 this.action_callback = callbacks;
478             }
479         },
480        
481         // when a barcode is scanned and parsed, the callback corresponding
482         // to its type is called with the parsed_barcode as a parameter. 
483         // (parsed_barcode is the result of parse_barcode(barcode)) 
484         // 
485         // callbacks is a Map of 'actions' : callback(parsed_barcode)
486         // that sets the callback for each action. if a callback for the
487         // specified action already exists, it is replaced. 
488         // 
489         // possible actions include : 
490         // 'product' | 'cashier' | 'client' | 'discount' 
491         set_action_callback: function(action, callback){
492             if(arguments.length == 2){
493                 this.action_callback[action] = callback;
494             }else{
495                 var actions = arguments[0];
496                 for(var action in actions){
497                     this.set_action_callback(action,actions[action]);
498                 }
499             }
500         },
501
502         //remove all action callbacks 
503         reset_action_callbacks: function(){
504             for(var action in this.action_callback){
505                 this.action_callback[action] = undefined;
506             }
507         },
508
509         scan: function(code){
510             var parsed_result = this.barcode_parser.parse_barcode(code);
511             
512             if(parsed_result.type in {'product':'', 'weight':'', 'price':''}){    //barcode is associated to a product
513                 if(this.action_callback['product']){
514                     this.action_callback['product'](parsed_result);
515                 }
516             }
517             else if (parsed_result.type in {'cashier':'', 'client':'', 'discount':''}){ 
518                 if(this.action_callback[parsed_result.type]){
519                     this.action_callback[parsed_result.type](parsed_result);
520                 }
521             }
522             else{
523                 this.action_callback['error'](parsed_result);
524             }
525         },
526
527         // starts catching keyboard events and tries to interpret codebar 
528         // calling the callbacks when needed.
529         connect: function(){
530
531             var self = this;
532             var code = "";
533             var timeStamp  = 0;
534             var timeout = null;
535
536             this.handler = function(e){
537
538                 if(e.which === 13){ //ignore returns
539                     e.preventDefault();
540                     return;
541                 }
542
543                 if(timeStamp + 50 < new Date().getTime()){
544                     code = "";
545                 }
546
547                 timeStamp = new Date().getTime();
548                 clearTimeout(timeout);
549
550                 code += String.fromCharCode(e.which);
551
552                 // we wait for a while after the last input to be sure that we are not mistakingly
553                 // returning a code which is a prefix of a bigger one :
554                 // Internal Ref 5449 vs EAN13 5449000...
555
556                 timeout = setTimeout(function(){
557                     if(code.length >= 3){
558                         self.scan(code);
559                     }
560                     code = "";
561                 },100);
562             };
563
564             $('body').on('keypress', this.handler);
565         },
566
567         // stops catching keyboard events 
568         disconnect: function(){
569             $('body').off('keypress', this.handler)
570         },
571
572         // the barcode scanner will listen on the hw_proxy/scanner interface for 
573         // scan events until disconnect_from_proxy is called
574         connect_to_proxy: function(){ 
575             var self = this;
576             this.remote_scanning = true;
577             if(this.remote_active >= 1){
578                 return;
579             }
580             this.remote_active = 1;
581
582             function waitforbarcode(){
583                 return self.proxy.connection.rpc('/hw_proxy/scanner',{},{timeout:7500})
584                     .then(function(barcode){
585                         if(!self.remote_scanning){ 
586                             self.remote_active = 0;
587                             return; 
588                         }
589                         self.scan(barcode);
590                         waitforbarcode();
591                     },
592                     function(){
593                         if(!self.remote_scanning){
594                             self.remote_active = 0;
595                             return;
596                         }
597                         setTimeout(waitforbarcode,5000);
598                     });
599             }
600             waitforbarcode();
601         },
602
603         // the barcode scanner will stop listening on the hw_proxy/scanner remote interface
604         disconnect_from_proxy: function(){
605             this.remote_scanning = false;
606         },
607     });
608
609 }