[IMP] point_of_sale: try harder to connect to proxy when the ip is already known
[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
4     // the JobQueue schedules a sequence of 'jobs'. each job is
5     // a function returning a deferred. the queue waits for each job to finish 
6     // before launching the next. Each job can also be scheduled with a delay. 
7     // the  is used to prevent parallel requests to the proxy.
8
9     module.JobQueue = function(){
10         var queue = [];
11         var running = false;
12         var scheduled_end_time = 0;
13         var end_of_queue = (new $.Deferred()).resolve();
14         var stoprepeat = false;
15
16         var run = function(){
17             if(end_of_queue.state() === 'resolved'){
18                 end_of_queue =  new $.Deferred();
19             }
20             if(queue.length > 0){
21                 running = true;
22                 var job = queue[0];
23                 if(!job.opts.repeat || stoprepeat){
24                     queue.shift();
25                     stoprepeat = false;
26                 }
27
28                 // the time scheduled for this job
29                 scheduled_end_time = (new Date()).getTime() + (job.opts.duration || 0);
30
31                 // we run the job and put in def when it finishes
32                 var def = job.fun() || (new $.Deferred()).resolve();
33                 
34                 // we don't care if a job fails ... 
35                 def.always(function(){
36                     // we run the next job after the scheduled_end_time, even if it finishes before
37                     setTimeout(function(){
38                         run();
39                     }, Math.max(0, scheduled_end_time - (new Date()).getTime()) ); 
40                 });
41             }else{
42                 running = false;
43                 scheduled_end_time = 0;
44                 end_of_queue.resolve();
45             }
46         };
47         
48         // adds a job to the schedule.
49         // opts : {
50         //    duration    : the job is guaranteed to finish no quicker than this (milisec)
51         //    repeat      : if true, the job will be endlessly repeated
52         //    important   : if true, the scheduled job cannot be canceled by a queue.clear()
53         // }
54         this.schedule  = function(fun, opts){
55             queue.push({fun:fun, opts:opts || {}});
56             if(!running){
57                 run();
58             }
59         }
60
61         // remove all jobs from the schedule (except the ones marked as important)
62         this.clear = function(){
63             queue = _.filter(queue,function(job){job.opts.important === true}); 
64         };
65
66         // end the repetition of the current job
67         this.stoprepeat = function(){
68             stoprepeat = true;
69         };
70         
71         // returns a deferred that resolves when all scheduled 
72         // jobs have been run.
73         // ( jobs added after the call to this method are considered as well )
74         this.finished = function(){
75             return end_of_queue;
76         }
77
78     };
79
80     // this object interfaces with the local proxy to communicate to the various hardware devices
81     // connected to the Point of Sale. As the communication only goes from the POS to the proxy,
82     // methods are used both to signal an event, and to fetch information. 
83
84     module.ProxyDevice  = instance.web.Class.extend(openerp.PropertiesMixin,{
85         init: function(parent,options){
86             openerp.PropertiesMixin.init.call(this,parent);
87             var self = this;
88             options = options || {};
89             url = options.url || 'http://localhost:8069';
90             
91             this.weighting = false;
92             this.debug_weight = 0;
93             this.use_debug_weight = false;
94
95             this.paying = false;
96             this.default_payment_status = {
97                 status: 'waiting',
98                 message: '',
99                 payment_method: undefined,
100                 receipt_client: undefined,
101                 receipt_shop:   undefined,
102             };    
103             this.custom_payment_status = this.default_payment_status;
104
105             this.receipt_queue = [];
106
107             this.notifications = {};
108             this.bypass_proxy = false;
109
110             this.connection = null; 
111             this.host       = '';
112             this.keptalive  = false;
113
114             this.set('status',{});
115
116             this.set_connection_status('disconnected');
117
118             this.on('change:status',this,function(eh,status){
119                 status = status.newValue;
120                 if(status.status === 'connected'){
121                     self.print_receipt();
122                 }
123             });
124
125             window.hw_proxy = this;
126         },
127         set_connection_status: function(status,drivers){
128             oldstatus = this.get('status');
129             newstatus = {};
130             newstatus.status = status;
131             newstatus.drivers = status === 'disconnected' ? {} : oldstatus.drivers;
132             newstatus.drivers = drivers ? drivers : newstatus.drivers;
133             this.set('status',newstatus);
134         },
135         disconnect: function(){
136             if(this.get('status').status !== 'disconnected'){
137                 this.connection.destroy();
138                 this.set_connection_status('disconnected');
139             }
140         },
141
142         // connects to the specified url
143         connect: function(url){
144             var self = this;
145             this.connection = new instance.web.Session(undefined,url, { use_cors: true});
146             this.host   = url;
147             this.set_connection_status('connecting',{});
148
149             return this.message('handshake').then(function(response){
150                     if(response){
151                         self.set_connection_status('connected');
152                         localStorage['hw_proxy_url'] = url;
153                         self.keepalive();
154                     }else{
155                         self.set_connection_status('disconnected');
156                         console.error('Connection refused by the Proxy');
157                     }
158                 },function(){
159                     self.set_connection_status('disconnected');
160                     console.error('Could not connect to the Proxy');
161                 });
162         },
163
164         // find a proxy and connects to it. for options see find_proxy
165         //   - force_ip : only try to connect to the specified ip. 
166         //   - port: what port to listen to (default 8069)
167         //   - progress(fac) : callback for search progress ( fac in [0,1] ) 
168         autoconnect: function(options){
169             var self = this;
170             this.set_connection_status('connecting',{});
171             var found_url = new $.Deferred();
172             var success = new $.Deferred();
173
174             if ( options.force_ip ){
175                 // if the ip is forced by server config, bailout on fail
176                 found_url = this.try_hard_to_connect(options.force_ip, options)
177             }else if( localStorage['hw_proxy_url'] ){
178                 // try harder when we remember a good proxy url
179                 found_url = this.try_hard_to_connect(localStorage['hw_proxy_url'], options)
180                     .then(null,function(){
181                         return self.find_proxy(options);
182                     });
183             }else{
184                 // just find something quick
185                 found_url = this.find_proxy(options);
186             }
187
188             success = found_url.then(function(url){
189                     return self.connect(url);
190                 });
191
192             success.fail(function(){
193                 self.set_connection_status('disconnected');
194             });
195
196             return success;
197         },
198
199         // starts a loop that updates the connection status
200         keepalive: function(){
201             var self = this;
202             if(!this.keptalive){
203                 this.keptalive = true;
204                 function status(){
205                     self.connection.rpc('/hw_proxy/status_json',{},{timeout:2500})       
206                         .then(function(driver_status){
207                             self.set_connection_status('connected',driver_status);
208                         },function(){
209                             if(self.get('status').status !== 'connecting'){
210                                 self.set_connection_status('disconnected');
211                             }
212                         }).always(function(){
213                             setTimeout(status,5000);
214                         });
215                 }
216                 status();
217             };
218         },
219
220         message : function(name,params){
221             var callbacks = this.notifications[name] || [];
222             for(var i = 0; i < callbacks.length; i++){
223                 callbacks[i](params);
224             }
225             if(this.get('status').status !== 'disconnected'){
226                 return this.connection.rpc('/hw_proxy/' + name, params || {});       
227             }else{
228                 return (new $.Deferred()).reject();
229             }
230         },
231
232         // try several time to connect to a known proxy url
233         try_hard_to_connect: function(url,options){
234             options   = options || {};
235             var port  = ':' + (options.port || '8069');
236
237             this.set_connection_status('connecting');
238
239             if(url.indexOf('//') < 0){
240                 url = 'http://'+url;
241             }
242
243             if(url.indexOf(':',5) < 0){
244                 url = url+port;
245             }
246
247             // try real hard to connect to url, with a 1sec timeout and up to 'retries' retries
248             function try_real_hard_to_connect(url, retries, done){
249
250                 done = done || new $.Deferred();
251
252                 var c = $.ajax({
253                     url: url + '/hw_proxy/hello',
254                     method: 'GET',
255                     timeout: 1000,
256                 })
257                 .done(function(){
258                     done.resolve(url);
259                 })
260                 .fail(function(){
261                     if(retries > 0){
262                         try_real_hard_to_connect(url,retries-1,done);
263                     }else{
264                         done.reject();
265                     }
266                 });
267                 return done;
268             }
269
270             return try_real_hard_to_connect(url,3);
271         },
272
273         // returns as a deferred a valid host url that can be used as proxy.
274         // options:
275         //   - port: what port to listen to (default 8069)
276         //   - progress(fac) : callback for search progress ( fac in [0,1] ) 
277         find_proxy: function(options){
278             options = options || {};
279             var self  = this;
280             var port  = ':' + (options.port || '8069');
281             var urls  = [];
282             var found = false;
283             var parallel = 8;
284             var done = new $.Deferred(); // will be resolved with the proxies valid urls
285             var threads  = [];
286             var progress = 0;
287
288
289             urls.push('http://localhost'+port);
290             for(var i = 0; i < 256; i++){
291                 urls.push('http://192.168.0.'+i+port);
292                 urls.push('http://192.168.1.'+i+port);
293                 urls.push('http://10.0.0.'+i+port);
294             }
295
296             var prog_inc = 1/urls.length; 
297
298             function update_progress(){
299                 progress = found ? 1 : progress + prog_inc;
300                 if(options.progress){
301                     options.progress(progress);
302                 }
303             }
304
305             function thread(done){
306                 var url = urls.shift();
307
308                 done = done || new $.Deferred();
309
310                 if( !url || found || !self.searching_for_proxy ){ 
311                     done.resolve();
312                     return done;
313                 }
314
315                 var c = $.ajax({
316                         url: url + '/hw_proxy/hello',
317                         method: 'GET',
318                         timeout: 400, 
319                     }).done(function(){
320                         found = true;
321                         update_progress();
322                         done.resolve(url);
323                     })
324                     .fail(function(){
325                         update_progress();
326                         thread(done);
327                     });
328
329                 return done;
330             }
331
332             this.searching_for_proxy = true;
333
334             for(var i = 0, len = Math.min(parallel,urls.length); i < len; i++){
335                 threads.push(thread());
336             }
337             
338             $.when.apply($,threads).then(function(){
339                 var urls = [];
340                 for(var i = 0; i < arguments.length; i++){
341                     if(arguments[i]){
342                         urls.push(arguments[i]);
343                     }
344                 }
345                 done.resolve(urls[0]);
346             });
347
348             return done;
349         },
350
351         stop_searching: function(){
352             this.searching_for_proxy = false;
353             this.set_connection_status('disconnected');
354         },
355
356         // this allows the client to be notified when a proxy call is made. The notification 
357         // callback will be executed with the same arguments as the proxy call
358         add_notification: function(name, callback){
359             if(!this.notifications[name]){
360                 this.notifications[name] = [];
361             }
362             this.notifications[name].push(callback);
363         },
364         
365         //a product has been scanned and recognized with success
366         // ean is a parsed ean object
367         scan_item_success: function(ean){
368             return this.message('scan_item_success',{ean: ean});
369         },
370
371         // a product has been scanned but not recognized
372         // ean is a parsed ean object
373         scan_item_error_unrecognized: function(ean){
374             return this.message('scan_item_error_unrecognized',{ean: ean});
375         },
376
377         //the client is asking for help
378         help_needed: function(){
379             return this.message('help_needed');
380         },
381
382         //the client does not need help anymore
383         help_canceled: function(){
384             return this.message('help_canceled');
385         },
386
387         //the client is starting to weight
388         weighting_start: function(){
389             var ret = new $.Deferred();
390             if(!this.weighting){
391                 this.weighting = true;
392                 this.message('weighting_start').always(function(){
393                     ret.resolve();
394                 });
395             }else{
396                 console.error('Weighting already started!!!');
397                 ret.resolve();
398             }
399             return ret;
400         },
401
402         // the client has finished weighting products
403         weighting_end: function(){
404             var ret = new $.Deferred();
405             if(this.weighting){
406                 this.weighting = false;
407                 this.message('weighting_end').always(function(){
408                     ret.resolve();
409                 });
410             }else{
411                 console.error('Weighting already ended !!!');
412                 ret.resolve();
413             }
414             return ret;
415         },
416
417         //returns the weight on the scale. 
418         // is called at regular interval (up to 10x/sec) between a weighting_start()
419         // and a weighting_end()
420         weighting_read_kg: function(){
421             var self = this;
422             var ret = new $.Deferred();
423             this.message('weighting_read_kg',{})
424                 .then(function(weight){
425                     ret.resolve(self.use_debug_weight ? self.debug_weight : weight);
426                 }, function(){ //failed to read weight
427                     ret.resolve(self.use_debug_weight ? self.debug_weight : 0.0);
428                 });
429             return ret;
430         },
431
432         // sets a custom weight, ignoring the proxy returned value. 
433         debug_set_weight: function(kg){
434             this.use_debug_weight = true;
435             this.debug_weight = kg;
436         },
437
438         // resets the custom weight and re-enable listening to the proxy for weight values
439         debug_reset_weight: function(){
440             this.use_debug_weight = false;
441             this.debug_weight = 0;
442         },
443
444
445         // the pos asks the client to pay 'price' units
446         payment_request: function(price){
447             var ret = new $.Deferred();
448             this.paying = true;
449             this.custom_payment_status = this.default_payment_status;
450             return this.message('payment_request',{'price':price});
451         },
452
453         payment_status: function(){
454             if(this.bypass_proxy){
455                 this.bypass_proxy = false;
456                 return (new $.Deferred()).resolve(this.custom_payment_status);
457             }else{
458                 return this.message('payment_status');
459             }
460         },
461         
462         // override what the proxy says and accept the payment
463         debug_accept_payment: function(){
464             this.bypass_proxy = true;
465             this.custom_payment_status = {
466                 status: 'paid',
467                 message: 'Successfull Payment, have a nice day',
468                 payment_method: 'AMEX',
469                 receipt_client: '<xml>bla</xml>',
470                 receipt_shop:   '<xml>bla</xml>',
471             };    
472         },
473
474         // override what the proxy says and reject the payment
475         debug_reject_payment: function(){
476             this.bypass_proxy = true;
477             this.custom_payment_status = {
478                 status: 'error-rejected',
479                 message: 'Sorry you don\'t have enough money :(',
480             };    
481         },
482         // the client cancels his payment
483         payment_cancel: function(){
484             this.paying = false;
485             this.custom_payment_status = 'waiting_for_payment';
486             return this.message('payment_cancel');
487         },
488
489         // called when the client logs in or starts to scan product
490         transaction_start: function(){
491             return this.message('transaction_start');
492         },
493
494         // called when the clients has finished his interaction with the machine
495         transaction_end: function(){
496             return this.message('transaction_end');
497         },
498
499         // called when the POS turns to cashier mode
500         cashier_mode_activated: function(){
501             return this.message('cashier_mode_activated');
502         },
503
504         // called when the POS turns to client mode
505         cashier_mode_deactivated: function(){
506             return this.message('cashier_mode_deactivated');
507         },
508         
509         // ask for the cashbox (the physical box where you store the cash) to be opened
510         open_cashbox: function(){
511             return this.message('open_cashbox');
512         },
513
514         /* ask the printer to print a receipt
515          * receipt is a JSON object with the following specs:
516          * receipt{
517          *  - orderlines : list of orderlines :
518          *     {
519          *          quantity:           (number) the number of items, or the weight, 
520          *          unit_name:          (string) the name of the item's unit (kg, dozen, ...)
521          *          price:              (number) the price of one unit of the item before discount
522          *          discount:           (number) the discount on the product in % [0,100] 
523          *          product_name:       (string) the name of the product
524          *          price_with_tax:     (number) the price paid for this orderline, tax included
525          *          price_without_tax:  (number) the price paid for this orderline, without taxes
526          *          tax:                (number) the price paid in taxes on this orderline
527          *          product_description:         (string) generic description of the product
528          *          product_description_sale:    (string) sales related information of the product
529          *     }
530          *  - paymentlines : list of paymentlines :
531          *     {
532          *          amount:             (number) the amount paid
533          *          journal:            (string) the name of the journal on wich the payment has been made  
534          *     }
535          *  - total_with_tax:     (number) the total of the receipt tax included
536          *  - total_without_tax:  (number) the total of the receipt without taxes
537          *  - total_tax:          (number) the total amount of taxes paid
538          *  - total_paid:         (number) the total sum paid by the client
539          *  - change:             (number) the amount of change given back to the client
540          *  - name:               (string) a unique name for this order
541          *  - client:             (string) name of the client. or null if no client is logged
542          *  - cashier:            (string) the name of the cashier
543          *  - date: {             the date at wich the payment has been done
544          *      year:             (number) the year  [2012, ...]
545          *      month:            (number) the month [0,11]
546          *      date:             (number) the day of the month [1,31]
547          *      day:              (number) the day of the week  [0,6] 
548          *      hour:             (number) the hour [0,23]
549          *      minute:           (number) the minute [0,59]
550          *    }
551          */
552         print_receipt: function(receipt){
553             var self = this;
554             if(receipt){
555                 this.receipt_queue.push(receipt);
556             }
557             var aborted = false;
558             function send_printing_job(){
559                 if (self.receipt_queue.length > 0){
560                     var r = self.receipt_queue.shift();
561                     self.message('print_receipt',{ receipt: r },{ timeout: 5000 })
562                         .then(function(){
563                             send_printing_job();
564                         },function(){
565                             self.receipt_queue.unshift(r)
566                         });
567                 }
568             }
569             send_printing_job();
570         },
571
572         // asks the proxy to log some information, as with the debug.log you can provide several arguments.
573         log: function(){
574             return this.message('log',{'arguments': _.toArray(arguments)});
575         },
576
577         // asks the proxy to print an invoice in pdf form ( used to print invoices generated by the server ) 
578         print_pdf_invoice: function(pdfinvoice){
579             return this.message('print_pdf_invoice',{pdfinvoice: pdfinvoice});
580         },
581     });
582
583     // this module interfaces with the barcode reader. It assumes the barcode reader
584     // is set-up to act like  a keyboard. Use connect() and disconnect() to activate 
585     // and deactivate the barcode reader. Use set_action_callbacks to tell it
586     // what to do when it reads a barcode.
587     module.BarcodeReader = instance.web.Class.extend({
588         actions:[
589             'product',
590             'cashier',
591             'client',
592             'discount',
593         ],
594         init: function(attributes){
595             this.pos = attributes.pos;
596             this.action_callback = {};
597             this.proxy = attributes.proxy;
598             this.remote_scanning = false;
599             this.remote_active = 0;
600
601             this.action_callback_stack = [];
602
603             this.weight_prefix_set   = attributes.weight_prefix_set   ||  {'21':''};
604             this.discount_prefix_set = attributes.discount_prefix_set ||  {'22':''};
605             this.price_prefix_set    = attributes.price_prefix_set    ||  {'23':''};
606             this.cashier_prefix_set  = attributes.cashier_prefix_set  ||  {'041':''};
607             this.client_prefix_set   = attributes.client_prefix_set   ||  {'042':''};
608
609         },
610
611         save_callbacks: function(){
612             var callbacks = {};
613             for(name in this.action_callback){
614                 callbacks[name] = this.action_callback[name];
615             }
616             this.action_callback_stack.push(callbacks);
617         },
618
619         restore_callbacks: function(){
620             if(this.action_callback_stack.length){
621                 var callbacks = this.action_callback_stack.pop();
622                 this.action_callback = callbacks;
623             }
624         },
625        
626         // when an ean is scanned and parsed, the callback corresponding
627         // to its type is called with the parsed_ean as a parameter. 
628         // (parsed_ean is the result of parse_ean(ean)) 
629         // 
630         // callbacks is a Map of 'actions' : callback(parsed_ean)
631         // that sets the callback for each action. if a callback for the
632         // specified action already exists, it is replaced. 
633         // 
634         // possible actions include : 
635         // 'product' | 'cashier' | 'client' | 'discount' 
636     
637         set_action_callback: function(action, callback){
638             if(arguments.length == 2){
639                 this.action_callback[action] = callback;
640             }else{
641                 var actions = arguments[0];
642                 for(action in actions){
643                     this.set_action_callback(action,actions[action]);
644                 }
645             }
646         },
647
648         //remove all action callbacks 
649         reset_action_callbacks: function(){
650             for(action in this.action_callback){
651                 this.action_callback[action] = undefined;
652             }
653         },
654         // returns the checksum of the ean, or -1 if the ean has not the correct length, ean must be a string
655         ean_checksum: function(ean){
656             var code = ean.split('');
657             if(code.length !== 13){
658                 return -1;
659             }
660             var oddsum = 0, evensum = 0, total = 0;
661             code = code.reverse().splice(1);
662             for(var i = 0; i < code.length; i++){
663                 if(i % 2 == 0){
664                     oddsum += Number(code[i]);
665                 }else{
666                     evensum += Number(code[i]);
667                 }
668             }
669             total = oddsum * 3 + evensum;
670             return Number((10 - total % 10) % 10);
671         },
672         // returns true if the ean is a valid EAN codebar number by checking the control digit.
673         // ean must be a string
674         check_ean: function(ean){
675             return this.ean_checksum(ean) === Number(ean[ean.length-1]);
676         },
677         // returns a valid zero padded ean13 from an ean prefix. the ean prefix must be a string.
678         sanitize_ean:function(ean){
679             ean = ean.substr(0,13);
680
681             for(var n = 0, count = (13 - ean.length); n < count; n++){
682                 ean = ean + '0';
683             }
684             return ean.substr(0,12) + this.ean_checksum(ean);
685         },
686         
687         // attempts to interpret an ean (string encoding an ean)
688         // it will check its validity then return an object containing various
689         // information about the ean.
690         // most importantly : 
691         // - code    : the ean
692         // - type   : the type of the ean: 
693         //      'price' |  'weight' | 'unit' | 'cashier' | 'client' | 'discount' | 'error'
694         //
695         // - prefix : the prefix that has ben used to determine the type
696         // - id     : the part of the ean that identifies something
697         // - value  : if the id encodes a numerical value, it will be put there
698         // - unit   : if the encoded value has a unit, it will be put there. 
699         //            not to be confused with the 'unit' type, which represent an unit of a 
700         //            unique product
701         // - base_code : the ean code with all the encoding parts set to zero; the one put on
702         //               the product in the backend
703
704         parse_ean: function(ean){
705             var parse_result = {
706                 encoding: 'ean13',
707                 type:'unknown',  
708                 prefix:'',
709                 code:ean,
710                 base_code: ean,
711                 id:'',
712                 value: 0,
713                 unit: 'none',
714             };
715
716             function match_prefix(prefix_set, type){
717                 for(prefix in prefix_set){
718                     if(ean.substring(0,prefix.length) === prefix){
719                         parse_result.prefix = prefix;
720                         parse_result.type = type;
721                         return true;
722                     }
723                 }
724                 return false;
725             }
726
727             if (!this.check_ean(ean)){
728                 parse_result.type = 'error';
729             } else if( match_prefix(this.price_prefix_set,'price')){
730                 parse_result.id = ean.substring(0,7);
731                 parse_result.base_code = this.sanitize_ean(ean.substring(0,7));
732                 parse_result.value = Number(ean.substring(7,12))/100.0;
733                 parse_result.unit  = 'euro';
734             } else if( match_prefix(this.weight_prefix_set,'weight')){
735                 parse_result.id = ean.substring(0,7);
736                 parse_result.value = Number(ean.substring(7,12))/1000.0;
737                 parse_result.base_code = this.sanitize_ean(ean.substring(0,7));
738                 parse_result.unit = 'Kg';
739             } else if( match_prefix(this.client_prefix_set,'client')){
740                 parse_result.id = ean.substring(0,7);
741                 parse_result.unit = 'Kg';
742             } else if( match_prefix(this.cashier_prefix_set,'cashier')){
743                 parse_result.id = ean.substring(0,7);
744             } else if( match_prefix(this.discount_prefix_set,'discount')){
745                 parse_result.id    = ean.substring(0,7);
746                 parse_result.base_code = this.sanitize_ean(ean.substring(0,7));
747                 parse_result.value = Number(ean.substring(7,12))/100.0;
748                 parse_result.unit  = '%';
749             } else {
750                 parse_result.type = 'unit';
751                 parse_result.prefix = '';
752                 parse_result.id = ean;
753             }
754             return parse_result;
755         },
756         
757         scan: function(code){
758             if(code.length < 3){
759                 return;
760             }else if(code.length === 13 && /^\d+$/.test(code)){
761                 var parse_result = this.parse_ean(code);
762             }else if(this.pos.db.get_product_by_reference(code)){
763                 var parse_result = {
764                     encoding: 'reference',
765                     type: 'unit',
766                     code: code,
767                     prefix: '',
768                 };
769             }else{
770                 return;
771             }
772
773             if (parse_result.type === 'error') {    //most likely a checksum error, raise warning
774                 console.warn('WARNING: barcode checksum error:',parse_result);
775             }else if(parse_result.type in {'unit':'', 'weight':'', 'price':''}){    //ean is associated to a product
776                 if(this.action_callback['product']){
777                     this.action_callback['product'](parse_result);
778                 }
779             }else{
780                 if(this.action_callback[parse_result.type]){
781                     this.action_callback[parse_result.type](parse_result);
782                 }
783             }
784         },
785
786         // starts catching keyboard events and tries to interpret codebar 
787         // calling the callbacks when needed.
788         connect: function(){
789
790             var self = this;
791             var code = "";
792             var timeStamp  = 0;
793             var onlynumbers = true;
794             var timeout = null;
795
796             this.handler = function(e){
797
798                 if(e.which === 13){ //ignore returns
799                     e.preventDefault();
800                     return;
801                 }
802
803                 if(timeStamp + 50 < new Date().getTime()){
804                     code = "";
805                     onlynumbers = true;
806                 }
807
808                 timeStamp = new Date().getTime();
809                 clearTimeout(timeout);
810
811                 if( e.which < 48 || e.which >= 58 ){ // not a number
812                     onlynumbers = false;
813                 }
814
815                 code += String.fromCharCode(e.which);
816
817                 // we wait for a while after the last input to be sure that we are not mistakingly
818                 // returning a code which is a prefix of a bigger one :
819                 // Internal Ref 5449 vs EAN13 5449000...
820
821                 timeout = setTimeout(function(){
822                     self.scan(code);
823                     code = "";
824                     onlynumbers = true;
825                 },100);
826             };
827
828             $('body').on('keypress', this.handler);
829         },
830
831         // stops catching keyboard events 
832         disconnect: function(){
833             $('body').off('keypress', this.handler)
834         },
835
836         // the barcode scanner will listen on the hw_proxy/scanner interface for 
837         // scan events until disconnect_from_proxy is called
838         connect_to_proxy: function(){ 
839             var self = this;
840             this.remote_scanning = true;
841             if(this.remote_active >= 1){
842                 return;
843             }
844             this.remote_active = 1;
845
846             function waitforbarcode(){
847                 return self.proxy.connection.rpc('/hw_proxy/scanner',{},{timeout:7500})
848                     .then(function(barcode){
849                         if(!self.remote_scanning){ 
850                             self.remote_active = 0;
851                             return; 
852                         }
853                         self.scan(barcode);
854                         waitforbarcode();
855                     },
856                     function(){
857                         if(!self.remote_scanning){
858                             self.remote_active = 0;
859                             return;
860                         }
861                         setTimeout(waitforbarcode,5000);
862                     });
863             }
864             waitforbarcode();
865         },
866
867         // the barcode scanner will stop listening on the hw_proxy/scanner remote interface
868         disconnect_from_proxy: function(){
869             this.remote_scanning = false;
870         },
871     });
872
873 }