[FIX] web_tip: don't display tip on element whose width or height is 0
[odoo/odoo.git] / addons / point_of_sale / static / src / js / screens.js
1
2 // this file contains the screens definitions. Screens are the
3 // content of the right pane of the pos, containing the main functionalities. 
4 // screens are contained in the PosWidget, in pos_widget.js
5 // all screens are present in the dom at all time, but only one is shown at the
6 // same time. 
7 //
8 // transition between screens is made possible by the use of the screen_selector,
9 // which is responsible of hiding and showing the screens, as well as maintaining
10 // the state of the screens between different orders.
11 //
12 // all screens inherit from ScreenWidget. the only addition from the base widgets
13 // are show() and hide() which shows and hides the screen but are also used to 
14 // bind and unbind actions on widgets and devices. The screen_selector guarantees
15 // that only one screen is shown at the same time and that show() is called after all
16 // hide()s
17
18 function openerp_pos_screens(instance, module){ //module is instance.point_of_sale
19     var QWeb = instance.web.qweb,
20     _t = instance.web._t;
21
22     var round_pr = instance.web.round_precision
23
24     module.ScreenSelector = instance.web.Class.extend({
25         init: function(options){
26             this.pos = options.pos;
27
28             this.screen_set = options.screen_set || {};
29
30             this.popup_set = options.popup_set || {};
31
32             this.default_screen = options.default_screen;
33
34             this.current_popup = null;
35
36             this.current_mode = options.default_mode || 'cashier';
37
38             this.current_screen = null; 
39
40             for(screen_name in this.screen_set){
41                 this.screen_set[screen_name].hide();
42             }
43             
44             for(popup_name in this.popup_set){
45                 this.popup_set[popup_name].hide();
46             }
47
48             this.pos.get('selectedOrder').set_screen_data({
49                 'screen': this.default_screen,
50             });
51
52             this.pos.bind('change:selectedOrder', this.load_saved_screen, this);
53         },
54         add_screen: function(screen_name, screen){
55             screen.hide();
56             this.screen_set[screen_name] = screen;
57             return this;
58         },
59         show_popup: function(name,options){
60             if(this.current_popup){
61                 this.close_popup();
62             }
63             this.current_popup = this.popup_set[name];
64             this.current_popup.show(options);
65         },
66         close_popup: function(){
67             if(this.current_popup){
68                 this.current_popup.close();
69                 this.current_popup.hide();
70                 this.current_popup = null;
71             }
72         },
73         load_saved_screen:  function(){
74             this.close_popup();
75             var selectedOrder = this.pos.get('selectedOrder');
76             // FIXME : this changing screen behaviour is sometimes confusing ... 
77             this.set_current_screen(selectedOrder.get_screen_data('screen') || this.default_screen,null,'refresh');
78             //this.set_current_screen(this.default_screen,null,'refresh');
79             
80         },
81         set_user_mode: function(user_mode){
82             if(user_mode !== this.current_mode){
83                 this.close_popup();
84                 this.current_mode = user_mode;
85                 this.load_saved_screen();
86             }
87         },
88         get_user_mode: function(){
89             return this.current_mode;
90         },
91         set_current_screen: function(screen_name,params,refresh){
92             var screen = this.screen_set[screen_name];
93             if(!screen){
94                 console.error("ERROR: set_current_screen("+screen_name+") : screen not found");
95             }
96
97             this.close_popup();
98
99             var order = this.pos.get('selectedOrder');
100             var old_screen_name = order.get_screen_data('screen');
101
102             order.set_screen_data('screen',screen_name);
103
104             if(params){
105                 order.set_screen_data('params',params);
106             }
107
108             if( screen_name !== old_screen_name ){
109                 order.set_screen_data('previous-screen',old_screen_name);
110             }
111
112             if ( refresh || screen !== this.current_screen){
113                 if(this.current_screen){
114                     this.current_screen.close();
115                     this.current_screen.hide();
116                 }
117                 this.current_screen = screen;
118                 this.current_screen.show();
119             }
120         },
121         get_current_screen: function(){
122             return this.pos.get('selectedOrder').get_screen_data('screen') || this.default_screen;
123         },
124         back: function(){
125             var previous = this.pos.get('selectedOrder').get_screen_data('previous-screen');
126             if(previous){
127                 this.set_current_screen(previous);
128             }
129         },
130         get_current_screen_param: function(param){
131             var params = this.pos.get('selectedOrder').get_screen_data('params');
132             return params ? params[param] : undefined;
133         },
134         set_default_screen: function(){
135             this.set_current_screen(this.default_screen);
136         },
137     });
138
139     module.ScreenWidget = module.PosBaseWidget.extend({
140
141         show_numpad:     true,  
142         show_leftpane:   true,
143
144         init: function(parent,options){
145             this._super(parent,options);
146             this.hidden = false;
147         },
148
149         help_button_action: function(){
150             this.pos_widget.screen_selector.show_popup('help');
151         },
152
153         barcode_product_screen:         'products',     //if defined, this screen will be loaded when a product is scanned
154
155         hotkeys_handlers: {},
156
157         // what happens when a product is scanned : 
158         // it will add the product to the order and go to barcode_product_screen. 
159         barcode_product_action: function(code){
160             var self = this;
161             if(self.pos.scan_product(code)){
162                 if(self.barcode_product_screen){ 
163                     self.pos_widget.screen_selector.set_current_screen(self.barcode_product_screen);
164                 }
165             }else{
166                 self.pos_widget.screen_selector.show_popup('error-barcode',code.code);
167             }
168         },
169
170         // what happens when a cashier id barcode is scanned.
171         // the default behavior is the following : 
172         // - if there's a user with a matching ean, put it as the active 'cashier', go to cashier mode, and return true
173         // - else : do nothing and return false. You probably want to extend this to show and appropriate error popup... 
174         barcode_cashier_action: function(code){
175             var users = this.pos.users;
176             for(var i = 0, len = users.length; i < len; i++){
177                 if(users[i].ean13 === code.code){
178                     this.pos.cashier = users[i];
179                     this.pos_widget.username.refresh();
180                     return true;
181                 }
182             }
183             this.pos_widget.screen_selector.show_popup('error-barcode',code.code);
184             return false;
185         },
186         
187         // what happens when a client id barcode is scanned.
188         // the default behavior is the following : 
189         // - if there's a user with a matching ean, put it as the active 'client' and return true
190         // - else : return false. 
191         barcode_client_action: function(code){
192             var partner = this.pos.db.get_partner_by_ean13(code.code);
193             if(partner){
194                 this.pos.get('selectedOrder').set_client(partner);
195                 this.pos_widget.username.refresh();
196                 return true;
197             }
198             this.pos_widget.screen_selector.show_popup('error-barcode',code.code);
199             return false;
200         },
201         
202         // what happens when a discount barcode is scanned : the default behavior
203         // is to set the discount on the last order.
204         barcode_discount_action: function(code){
205             var last_orderline = this.pos.get('selectedOrder').getLastOrderline();
206             if(last_orderline){
207                 last_orderline.set_discount(code.value)
208             }
209         },
210         // What happens when an invalid barcode is scanned : shows an error popup.
211         barcode_error_action: function(code){
212             this.pos_widget.screen_selector.show_popup('error-barcode',code.code);
213         },
214
215         // this method shows the screen and sets up all the widget related to this screen. Extend this method
216         // if you want to alter the behavior of the screen.
217         show: function(){
218             var self = this;
219
220             this.hidden = false;
221             if(this.$el){
222                 this.$el.removeClass('oe_hidden');
223             }
224
225             var self = this;
226
227             this.pos_widget.set_numpad_visible(this.show_numpad);
228             this.pos_widget.set_leftpane_visible(this.show_leftpane);
229
230             this.pos_widget.username.set_user_mode(this.pos_widget.screen_selector.get_user_mode());
231
232             this.pos.barcode_reader.set_action_callback({
233                 'cashier': self.barcode_cashier_action ? function(code){ self.barcode_cashier_action(code); } : undefined ,
234                 'product': self.barcode_product_action ? function(code){ self.barcode_product_action(code); } : undefined ,
235                 'client' : self.barcode_client_action ?  function(code){ self.barcode_client_action(code);  } : undefined ,
236                 'discount': self.barcode_discount_action ? function(code){ self.barcode_discount_action(code); } : undefined,
237                 'error'   : self.barcode_error_action ?  function(code){ self.barcode_error_action(code);   } : undefined,
238             });
239         },
240
241         // this method is called when the screen is closed to make place for a new screen. this is a good place
242         // to put your cleanup stuff as it is guaranteed that for each show() there is one and only one close()
243         close: function(){
244             if(this.pos.barcode_reader){
245                 this.pos.barcode_reader.reset_action_callbacks();
246             }
247         },
248
249         // this methods hides the screen. It's not a good place to put your cleanup stuff as it is called on the
250         // POS initialization.
251         hide: function(){
252             this.hidden = true;
253             if(this.$el){
254                 this.$el.addClass('oe_hidden');
255             }
256         },
257
258         // we need this because some screens re-render themselves when they are hidden
259         // (due to some events, or magic, or both...)  we must make sure they remain hidden.
260         // the good solution would probably be to make them not re-render themselves when they
261         // are hidden. 
262         renderElement: function(){
263             this._super();
264             if(this.hidden){
265                 if(this.$el){
266                     this.$el.addClass('oe_hidden');
267                 }
268             }
269         },
270     });
271
272     module.PopUpWidget = module.PosBaseWidget.extend({
273         show: function(){
274             if(this.$el){
275                 this.$el.removeClass('oe_hidden');
276             }
277         },
278         /* called before hide, when a popup is closed */
279         close: function(){
280         },
281         /* hides the popup. keep in mind that this is called in the initialization pass of the 
282          * pos instantiation, so you don't want to do anything fancy in here */
283         hide: function(){
284             if(this.$el){
285                 this.$el.addClass('oe_hidden');
286             }
287         },
288     });
289
290
291     module.FullscreenPopup = module.PopUpWidget.extend({
292         template:'FullscreenPopupWidget',
293         show: function(){
294             var self = this;
295             this._super();
296             this.renderElement();
297             this.$('.button.fullscreen').off('click').click(function(){
298                 window.document.body.webkitRequestFullscreen();
299                 self.pos_widget.screen_selector.close_popup();
300             });
301             this.$('.button.cancel').off('click').click(function(){
302                 self.pos_widget.screen_selector.close_popup();
303             });
304         },
305         ismobile: function(){
306             return typeof window.orientation !== 'undefined'; 
307         }
308     });
309
310
311     module.ChooseReceiptPopupWidget = module.PopUpWidget.extend({
312         template:'ChooseReceiptPopupWidget',
313         show: function(){
314             this._super();
315             this.renderElement();
316             var self = this;
317             var currentOrder = self.pos.get('selectedOrder');
318             
319             this.$('.button.receipt').off('click').click(function(){
320                 currentOrder.set_receipt_type('receipt');
321                 self.pos_widget.screen_selector.set_current_screen('products');
322             });
323
324             this.$('.button.invoice').off('click').click(function(){
325                 currentOrder.set_receipt_type('invoice');
326                 self.pos_widget.screen_selector.set_current_screen('products');
327             });
328         },
329         get_client_name: function(){
330             var client = this.pos.get('selectedOrder').get_client();
331             if( client ){
332                 return client.name;
333             }else{
334                 return '';
335             }
336         },
337     });
338
339     module.ErrorPopupWidget = module.PopUpWidget.extend({
340         template:'ErrorPopupWidget',
341         show: function(text){
342             var self = this;
343             this._super();
344
345             $('body').append('<audio src="/point_of_sale/static/src/sounds/error.wav" autoplay="true"></audio>');
346
347             if( text && (text.message || text.comment) ){
348                 this.$('.message').text(text.message);
349                 this.$('.comment').text(text.comment);
350             }
351
352             this.pos.barcode_reader.save_callbacks();
353             this.pos.barcode_reader.reset_action_callbacks();
354             this.$('.footer .button').off('click').click(function(){
355                 self.pos_widget.screen_selector.close_popup();
356             });
357         },
358         close:function(){
359             this._super();
360             this.pos.barcode_reader.restore_callbacks();
361         },
362     });
363
364     module.ErrorTracebackPopupWidget = module.ErrorPopupWidget.extend({
365         template:'ErrorTracebackPopupWidget',
366     });
367
368     module.ErrorSessionPopupWidget = module.ErrorPopupWidget.extend({
369         template:'ErrorSessionPopupWidget',
370     });
371
372     module.ErrorBarcodePopupWidget = module.ErrorPopupWidget.extend({
373         template:'ErrorBarcodePopupWidget',
374         show: function(barcode){
375             this._super();
376             this.$('.barcode').text(barcode);
377
378         },
379     });
380
381     module.ConfirmPopupWidget = module.PopUpWidget.extend({
382         template: 'ConfirmPopupWidget',
383         show: function(options){
384             var self = this;
385             this._super();
386
387             this.message = options.message || '';
388             this.comment = options.comment || '';
389             this.renderElement();
390             
391             this.$('.button.cancel').click(function(){
392                 self.pos_widget.screen_selector.close_popup();
393                 if( options.cancel ){
394                     options.cancel.call(self);
395                 }
396             });
397
398             this.$('.button.confirm').click(function(){
399                 self.pos_widget.screen_selector.close_popup();
400                 if( options.confirm ){
401                     options.confirm.call(self);
402                 }
403             });
404         },
405     });
406
407     module.ErrorInvoiceTransferPopupWidget = module.ErrorPopupWidget.extend({
408         template: 'ErrorInvoiceTransferPopupWidget',
409     });
410
411     module.UnsentOrdersPopupWidget = module.PopUpWidget.extend({
412         template: 'UnsentOrdersPopupWidget',
413         show: function(options){
414             var self = this;
415             this._super(options);
416             this.renderElement();
417             this.$('.button.confirm').click(function(){
418                 self.pos_widget.screen_selector.close_popup();
419             });
420         },
421     });
422
423     module.ScaleScreenWidget = module.ScreenWidget.extend({
424         template:'ScaleScreenWidget',
425
426         next_screen: 'products',
427         previous_screen: 'products',
428
429         show_leftpane:   false,
430
431         show: function(){
432             this._super();
433             var self = this;
434             var queue = this.pos.proxy_queue;
435
436             this.set_weight(0);
437             this.renderElement();
438
439             this.hotkey_handler = function(event){
440                 if(event.which === 13){
441                     self.order_product();
442                     self.pos_widget.screen_selector.set_current_screen(self.next_screen);
443                 }else if(event.which === 27){
444                     self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
445                 }
446             };
447
448             $('body').on('keyup',this.hotkey_handler);
449
450             this.$('.back').click(function(){
451                 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
452             });
453
454             this.$('.next,.buy-product').click(function(){
455                 self.order_product();
456                 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
457             });
458
459             queue.schedule(function(){
460                 return self.pos.proxy.scale_read().then(function(weight){
461                     self.set_weight(weight.weight);
462                 });
463             },{duration:50, repeat: true});
464
465         },
466         get_product: function(){
467             var ss = this.pos_widget.screen_selector;
468             if(ss){
469                 return ss.get_current_screen_param('product');
470             }else{
471                 return undefined;
472             }
473         },
474         order_product: function(){
475             this.pos.get('selectedOrder').addProduct(this.get_product(),{ quantity: this.weight });
476         },
477         get_product_name: function(){
478             var product = this.get_product();
479             return (product ? product.name : undefined) || 'Unnamed Product';
480         },
481         get_product_price: function(){
482             var product = this.get_product();
483             return (product ? product.price : 0) || 0;
484         },
485         set_weight: function(weight){
486             this.weight = weight;
487             this.$('.weight').text(this.get_product_weight_string());
488             this.$('.computed-price').text(this.get_computed_price_string());
489         },
490         get_product_weight_string: function(){
491             var product = this.get_product();
492             var defaultstr = (this.weight || 0).toFixed(3) + ' Kg';
493             if(!product || !this.pos){
494                 return defaultstr;
495             }
496             var unit_id = product.uos_id || product.uom_id;
497             if(!unit_id){
498                 return defaultstr;
499             }
500             var unit = this.pos.units_by_id[unit_id[0]];
501             var weight = round_pr(this.weight || 0, unit.rounding);
502             var weightstr = weight.toFixed(Math.ceil(Math.log(1.0/unit.rounding) / Math.log(10) ));
503                 weightstr += ' Kg';
504             return weightstr;
505         },
506         get_computed_price_string: function(){
507             return this.format_currency(this.get_product_price() * this.weight);
508         },
509         close: function(){
510             var self = this;
511             this._super();
512             $('body').off('keyup',this.hotkey_handler);
513
514             this.pos.proxy_queue.clear();
515         },
516     });
517
518     module.ProductScreenWidget = module.ScreenWidget.extend({
519         template:'ProductScreenWidget',
520
521         show_numpad:     true,
522         show_leftpane:   true,
523
524         start: function(){ //FIXME this should work as renderElement... but then the categories aren't properly set. explore why
525             var self = this;
526
527             this.product_list_widget = new module.ProductListWidget(this,{
528                 click_product_action: function(product){
529                     if(product.to_weight && self.pos.config.iface_electronic_scale){
530                         self.pos_widget.screen_selector.set_current_screen('scale',{product: product});
531                     }else{
532                         self.pos.get('selectedOrder').addProduct(product);
533                     }
534                 },
535                 product_list: this.pos.db.get_product_by_category(0)
536             });
537             this.product_list_widget.replace(this.$('.placeholder-ProductListWidget'));
538
539             this.product_categories_widget = new module.ProductCategoriesWidget(this,{
540                 product_list_widget: this.product_list_widget,
541             });
542             this.product_categories_widget.replace(this.$('.placeholder-ProductCategoriesWidget'));
543         },
544
545         show: function(){
546             this._super();
547             var self = this;
548
549             this.product_categories_widget.reset_category();
550
551             this.pos_widget.order_widget.set_editable(true);
552         },
553
554         close: function(){
555             this._super();
556
557             this.pos_widget.order_widget.set_editable(false);
558
559             if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
560                 this.pos_widget.onscreen_keyboard.hide();
561             }
562         },
563     });
564
565     module.ClientListScreenWidget = module.ScreenWidget.extend({
566         template: 'ClientListScreenWidget',
567
568         init: function(parent, options){
569             this._super(parent, options);
570             this.partner_cache = new module.DomCache();
571         },
572
573         show_leftpane: false,
574
575         auto_back: true,
576
577         show: function(){
578             var self = this;
579             this._super();
580
581             this.renderElement();
582             this.details_visible = false;
583             this.old_client = this.pos.get('selectedOrder').get('client');
584             this.new_client = this.old_client;
585
586             this.$('.back').click(function(){
587                 self.pos_widget.screen_selector.back();
588             });
589
590             this.$('.next').click(function(){
591                 self.save_changes();
592                 self.pos_widget.screen_selector.back();
593             });
594
595             var partners = this.pos.db.get_partners_sorted();
596             this.render_list(partners);
597
598             if( this.old_client ){
599                 this.display_client_details('show',this.old_client,0);
600             }
601
602             this.$('.client-list-contents').delegate('.client-line','click',function(event){
603                 self.line_select(event,$(this),parseInt($(this).data('id')));
604             });
605
606             var search_timeout = null;
607             this.$('.searchbox input').on('keyup',function(event){
608                 clearTimeout(search_timeout);
609
610                 var query = this.value;
611
612                 search_timeout = setTimeout(function(){
613                     self.perform_search(query,event.which === 13);
614                 },70);
615             });
616
617             this.$('.searchbox .search-clear').click(function(){
618                 self.clear_search();
619             });
620         },
621         perform_search: function(query, associate_result){
622             if(query){
623                 var customers = this.pos.db.search_partner(query);
624                 this.display_client_details('hide');
625                 if ( associate_result && customers.length === 1){
626                     this.new_client = customers[0];
627                     this.save_changes();
628                     this.pos_widget.screen_selector.back();
629                 }
630                 this.render_list(customers);
631             }else{
632                 var customers = this.pos.db.get_partners_sorted();
633                 this.render_list(customers);
634             }
635         },
636         clear_search: function(){
637             var customers = this.pos.db.get_partners_sorted();
638             this.render_list(customers);
639             this.$('.searchbox input')[0].value = '';
640             this.$('.searchbox input').focus();
641         },
642         render_list: function(partners){
643             var contents = this.$el[0].querySelector('.client-list-contents');
644             contents.innerHtml = "";
645             for(var i = 0, len = partners.length; i < len; i++){
646                 var partner    = partners[i];
647                 var clientline = this.partner_cache.get_node(partner.id);
648                 if(!clientline){
649                     var clientline_html = QWeb.render('ClientLine',{partner:partners[i]});
650                     var clientline = document.createElement('tbody');
651                     clientline.innerHTML = clientline_html;
652                     clientline = clientline.childNodes[1];
653                     this.partner_cache.cache_node(partner.id,clientline);
654                 }
655                 if( partners === this.new_client ){
656                     clientline.classList.add('highlight');
657                 }else{
658                     clientline.classList.remove('highlight');
659                 }
660                 contents.appendChild(clientline);
661             }
662         },
663         save_changes: function(){
664             if( this.has_client_changed() ){
665                 this.pos.get('selectedOrder').set_client(this.new_client);
666             }
667         },
668         has_client_changed: function(){
669             if( this.old_client && this.new_client ){
670                 return this.old_client.id !== this.new_client.id;
671             }else{
672                 return !!this.old_client !== !!this.new_client;
673             }
674         },
675         toggle_save_button: function(){
676             var $button = this.$('.button.next');
677             if( this.new_client ){
678                 if( !this.old_client){
679                     $button.text(_t('Set Customer'));
680                 }else{
681                     $button.text(_t('Change Customer'));
682                 }
683             }else{
684                 $button.text(_t('Deselect Customer'));
685             }
686             $button.toggleClass('oe_hidden',!this.has_client_changed());
687         },
688         line_select: function(event,$line,id){
689             var partner = this.pos.db.get_partner_by_id(id);
690             this.$('.client-list .lowlight').removeClass('lowlight');
691             if ( $line.hasClass('highlight') ){
692                 $line.removeClass('highlight');
693                 $line.addClass('lowlight');
694                 this.display_client_details('hide',partner);
695                 this.new_client = null;
696                 this.toggle_save_button();
697             }else{
698                 this.$('.client-list .highlight').removeClass('highlight');
699                 $line.addClass('highlight');
700                 var y = event.pageY - $line.parent().offset().top
701                 this.display_client_details('show',partner,y);
702                 this.new_client = partner;
703                 this.toggle_save_button();
704             }
705         },
706         partner_icon_url: function(id){
707             return '/web/binary/image?model=res.partner&id='+id+'&field=image_small';
708         },
709         display_client_details: function(visibility,partner,clickpos){
710             if(visibility === 'show'){
711                 var contents = this.$('.client-details-contents');
712                 var parent   = this.$('.client-list').parent();
713                 var old_scroll   = parent.scrollTop();
714                 var old_height   = contents.height();
715                 contents.empty();
716                 contents.append($(QWeb.render('ClientDetails',{widget:this,partner:partner})));
717                 var new_height   = contents.height();
718
719                 if(!this.details_visible){
720                     if(clickpos < old_scroll + new_height + 20 ){
721                         parent.scrollTop( clickpos - 20 );
722                     }else{
723                         parent.scrollTop(parent.scrollTop() + new_height);
724                     }
725                 }else{
726                     parent.scrollTop(parent.scrollTop() - old_height + new_height);
727                 }
728
729                 this.details_visible = true;
730             }else if(visibility === 'hide'){
731                 var contents = this.$('.client-details-contents');
732                 var parent   = this.$('.client-list').parent();
733                 var scroll   = parent.scrollTop();
734                 var height   = contents.height();
735                 contents.empty();
736                 if( height > scroll ){
737                     contents.css({height:height+'px'});
738                     contents.animate({height:0},400,function(){
739                         contents.css({height:''});
740                     });
741                     //parent.scrollTop(0);
742                 }else{
743                     parent.scrollTop( parent.scrollTop() - height);
744                 }
745                 this.details_visible = false;
746             }
747         },
748         close: function(){
749             this._super();
750         },
751     });
752
753     module.ReceiptScreenWidget = module.ScreenWidget.extend({
754         template: 'ReceiptScreenWidget',
755         show_numpad:     false,
756         show_leftpane:   false,
757
758         show: function(){
759             this._super();
760             var self = this;
761
762             this.refresh();
763             this.print();
764
765             // The problem is that in chrome the print() is asynchronous and doesn't
766             // execute until all rpc are finished. So it conflicts with the rpc used
767             // to send the orders to the backend, and the user is able to go to the next 
768             // screen before the printing dialog is opened. The problem is that what's 
769             // printed is whatever is in the page when the dialog is opened and not when it's called,
770             // and so you end up printing the product list instead of the receipt... 
771             //
772             // Fixing this would need a re-architecturing
773             // of the code to postpone sending of orders after printing.
774             //
775             // But since the print dialog also blocks the other asynchronous calls, the
776             // button enabling in the setTimeout() is blocked until the printing dialog is 
777             // closed. But the timeout has to be big enough or else it doesn't work
778             // 2 seconds is the same as the default timeout for sending orders and so the dialog
779             // should have appeared before the timeout... so yeah that's not ultra reliable. 
780
781             this.lock_screen(true);  
782             setTimeout(function(){
783                 self.lock_screen(false);  
784             }, 2000);
785         },
786         lock_screen: function(locked) {
787             this._locked = locked;
788             if (locked) {
789                 this.$('.next').removeClass('highlight');
790             } else {
791                 this.$('.next').addClass('highlight');
792             }
793         },
794         print: function() {
795             window.print();
796         },
797         finish_order: function() {
798             if (!this._locked) {
799                 this.pos.get_order().finalize();
800             }
801         },
802         renderElement: function() {
803             var self = this;
804             this._super();
805             this.$('.next').click(function(){
806                 self.finish_order();
807             });
808             this.$('.button.print').click(function(){
809                 self.print();
810             });
811         },
812         refresh: function() {
813             var order = this.pos.get_order();
814             this.$('.pos-receipt-container').html(QWeb.render('PosTicket',{
815                     widget:this,
816                     order: order,
817                     orderlines: order.get('orderLines').models,
818                     paymentlines: order.get('paymentLines').models,
819                 }));
820         },
821     });
822
823     module.PaymentScreenWidget = module.ScreenWidget.extend({
824         template:      'PaymentScreenWidget',
825         back_screen:   'product',
826         next_screen:   'receipt',
827         show_leftpane: false,
828         show_numpad:   false,
829         init: function(parent, options) {
830             var self = this;
831             this._super(parent, options);
832
833             this.pos.bind('change:selectedOrder',function(){
834                     this.renderElement();
835                     this.watch_order_changes();
836                 },this);
837             this.watch_order_changes();
838
839             this.inputbuffer = "";
840             this.firstinput  = true;
841             this.keyboard_handler = function(event){
842                 var key = '';
843                 if ( event.keyCode === 13 ) {         // Enter
844                     self.validate_order();
845                 } else if ( event.keyCode === 190 ) { // Dot
846                     key = '.';
847                 } else if ( event.keyCode === 46 ) {  // Delete
848                     key = 'CLEAR';
849                 } else if ( event.keyCode === 8 ) {   // Backspace 
850                     key = 'BACKSPACE';
851                     event.preventDefault(); // Prevents history back nav
852                 } else if ( event.keyCode >= 48 && event.keyCode <= 57 ){       // Numbers
853                     key = '' + (event.keyCode - 48);
854                 } else if ( event.keyCode >= 96 && event.keyCode <= 105 ){      // Numpad Numbers
855                     key = '' + (event.keyCode - 96);
856                 } else if ( event.keyCode === 189 || event.keyCode === 109 ) {  // Minus
857                     key = '-';
858                 } else if ( event.keyCode === 107 ) { // Plus
859                     key = '+';
860                 }
861
862                 self.payment_input(key);
863
864             };
865         },
866         // resets the current input buffer
867         reset_input: function(){
868             var line = this.pos.get_order().selected_paymentline;
869             this.firstinput  = true;
870             if (line) {
871                 this.inputbuffer = this.format_currency_no_symbol(line.get_amount());
872             } else {
873                 this.inputbuffer = "";
874             }
875         },
876         // handle both keyboard and numpad input. Accepts
877         // a string that represents the key pressed.
878         payment_input: function(input) {
879             var oldbuf = this.inputbuffer.slice(0);
880
881             if (input === '.') {
882                 if (this.firstinput) {
883                     this.inputbuffer = "0.";
884                 }else if (!this.inputbuffer.length || this.inputbuffer === '-') {
885                     this.inputbuffer += "0.";
886                 } else if (this.inputbuffer.indexOf('.') < 0){
887                     this.inputbuffer = this.inputbuffer + '.';
888                 }
889             } else if (input === 'CLEAR') {
890                 this.inputbuffer = ""; 
891             } else if (input === 'BACKSPACE') { 
892                 this.inputbuffer = this.inputbuffer.substring(0,this.inputbuffer.length - 1);
893             } else if (input === '+') {
894                 if ( this.inputbuffer[0] === '-' ) {
895                     this.inputbuffer = this.inputbuffer.substring(1,this.inputbuffer.length);
896                 }
897             } else if (input === '-') {
898                 if ( this.inputbuffer[0] === '-' ) {
899                     this.inputbuffer = this.inputbuffer.substring(1,this.inputbuffer.length);
900                 } else {
901                     this.inputbuffer = '-' + this.inputbuffer;
902                 }
903             } else if (input[0] === '+' && !isNaN(parseFloat(input))) {
904                 this.inputbuffer = '' + ((parseFloat(this.inputbuffer) || 0) + parseFloat(input));
905             } else if (!isNaN(parseInt(input))) {
906                 if (this.firstinput) {
907                     this.inputbuffer = '' + input;
908                 } else {
909                     this.inputbuffer += input;
910                 }
911             }
912
913             this.firstinput = false;
914
915             if (this.inputbuffer !== oldbuf) {
916                 var order = this.pos.get_order();
917                 if (order.selected_paymentline) {
918                     order.selected_paymentline.set_amount(parseFloat(this.inputbuffer));
919                     this.order_changes();
920                     this.render_paymentlines();
921                     this.$('.paymentline.selected .edit').text(this.inputbuffer);
922                 }
923             }
924         },
925         click_numpad: function(button) {
926             this.payment_input(button.data('action'));
927         },
928         render_numpad: function() {
929             var self = this;
930             var numpad = $(QWeb.render('PaymentScreen-Numpad', { widget:this }));
931             numpad.on('click','button',function(){
932                 self.click_numpad($(this));
933             });
934             return numpad;
935         },
936         click_delete_paymentline: function(cid){
937             var lines = this.pos.get_order().get('paymentLines').models;
938             for ( var i = 0; i < lines.length; i++ ) {
939                 if (lines[i].cid === cid) {
940                     this.pos.get_order().removePaymentline(lines[i]);
941                     this.reset_input();
942                     this.render_paymentlines();
943                     return;
944                 }
945             }
946         },
947         click_paymentline: function(cid){
948             var lines = this.pos.get_order().get('paymentLines').models;
949             for ( var i = 0; i < lines.length; i++ ) {
950                 if (lines[i].cid === cid) {
951                     this.pos.get_order().selectPaymentline(lines[i]);
952                     this.reset_input();
953                     this.render_paymentlines();
954                     return;
955                 }
956             }
957         },
958         render_paymentlines: function() {
959             var self  = this;
960             var order = this.pos.get_order();
961             var lines = order.get('paymentLines').models;
962
963             this.$('.paymentlines-container').empty();
964             var lines = $(QWeb.render('PaymentScreen-Paymentlines', { 
965                 widget: this, 
966                 order: order,
967                 paymentlines: lines,
968             }));
969
970             lines.on('click','.delete-button',function(){
971                 self.click_delete_paymentline($(this).data('cid'));
972             });
973
974             lines.on('click','.paymentline',function(){
975                 self.click_paymentline($(this).data('cid'));
976             });
977                 
978             lines.appendTo(this.$('.paymentlines-container'));
979         },
980         click_paymentmethods: function(id) {
981             var cashregister = null;
982             for ( var i = 0; i < this.pos.cashregisters.length; i++ ) {
983                 if ( this.pos.cashregisters[i].journal_id[0] === id ){
984                     cashregister = this.pos.cashregisters[i];
985                     break;
986                 }
987             }
988             this.pos.get_order().addPaymentline( cashregister );
989             this.reset_input();
990             this.render_paymentlines();
991         },
992         render_paymentmethods: function() {
993             var self = this;
994             var methods = $(QWeb.render('PaymentScreen-Paymentmethods', { widget:this }));
995                 methods.on('click','.paymentmethod',function(){
996                     self.click_paymentmethods($(this).data('id'));
997                 });
998             return methods;
999         },
1000         click_invoice: function(){
1001             var order = this.pos.get_order();
1002             order.set_to_invoice(!order.is_to_invoice());
1003             if (order.is_to_invoice()) {
1004                 this.$('.js_invoice').addClass('highlight');
1005             } else {
1006                 this.$('.js_invoice').removeClass('highlight');
1007             }
1008         },
1009         renderElement: function() {
1010             var self = this;
1011             this._super();
1012
1013             var numpad = this.render_numpad();
1014             numpad.appendTo(this.$('.payment-numpad'));
1015
1016             var methods = this.render_paymentmethods();
1017             methods.appendTo(this.$('.paymentmethods-container'));
1018
1019             this.render_paymentlines();
1020
1021             this.$('.back').click(function(){
1022                 self.pos_widget.screen_selector.back();
1023             });
1024
1025             this.$('.next').click(function(){
1026                 self.validate_order();
1027             });
1028
1029             this.$('.js_invoice').click(function(){
1030                 self.click_invoice();
1031             });
1032
1033         },
1034         show: function(){
1035             this.pos.get_order().clean_empty_paymentlines();
1036             this.reset_input();
1037             this.render_paymentlines();
1038             this.order_changes();
1039             window.document.body.addEventListener('keydown',this.keyboard_handler);
1040             this._super();
1041         },
1042         hide: function(){
1043             window.document.body.removeEventListener('keydown',this.keyboard_handler);
1044             this._super();
1045         },
1046         // sets up listeners to watch for order changes
1047         watch_order_changes: function() {
1048             var self = this;
1049             var order = this.pos.get_order();
1050             if(this.old_order){
1051                 this.old_order.unbind(null,null,this);
1052             }
1053             order.bind('all',function(){
1054                 self.order_changes();
1055             });
1056             this.old_order = order;
1057         },
1058         // called when the order is changed, used to show if
1059         // the order is paid or not
1060         order_changes: function(){
1061             var self = this;
1062             var order = this.pos.get_order();
1063             if (order.isPaid()) {
1064                 self.$('.next').addClass('highlight');
1065             }else{
1066                 self.$('.next').removeClass('highlight');
1067             }
1068         },
1069         // Check if the order is paid, then sends it to the backend,
1070         // and complete the sale process
1071         validate_order: function() {
1072             var self = this;
1073
1074             var order = this.pos.get_order();
1075
1076             if (!order.isPaid() || this.invoicing) {
1077                 return;
1078             }
1079
1080             if (order.isPaidWithCash() && this.pos.config.iface_cashdrawer) { 
1081                     this.pos.proxy.open_cashbox();
1082             }
1083
1084             if (order.is_to_invoice()) {
1085                 var invoiced = this.pos.push_and_invoice_order(order);
1086                 this.invoicing = true;
1087
1088                 invoiced.fail(function(error){
1089                     self.invoicing = false;
1090                     if (error === 'error-no-client') {
1091                         self.pos_widget.screen_selector.show_popup('confirm',{
1092                             message: _t('Please select the Customer'),
1093                             comment: _t('You need to select the customer before you can invoice an order.'),
1094                             confirm: function(){
1095                                 self.pos_widget.screen_selector.set_current_screen('clientlist');
1096                             },
1097                         });
1098                     } else {
1099                         self.pos_widget.screen_selector.show_popup('error-invoice-transfer');
1100                     }
1101                 });
1102
1103                 invoiced.done(function(){
1104                     self.invoicing = false;
1105                     order.finalize();
1106                 });
1107             } else {
1108                 this.pos.push_order(order) 
1109                 if (this.pos.config.iface_print_via_proxy) {
1110                     var receipt = currentOrder.export_for_printing();
1111                     this.pos.proxy.print_receipt(QWeb.render('XmlReceipt',{
1112                         receipt: receipt, widget: self,
1113                     }));
1114                     order.finalize();    //finish order and go back to scan screen
1115                 } else {
1116                     this.pos_widget.screen_selector.set_current_screen(this.next_screen);
1117                 }
1118             }
1119         },
1120     });
1121
1122 }