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