[MERGE] from master
[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 openerp.point_of_sale.load_screens = function load_screens(instance, module){ //module is instance.point_of_sale
19     "use strict";
20
21     var QWeb = instance.web.qweb,
22     _t = instance.web._t;
23
24     var round_pr = instance.web.round_precision
25
26     module.ScreenSelector = instance.web.Class.extend({
27         init: function(options){
28             this.pos = options.pos;
29             this.screen_set     = options.screen_set || {};
30             this.popup_set      = options.popup_set || {};
31             this.default_screen = options.default_screen;
32             this.startup_screen = options.startup_screen;
33             this.current_popup  = null;
34             this.current_mode   = options.default_mode || 'cashier';
35             this.current_screen = null; 
36
37             for (var screen_name in this.screen_set) {
38                 this.screen_set[screen_name].hide();
39             }
40             
41             for (var popup_name in this.popup_set) {
42                 this.popup_set[popup_name].hide();
43             }
44
45             if (this.pos.get_order()) {
46                 this.pos.get_order().set_screen_data({
47                     'screen': this.default_screen,
48                 });
49             }
50
51             this.pos.bind('change:selectedOrder', this.load_saved_screen, this);
52         },
53         add_screen: function(screen_name, screen){
54             screen.hide();
55             this.screen_set[screen_name] = screen;
56             return this;
57         },
58         show_popup: function(name,options){
59             if(this.current_popup){
60                 this.close_popup();
61             }
62             this.current_popup = this.popup_set[name];
63             this.current_popup.show(options);
64         },
65         close_popup: function(){
66             if(this.current_popup){
67                 this.current_popup.close();
68                 this.current_popup.hide();
69                 this.current_popup = null;
70             }
71         },
72         load_saved_screen:  function(options){
73             options = options || {};
74             this.close_popup();
75             var selectedOrder = this.pos.get_order();
76             // FIXME : this changing screen behaviour is sometimes confusing ... 
77             this.set_current_screen(selectedOrder.get_screen_data('screen') || options.default_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_order();
100             if (order) {
101                 var old_screen_name = order.get_screen_data('screen');
102
103                 order.set_screen_data('screen',screen_name);
104
105                 if(params){
106                     order.set_screen_data('params',params);
107                 }
108
109                 if( screen_name !== old_screen_name ){
110                     order.set_screen_data('previous-screen',old_screen_name);
111                 }
112             }
113
114             if ( refresh || screen !== this.current_screen){
115                 if(this.current_screen){
116                     this.current_screen.close();
117                     this.current_screen.hide();
118                 }
119                 this.current_screen = screen;
120                 this.current_screen.show();
121             }
122         },
123         get_current_screen: function(){
124             return this.pos.get_order().get_screen_data('screen') || this.default_screen;
125         },
126         back: function(){
127             var previous = this.pos.get_order().get_screen_data('previous-screen');
128             if(previous){
129                 this.set_current_screen(previous);
130             }
131         },
132         get_current_screen_param: function(param){
133             var params = this.pos.get_order().get_screen_data('params');
134             return params ? params[param] : undefined;
135         },
136         set_default_screen: function(){
137             this.set_current_screen(this.default_screen);
138         },
139         change_default_screen: function(screen){ 
140             this.default_screen = screen;
141         },
142     });
143
144     module.ScreenWidget = module.PosBaseWidget.extend({
145
146         show_numpad:     true,  
147         show_leftpane:   true,
148
149         init: function(parent,options){
150             this._super(parent,options);
151             this.hidden = false;
152         },
153
154         help_button_action: function(){
155             this.pos_widget.screen_selector.show_popup('help');
156         },
157
158         barcode_product_screen:         'products',     //if defined, this screen will be loaded when a product is scanned
159
160         hotkeys_handlers: {},
161
162         // what happens when a product is scanned : 
163         // it will add the product to the order and go to barcode_product_screen. 
164         barcode_product_action: function(code){
165             var self = this;
166             if(self.pos.scan_product(code)){
167                 if(self.barcode_product_screen){ 
168                     self.pos_widget.screen_selector.set_current_screen(self.barcode_product_screen);
169                 }
170             }else{
171                 self.pos_widget.screen_selector.show_popup('error-barcode',code.code);
172             }
173         },
174
175         // what happens when a cashier id barcode is scanned.
176         // the default behavior is the following : 
177         // - if there's a user with a matching barcode, put it as the active 'cashier', go to cashier mode, and return true
178         // - else : do nothing and return false. You probably want to extend this to show and appropriate error popup... 
179         barcode_cashier_action: function(code){
180             var users = this.pos.users;
181             for(var i = 0, len = users.length; i < len; i++){
182                 if(users[i].barcode === code.code){
183                     this.pos.set_cashier(users[i]);
184                     this.pos_widget.username.renderElement();
185                     return true;
186                 }
187             }
188             this.pos_widget.screen_selector.show_popup('error-barcode',code.code);
189             return false;
190         },
191         
192         // what happens when a client id barcode is scanned.
193         // the default behavior is the following : 
194         // - if there's a user with a matching barcode, put it as the active 'client' and return true
195         // - else : return false. 
196         barcode_client_action: function(code){
197             var partner = this.pos.db.get_partner_by_barcode(code.code);
198             if(partner){
199                 this.pos.get_order().set_client(partner);
200                 return true;
201             }
202             this.pos_widget.screen_selector.show_popup('error-barcode',code.code);
203             return false;
204         },
205         
206         // what happens when a discount barcode is scanned : the default behavior
207         // is to set the discount on the last order.
208         barcode_discount_action: function(code){
209             var last_orderline = this.pos.get_order().get_last_orderline();
210             if(last_orderline){
211                 last_orderline.set_discount(code.value)
212             }
213         },
214         // What happens when an invalid barcode is scanned : shows an error popup.
215         barcode_error_action: function(code){
216             this.pos_widget.screen_selector.show_popup('error-barcode',code.code);
217         },
218
219         // this method shows the screen and sets up all the widget related to this screen. Extend this method
220         // if you want to alter the behavior of the screen.
221         show: function(){
222             var self = this;
223
224             this.hidden = false;
225             if(this.$el){
226                 this.$el.removeClass('oe_hidden');
227             }
228
229             var self = this;
230
231             this.pos_widget.set_numpad_visible(this.show_numpad);
232             this.pos_widget.set_leftpane_visible(this.show_leftpane);
233
234             this.pos_widget.username.set_user_mode(this.pos_widget.screen_selector.get_user_mode());
235             this.pos.barcode_reader.set_action_callback({
236                 'cashier': self.barcode_cashier_action ? function(code){ self.barcode_cashier_action(code); } : undefined ,
237                 'product': self.barcode_product_action ? function(code){ self.barcode_product_action(code); } : undefined ,
238                 'client' : self.barcode_client_action ?  function(code){ self.barcode_client_action(code);  } : undefined ,
239                 'discount': self.barcode_discount_action ? function(code){ self.barcode_discount_action(code); } : undefined,
240                 'error'   : self.barcode_error_action ?  function(code){ self.barcode_error_action(code);   } : undefined,
241             });
242         },
243
244         // this method is called when the screen is closed to make place for a new screen. this is a good place
245         // to put your cleanup stuff as it is guaranteed that for each show() there is one and only one close()
246         close: function(){
247             if(this.pos.barcode_reader){
248                 this.pos.barcode_reader.reset_action_callbacks();
249             }
250         },
251
252         // this methods hides the screen. It's not a good place to put your cleanup stuff as it is called on the
253         // POS initialization.
254         hide: function(){
255             this.hidden = true;
256             if(this.$el){
257                 this.$el.addClass('oe_hidden');
258             }
259         },
260
261         // we need this because some screens re-render themselves when they are hidden
262         // (due to some events, or magic, or both...)  we must make sure they remain hidden.
263         // the good solution would probably be to make them not re-render themselves when they
264         // are hidden. 
265         renderElement: function(){
266             this._super();
267             if(this.hidden){
268                 if(this.$el){
269                     this.$el.addClass('oe_hidden');
270                 }
271             }
272         },
273     });
274
275     module.PopUpWidget = module.PosBaseWidget.extend({
276         show: function(){
277             if(this.$el){
278                 this.$el.removeClass('oe_hidden');
279             }
280         },
281         /* called before hide, when a popup is closed */
282         close: function(){
283         },
284         /* hides the popup. keep in mind that this is called in the initialization pass of the 
285          * pos instantiation, so you don't want to do anything fancy in here */
286         hide: function(){
287             if(this.$el){
288                 this.$el.addClass('oe_hidden');
289             }
290         },
291     });
292
293     module.FullscreenPopup = module.PopUpWidget.extend({
294         template:'FullscreenPopupWidget',
295         show: function(){
296             var self = this;
297             this._super();
298             this.renderElement();
299             this.$('.button.fullscreen').off('click').click(function(){
300                 window.document.body.webkitRequestFullscreen();
301                 self.pos_widget.screen_selector.close_popup();
302             });
303             this.$('.button.cancel').off('click').click(function(){
304                 self.pos_widget.screen_selector.close_popup();
305             });
306         },
307         ismobile: function(){
308             return typeof window.orientation !== 'undefined'; 
309         }
310     });
311
312
313     module.ErrorPopupWidget = module.PopUpWidget.extend({
314         template:'ErrorPopupWidget',
315         show: function(options){
316             options = options || {};
317             var self = this;
318             this._super();
319
320             $('body').append('<audio src="/point_of_sale/static/src/sounds/error.wav" autoplay="true"></audio>');
321
322             this.message = options.message || _t('Error');
323             this.comment = options.comment || '';
324
325             this.renderElement();
326
327             this.pos.barcode_reader.save_callbacks();
328             this.pos.barcode_reader.reset_action_callbacks();
329
330             this.$('.footer .button').click(function(){
331                 self.pos_widget.screen_selector.close_popup();
332                 if ( options.confirm ) {
333                     options.confirm.call(self);
334                 }
335             });
336         },
337         close:function(){
338             this._super();
339             this.pos.barcode_reader.restore_callbacks();
340         },
341     });
342
343     module.ErrorTracebackPopupWidget = module.ErrorPopupWidget.extend({
344         template:'ErrorTracebackPopupWidget',
345     });
346
347     module.ErrorBarcodePopupWidget = module.ErrorPopupWidget.extend({
348         template:'ErrorBarcodePopupWidget',
349         show: function(barcode){
350             this.barcode = barcode;
351             this._super();
352         },
353     });
354
355     module.ConfirmPopupWidget = module.PopUpWidget.extend({
356         template: 'ConfirmPopupWidget',
357         show: function(options){
358             options = options || {};
359             var self = this;
360             this._super();
361
362             this.message = options.message || '';
363             this.comment = options.comment || '';
364             this.renderElement();
365             
366             this.$('.button.cancel').click(function(){
367                 self.pos_widget.screen_selector.close_popup();
368                 if( options.cancel ){
369                     options.cancel.call(self);
370                 }
371             });
372
373             this.$('.button.confirm').click(function(){
374                 self.pos_widget.screen_selector.close_popup();
375                 if( options.confirm ){
376                     options.confirm.call(self);
377                 }
378             });
379         },
380     });
381
382     /**
383      * A popup that allows the user to select one item from a list. 
384      *
385      * show_popup('selection',{
386      *  message: 'Pick an Option',
387      *      message: "Popup Title",
388      *      list: [
389      *          { label: 'foobar',  item: 45 },
390      *          { label: 'bar foo', item: 'stuff' },
391      *      ],
392      *      confirm: function(item) {
393      *          // get the item selected by the user.
394      *      },
395      *      cancel: function(){
396      *          // user chose nothing
397      *      }
398      *  });
399      */
400
401     module.SelectionPopupWidget = module.PopUpWidget.extend({
402         template: 'SelectionPopupWidget',
403         show: function(options){
404             options = options || {};
405             var self = this;
406             this._super();
407
408             this.message = options.message || '';
409             this.list    = options.list    || [];
410             this.renderElement();
411
412             this.$('.button.cancel').click(function(){
413                 self.pos_widget.screen_selector.close_popup();
414                 if (options.cancel){
415                     options.cancel.call(self);
416                 }
417             });
418
419             this.$('.selection-item').click(function(){
420                 self.pos_widget.screen_selector.close_popup();
421                 if (options.confirm) {
422                     var item = self.list[parseInt($(this).data('item-index'))];
423                     item = item ? item.item : item;
424                     options.confirm.call(self,item);
425                 }
426             });
427         },
428     });
429
430     module.TextInputPopupWidget = module.PopUpWidget.extend({
431         template: 'TextInputPopupWidget',
432         show: function(options){
433             options = options || {};
434             var self = this;
435             this._super();
436
437             this.message = options.message || '';
438             this.comment = options.comment || '';
439             this.value   = options.value   || '';
440             this.renderElement();
441             this.$('input,textarea').focus();
442             
443             this.$('.button.cancel').click(function(){
444                 self.pos_widget.screen_selector.close_popup();
445                 if( options.cancel ){
446                     options.cancel.call(self);
447                 }
448             });
449
450             this.$('.button.confirm').click(function(){
451                 self.pos_widget.screen_selector.close_popup();
452                 var value = self.$('input,textarea').val();
453                 if( options.confirm ){
454                     options.confirm.call(self,value);
455                 }
456             });
457         },
458     });
459
460     module.TextAreaPopupWidget = module.TextInputPopupWidget.extend({
461         template: 'TextAreaPopupWidget',
462     });
463
464     module.NumberPopupWidget = module.PopUpWidget.extend({
465         template: 'NumberPopupWidget',
466         click_numpad_button: function($el,event){
467             this.numpad_input($el.data('action'));
468         },
469         numpad_input: function(input) { //FIXME -> Deduplicate code
470             var oldbuf = this.inputbuffer.slice(0);
471
472             if (input === '.') {
473                 if (this.firstinput) {
474                     this.inputbuffer = "0.";
475                 }else if (!this.inputbuffer.length || this.inputbuffer === '-') {
476                     this.inputbuffer += "0.";
477                 } else if (this.inputbuffer.indexOf('.') < 0){
478                     this.inputbuffer = this.inputbuffer + '.';
479                 }
480             } else if (input === 'CLEAR') {
481                 this.inputbuffer = ""; 
482             } else if (input === 'BACKSPACE') { 
483                 this.inputbuffer = this.inputbuffer.substring(0,this.inputbuffer.length - 1);
484             } else if (input === '+') {
485                 if ( this.inputbuffer[0] === '-' ) {
486                     this.inputbuffer = this.inputbuffer.substring(1,this.inputbuffer.length);
487                 }
488             } else if (input === '-') {
489                 if ( this.inputbuffer[0] === '-' ) {
490                     this.inputbuffer = this.inputbuffer.substring(1,this.inputbuffer.length);
491                 } else {
492                     this.inputbuffer = '-' + this.inputbuffer;
493                 }
494             } else if (input[0] === '+' && !isNaN(parseFloat(input))) {
495                 this.inputbuffer = '' + ((parseFloat(this.inputbuffer) || 0) + parseFloat(input));
496             } else if (!isNaN(parseInt(input))) {
497                 if (this.firstinput) {
498                     this.inputbuffer = '' + input;
499                 } else {
500                     this.inputbuffer += input;
501                 }
502             }
503
504             this.firstinput = this.inputbuffer.length === 0;
505
506             if (this.inputbuffer !== oldbuf) {
507                 this.$('.value').text(this.inputbuffer);
508             }
509         },
510         show: function(options){
511             options = options || {};
512             var self = this;
513             this._super();
514
515             this.message = options.message || '';
516             this.comment = options.comment || '';
517             this.inputbuffer = options.value   || '';
518             this.renderElement();
519             this.firstinput = true;
520             
521             this.$('.input-button,.mode-button').click(function(event){
522                 self.click_numpad_button($(this),event);
523             });
524             this.$('.button.cancel').click(function(){
525                 self.pos_widget.screen_selector.close_popup();
526                 if( options.cancel ){
527                     options.cancel.call(self);
528                 }
529             });
530
531             this.$('.button.confirm').click(function(){
532                 self.pos_widget.screen_selector.close_popup();
533                 if( options.confirm ){
534                     options.confirm.call(self,self.inputbuffer);
535                 }
536             });
537         },
538     });
539
540     module.PasswordPopupWidget = module.NumberPopupWidget.extend({
541         renderElement: function(){
542             this._super();
543             this.$('.popup').addClass('popup-password');    // HELLO HACK !
544         },
545     });
546
547     module.ErrorNoClientPopupWidget = module.ErrorPopupWidget.extend({
548         template: 'ErrorNoClientPopupWidget',
549     });
550
551     module.ErrorInvoiceTransferPopupWidget = module.ErrorPopupWidget.extend({
552         template: 'ErrorInvoiceTransferPopupWidget',
553     });
554
555     module.UnsentOrdersPopupWidget = module.ConfirmPopupWidget.extend({
556         template: 'UnsentOrdersPopupWidget',
557     });
558
559     module.UnpaidOrdersPopupWidget = module.ConfirmPopupWidget.extend({
560         template: 'UnpaidOrdersPopupWidget',
561     });
562
563     module.ScaleScreenWidget = module.ScreenWidget.extend({
564         template:'ScaleScreenWidget',
565
566         next_screen: 'products',
567         previous_screen: 'products',
568
569         show_leftpane:   false,
570
571         show: function(){
572             this._super();
573             var self = this;
574             var queue = this.pos.proxy_queue;
575
576             this.set_weight(0);
577             this.renderElement();
578
579             this.hotkey_handler = function(event){
580                 if(event.which === 13){
581                     self.order_product();
582                     self.pos_widget.screen_selector.set_current_screen(self.next_screen);
583                 }else if(event.which === 27){
584                     self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
585                 }
586             };
587
588             $('body').on('keyup',this.hotkey_handler);
589
590             this.$('.back').click(function(){
591                 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
592             });
593
594             this.$('.next,.buy-product').click(function(){
595                 self.order_product();
596                 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
597             });
598
599             queue.schedule(function(){
600                 return self.pos.proxy.scale_read().then(function(weight){
601                     self.set_weight(weight.weight);
602                 });
603             },{duration:50, repeat: true});
604
605         },
606         get_product: function(){
607             var ss = this.pos_widget.screen_selector;
608             if(ss){
609                 return ss.get_current_screen_param('product');
610             }else{
611                 return undefined;
612             }
613         },
614         order_product: function(){
615             this.pos.get_order().add_product(this.get_product(),{ quantity: this.weight });
616         },
617         get_product_name: function(){
618             var product = this.get_product();
619             return (product ? product.display_name : undefined) || 'Unnamed Product';
620         },
621         get_product_price: function(){
622             var product = this.get_product();
623             return (product ? product.price : 0) || 0;
624         },
625         set_weight: function(weight){
626             this.weight = weight;
627             this.$('.weight').text(this.get_product_weight_string());
628             this.$('.computed-price').text(this.get_computed_price_string());
629         },
630         get_product_weight_string: function(){
631             var product = this.get_product();
632             var defaultstr = (this.weight || 0).toFixed(3) + ' Kg';
633             if(!product || !this.pos){
634                 return defaultstr;
635             }
636             var unit_id = product.uom_id;
637             if(!unit_id){
638                 return defaultstr;
639             }
640             var unit = this.pos.units_by_id[unit_id[0]];
641             var weight = round_pr(this.weight || 0, unit.rounding);
642             var weightstr = weight.toFixed(Math.ceil(Math.log(1.0/unit.rounding) / Math.log(10) ));
643                 weightstr += ' Kg';
644             return weightstr;
645         },
646         get_computed_price_string: function(){
647             return this.format_currency(this.get_product_price() * this.weight);
648         },
649         close: function(){
650             var self = this;
651             this._super();
652             $('body').off('keyup',this.hotkey_handler);
653
654             this.pos.proxy_queue.clear();
655         },
656     });
657
658     module.ProductScreenWidget = module.ScreenWidget.extend({
659         template:'ProductScreenWidget',
660
661         show_numpad:     true,
662         show_leftpane:   true,
663
664         start: function(){ //FIXME this should work as renderElement... but then the categories aren't properly set. explore why
665             var self = this;
666
667             this.product_list_widget = new module.ProductListWidget(this,{
668                 click_product_action: function(product){
669                     if(product.to_weight && self.pos.config.iface_electronic_scale){
670                         self.pos_widget.screen_selector.set_current_screen('scale',{product: product});
671                     }else{
672                         self.pos.get_order().add_product(product);
673                     }
674                 },
675                 product_list: this.pos.db.get_product_by_category(0)
676             });
677             this.product_list_widget.replace(this.$('.placeholder-ProductListWidget'));
678
679             this.product_categories_widget = new module.ProductCategoriesWidget(this,{
680                 product_list_widget: this.product_list_widget,
681             });
682             this.product_categories_widget.replace(this.$('.placeholder-ProductCategoriesWidget'));
683         },
684
685         show: function(){
686             this._super();
687             var self = this;
688
689             this.product_categories_widget.reset_category();
690
691             this.pos_widget.order_widget.set_editable(true);
692         },
693
694         close: function(){
695             this._super();
696
697             this.pos_widget.order_widget.set_editable(false);
698
699             if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
700                 this.pos_widget.onscreen_keyboard.hide();
701             }
702         },
703     });
704
705     module.ClientListScreenWidget = module.ScreenWidget.extend({
706         template: 'ClientListScreenWidget',
707
708         init: function(parent, options){
709             this._super(parent, options);
710             this.partner_cache = new module.DomCache();
711         },
712
713         show_leftpane: false,
714
715         auto_back: true,
716
717         show: function(){
718             var self = this;
719             this._super();
720
721             this.renderElement();
722             this.details_visible = false;
723             this.old_client = this.pos.get_order().get_client()
724             this.new_client = this.old_client;
725
726             this.$('.back').click(function(){
727                 self.pos_widget.screen_selector.back();
728             });
729
730             this.$('.next').click(function(){
731                 self.save_changes();
732                 self.pos_widget.screen_selector.back();
733             });
734
735             this.$('.new-customer').click(function(){
736                 self.display_client_details('edit',{
737                     'country_id': self.pos.company.country_id,
738                 });
739             });
740
741             var partners = this.pos.db.get_partners_sorted(1000);
742             this.render_list(partners);
743             
744             this.reload_partners();
745
746             if( this.old_client ){
747                 this.display_client_details('show',this.old_client,0);
748             }
749
750             this.$('.client-list-contents').delegate('.client-line','click',function(event){
751                 self.line_select(event,$(this),parseInt($(this).data('id')));
752             });
753
754             var search_timeout = null;
755
756             if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
757                 this.pos_widget.onscreen_keyboard.connect(this.$('.searchbox input'));
758             }
759
760             this.$('.searchbox input').on('keyup',function(event){
761                 clearTimeout(search_timeout);
762
763                 var query = this.value;
764
765                 search_timeout = setTimeout(function(){
766                     self.perform_search(query,event.which === 13);
767                 },70);
768             });
769
770             this.$('.searchbox .search-clear').click(function(){
771                 self.clear_search();
772             });
773         },
774         barcode_client_action: function(code){
775             if (this.editing_client) {
776                 this.$('.detail.barcode').val(code.code);
777             } else if (this.pos.db.get_partner_by_barcode(code.code)) {
778                 this.display_client_details('show',this.pos.db.get_partner_by_barcode(code.code));
779             }
780         },
781         perform_search: function(query, associate_result){
782             if(query){
783                 var customers = this.pos.db.search_partner(query);
784                 this.display_client_details('hide');
785                 if ( associate_result && customers.length === 1){
786                     this.new_client = customers[0];
787                     this.save_changes();
788                     this.pos_widget.screen_selector.back();
789                 }
790                 this.render_list(customers);
791             }else{
792                 var customers = this.pos.db.get_partners_sorted();
793                 this.render_list(customers);
794             }
795         },
796         clear_search: function(){
797             var customers = this.pos.db.get_partners_sorted(1000);
798             this.render_list(customers);
799             this.$('.searchbox input')[0].value = '';
800             this.$('.searchbox input').focus();
801         },
802         render_list: function(partners){
803             var contents = this.$el[0].querySelector('.client-list-contents');
804             contents.innerHTML = "";
805             for(var i = 0, len = Math.min(partners.length,1000); i < len; i++){
806                 var partner    = partners[i];
807                 var clientline = this.partner_cache.get_node(partner.id);
808                 if(!clientline){
809                     var clientline_html = QWeb.render('ClientLine',{widget: this, partner:partners[i]});
810                     var clientline = document.createElement('tbody');
811                     clientline.innerHTML = clientline_html;
812                     clientline = clientline.childNodes[1];
813                     this.partner_cache.cache_node(partner.id,clientline);
814                 }
815                 if( partners === this.new_client ){
816                     clientline.classList.add('highlight');
817                 }else{
818                     clientline.classList.remove('highlight');
819                 }
820                 contents.appendChild(clientline);
821             }
822         },
823         save_changes: function(){
824             if( this.has_client_changed() ){
825                 this.pos.get_order().set_client(this.new_client);
826             }
827         },
828         has_client_changed: function(){
829             if( this.old_client && this.new_client ){
830                 return this.old_client.id !== this.new_client.id;
831             }else{
832                 return !!this.old_client !== !!this.new_client;
833             }
834         },
835         toggle_save_button: function(){
836             var $button = this.$('.button.next');
837             if (this.editing_client) {
838                 $button.addClass('oe_hidden');
839                 return;
840             } else if( this.new_client ){
841                 if( !this.old_client){
842                     $button.text(_t('Set Customer'));
843                 }else{
844                     $button.text(_t('Change Customer'));
845                 }
846             }else{
847                 $button.text(_t('Deselect Customer'));
848             }
849             $button.toggleClass('oe_hidden',!this.has_client_changed());
850         },
851         line_select: function(event,$line,id){
852             var partner = this.pos.db.get_partner_by_id(id);
853             this.$('.client-list .lowlight').removeClass('lowlight');
854             if ( $line.hasClass('highlight') ){
855                 $line.removeClass('highlight');
856                 $line.addClass('lowlight');
857                 this.display_client_details('hide',partner);
858                 this.new_client = null;
859                 this.toggle_save_button();
860             }else{
861                 this.$('.client-list .highlight').removeClass('highlight');
862                 $line.addClass('highlight');
863                 var y = event.pageY - $line.parent().offset().top
864                 this.display_client_details('show',partner,y);
865                 this.new_client = partner;
866                 this.toggle_save_button();
867             }
868         },
869         partner_icon_url: function(id){
870             return '/web/binary/image?model=res.partner&id='+id+'&field=image_small';
871         },
872
873         // ui handle for the 'edit selected customer' action
874         edit_client_details: function(partner) {
875             this.display_client_details('edit',partner);
876         },
877
878         // ui handle for the 'cancel customer edit changes' action
879         undo_client_details: function(partner) {
880             if (!partner.id) {
881                 this.display_client_details('hide');
882             } else {
883                 this.display_client_details('show',partner);
884             }
885         },
886
887         // what happens when we save the changes on the client edit form -> we fetch the fields, sanitize them,
888         // send them to the backend for update, and call saved_client_details() when the server tells us the
889         // save was successfull.
890         save_client_details: function(partner) {
891             var self = this;
892             
893             var fields = {}
894             this.$('.client-details-contents .detail').each(function(idx,el){
895                 fields[el.name] = el.value;
896             });
897
898             if (!fields.name) {
899                 this.pos_widget.screen_selector.show_popup('error',{
900                     message: _t('A Customer Name Is Required'),
901                 });
902                 return;
903             }
904             
905             if (this.uploaded_picture) {
906                 fields.image = this.uploaded_picture;
907             }
908
909             fields.id           = partner.id || false;
910             fields.country_id   = fields.country_id || false;
911             fields.barcode        = fields.barcode ? this.pos.barcode_reader.sanitize_ean(fields.barcode) : false; 
912
913             new instance.web.Model('res.partner').call('create_from_ui',[fields]).then(function(partner_id){
914                 self.saved_client_details(partner_id);
915             },function(err,event){
916                 event.preventDefault();
917                 self.pos_widget.screen_selector.show_popup('error',{
918                     'message':_t('Error: Could not Save Changes'),
919                     'comment':_t('Your Internet connection is probably down.'),
920                 });
921             });
922         },
923         
924         // what happens when we've just pushed modifications for a partner of id partner_id
925         saved_client_details: function(partner_id){
926             var self = this;
927             this.reload_partners().then(function(){
928                 var partner = self.pos.db.get_partner_by_id(partner_id);
929                 if (partner) {
930                     self.new_client = partner;
931                     self.toggle_save_button();
932                     self.display_client_details('show',partner);
933                 } else {
934                     // should never happen, because create_from_ui must return the id of the partner it
935                     // has created, and reload_partner() must have loaded the newly created partner. 
936                     self.display_client_details('hide');
937                 }
938             });
939         },
940
941         // resizes an image, keeping the aspect ratio intact,
942         // the resize is useful to avoid sending 12Mpixels jpegs
943         // over a wireless connection.
944         resize_image_to_dataurl: function(img, maxwidth, maxheight, callback){
945             img.onload = function(){
946                 var png = new Image();
947                 var canvas = document.createElement('canvas');
948                 var ctx    = canvas.getContext('2d');
949                 var ratio  = 1;
950
951                 if (img.width > maxwidth) {
952                     ratio = maxwidth / img.width;
953                 }
954                 if (img.height * ratio > maxheight) {
955                     ratio = maxheight / img.height;
956                 }
957                 var width  = Math.floor(img.width * ratio);
958                 var height = Math.floor(img.height * ratio);
959
960                 canvas.width  = width;
961                 canvas.height = height;
962                 ctx.drawImage(img,0,0,width,height);
963
964                 var dataurl = canvas.toDataURL();
965                 callback(dataurl);
966             }
967         },
968
969         // Loads and resizes a File that contains an image.
970         // callback gets a dataurl in case of success.
971         load_image_file: function(file, callback){
972             var self = this;
973             if (!file.type.match(/image.*/)) {
974                 this.pos_widget.screen_selector.show_popup('error',{
975                     message:_t('Unsupported File Format'),
976                     comment:_t('Only web-compatible Image formats such as .png or .jpeg are supported'),
977                 });
978                 return;
979             }
980             
981             var reader = new FileReader();
982             reader.onload = function(event){
983                 var dataurl = event.target.result;
984                 var img     = new Image();
985                 img.src = dataurl;
986                 self.resize_image_to_dataurl(img,800,600,callback);
987             }
988             reader.onerror = function(){
989                 self.pos_widget.screen_selector.show_popup('error',{
990                     message:_t('Could Not Read Image'),
991                     comment:_t('The provided file could not be read due to an unknown error'),
992                 });
993             };
994             reader.readAsDataURL(file);
995         },
996
997         // This fetches partner changes on the server, and in case of changes, 
998         // rerenders the affected views
999         reload_partners: function(){
1000             var self = this;
1001             return this.pos.load_new_partners().then(function(){
1002                 self.render_list(self.pos.db.get_partners_sorted(1000));
1003                 
1004                 // update the currently assigned client if it has been changed in db.
1005                 var curr_client = self.pos.get_order().get_client();
1006                 if (curr_client) {
1007                     self.pos.get_order().set_client(self.pos.db.get_partner_by_id(curr_client.id));
1008                 }
1009             });
1010         },
1011
1012         // Shows,hides or edit the customer details box :
1013         // visibility: 'show', 'hide' or 'edit'
1014         // partner:    the partner object to show or edit
1015         // clickpos:   the height of the click on the list (in pixel), used
1016         //             to maintain consistent scroll.
1017         display_client_details: function(visibility,partner,clickpos){
1018             var self = this;
1019             var contents = this.$('.client-details-contents');
1020             var parent   = this.$('.client-list').parent();
1021             var scroll   = parent.scrollTop();
1022             var height   = contents.height();
1023
1024             contents.off('click','.button.edit'); 
1025             contents.off('click','.button.save'); 
1026             contents.off('click','.button.undo'); 
1027             contents.on('click','.button.edit',function(){ self.edit_client_details(partner); });
1028             contents.on('click','.button.save',function(){ self.save_client_details(partner); });
1029             contents.on('click','.button.undo',function(){ self.undo_client_details(partner); });
1030             this.editing_client = false;
1031             this.uploaded_picture = null;
1032
1033             if(visibility === 'show'){
1034                 contents.empty();
1035                 contents.append($(QWeb.render('ClientDetails',{widget:this,partner:partner})));
1036
1037                 var new_height   = contents.height();
1038
1039                 if(!this.details_visible){
1040                     if(clickpos < scroll + new_height + 20 ){
1041                         parent.scrollTop( clickpos - 20 );
1042                     }else{
1043                         parent.scrollTop(parent.scrollTop() + new_height);
1044                     }
1045                 }else{
1046                     parent.scrollTop(parent.scrollTop() - height + new_height);
1047                 }
1048
1049                 this.details_visible = true;
1050                 this.toggle_save_button();
1051             } else if (visibility === 'edit') {
1052                 this.editing_client = true;
1053                 contents.empty();
1054                 contents.append($(QWeb.render('ClientDetailsEdit',{widget:this,partner:partner})));
1055                 this.toggle_save_button();
1056
1057                 contents.find('.image-uploader').on('change',function(){
1058                     self.load_image_file(event.target.files[0],function(res){
1059                         if (res) {
1060                             contents.find('.client-picture img, .client-picture .fa').remove();
1061                             contents.find('.client-picture').append("<img src='"+res+"'>");
1062                             contents.find('.detail.picture').remove();
1063                             self.uploaded_picture = res;
1064                         }
1065                     });
1066                 });
1067             } else if (visibility === 'hide') {
1068                 contents.empty();
1069                 if( height > scroll ){
1070                     contents.css({height:height+'px'});
1071                     contents.animate({height:0},400,function(){
1072                         contents.css({height:''});
1073                     });
1074                 }else{
1075                     parent.scrollTop( parent.scrollTop() - height);
1076                 }
1077                 this.details_visible = false;
1078                 this.toggle_save_button();
1079             }
1080         },
1081         close: function(){
1082             this._super();
1083         },
1084     });
1085
1086     module.ReceiptScreenWidget = module.ScreenWidget.extend({
1087         template: 'ReceiptScreenWidget',
1088         show_numpad:     false,
1089         show_leftpane:   false,
1090
1091         show: function(){
1092             this._super();
1093             var self = this;
1094
1095             this.refresh();
1096
1097             if (!this.pos.get_order()._printed && this.pos.config.iface_print_auto) {
1098                 this.print();
1099             }
1100
1101             // The problem is that in chrome the print() is asynchronous and doesn't
1102             // execute until all rpc are finished. So it conflicts with the rpc used
1103             // to send the orders to the backend, and the user is able to go to the next 
1104             // screen before the printing dialog is opened. The problem is that what's 
1105             // printed is whatever is in the page when the dialog is opened and not when it's called,
1106             // and so you end up printing the product list instead of the receipt... 
1107             //
1108             // Fixing this would need a re-architecturing
1109             // of the code to postpone sending of orders after printing.
1110             //
1111             // But since the print dialog also blocks the other asynchronous calls, the
1112             // button enabling in the setTimeout() is blocked until the printing dialog is 
1113             // closed. But the timeout has to be big enough or else it doesn't work
1114             // 2 seconds is the same as the default timeout for sending orders and so the dialog
1115             // should have appeared before the timeout... so yeah that's not ultra reliable. 
1116
1117             this.lock_screen(true);
1118             setTimeout(function(){
1119                 self.lock_screen(false);
1120             }, 2000);
1121         },
1122         lock_screen: function(locked) {
1123             this._locked = locked;
1124             if (locked) {
1125                 this.$('.next').removeClass('highlight');
1126             } else {
1127                 this.$('.next').addClass('highlight');
1128             }
1129         },
1130         print: function() {
1131             this.pos.get_order()._printed = true;
1132             window.print();
1133         },
1134         finish_order: function() {
1135             if (!this._locked) {
1136                 this.pos.get_order().finalize();
1137             }
1138         },
1139         renderElement: function() {
1140             var self = this;
1141             this._super();
1142             this.$('.next').click(function(){
1143                 self.finish_order();
1144             });
1145             this.$('.button.print').click(function(){
1146                 self.print();
1147             });
1148         },
1149         refresh: function() {
1150             var order = this.pos.get_order();
1151             this.$('.pos-receipt-container').html(QWeb.render('PosTicket',{
1152                     widget:this,
1153                     order: order,
1154                     receipt: order.export_for_printing(),
1155                     orderlines: order.get_orderlines(),
1156                     paymentlines: order.get_paymentlines(),
1157                 }));
1158         },
1159     });
1160
1161     module.PaymentScreenWidget = module.ScreenWidget.extend({
1162         template:      'PaymentScreenWidget',
1163         back_screen:   'product',
1164         next_screen:   'receipt',
1165         show_leftpane: false,
1166         show_numpad:   false,
1167         init: function(parent, options) {
1168             var self = this;
1169             this._super(parent, options);
1170
1171             this.pos.bind('change:selectedOrder',function(){
1172                     this.renderElement();
1173                     this.watch_order_changes();
1174                 },this);
1175             this.watch_order_changes();
1176
1177             this.inputbuffer = "";
1178             this.firstinput  = true;
1179             this.keyboard_handler = function(event){
1180                 var key = '';
1181                 if ( event.keyCode === 13 ) {         // Enter
1182                     self.validate_order();
1183                 } else if ( event.keyCode === 190 ) { // Dot
1184                     key = '.';
1185                 } else if ( event.keyCode === 46 ) {  // Delete
1186                     key = 'CLEAR';
1187                 } else if ( event.keyCode === 8 ) {   // Backspace 
1188                     key = 'BACKSPACE';
1189                     event.preventDefault(); // Prevents history back nav
1190                 } else if ( event.keyCode >= 48 && event.keyCode <= 57 ){       // Numbers
1191                     key = '' + (event.keyCode - 48);
1192                 } else if ( event.keyCode >= 96 && event.keyCode <= 105 ){      // Numpad Numbers
1193                     key = '' + (event.keyCode - 96);
1194                 } else if ( event.keyCode === 189 || event.keyCode === 109 ) {  // Minus
1195                     key = '-';
1196                 } else if ( event.keyCode === 107 ) { // Plus
1197                     key = '+';
1198                 }
1199
1200                 self.payment_input(key);
1201
1202             };
1203         },
1204         // resets the current input buffer
1205         reset_input: function(){
1206             var line = this.pos.get_order().selected_paymentline;
1207             this.firstinput  = true;
1208             if (line) {
1209                 this.inputbuffer = this.format_currency_no_symbol(line.get_amount());
1210             } else {
1211                 this.inputbuffer = "";
1212             }
1213         },
1214         // handle both keyboard and numpad input. Accepts
1215         // a string that represents the key pressed.
1216         payment_input: function(input) {
1217             var oldbuf = this.inputbuffer.slice(0);
1218
1219             if (input === '.') {
1220                 if (this.firstinput) {
1221                     this.inputbuffer = "0.";
1222                 }else if (!this.inputbuffer.length || this.inputbuffer === '-') {
1223                     this.inputbuffer += "0.";
1224                 } else if (this.inputbuffer.indexOf('.') < 0){
1225                     this.inputbuffer = this.inputbuffer + '.';
1226                 }
1227             } else if (input === 'CLEAR') {
1228                 this.inputbuffer = ""; 
1229             } else if (input === 'BACKSPACE') { 
1230                 this.inputbuffer = this.inputbuffer.substring(0,this.inputbuffer.length - 1);
1231             } else if (input === '+') {
1232                 if ( this.inputbuffer[0] === '-' ) {
1233                     this.inputbuffer = this.inputbuffer.substring(1,this.inputbuffer.length);
1234                 }
1235             } else if (input === '-') {
1236                 if ( this.inputbuffer[0] === '-' ) {
1237                     this.inputbuffer = this.inputbuffer.substring(1,this.inputbuffer.length);
1238                 } else {
1239                     this.inputbuffer = '-' + this.inputbuffer;
1240                 }
1241             } else if (input[0] === '+' && !isNaN(parseFloat(input))) {
1242                 this.inputbuffer = '' + ((parseFloat(this.inputbuffer) || 0) + parseFloat(input));
1243             } else if (!isNaN(parseInt(input))) {
1244                 if (this.firstinput) {
1245                     this.inputbuffer = '' + input;
1246                 } else {
1247                     this.inputbuffer += input;
1248                 }
1249             }
1250
1251             this.firstinput = this.inputbuffer.length === 0;
1252
1253             if (this.inputbuffer !== oldbuf) {
1254                 var order = this.pos.get_order();
1255                 if (order.selected_paymentline) {
1256                     order.selected_paymentline.set_amount(parseFloat(this.inputbuffer));
1257                     this.order_changes();
1258                     this.render_paymentlines();
1259                     this.$('.paymentline.selected .edit').text(this.inputbuffer);
1260                 }
1261             }
1262         },
1263         click_numpad: function(button) {
1264             this.payment_input(button.data('action'));
1265         },
1266         render_numpad: function() {
1267             var self = this;
1268             var numpad = $(QWeb.render('PaymentScreen-Numpad', { widget:this }));
1269             numpad.on('click','button',function(){
1270                 self.click_numpad($(this));
1271             });
1272             return numpad;
1273         },
1274         click_delete_paymentline: function(cid){
1275             var lines = this.pos.get_order().get_paymentlines();
1276             for ( var i = 0; i < lines.length; i++ ) {
1277                 if (lines[i].cid === cid) {
1278                     this.pos.get_order().remove_paymentline(lines[i]);
1279                     this.reset_input();
1280                     this.render_paymentlines();
1281                     return;
1282                 }
1283             }
1284         },
1285         click_paymentline: function(cid){
1286             var lines = this.pos.get_order().get_paymentlines();
1287             for ( var i = 0; i < lines.length; i++ ) {
1288                 if (lines[i].cid === cid) {
1289                     this.pos.get_order().select_paymentline(lines[i]);
1290                     this.reset_input();
1291                     this.render_paymentlines();
1292                     return;
1293                 }
1294             }
1295         },
1296         render_paymentlines: function() {
1297             var self  = this;
1298             var order = this.pos.get_order();
1299             if (!order) {
1300                 return;
1301             }
1302
1303             var lines = order.get_paymentlines();
1304
1305             this.$('.paymentlines-container').empty();
1306             var lines = $(QWeb.render('PaymentScreen-Paymentlines', { 
1307                 widget: this, 
1308                 order: order,
1309                 paymentlines: lines,
1310             }));
1311
1312             lines.on('click','.delete-button',function(){
1313                 self.click_delete_paymentline($(this).data('cid'));
1314             });
1315
1316             lines.on('click','.paymentline',function(){
1317                 self.click_paymentline($(this).data('cid'));
1318             });
1319                 
1320             lines.appendTo(this.$('.paymentlines-container'));
1321         },
1322         click_paymentmethods: function(id) {
1323             var cashregister = null;
1324             for ( var i = 0; i < this.pos.cashregisters.length; i++ ) {
1325                 if ( this.pos.cashregisters[i].journal_id[0] === id ){
1326                     cashregister = this.pos.cashregisters[i];
1327                     break;
1328                 }
1329             }
1330             this.pos.get_order().add_paymentline( cashregister );
1331             this.reset_input();
1332             this.render_paymentlines();
1333         },
1334         render_paymentmethods: function() {
1335             var self = this;
1336             var methods = $(QWeb.render('PaymentScreen-Paymentmethods', { widget:this }));
1337                 methods.on('click','.paymentmethod',function(){
1338                     self.click_paymentmethods($(this).data('id'));
1339                 });
1340             return methods;
1341         },
1342         click_invoice: function(){
1343             var order = this.pos.get_order();
1344             order.set_to_invoice(!order.is_to_invoice());
1345             if (order.is_to_invoice()) {
1346                 this.$('.js_invoice').addClass('highlight');
1347             } else {
1348                 this.$('.js_invoice').removeClass('highlight');
1349             }
1350         },
1351         click_set_customer: function(){
1352             this.pos_widget.screen_selector.set_current_screen('clientlist');
1353         },
1354         click_back: function(){
1355             this.pos_widget.screen_selector.set_current_screen('products');
1356         },
1357         renderElement: function() {
1358             var self = this;
1359             this._super();
1360
1361             var numpad = this.render_numpad();
1362             numpad.appendTo(this.$('.payment-numpad'));
1363
1364             var methods = this.render_paymentmethods();
1365             methods.appendTo(this.$('.paymentmethods-container'));
1366
1367             this.render_paymentlines();
1368
1369             this.$('.back').click(function(){
1370                 self.click_back();
1371             });
1372
1373             this.$('.next').click(function(){
1374                 self.validate_order();
1375             });
1376
1377             this.$('.js_set_customer').click(function(){
1378                 self.click_set_customer();
1379             });
1380             this.$('.js_invoice').click(function(){
1381                 self.click_invoice();
1382             });
1383
1384         },
1385         show: function(){
1386             this.pos.get_order().clean_empty_paymentlines();
1387             this.reset_input();
1388             this.render_paymentlines();
1389             this.order_changes();
1390             window.document.body.addEventListener('keydown',this.keyboard_handler);
1391             this._super();
1392         },
1393         hide: function(){
1394             window.document.body.removeEventListener('keydown',this.keyboard_handler);
1395             this._super();
1396         },
1397         // sets up listeners to watch for order changes
1398         watch_order_changes: function() {
1399             var self = this;
1400             var order = this.pos.get_order();
1401             if (!order) {
1402                 return;
1403             }
1404             if(this.old_order){
1405                 this.old_order.unbind(null,null,this);
1406             }
1407             order.bind('all',function(){
1408                 self.order_changes();
1409             });
1410             this.old_order = order;
1411         },
1412         // called when the order is changed, used to show if
1413         // the order is paid or not
1414         order_changes: function(){
1415             var self = this;
1416             var order = this.pos.get_order();
1417             if (!order) {
1418                 return;
1419             } else if (order.is_paid()) {
1420                 self.$('.next').addClass('highlight');
1421             }else{
1422                 self.$('.next').removeClass('highlight');
1423             }
1424         },
1425         print_escpos_receipt: function(){
1426             var env = {
1427                 widget:  this,
1428                 pos:     this.pos,
1429                 order:   this.pos.get_order(),
1430                 receipt: this.pos.get_order().export_for_printing(),
1431             };
1432
1433             this.pos.proxy.print_receipt(QWeb.render('XmlReceipt',env));
1434         },
1435
1436         // Check if the order is paid, then sends it to the backend,
1437         // and complete the sale process
1438         validate_order: function() {
1439             var self = this;
1440
1441             var order = this.pos.get_order();
1442
1443             // FIXME: this check is there because the backend is unable to
1444             // process empty orders. This is not the right place to fix it.
1445             if (order.get_orderlines().length === 0) {
1446                 this.pos_widget.screen_selector.show_popup('error',{
1447                     'message': _t('Empty Order'),
1448                     'comment': _t('There must be at least one product in your order before it can be validated'),
1449                 });
1450                 return;
1451             }
1452
1453             if (!order.is_paid() || this.invoicing) {
1454                 return;
1455             }
1456
1457             // The exact amount must be paid if there is no cash payment method defined.
1458             if (Math.abs(order.get_total_with_tax() - order.get_total_paid()) > 0.00001) {
1459                 var cash = false;
1460                 for (var i = 0; i < this.pos.cashregisters.length; i++) {
1461                     cash = cash || (this.pos.cashregisters[i].journal.type === 'cash');
1462                 }
1463                 if (!cash) {
1464                     this.pos_widget.screen_selector.show_popup('error',{
1465                         message: _t('Cannot return change without a cash payment method'),
1466                         comment: _t('There is no cash payment method available in this point of sale to handle the change.\n\n Please pay the exact amount or add a cash payment method in the point of sale configuration'),
1467                     });
1468                     return;
1469                 }
1470             }
1471
1472             if (order.is_paid_with_cash() && this.pos.config.iface_cashdrawer) { 
1473
1474                     this.pos.proxy.open_cashbox();
1475             }
1476
1477             if (order.is_to_invoice()) {
1478                 var invoiced = this.pos.push_and_invoice_order(order);
1479                 this.invoicing = true;
1480
1481                 invoiced.fail(function(error){
1482                     self.invoicing = false;
1483                     if (error === 'error-no-client') {
1484                         self.pos_widget.screen_selector.show_popup('confirm',{
1485                             message: _t('Please select the Customer'),
1486                             comment: _t('You need to select the customer before you can invoice an order.'),
1487                             confirm: function(){
1488                                 self.pos_widget.screen_selector.set_current_screen('clientlist');
1489                             },
1490                         });
1491                     } else {
1492                         self.pos_widget.screen_selector.show_popup('error',{
1493                             message: _t('The order could not be sent'),
1494                             comment: _t('Check your internet connection and try again.'),
1495                         });
1496                     }
1497                 });
1498
1499                 invoiced.done(function(){
1500                     self.invoicing = false;
1501                     order.finalize();
1502                 });
1503             } else {
1504                 this.pos.push_order(order) 
1505                 if (this.pos.config.iface_print_via_proxy) {
1506                     this.print_escpos_receipt();
1507                     order.finalize();    //finish order and go back to scan screen
1508                 } else {
1509                     this.pos_widget.screen_selector.set_current_screen(this.next_screen);
1510                 }
1511             }
1512         },
1513     });
1514
1515 }
1516