2 function openerp_pos_devices(instance,module){ //module is instance.point_of_sale
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.
9 module.JobQueue = function(){
12 var scheduled_end_time = 0;
13 var end_of_queue = (new $.Deferred()).resolve();
14 var stoprepeat = false;
17 if(end_of_queue.state() === 'resolved'){
18 end_of_queue = new $.Deferred();
23 if(!job.opts.repeat || stoprepeat){
28 // the time scheduled for this job
29 scheduled_end_time = (new Date()).getTime() + (job.opts.duration || 0);
31 // we run the job and put in def when it finishes
32 var def = job.fun() || (new $.Deferred()).resolve();
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(){
39 }, Math.max(0, scheduled_end_time - (new Date()).getTime()) );
43 scheduled_end_time = 0;
44 end_of_queue.resolve();
48 // adds a job to the schedule.
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()
54 this.schedule = function(fun, opts){
55 queue.push({fun:fun, opts:opts || {}});
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});
66 // end the repetition of the current job
67 this.stoprepeat = function(){
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(){
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.
84 module.ProxyDevice = instance.web.Class.extend(openerp.PropertiesMixin,{
85 init: function(parent,options){
86 openerp.PropertiesMixin.init.call(this,parent);
88 options = options || {};
89 url = options.url || 'http://localhost:8069';
91 this.weighting = false;
92 this.debug_weight = 0;
93 this.use_debug_weight = false;
96 this.default_payment_status = {
99 payment_method: undefined,
100 receipt_client: undefined,
101 receipt_shop: undefined,
103 this.custom_payment_status = this.default_payment_status;
105 this.receipt_queue = [];
107 this.notifications = {};
108 this.bypass_proxy = false;
110 this.connection = null;
112 this.keptalive = false;
114 this.set('status',{});
116 this.set_connection_status('disconnected');
118 this.on('change:status',this,function(eh,status){
119 status = status.newValue;
120 if(status.status === 'connected'){
121 self.print_receipt();
125 window.hw_proxy = this;
127 set_connection_status: function(status,drivers){
128 oldstatus = this.get('status');
130 newstatus.status = status;
131 newstatus.drivers = status === 'disconnected' ? {} : oldstatus.drivers;
132 newstatus.drivers = drivers ? drivers : newstatus.drivers;
133 this.set('status',newstatus);
135 disconnect: function(){
136 if(this.get('status').status !== 'disconnected'){
137 this.connection.destroy();
138 this.set_connection_status('disconnected');
142 // connects to the specified url
143 connect: function(url){
145 this.connection = new instance.web.Session(undefined,url, { use_cors: true});
147 this.set_connection_status('connecting',{});
149 return this.message('handshake').then(function(response){
151 self.set_connection_status('connected');
152 localStorage['hw_proxy_url'] = url;
155 self.set_connection_status('disconnected');
156 console.error('Connection refused by the Proxy');
159 self.set_connection_status('disconnected');
160 console.error('Could not connect to the Proxy');
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){
170 this.set_connection_status('connecting',{});
171 var found_url = new $.Deferred();
172 var success = new $.Deferred();
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);
184 // just find something quick
185 found_url = this.find_proxy(options);
188 success = found_url.then(function(url){
189 return self.connect(url);
192 success.fail(function(){
193 self.set_connection_status('disconnected');
199 // starts a loop that updates the connection status
200 keepalive: function(){
203 this.keptalive = true;
205 self.connection.rpc('/hw_proxy/status_json',{},{timeout:2500})
206 .then(function(driver_status){
207 self.set_connection_status('connected',driver_status);
209 if(self.get('status').status !== 'connecting'){
210 self.set_connection_status('disconnected');
212 }).always(function(){
213 setTimeout(status,5000);
220 message : function(name,params){
221 var callbacks = this.notifications[name] || [];
222 for(var i = 0; i < callbacks.length; i++){
223 callbacks[i](params);
225 if(this.get('status').status !== 'disconnected'){
226 return this.connection.rpc('/hw_proxy/' + name, params || {});
228 return (new $.Deferred()).reject();
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');
237 this.set_connection_status('connecting');
239 if(url.indexOf('//') < 0){
243 if(url.indexOf(':',5) < 0){
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){
250 done = done || new $.Deferred();
253 url: url + '/hw_proxy/hello',
262 try_real_hard_to_connect(url,retries-1,done);
270 return try_real_hard_to_connect(url,3);
273 // returns as a deferred a valid host url that can be used as proxy.
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 || {};
280 var port = ':' + (options.port || '8069');
284 var done = new $.Deferred(); // will be resolved with the proxies valid urls
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);
296 var prog_inc = 1/urls.length;
298 function update_progress(){
299 progress = found ? 1 : progress + prog_inc;
300 if(options.progress){
301 options.progress(progress);
305 function thread(done){
306 var url = urls.shift();
308 done = done || new $.Deferred();
310 if( !url || found || !self.searching_for_proxy ){
316 url: url + '/hw_proxy/hello',
332 this.searching_for_proxy = true;
334 for(var i = 0, len = Math.min(parallel,urls.length); i < len; i++){
335 threads.push(thread());
338 $.when.apply($,threads).then(function(){
340 for(var i = 0; i < arguments.length; i++){
342 urls.push(arguments[i]);
345 done.resolve(urls[0]);
351 stop_searching: function(){
352 this.searching_for_proxy = false;
353 this.set_connection_status('disconnected');
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] = [];
362 this.notifications[name].push(callback);
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});
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});
377 //the client is asking for help
378 help_needed: function(){
379 return this.message('help_needed');
382 //the client does not need help anymore
383 help_canceled: function(){
384 return this.message('help_canceled');
387 //the client is starting to weight
388 weighting_start: function(){
389 var ret = new $.Deferred();
391 this.weighting = true;
392 this.message('weighting_start').always(function(){
396 console.error('Weighting already started!!!');
402 // the client has finished weighting products
403 weighting_end: function(){
404 var ret = new $.Deferred();
406 this.weighting = false;
407 this.message('weighting_end').always(function(){
411 console.error('Weighting already ended !!!');
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(){
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);
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;
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;
445 // the pos asks the client to pay 'price' units
446 payment_request: function(price){
447 var ret = new $.Deferred();
449 this.custom_payment_status = this.default_payment_status;
450 return this.message('payment_request',{'price':price});
453 payment_status: function(){
454 if(this.bypass_proxy){
455 this.bypass_proxy = false;
456 return (new $.Deferred()).resolve(this.custom_payment_status);
458 return this.message('payment_status');
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 = {
467 message: 'Successfull Payment, have a nice day',
468 payment_method: 'AMEX',
469 receipt_client: '<xml>bla</xml>',
470 receipt_shop: '<xml>bla</xml>',
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 :(',
482 // the client cancels his payment
483 payment_cancel: function(){
485 this.custom_payment_status = 'waiting_for_payment';
486 return this.message('payment_cancel');
489 // called when the client logs in or starts to scan product
490 transaction_start: function(){
491 return this.message('transaction_start');
494 // called when the clients has finished his interaction with the machine
495 transaction_end: function(){
496 return this.message('transaction_end');
499 // called when the POS turns to cashier mode
500 cashier_mode_activated: function(){
501 return this.message('cashier_mode_activated');
504 // called when the POS turns to client mode
505 cashier_mode_deactivated: function(){
506 return this.message('cashier_mode_deactivated');
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');
514 /* ask the printer to print a receipt
515 * receipt is a JSON object with the following specs:
517 * - orderlines : list of orderlines :
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
530 * - paymentlines : list of paymentlines :
532 * amount: (number) the amount paid
533 * journal: (string) the name of the journal on wich the payment has been made
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]
552 print_receipt: function(receipt){
555 this.receipt_queue.push(receipt);
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 })
565 self.receipt_queue.unshift(r)
572 // asks the proxy to log some information, as with the debug.log you can provide several arguments.
574 return this.message('log',{'arguments': _.toArray(arguments)});
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});
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({
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;
601 this.action_callback_stack = [];
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':''};
611 save_callbacks: function(){
613 for(name in this.action_callback){
614 callbacks[name] = this.action_callback[name];
616 this.action_callback_stack.push(callbacks);
619 restore_callbacks: function(){
620 if(this.action_callback_stack.length){
621 var callbacks = this.action_callback_stack.pop();
622 this.action_callback = callbacks;
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))
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.
634 // possible actions include :
635 // 'product' | 'cashier' | 'client' | 'discount'
637 set_action_callback: function(action, callback){
638 if(arguments.length == 2){
639 this.action_callback[action] = callback;
641 var actions = arguments[0];
642 for(action in actions){
643 this.set_action_callback(action,actions[action]);
648 //remove all action callbacks
649 reset_action_callbacks: function(){
650 for(action in this.action_callback){
651 this.action_callback[action] = undefined;
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){
660 var oddsum = 0, evensum = 0, total = 0;
661 code = code.reverse().splice(1);
662 for(var i = 0; i < code.length; i++){
664 oddsum += Number(code[i]);
666 evensum += Number(code[i]);
669 total = oddsum * 3 + evensum;
670 return Number((10 - total % 10) % 10);
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]);
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);
681 for(var n = 0, count = (13 - ean.length); n < count; n++){
684 return ean.substr(0,12) + this.ean_checksum(ean);
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 :
692 // - type : the type of the ean:
693 // 'price' | 'weight' | 'unit' | 'cashier' | 'client' | 'discount' | 'error'
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
701 // - base_code : the ean code with all the encoding parts set to zero; the one put on
702 // the product in the backend
704 parse_ean: function(ean){
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;
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 = '%';
750 parse_result.type = 'unit';
751 parse_result.prefix = '';
752 parse_result.id = ean;
757 scan: function(code){
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)){
764 encoding: 'reference',
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);
780 if(this.action_callback[parse_result.type]){
781 this.action_callback[parse_result.type](parse_result);
786 // starts catching keyboard events and tries to interpret codebar
787 // calling the callbacks when needed.
793 var onlynumbers = true;
796 this.handler = function(e){
798 if(e.which === 13){ //ignore returns
803 if(timeStamp + 50 < new Date().getTime()){
808 timeStamp = new Date().getTime();
809 clearTimeout(timeout);
811 if( e.which < 48 || e.which >= 58 ){ // not a number
815 code += String.fromCharCode(e.which);
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...
821 timeout = setTimeout(function(){
828 $('body').on('keypress', this.handler);
831 // stops catching keyboard events
832 disconnect: function(){
833 $('body').off('keypress', this.handler)
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(){
840 this.remote_scanning = true;
841 if(this.remote_active >= 1){
844 this.remote_active = 1;
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;
857 if(!self.remote_scanning){
858 self.remote_active = 0;
861 setTimeout(waitforbarcode,5000);
867 // the barcode scanner will stop listening on the hw_proxy/scanner remote interface
868 disconnect_from_proxy: function(){
869 this.remote_scanning = false;