function openerp_pos_devices(instance,module){ //module is instance.point_of_sale var _t = instance.web._t; // the JobQueue schedules a sequence of 'jobs'. each job is // a function returning a deferred. the queue waits for each job to finish // before launching the next. Each job can also be scheduled with a delay. // the is used to prevent parallel requests to the proxy. module.JobQueue = function(){ var queue = []; var running = false; var scheduled_end_time = 0; var end_of_queue = (new $.Deferred()).resolve(); var stoprepeat = false; var run = function(){ if(end_of_queue.state() === 'resolved'){ end_of_queue = new $.Deferred(); } if(queue.length > 0){ running = true; var job = queue[0]; if(!job.opts.repeat || stoprepeat){ queue.shift(); stoprepeat = false; } // the time scheduled for this job scheduled_end_time = (new Date()).getTime() + (job.opts.duration || 0); // we run the job and put in def when it finishes var def = job.fun() || (new $.Deferred()).resolve(); // we don't care if a job fails ... def.always(function(){ // we run the next job after the scheduled_end_time, even if it finishes before setTimeout(function(){ run(); }, Math.max(0, scheduled_end_time - (new Date()).getTime()) ); }); }else{ running = false; scheduled_end_time = 0; end_of_queue.resolve(); } }; // adds a job to the schedule. // opts : { // duration : the job is guaranteed to finish no quicker than this (milisec) // repeat : if true, the job will be endlessly repeated // important : if true, the scheduled job cannot be canceled by a queue.clear() // } this.schedule = function(fun, opts){ queue.push({fun:fun, opts:opts || {}}); if(!running){ run(); } } // remove all jobs from the schedule (except the ones marked as important) this.clear = function(){ queue = _.filter(queue,function(job){job.opts.important === true}); }; // end the repetition of the current job this.stoprepeat = function(){ stoprepeat = true; }; // returns a deferred that resolves when all scheduled // jobs have been run. // ( jobs added after the call to this method are considered as well ) this.finished = function(){ return end_of_queue; } }; // this object interfaces with the local proxy to communicate to the various hardware devices // connected to the Point of Sale. As the communication only goes from the POS to the proxy, // methods are used both to signal an event, and to fetch information. module.ProxyDevice = instance.web.Class.extend(openerp.PropertiesMixin,{ init: function(parent,options){ openerp.PropertiesMixin.init.call(this,parent); var self = this; options = options || {}; url = options.url || 'http://localhost:8069'; this.pos = parent; this.weighting = false; this.debug_weight = 0; this.use_debug_weight = false; this.paying = false; this.default_payment_status = { status: 'waiting', message: '', payment_method: undefined, receipt_client: undefined, receipt_shop: undefined, }; this.custom_payment_status = this.default_payment_status; this.receipt_queue = []; this.notifications = {}; this.bypass_proxy = false; this.connection = null; this.host = ''; this.keptalive = false; this.set('status',{}); this.set_connection_status('disconnected'); this.on('change:status',this,function(eh,status){ status = status.newValue; if(status.status === 'connected'){ self.print_receipt(); } }); window.hw_proxy = this; }, set_connection_status: function(status,drivers){ oldstatus = this.get('status'); newstatus = {}; newstatus.status = status; newstatus.drivers = status === 'disconnected' ? {} : oldstatus.drivers; newstatus.drivers = drivers ? drivers : newstatus.drivers; this.set('status',newstatus); }, disconnect: function(){ if(this.get('status').status !== 'disconnected'){ this.connection.destroy(); this.set_connection_status('disconnected'); } }, // connects to the specified url connect: function(url){ var self = this; this.connection = new instance.web.Session(undefined,url, { use_cors: true}); this.host = url; this.set_connection_status('connecting',{}); return this.message('handshake').then(function(response){ if(response){ self.set_connection_status('connected'); localStorage['hw_proxy_url'] = url; self.keepalive(); }else{ self.set_connection_status('disconnected'); console.error('Connection refused by the Proxy'); } },function(){ self.set_connection_status('disconnected'); console.error('Could not connect to the Proxy'); }); }, // find a proxy and connects to it. for options see find_proxy // - force_ip : only try to connect to the specified ip. // - port: what port to listen to (default 8069) // - progress(fac) : callback for search progress ( fac in [0,1] ) autoconnect: function(options){ var self = this; this.set_connection_status('connecting',{}); var found_url = new $.Deferred(); var success = new $.Deferred(); if ( options.force_ip ){ // if the ip is forced by server config, bailout on fail found_url = this.try_hard_to_connect(options.force_ip, options) }else if( localStorage['hw_proxy_url'] ){ // try harder when we remember a good proxy url found_url = this.try_hard_to_connect(localStorage['hw_proxy_url'], options) .then(null,function(){ return self.find_proxy(options); }); }else{ // just find something quick found_url = this.find_proxy(options); } success = found_url.then(function(url){ return self.connect(url); }); success.fail(function(){ self.set_connection_status('disconnected'); }); return success; }, // starts a loop that updates the connection status keepalive: function(){ var self = this; if(!this.keptalive){ this.keptalive = true; function status(){ self.connection.rpc('/hw_proxy/status_json',{},{timeout:2500}) .then(function(driver_status){ self.set_connection_status('connected',driver_status); },function(){ if(self.get('status').status !== 'connecting'){ self.set_connection_status('disconnected'); } }).always(function(){ setTimeout(status,5000); }); } status(); }; }, message : function(name,params){ var callbacks = this.notifications[name] || []; for(var i = 0; i < callbacks.length; i++){ callbacks[i](params); } if(this.get('status').status !== 'disconnected'){ return this.connection.rpc('/hw_proxy/' + name, params || {}); }else{ return (new $.Deferred()).reject(); } }, // try several time to connect to a known proxy url try_hard_to_connect: function(url,options){ options = options || {}; var port = ':' + (options.port || '8069'); this.set_connection_status('connecting'); if(url.indexOf('//') < 0){ url = 'http://'+url; } if(url.indexOf(':',5) < 0){ url = url+port; } // try real hard to connect to url, with a 1sec timeout and up to 'retries' retries function try_real_hard_to_connect(url, retries, done){ done = done || new $.Deferred(); var c = $.ajax({ url: url + '/hw_proxy/hello', method: 'GET', timeout: 1000, }) .done(function(){ done.resolve(url); }) .fail(function(){ if(retries > 0){ try_real_hard_to_connect(url,retries-1,done); }else{ done.reject(); } }); return done; } return try_real_hard_to_connect(url,3); }, // returns as a deferred a valid host url that can be used as proxy. // options: // - port: what port to listen to (default 8069) // - progress(fac) : callback for search progress ( fac in [0,1] ) find_proxy: function(options){ options = options || {}; var self = this; var port = ':' + (options.port || '8069'); var urls = []; var found = false; var parallel = 8; var done = new $.Deferred(); // will be resolved with the proxies valid urls var threads = []; var progress = 0; urls.push('http://localhost'+port); for(var i = 0; i < 256; i++){ urls.push('http://192.168.0.'+i+port); urls.push('http://192.168.1.'+i+port); urls.push('http://10.0.0.'+i+port); } var prog_inc = 1/urls.length; function update_progress(){ progress = found ? 1 : progress + prog_inc; if(options.progress){ options.progress(progress); } } function thread(done){ var url = urls.shift(); done = done || new $.Deferred(); if( !url || found || !self.searching_for_proxy ){ done.resolve(); return done; } var c = $.ajax({ url: url + '/hw_proxy/hello', method: 'GET', timeout: 400, }).done(function(){ found = true; update_progress(); done.resolve(url); }) .fail(function(){ update_progress(); thread(done); }); return done; } this.searching_for_proxy = true; for(var i = 0, len = Math.min(parallel,urls.length); i < len; i++){ threads.push(thread()); } $.when.apply($,threads).then(function(){ var urls = []; for(var i = 0; i < arguments.length; i++){ if(arguments[i]){ urls.push(arguments[i]); } } done.resolve(urls[0]); }); return done; }, stop_searching: function(){ this.searching_for_proxy = false; this.set_connection_status('disconnected'); }, // this allows the client to be notified when a proxy call is made. The notification // callback will be executed with the same arguments as the proxy call add_notification: function(name, callback){ if(!this.notifications[name]){ this.notifications[name] = []; } this.notifications[name].push(callback); }, // returns the weight on the scale. scale_read: function(){ var self = this; var ret = new $.Deferred(); if (self.use_debug_weight) { return (new $.Deferred()).resolve({weight:this.debug_weight, unit:'Kg', info:'ok'}); } this.message('scale_read',{}) .then(function(weight){ ret.resolve(weight); }, function(){ //failed to read weight ret.resolve({weight:0.0, unit:'Kg', info:'ok'}); }); return ret; }, // sets a custom weight, ignoring the proxy returned value. debug_set_weight: function(kg){ this.use_debug_weight = true; this.debug_weight = kg; }, // resets the custom weight and re-enable listening to the proxy for weight values debug_reset_weight: function(){ this.use_debug_weight = false; this.debug_weight = 0; }, // ask for the cashbox (the physical box where you store the cash) to be opened open_cashbox: function(){ return this.message('open_cashbox'); }, /* * ask the printer to print a receipt */ print_receipt: function(receipt){ var self = this; if(receipt){ this.receipt_queue.push(receipt); } var aborted = false; function send_printing_job(){ if (self.receipt_queue.length > 0){ var r = self.receipt_queue.shift(); self.message('print_xml_receipt',{ receipt: r },{ timeout: 5000 }) .then(function(){ send_printing_job(); },function(error){ if (error) { self.pos.pos_widget.screen_selector.show_popup('error-traceback',{ 'message': _t('Printing Error: ') + error.data.message, 'comment': error.data.debug, }); return; } self.receipt_queue.unshift(r) }); } } send_printing_job(); }, // asks the proxy to log some information, as with the debug.log you can provide several arguments. log: function(){ return this.message('log',{'arguments': _.toArray(arguments)}); }, }); // this module interfaces with the barcode reader. It assumes the barcode reader // is set-up to act like a keyboard. Use connect() and disconnect() to activate // and deactivate the barcode reader. Use set_action_callbacks to tell it // what to do when it reads a barcode. module.BarcodeReader = instance.web.Class.extend({ actions:[ 'product', 'cashier', 'client', ], init: function(attributes){ this.pos = attributes.pos; this.action_callback = {}; this.proxy = attributes.proxy; this.remote_scanning = false; this.remote_active = 0; this.action_callback_stack = []; this.add_barcode_patterns(attributes.patterns || { 'product': ['xxxxxxxxxxxxx'], 'cashier': ['041xxxxxxxxxx'], 'client': ['042xxxxxxxxxx'], 'weight': ['21xxxxxNNDDDx'], 'discount': ['22xxxxxxxxNNx'], 'price': ['23xxxxxNNNDDx'], }); }, add_barcode_patterns: function(patterns){ this.patterns = this.patterns || {}; for(type in patterns){ this.patterns[type] = this.patterns[type] || []; var patternlist = patterns[type]; if( typeof patternlist === 'string'){ patternlist = patternlist.split(','); } for(var i = 0; i < patternlist.length; i++){ var pattern = patternlist[i].trim().substring(0,12); if(!pattern.length){ continue; } pattern = pattern.replace(/[x\*]/gi,'x'); while(pattern.length < 12){ pattern += 'x'; } this.patterns[type].push(pattern); } } this.sorted_patterns = []; for (var type in this.patterns){ var patterns = this.patterns[type]; for(var i = 0; i < patterns.length; i++){ var pattern = patterns[i]; var score = 0; for(var j = 0; j < pattern.length; j++){ if(pattern[j] != 'x'){ score++; } } this.sorted_patterns.push({type:type, pattern:pattern,score:score}); } } this.sorted_patterns.sort(function(a,b){ return b.score - a.score; }); }, save_callbacks: function(){ var callbacks = {}; for(name in this.action_callback){ callbacks[name] = this.action_callback[name]; } this.action_callback_stack.push(callbacks); }, restore_callbacks: function(){ if(this.action_callback_stack.length){ var callbacks = this.action_callback_stack.pop(); this.action_callback = callbacks; } }, // when an ean is scanned and parsed, the callback corresponding // to its type is called with the parsed_ean as a parameter. // (parsed_ean is the result of parse_ean(ean)) // // callbacks is a Map of 'actions' : callback(parsed_ean) // that sets the callback for each action. if a callback for the // specified action already exists, it is replaced. // // possible actions include : // 'product' | 'cashier' | 'client' | 'discount' set_action_callback: function(action, callback){ if(arguments.length == 2){ this.action_callback[action] = callback; }else{ var actions = arguments[0]; for(action in actions){ this.set_action_callback(action,actions[action]); } } }, //remove all action callbacks reset_action_callbacks: function(){ for(action in this.action_callback){ this.action_callback[action] = undefined; } }, // returns the checksum of the ean, or -1 if the ean has not the correct length, ean must be a string ean_checksum: function(ean){ var code = ean.split(''); if(code.length !== 13){ return -1; } var oddsum = 0, evensum = 0, total = 0; code = code.reverse().splice(1); for(var i = 0; i < code.length; i++){ if(i % 2 == 0){ oddsum += Number(code[i]); }else{ evensum += Number(code[i]); } } total = oddsum * 3 + evensum; return Number((10 - total % 10) % 10); }, // returns true if the ean is a valid EAN codebar number by checking the control digit. // ean must be a string check_ean: function(ean){ return /^\d+$/.test(ean) && this.ean_checksum(ean) === Number(ean[ean.length-1]); }, // returns a valid zero padded ean13 from an ean prefix. the ean prefix must be a string. sanitize_ean:function(ean){ ean = ean.substr(0,13); for(var n = 0, count = (13 - ean.length); n < count; n++){ ean = ean + '0'; } return ean.substr(0,12) + this.ean_checksum(ean); }, // attempts to interpret an ean (string encoding an ean) // it will check its validity then return an object containing various // information about the ean. // most importantly : // - code : the ean // - type : the type of the ean: // 'price' | 'weight' | 'product' | 'cashier' | 'client' | 'discount' | 'error' // // - value : if the id encodes a numerical value, it will be put there // - base_code : the ean code with all the encoding parts set to zero; the one put on // the product in the backend parse_ean: function(ean){ var self = this; var parse_result = { encoding: 'ean13', type:'error', code:ean, base_code: ean, value: 0, }; if (!this.check_ean(ean)){ return parse_result; } function is_number(char){ n = char.charCodeAt(0); return n >= 48 && n <= 57; } function match_pattern(ean,pattern){ for(var i = 0; i < pattern.length; i++){ var p = pattern[i]; var e = ean[i]; if( is_number(p) && p !== e ){ return false; } } return true; } function get_value(ean,pattern){ var value = 0; var decimals = 0; for(var i = 0; i < pattern.length; i++){ var p = pattern[i]; var v = parseInt(ean[i]); if( p === 'N'){ value *= 10; value += v; }else if( p === 'D'){ decimals += 1; value += v * Math.pow(10,-decimals); } } return value; } function get_basecode(ean,pattern){ var base = ''; for(var i = 0; i < pattern.length; i++){ var p = pattern[i]; var v = ean[i]; if( p === 'x' || is_number(p)){ base += v; }else{ base += '0'; } } return self.sanitize_ean(base); } var patterns = this.sorted_patterns; for(var i = 0; i < patterns.length; i++){ if(match_pattern(ean,patterns[i].pattern)){ parse_result.type = patterns[i].type; parse_result.value = get_value(ean,patterns[i].pattern); parse_result.base_code = get_basecode(ean,patterns[i].pattern); return parse_result; } } return parse_result; }, scan: function(code){ if(code.length < 3){ return; }else if(code.length === 13 && this.check_ean(code)){ var parse_result = this.parse_ean(code); }else if(code.length === 12 && this.check_ean('0'+code)){ // many barcode scanners strip the leading zero of ean13 barcodes. // This is because ean-13 are UCP-A with an additional zero at the beginning, // so by stripping zeros you get retrocompatibility with UCP-A systems. var parse_result = this.parse_ean('0'+code); }else if(this.pos.db.get_product_by_reference(code)){ var parse_result = { encoding: 'reference', type: 'product', code: code, }; }else{ var parse_result = { encoding: 'error', type: 'error', code: code, }; } if(parse_result.type in {'product':'', 'weight':'', 'price':''}){ //ean is associated to a product if(this.action_callback['product']){ this.action_callback['product'](parse_result); } }else{ if(this.action_callback[parse_result.type]){ this.action_callback[parse_result.type](parse_result); } } }, // starts catching keyboard events and tries to interpret codebar // calling the callbacks when needed. connect: function(){ var self = this; var code = ""; var timeStamp = 0; var onlynumbers = true; var timeout = null; this.handler = function(e){ if(e.which === 13){ //ignore returns e.preventDefault(); return; } if(timeStamp + 50 < new Date().getTime()){ code = ""; onlynumbers = true; } timeStamp = new Date().getTime(); clearTimeout(timeout); if( e.which < 48 || e.which >= 58 ){ // not a number onlynumbers = false; } code += String.fromCharCode(e.which); // we wait for a while after the last input to be sure that we are not mistakingly // returning a code which is a prefix of a bigger one : // Internal Ref 5449 vs EAN13 5449000... timeout = setTimeout(function(){ self.scan(code); code = ""; onlynumbers = true; },100); }; $('body').on('keypress', this.handler); }, // stops catching keyboard events disconnect: function(){ $('body').off('keypress', this.handler) }, // the barcode scanner will listen on the hw_proxy/scanner interface for // scan events until disconnect_from_proxy is called connect_to_proxy: function(){ var self = this; this.remote_scanning = true; if(this.remote_active >= 1){ return; } this.remote_active = 1; function waitforbarcode(){ return self.proxy.connection.rpc('/hw_proxy/scanner',{},{timeout:7500}) .then(function(barcode){ if(!self.remote_scanning){ self.remote_active = 0; return; } self.scan(barcode); waitforbarcode(); }, function(){ if(!self.remote_scanning){ self.remote_active = 0; return; } setTimeout(waitforbarcode,5000); }); } waitforbarcode(); }, // the barcode scanner will stop listening on the hw_proxy/scanner remote interface disconnect_from_proxy: function(){ this.remote_scanning = false; }, }); }