2 function openerp_pos_devices(instance,module){ //module is instance.point_of_sale
3 var _t = instance.web._t;
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.
10 module.JobQueue = function(){
13 var scheduled_end_time = 0;
14 var end_of_queue = (new $.Deferred()).resolve();
15 var stoprepeat = false;
18 if(end_of_queue.state() === 'resolved'){
19 end_of_queue = new $.Deferred();
24 if(!job.opts.repeat || stoprepeat){
29 // the time scheduled for this job
30 scheduled_end_time = (new Date()).getTime() + (job.opts.duration || 0);
32 // we run the job and put in def when it finishes
33 var def = job.fun() || (new $.Deferred()).resolve();
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(){
40 }, Math.max(0, scheduled_end_time - (new Date()).getTime()) );
44 scheduled_end_time = 0;
45 end_of_queue.resolve();
49 // adds a job to the schedule.
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()
55 this.schedule = function(fun, opts){
56 queue.push({fun:fun, opts:opts || {}});
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});
67 // end the repetition of the current job
68 this.stoprepeat = function(){
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(){
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.
86 module.ProxyDevice = instance.web.Class.extend(openerp.PropertiesMixin,{
87 init: function(parent,options){
88 openerp.PropertiesMixin.init.call(this,parent);
90 options = options || {};
91 url = options.url || 'http://localhost:8069';
95 this.weighting = false;
96 this.debug_weight = 0;
97 this.use_debug_weight = false;
100 this.default_payment_status = {
103 payment_method: undefined,
104 receipt_client: undefined,
105 receipt_shop: undefined,
107 this.custom_payment_status = this.default_payment_status;
109 this.receipt_queue = [];
111 this.notifications = {};
112 this.bypass_proxy = false;
114 this.connection = null;
116 this.keptalive = false;
118 this.set('status',{});
120 this.set_connection_status('disconnected');
122 this.on('change:status',this,function(eh,status){
123 status = status.newValue;
124 if(status.status === 'connected'){
125 self.print_receipt();
129 window.hw_proxy = this;
131 set_connection_status: function(status,drivers){
132 oldstatus = this.get('status');
134 newstatus.status = status;
135 newstatus.drivers = status === 'disconnected' ? {} : oldstatus.drivers;
136 newstatus.drivers = drivers ? drivers : newstatus.drivers;
137 this.set('status',newstatus);
139 disconnect: function(){
140 if(this.get('status').status !== 'disconnected'){
141 this.connection.destroy();
142 this.set_connection_status('disconnected');
146 // connects to the specified url
147 connect: function(url){
149 this.connection = new instance.web.Session(undefined,url, { use_cors: true});
151 this.set_connection_status('connecting',{});
153 return this.message('handshake').then(function(response){
155 self.set_connection_status('connected');
156 localStorage['hw_proxy_url'] = url;
159 self.set_connection_status('disconnected');
160 console.error('Connection refused by the Proxy');
163 self.set_connection_status('disconnected');
164 console.error('Could not connect to the Proxy');
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){
174 this.set_connection_status('connecting',{});
175 var found_url = new $.Deferred();
176 var success = new $.Deferred();
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);
188 // just find something quick
189 found_url = this.find_proxy(options);
192 success = found_url.then(function(url){
193 return self.connect(url);
196 success.fail(function(){
197 self.set_connection_status('disconnected');
203 // starts a loop that updates the connection status
204 keepalive: function(){
207 this.keptalive = true;
209 self.connection.rpc('/hw_proxy/status_json',{},{timeout:2500})
210 .then(function(driver_status){
211 self.set_connection_status('connected',driver_status);
213 if(self.get('status').status !== 'connecting'){
214 self.set_connection_status('disconnected');
216 }).always(function(){
217 setTimeout(status,5000);
224 message : function(name,params){
225 var callbacks = this.notifications[name] || [];
226 for(var i = 0; i < callbacks.length; i++){
227 callbacks[i](params);
229 if(this.get('status').status !== 'disconnected'){
230 return this.connection.rpc('/hw_proxy/' + name, params || {});
232 return (new $.Deferred()).reject();
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');
241 this.set_connection_status('connecting');
243 if(url.indexOf('//') < 0){
247 if(url.indexOf(':',5) < 0){
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){
254 done = done || new $.Deferred();
257 url: url + '/hw_proxy/hello',
266 try_real_hard_to_connect(url,retries-1,done);
274 return try_real_hard_to_connect(url,3);
277 // returns as a deferred a valid host url that can be used as proxy.
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 || {};
284 var port = ':' + (options.port || '8069');
288 var done = new $.Deferred(); // will be resolved with the proxies valid urls
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);
300 var prog_inc = 1/urls.length;
302 function update_progress(){
303 progress = found ? 1 : progress + prog_inc;
304 if(options.progress){
305 options.progress(progress);
309 function thread(done){
310 var url = urls.shift();
312 done = done || new $.Deferred();
314 if( !url || found || !self.searching_for_proxy ){
320 url: url + '/hw_proxy/hello',
336 this.searching_for_proxy = true;
338 for(var i = 0, len = Math.min(parallel,urls.length); i < len; i++){
339 threads.push(thread());
342 $.when.apply($,threads).then(function(){
344 for(var i = 0; i < arguments.length; i++){
346 urls.push(arguments[i]);
349 done.resolve(urls[0]);
355 stop_searching: function(){
356 this.searching_for_proxy = false;
357 this.set_connection_status('disconnected');
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] = [];
366 this.notifications[name].push(callback);
369 // returns the weight on the scale.
370 scale_read: function(){
372 var ret = new $.Deferred();
373 console.log('scale_read');
374 this.message('scale_read',{})
375 .then(function(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'});
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;
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;
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');
402 * ask the printer to print a receipt
404 print_receipt: function(receipt){
407 this.receipt_queue.push(receipt);
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 })
418 self.pos.pos_widget.screen_selector.show_popup('error-traceback',{
419 'message': _t('Printing Error: ') + error.data.message,
420 'comment': error.data.debug,
424 self.receipt_queue.unshift(r)
431 // asks the proxy to log some information, as with the debug.log you can provide several arguments.
433 return this.message('log',{'arguments': _.toArray(arguments)});
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({
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;
456 this.action_callback_stack = [];
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'],
469 add_barcode_patterns: function(patterns){
470 this.patterns = this.patterns || {};
471 for(type in patterns){
472 this.patterns[type] = this.patterns[type] || [];
474 var patternlist = patterns[type];
475 if( typeof patternlist === 'string'){
476 patternlist = patternlist.split(',');
478 for(var i = 0; i < patternlist.length; i++){
479 var pattern = patternlist[i].trim().substring(0,12);
483 pattern = pattern.replace(/[x\*]/gi,'x');
484 while(pattern.length < 12){
487 this.patterns[type].push(pattern);
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];
497 for(var j = 0; j < pattern.length; j++){
498 if(pattern[j] != 'x'){
502 this.sorted_patterns.push({type:type, pattern:pattern,score:score});
505 this.sorted_patterns.sort(function(a,b){
506 return b.score - a.score;
511 save_callbacks: function(){
513 for(name in this.action_callback){
514 callbacks[name] = this.action_callback[name];
516 this.action_callback_stack.push(callbacks);
519 restore_callbacks: function(){
520 if(this.action_callback_stack.length){
521 var callbacks = this.action_callback_stack.pop();
522 this.action_callback = callbacks;
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))
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.
534 // possible actions include :
535 // 'product' | 'cashier' | 'client' | 'discount'
537 set_action_callback: function(action, callback){
538 if(arguments.length == 2){
539 this.action_callback[action] = callback;
541 var actions = arguments[0];
542 for(action in actions){
543 this.set_action_callback(action,actions[action]);
548 //remove all action callbacks
549 reset_action_callbacks: function(){
550 for(action in this.action_callback){
551 this.action_callback[action] = undefined;
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){
560 var oddsum = 0, evensum = 0, total = 0;
561 code = code.reverse().splice(1);
562 for(var i = 0; i < code.length; i++){
564 oddsum += Number(code[i]);
566 evensum += Number(code[i]);
569 total = oddsum * 3 + evensum;
570 return Number((10 - total % 10) % 10);
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]);
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);
581 for(var n = 0, count = (13 - ean.length); n < count; n++){
584 return ean.substr(0,12) + this.ean_checksum(ean);
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 :
592 // - type : the type of the ean:
593 // 'price' | 'weight' | 'product' | 'cashier' | 'client' | 'discount' | 'error'
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
599 parse_ean: function(ean){
609 if (!this.check_ean(ean)){
613 function is_number(char){
614 n = char.charCodeAt(0);
615 return n >= 48 && n <= 57;
618 function match_pattern(ean,pattern){
619 for(var i = 0; i < pattern.length; i++){
622 if( is_number(p) && p !== e ){
629 function get_value(ean,pattern){
632 for(var i = 0; i < pattern.length; i++){
634 var v = parseInt(ean[i]);
638 }else if( p === 'D'){
640 value += v * Math.pow(10,-decimals);
646 function get_basecode(ean,pattern){
648 for(var i = 0; i < pattern.length; i++){
651 if( p === 'x' || is_number(p)){
657 return self.sanitize_ean(base);
660 var patterns = this.sorted_patterns;
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);
674 scan: function(code){
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)){
686 encoding: 'reference',
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);
703 if(this.action_callback[parse_result.type]){
704 this.action_callback[parse_result.type](parse_result);
709 // starts catching keyboard events and tries to interpret codebar
710 // calling the callbacks when needed.
716 var onlynumbers = true;
719 this.handler = function(e){
721 if(e.which === 13){ //ignore returns
726 if(timeStamp + 50 < new Date().getTime()){
731 timeStamp = new Date().getTime();
732 clearTimeout(timeout);
734 if( e.which < 48 || e.which >= 58 ){ // not a number
738 code += String.fromCharCode(e.which);
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...
744 timeout = setTimeout(function(){
751 $('body').on('keypress', this.handler);
754 // stops catching keyboard events
755 disconnect: function(){
756 $('body').off('keypress', this.handler)
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(){
763 this.remote_scanning = true;
764 if(this.remote_active >= 1){
767 this.remote_active = 1;
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;
780 if(!self.remote_scanning){
781 self.remote_active = 0;
784 setTimeout(waitforbarcode,5000);
790 // the barcode scanner will stop listening on the hw_proxy/scanner remote interface
791 disconnect_from_proxy: function(){
792 this.remote_scanning = false;