c58e7d860a0d3472e7b7184d571b61c1b76b2a9b
[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     module.ScreenSelector = instance.web.Class.extend({
23         init: function(options){
24             this.pos = options.pos;
25
26             this.screen_set = options.screen_set || {};
27
28             this.popup_set = options.popup_set || {};
29
30             this.default_client_screen = options.default_client_screen;
31             this.default_cashier_screen = options.default_cashier_screen;
32
33             this.current_popup = null;
34
35             this.current_mode = options.default_mode || 'client';
36
37             this.current_screen = null; 
38
39             for(screen_name in this.screen_set){
40                 this.screen_set[screen_name].hide();
41             }
42             
43             for(popup_name in this.popup_set){
44                 this.popup_set[popup_name].hide();
45             }
46
47             this.selected_order = this.pos.get('selectedOrder');
48             this.selected_order.set_screen_data({
49                 client_screen: this.default_client_screen,
50                 cashier_screen: this.default_cashier_screen,
51             });
52
53             this.pos.bind('change:selectedOrder', this.load_saved_screen, this);
54         },
55         add_screen: function(screen_name, screen){
56             screen.hide();
57             this.screen_set[screen_name] = screen;
58             return this;
59         },
60         show_popup: function(name){
61             if(this.current_popup){
62                 this.close_popup();
63             }
64             this.current_popup = this.popup_set[name];
65             this.current_popup.show();
66         },
67         close_popup: function(){
68             if(this.current_popup){
69                 this.current_popup.close();
70                 this.current_popup.hide();
71                 this.current_popup = null;
72             }
73         },
74         load_saved_screen:  function(){
75             this.close_popup();
76
77             var selectedOrder = this.pos.get('selectedOrder');
78             
79             if(this.current_mode === 'client'){
80                 this.set_current_screen(selectedOrder.get_screen_data('client_screen') || this.default_client_screen,null,'refresh');
81             }else if(this.current_mode === 'cashier'){
82                 this.set_current_screen(selectedOrder.get_screen_data('cashier_screen') || this.default_cashier_screen,null,'refresh');
83             }
84             this.selected_order = selectedOrder;
85         },
86         set_user_mode: function(user_mode){
87             if(user_mode !== this.current_mode){
88                 this.close_popup();
89                 this.current_mode = user_mode;
90                 this.load_saved_screen();
91             }
92         },
93         get_user_mode: function(){
94             return this.current_mode;
95         },
96         set_current_screen: function(screen_name,params,refresh){
97             var screen = this.screen_set[screen_name];
98             if(!screen){
99                 console.error("ERROR: set_current_screen("+screen_name+") : screen not found");
100             }
101
102             this.close_popup();
103             var selectedOrder = this.pos.get('selectedOrder');
104             if(this.current_mode === 'client'){
105                 selectedOrder.set_screen_data('client_screen',screen_name);
106                 if(params){ 
107                     selectedOrder.set_screen_data('client_screen_params',params); 
108                 }
109             }else{
110                 selectedOrder.set_screen_data('cashier_screen',screen_name);
111                 if(params){
112                     selectedOrder.set_screen_data('cashier_screen_params',params);
113                 }
114             }
115
116             if(screen && (refresh || screen !== this.current_screen)){
117                 if(this.current_screen){
118                     this.current_screen.close();
119                     this.current_screen.hide();
120                 }
121                 this.current_screen = screen;
122                 this.current_screen.show();
123             }
124         },
125         get_current_screen_param: function(param){
126             var selected_order = this.pos.get('selectedOrder');
127             if(this.current_mode === 'client'){
128                 var params = selected_order.get_screen_data('client_screen_params');
129             }else{
130                 var params = selected_order.get_screen_data('cashier_screen_params');
131             }
132             if(params){
133                 return params[param];
134             }else{
135                 return undefined;
136             }
137         },
138         set_default_screen: function(){
139             this.set_current_screen(this.current_mode === 'client' ? this.default_client_screen : this.default_cashier_screen);
140         },
141     });
142
143     module.ScreenWidget = module.PosBaseWidget.extend({
144
145         show_numpad:     true,  
146         show_leftpane:   true,
147
148         init: function(parent,options){
149             this._super(parent,options);
150             this.hidden = false;
151         },
152
153         help_button_action: function(){
154             this.pos_widget.screen_selector.show_popup('help');
155         },
156
157         barcode_product_screen:         'products',     //if defined, this screen will be loaded when a product is scanned
158         barcode_product_error_popup:    'error-product',    //if defined, this popup will be loaded when there's an error in the popup
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. Or show barcode_product_error_popup if 
164         // there's an error.
165         barcode_product_action: function(code){
166             var self = this;
167             if(self.pos.scan_product(code)){
168                 self.pos.proxy.scan_item_success(code);
169                 if(self.barcode_product_screen){ 
170                     self.pos_widget.screen_selector.set_current_screen(self.barcode_product_screen);
171                 }
172             }else{
173                 self.pos.proxy.scan_item_error_unrecognized(code);
174                 if(self.barcode_product_error_popup && self.pos_widget.screen_selector.get_user_mode() !== 'cashier'){
175                     self.pos_widget.screen_selector.show_popup(self.barcode_product_error_popup);
176                 }
177             }
178         },
179
180         // what happens when a cashier id barcode is scanned.
181         // the default behavior is the following : 
182         // - if there's a user with a matching ean, put it as the active 'cashier', go to cashier mode, and return true
183         // - else : do nothing and return false. You probably want to extend this to show and appropriate error popup... 
184         barcode_cashier_action: function(code){
185             var users = this.pos.users;
186             for(var i = 0, len = users.length; i < len; i++){
187                 if(users[i].ean13 === code.code){
188                     this.pos.cashier = users[i];
189                     this.pos_widget.username.refresh();
190                     this.pos.proxy.cashier_mode_activated();
191                     this.pos_widget.screen_selector.set_user_mode('cashier');
192                     return true;
193                 }
194             }
195             this.pos.proxy.scan_item_error_unrecognized(code);
196             return false;
197         },
198         
199         // what happens when a client id barcode is scanned.
200         // the default behavior is the following : 
201         // - if there's a user with a matching ean, put it as the active 'client' and return true
202         // - else : return false. 
203         barcode_client_action: function(code){
204             var partners = this.pos.partners;
205             for(var i = 0, len = partners.length; i < len; i++){
206                 if(partners[i].ean13 === code.code){
207                     this.pos.get('selectedOrder').set_client(partners[i]);
208                     this.pos_widget.username.refresh();
209                     this.pos.proxy.scan_item_success(code);
210                     return true;
211                 }
212             }
213             this.pos.proxy.scan_item_error_unrecognized(code);
214             return false;
215             //TODO start the transaction
216         },
217         
218         // what happens when a discount barcode is scanned : the default behavior
219         // is to set the discount on the last order.
220         barcode_discount_action: function(code){
221             this.pos.proxy.scan_item_success(code);
222             var last_orderline = this.pos.get('selectedOrder').getLastOrderline();
223             if(last_orderline){
224                 last_orderline.set_discount(code.value)
225             }
226         },
227
228         // shows an action bar on the screen. The actionbar is automatically shown when you add a button
229         // with add_action_button()
230         show_action_bar: function(){
231             this.pos_widget.action_bar.show();
232         },
233
234         // hides the action bar. The actionbar is automatically hidden when it is empty
235         hide_action_bar: function(){
236             this.pos_widget.action_bar.hide();
237         },
238
239         // adds a new button to the action bar. The button definition takes three parameters, all optional :
240         // - label: the text below the button
241         // - icon:  a small icon that will be shown
242         // - click: a callback that will be executed when the button is clicked.
243         // the method returns a reference to the button widget, and automatically show the actionbar.
244         add_action_button: function(button_def){
245             this.show_action_bar();
246             return this.pos_widget.action_bar.add_new_button(button_def);
247         },
248
249         // this method shows the screen and sets up all the widget related to this screen. Extend this method
250         // if you want to alter the behavior of the screen.
251         show: function(){
252             var self = this;
253
254             this.hidden = false;
255             if(this.$el){
256                 this.$el.removeClass('oe_hidden');
257             }
258
259             if(this.pos_widget.action_bar.get_button_count() > 0){
260                 this.show_action_bar();
261             }else{
262                 this.hide_action_bar();
263             }
264             
265             // we add the help button by default. we do this because the buttons are cleared on each refresh so that
266             // the button stay local to each screen
267             this.pos_widget.left_action_bar.add_new_button({
268                     label: _t('Help'),
269                     icon: '/point_of_sale/static/src/img/icons/png48/help.png',
270                     click: function(){ self.help_button_action(); },
271                 });
272
273             var self = this;
274             this.cashier_mode = this.pos_widget.screen_selector.get_user_mode() === 'cashier';
275
276             this.pos_widget.set_numpad_visible(this.show_numpad && this.cashier_mode);
277             this.pos_widget.set_leftpane_visible(this.show_leftpane);
278             this.pos_widget.set_left_action_bar_visible(this.show_leftpane && !this.cashier_mode);
279             this.pos_widget.set_cashier_controls_visible(this.cashier_mode);
280
281             if(this.cashier_mode && this.pos.config.iface_self_checkout){
282                 this.pos_widget.client_button.show();
283             }else{
284                 this.pos_widget.client_button.hide();
285             }
286             if(this.cashier_mode){
287                 this.pos_widget.close_button.show();
288             }else{
289                 this.pos_widget.close_button.hide();
290             }
291             
292             this.pos_widget.username.set_user_mode(this.pos_widget.screen_selector.get_user_mode());
293
294             this.pos.barcode_reader.set_action_callback({
295                 'cashier': self.barcode_cashier_action ? function(code){ self.barcode_cashier_action(code); } : undefined ,
296                 'product': self.barcode_product_action ? function(code){ self.barcode_product_action(code); } : undefined ,
297                 'client' : self.barcode_client_action ?  function(code){ self.barcode_client_action(code);  } : undefined ,
298                 'discount': self.barcode_discount_action ? function(code){ self.barcode_discount_action(code); } : undefined,
299             });
300         },
301
302         // this method is called when the screen is closed to make place for a new screen. this is a good place
303         // to put your cleanup stuff as it is guaranteed that for each show() there is one and only one close()
304         close: function(){
305             if(this.pos.barcode_reader){
306                 this.pos.barcode_reader.reset_action_callbacks();
307             }
308             this.pos_widget.action_bar.destroy_buttons();
309             this.pos_widget.left_action_bar.destroy_buttons();
310         },
311
312         // this methods hides the screen. It's not a good place to put your cleanup stuff as it is called on the
313         // POS initialization.
314         hide: function(){
315             this.hidden = true;
316             if(this.$el){
317                 this.$el.addClass('oe_hidden');
318             }
319         },
320
321         // we need this because some screens re-render themselves when they are hidden
322         // (due to some events, or magic, or both...)  we must make sure they remain hidden.
323         // the good solution would probably be to make them not re-render themselves when they
324         // are hidden. 
325         renderElement: function(){
326             this._super();
327             if(this.hidden){
328                 if(this.$el){
329                     this.$el.addClass('oe_hidden');
330                 }
331             }
332         },
333     });
334
335     module.PopUpWidget = module.PosBaseWidget.extend({
336         show: function(){
337             if(this.$el){
338                 this.$el.removeClass('oe_hidden');
339             }
340         },
341         /* called before hide, when a popup is closed */
342         close: function(){
343         },
344         /* hides the popup. keep in mind that this is called in the initialization pass of the 
345          * pos instantiation, so you don't want to do anything fancy in here */
346         hide: function(){
347             if(this.$el){
348                 this.$el.addClass('oe_hidden');
349             }
350         },
351     });
352
353     module.HelpPopupWidget = module.PopUpWidget.extend({
354         template:'HelpPopupWidget',
355         show: function(){
356             this._super();
357             this.pos.proxy.help_needed();
358             var self = this;
359             
360             this.$el.find('.button').off('click').click(function(){
361                 self.pos_widget.screen_selector.close_popup();
362             });
363         },
364         close:function(){
365             this.pos.proxy.help_canceled();
366         },
367     });
368
369     module.ChooseReceiptPopupWidget = module.PopUpWidget.extend({
370         template:'ChooseReceiptPopupWidget',
371         show: function(){
372             this._super();
373             this.renderElement();
374             var self = this;
375             var currentOrder = self.pos.get('selectedOrder');
376             
377             this.$('.button.receipt').off('click').click(function(){
378                 currentOrder.set_receipt_type('receipt');
379                 self.pos_widget.screen_selector.set_current_screen('products');
380             });
381
382             this.$('.button.invoice').off('click').click(function(){
383                 currentOrder.set_receipt_type('invoice');
384                 self.pos_widget.screen_selector.set_current_screen('products');
385             });
386         },
387         get_client_name: function(){
388             var client = this.pos.get('selectedOrder').get_client();
389             if( client ){
390                 return client.name;
391             }else{
392                 return '';
393             }
394         },
395     });
396
397     module.ErrorPopupWidget = module.PopUpWidget.extend({
398         template:'ErrorPopupWidget',
399         show: function(){
400             var self = this;
401             this._super();
402             this.pos.proxy.help_needed();
403             this.pos.proxy.scan_item_error_unrecognized();
404
405             this.pos.barcode_reader.save_callbacks();
406             this.pos.barcode_reader.reset_action_callbacks();
407             this.pos.barcode_reader.set_action_callback({
408                 'cashier': function(code){
409                     clearInterval(this.intervalID);
410                     self.pos.proxy.cashier_mode_activated();
411                     self.pos_widget.screen_selector.set_user_mode('cashier');
412                 },
413             });
414             this.$('.footer .button').off('click').click(function(){
415                 self.pos_widget.screen_selector.close_popup();
416             });
417         },
418         close:function(){
419             this._super();
420             this.pos.proxy.help_canceled();
421             this.pos.barcode_reader.restore_callbacks();
422         },
423     });
424
425     module.ProductErrorPopupWidget = module.ErrorPopupWidget.extend({
426         template:'ProductErrorPopupWidget',
427     });
428
429     module.ErrorSessionPopupWidget = module.ErrorPopupWidget.extend({
430         template:'ErrorSessionPopupWidget',
431     });
432
433     module.ErrorNegativePricePopupWidget = module.ErrorPopupWidget.extend({
434         template:'ErrorNegativePricePopupWidget',
435     });
436
437     module.ErrorNoClientPopupWidget = module.ErrorPopupWidget.extend({
438         template: 'ErrorNoClientPopupWidget',
439     });
440
441     module.ErrorInvoiceTransferPopupWidget = module.ErrorPopupWidget.extend({
442         template: 'ErrorInvoiceTransferPopupWidget',
443     });
444                 
445     module.ScaleInviteScreenWidget = module.ScreenWidget.extend({
446         template:'ScaleInviteScreenWidget',
447
448         next_screen:'scale',
449         previous_screen:'products',
450
451         show: function(){
452             this._super();
453             var self = this;
454             var queue = this.pos.proxy_queue;
455
456             queue.schedule(function(){
457                 return self.pos.proxy.weighting_start();
458             },{ important: true });
459             
460             queue.schedule(function(){
461                 return self.pos.proxy.weighting_read_kg().then(function(weight){
462                     if(weight > 0.001){
463                         self.pos_widget.screen_selector.set_current_screen(self.next_screen);
464                     }
465                 });
466             },{duration: 100, repeat: true});
467
468             this.add_action_button({
469                     label: _t('Back'),
470                     icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
471                     click: function(){  
472                         self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
473                     }
474                 });
475         },
476         close: function(){
477             this._super();
478             var self = this;
479             this.pos.proxy_queue.clear();
480             this.pos.proxy_queue.schedule(function(){
481                 return self.pos.proxy.weighting_end();
482             },{ important: true });
483         },
484     });
485
486     module.ScaleScreenWidget = module.ScreenWidget.extend({
487         template:'ScaleScreenWidget',
488
489         next_screen: 'products',
490         previous_screen: 'products',
491
492         show: function(){
493             this._super();
494             var self = this;
495             var queue = this.pos.proxy_queue;
496
497             this.set_weight(0);
498             this.renderElement();
499
500             this.hotkey_handler = function(event){
501                 if(event.which === 13){
502                     self.order_product();
503                     self.pos_widget.screen_selector.set_current_screen(self.next_screen);
504                 }else if(event.which === 27){
505                     self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
506                 }
507             };
508
509             $('body').on('keyup',this.hotkey_handler);
510
511             this.add_action_button({
512                     label: _t('Back'),
513                     icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
514                     click: function(){
515                         self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
516                     }
517                 });
518
519             this.validate_button = this.add_action_button({
520                     label: _t('Validate'),
521                     icon: '/point_of_sale/static/src/img/icons/png48/validate.png',
522                     click: function(){
523                         self.order_product();
524                         self.pos_widget.screen_selector.set_current_screen(self.next_screen);
525                     },
526                 });
527             
528             queue.schedule(function(){
529                 return self.pos.proxy.scale_read().then(function(weight){
530                     self.set_weight(weight.weight);
531                 });
532             },{duration:50, repeat: true});
533
534         },
535         renderElement: function(){
536             var self = this;
537             this._super();
538             this.$('.product-picture').click(function(){
539                 self.order_product();
540                 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
541             });
542         },
543         get_product: function(){
544             var ss = this.pos_widget.screen_selector;
545             if(ss){
546                 return ss.get_current_screen_param('product');
547             }else{
548                 return undefined;
549             }
550         },
551         order_product: function(){
552             this.pos.get('selectedOrder').addProduct(this.get_product(),{ quantity: this.weight });
553         },
554         get_product_name: function(){
555             var product = this.get_product();
556             return (product ? product.name : undefined) || 'Unnamed Product';
557         },
558         get_product_price: function(){
559             var product = this.get_product();
560             return (product ? product.price : 0) || 0;
561         },
562         set_weight: function(weight){
563             this.weight = weight;
564             this.$('.js-weight').text(this.get_product_weight_string());
565         },
566         get_product_weight_string: function(){
567             return (this.weight || 0).toFixed(3) + ' Kg';
568         },
569         get_product_image_url: function(){
570             var product = this.get_product();
571             if(product){
572                 return window.location.origin + '/web/binary/image?model=product.product&field=image_medium&id='+product.id;
573             }else{
574                 return "";
575             }
576         },
577         close: function(){
578             var self = this;
579             this._super();
580             $('body').off('keyup',this.hotkey_handler);
581
582             this.pos.proxy_queue.clear();
583         },
584     });
585
586
587     module.ClientPaymentScreenWidget =  module.ScreenWidget.extend({
588         template:'ClientPaymentScreenWidget',
589
590         next_screen: 'welcome',
591         previous_screen: 'products',
592
593         show: function(){
594             this._super();
595             var self = this;
596            
597             this.queue = new module.JobQueue();
598             this.canceled = false;
599             this.paid     = false;
600
601             // initiates the connection to the payment terminal and starts the update requests
602             this.start = function(){
603                 var def = new $.Deferred();
604                 self.pos.proxy.payment_request(self.pos.get('selectedOrder').getDueLeft())
605                     .done(function(ack){
606                         if(ack === 'ok'){
607                             self.queue.schedule(self.update);
608                         }else if(ack.indexOf('error') === 0){
609                             console.error('cannot make payment. TODO');
610                         }else{
611                             console.error('unknown payment request return value:',ack);
612                         }
613                         def.resolve();
614                     });
615                 return def;
616             };
617             
618             // gets updated status from the payment terminal and performs the appropriate consequences
619             this.update = function(){
620                 var def = new $.Deferred();
621                 if(self.canceled){
622                     return def.resolve();
623                 }
624                 self.pos.proxy.payment_status()
625                     .done(function(status){
626                         if(status.status === 'paid'){
627
628                             var currentOrder = self.pos.get('selectedOrder');
629                             
630                             //we get the first cashregister marked as self-checkout
631                             var selfCheckoutRegisters = [];
632                             for(var i = 0; i < self.pos.cashregisters.length; i++){
633                                 var cashregister = self.pos.cashregisters[i];
634                                 if(cashregister.self_checkout_payment_method){
635                                     selfCheckoutRegisters.push(cashregister);
636                                 }
637                             }
638
639                             var cashregister = selfCheckoutRegisters[0] || self.pos.cashregisters[0];
640                             currentOrder.addPaymentline(cashregister);
641                             self.pos.push_order(currentOrder)
642                             currentOrder.destroy();
643                             self.pos.proxy.transaction_end();
644                             self.pos_widget.screen_selector.set_current_screen(self.next_screen);
645                             self.paid = true;
646                         }else if(status.status.indexOf('error') === 0){
647                             console.error('error in payment request. TODO');
648                         }else if(status.status === 'waiting'){
649                             self.queue.schedule(self.update,200);
650                         }else{
651                             console.error('unknown status value:',status.status);
652                         }
653                         def.resolve();
654                     });
655                 return def;
656             }
657             
658             // cancels a payment.
659             this.cancel = function(){
660                 if(!self.paid && !self.canceled){
661                     self.canceled = true;
662                     self.pos.proxy.payment_cancel();
663                     self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
664                     self.queue.clear();
665                 }
666                 return (new $.Deferred()).resolve();
667             }
668             
669             if(this.pos.get('selectedOrder').getDueLeft() <= 0){
670                 this.pos_widget.screen_selector.show_popup('error-negative-price');
671             }else{
672                 this.queue.schedule(this.start);
673             }
674
675             this.add_action_button({
676                     label: _t('Back'),
677                     icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
678                     click: function(){  
679                        self.queue.schedule(self.cancel);
680                        self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
681                     }
682                 });
683         },
684         close: function(){
685             if(this.queue){
686                 this.queue.schedule(this.cancel);
687             }
688             //TODO CANCEL
689             this._super();
690         },
691     });
692
693     module.WelcomeScreenWidget = module.ScreenWidget.extend({
694         template:'WelcomeScreenWidget',
695
696         next_screen: 'products',
697
698         show_numpad:     false,
699         show_leftpane:   false,
700         start: function(){
701             this._super();
702             $('.goodbye-message').click(function(){
703                 $(this).addClass('oe_hidden');
704             });
705         },
706
707         barcode_product_action: function(code){
708             this.pos.proxy.transaction_start();
709             this._super(code);
710         },
711
712         barcode_client_action: function(code){
713             this.pos.proxy.transaction_start();
714             this._super(code);
715             $('.goodbye-message').addClass('oe_hidden');
716             this.pos_widget.screen_selector.show_popup('choose-receipt');
717         },
718         
719         show: function(){
720             this._super();
721             var self = this;
722
723             this.add_action_button({
724                     label: _t('Help'),
725                     icon: '/point_of_sale/static/src/img/icons/png48/help.png',
726                     click: function(){ 
727                         $('.goodbye-message').css({opacity:1}).addClass('oe_hidden');
728                         self.help_button_action();
729                     },
730                 });
731
732             $('.goodbye-message').css({opacity:1}).removeClass('oe_hidden');
733             setTimeout(function(){
734                 $('.goodbye-message').animate({opacity:0},500,'swing',function(){$('.goodbye-message').addClass('oe_hidden');});
735             },5000);
736         },
737     });
738     
739     module.ProductScreenWidget = module.ScreenWidget.extend({
740         template:'ProductScreenWidget',
741
742         scale_screen: 'scale',
743         client_scale_screen : 'scale_invite',
744         client_next_screen:  'client_payment',
745
746         show_numpad:     true,
747         show_leftpane:   true,
748
749         start: function(){ //FIXME this should work as renderElement... but then the categories aren't properly set. explore why
750             var self = this;
751
752             this.product_list_widget = new module.ProductListWidget(this,{
753                 click_product_action: function(product){
754                     if(product.to_weight && self.pos.config.iface_electronic_scale){
755                         self.pos_widget.screen_selector.set_current_screen( self.cashier_mode ? self.scale_screen : self.client_scale_screen, {product: product});
756                     }else{
757                         self.pos.get('selectedOrder').addProduct(product);
758                     }
759                 },
760                 product_list: this.pos.db.get_product_by_category(0)
761             });
762             this.product_list_widget.replace($('.placeholder-ProductListWidget'));
763
764             this.product_categories_widget = new module.ProductCategoriesWidget(this,{
765                 product_list_widget: this.product_list_widget,
766             });
767             this.product_categories_widget.replace($('.placeholder-ProductCategoriesWidget'));
768         },
769
770         show: function(){
771             this._super();
772             var self = this;
773
774             this.product_categories_widget.reset_category();
775
776             this.pos_widget.order_widget.set_editable(true);
777
778             if(this.pos_widget.screen_selector.current_mode === 'client'){ 
779                 this.add_action_button({
780                         label: _t('Pay'),
781                         icon: '/point_of_sale/static/src/img/icons/png48/go-next.png',
782                         click: function(){  
783                             self.pos_widget.screen_selector.set_current_screen(self.client_next_screen);
784                         }
785                     });
786             }
787         },
788
789         close: function(){
790             this._super();
791
792             this.pos_widget.order_widget.set_editable(false);
793
794             if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
795                 this.pos_widget.onscreen_keyboard.hide();
796             }
797         },
798     });
799
800     module.ReceiptScreenWidget = module.ScreenWidget.extend({
801         template: 'ReceiptScreenWidget',
802
803         show_numpad:     true,
804         show_leftpane:   true,
805
806         show: function(){
807             this._super();
808             var self = this;
809
810             var print_button = this.add_action_button({
811                     label: _t('Print'),
812                     icon: '/point_of_sale/static/src/img/icons/png48/printer.png',
813                     click: function(){ self.print(); },
814                 });
815
816             var finish_button = this.add_action_button({
817                     label: _t('Next Order'),
818                     icon: '/point_of_sale/static/src/img/icons/png48/go-next.png',
819                     click: function() { self.finishOrder(); },
820                 });
821
822             this.refresh();
823             this.print();
824
825             //
826             // The problem is that in chrome the print() is asynchronous and doesn't
827             // execute until all rpc are finished. So it conflicts with the rpc used
828             // to send the orders to the backend, and the user is able to go to the next 
829             // screen before the printing dialog is opened. The problem is that what's 
830             // printed is whatever is in the page when the dialog is opened and not when it's called,
831             // and so you end up printing the product list instead of the receipt... 
832             //
833             // Fixing this would need a re-architecturing
834             // of the code to postpone sending of orders after printing.
835             //
836             // But since the print dialog also blocks the other asynchronous calls, the
837             // button enabling in the setTimeout() is blocked until the printing dialog is 
838             // closed. But the timeout has to be big enough or else it doesn't work
839             // 2 seconds is the same as the default timeout for sending orders and so the dialog
840             // should have appeared before the timeout... so yeah that's not ultra reliable. 
841
842             finish_button.set_disabled(true);   
843             setTimeout(function(){
844                 finish_button.set_disabled(false);
845             }, 2000);
846         },
847         print: function() {
848             window.print();
849         },
850         finishOrder: function() {
851             this.pos.get('selectedOrder').destroy();
852         },
853         refresh: function() {
854             var order = this.pos.get('selectedOrder');
855             $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{
856                     widget:this,
857                     order: order,
858                     orderlines: order.get('orderLines').models,
859                     paymentlines: order.get('paymentLines').models,
860                 }));
861         },
862         close: function(){
863             this._super();
864         }
865     });
866
867     module.PaymentScreenWidget = module.ScreenWidget.extend({
868         template: 'PaymentScreenWidget',
869         back_screen: 'products',
870         next_screen: 'receipt',
871         init: function(parent, options) {
872             var self = this;
873             this._super(parent,options);
874
875             this.pos.bind('change:selectedOrder',function(){
876                     this.bind_events();
877                     this.renderElement();
878                 },this);
879
880             this.bind_events();
881
882             this.line_delete_handler = function(event){
883                 var node = this;
884                 while(node && !node.classList.contains('paymentline')){
885                     node = node.parentNode;
886                 }
887                 if(node){
888                     self.pos.get('selectedOrder').removePaymentline(node.line)   
889                 }
890                 event.stopPropagation();
891             };
892
893             this.line_change_handler = function(event){
894                 var node = this;
895                 while(node && !node.classList.contains('paymentline')){
896                     node = node.parentNode;
897                 }
898                 if(node){
899                     node.line.set_amount(this.value);
900                 }
901                 
902             };
903
904             this.line_click_handler = function(event){
905                 var node = this;
906                 while(node && !node.classList.contains('paymentline')){
907                     node = node.parentNode;
908                 }
909                 if(node){
910                     self.pos.get('selectedOrder').selectPaymentline(node.line);
911                 }
912             };
913
914             this.hotkey_handler = function(event){
915                 if(event.which === 13){
916                     self.validate_order();
917                 }else if(event.which === 27){
918                     self.back();
919                 }
920             };
921
922         },
923         show: function(){
924             this._super();
925             var self = this;
926             
927             this.enable_numpad();
928             this.focus_selected_line();
929             
930             document.body.addEventListener('keyup', this.hotkey_handler);
931             
932
933
934             this.add_action_button({
935                     label: _t('Back'),
936                     icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
937                     click: function(){  
938                         self.back();
939                     },
940                 });
941
942             this.add_action_button({
943                     label: _t('Validate'),
944                     name: 'validation',
945                     icon: '/point_of_sale/static/src/img/icons/png48/validate.png',
946                     click: function(){
947                         self.validate_order();
948                     },
949                 });
950            
951             if( this.pos.config.iface_invoicing ){
952                 this.add_action_button({
953                         label: 'Invoice',
954                         name: 'invoice',
955                         icon: '/point_of_sale/static/src/img/icons/png48/invoice.png',
956                         click: function(){
957                             self.validate_order({invoice: true});
958                         },
959                     });
960             }
961
962             if( this.pos.config.iface_cashdrawer ){
963                 this.add_action_button({
964                         label: _t('Cash'),
965                         name: 'cashbox',
966                         icon: '/point_of_sale/static/src/img/open-cashbox.png',
967                         click: function(){
968                             self.pos.proxy.open_cashbox();
969                         },
970                     });
971             }
972
973             this.update_payment_summary();
974
975         },
976         close: function(){
977             this._super();
978             this.disable_numpad();
979             document.body.removeEventListener('keyup',this.hotkey_handler);
980         },
981         remove_empty_lines: function(){
982             var order = this.pos.get('selectedOrder');
983             var lines = order.get('paymentLines').models.slice(0);
984             for(var i = 0; i < lines.length; i++){ 
985                 var line = lines[i];
986                 if(line.get_amount() === 0){
987                     order.removePaymentline(line);
988                 }
989             }
990         },
991         back: function() {
992             this.remove_empty_lines();
993             this.pos_widget.screen_selector.set_current_screen(this.back_screen);
994         },
995         bind_events: function() {
996             if(this.old_order){
997                 this.old_order.unbind(null,null,this);
998             }
999             var order = this.pos.get('selectedOrder');
1000                 order.bind('change:selected_paymentline',this.focus_selected_line,this);
1001
1002             this.old_order = order;
1003
1004             if(this.old_paymentlines){
1005                 this.old_paymentlines.unbind(null,null,this);
1006             }
1007             var paymentlines = order.get('paymentLines');
1008                 paymentlines.bind('add', this.add_paymentline, this);
1009                 paymentlines.bind('change:selected', this.rerender_paymentline, this);
1010                 paymentlines.bind('change:amount', function(line){
1011                         if(!line.selected && line.node){
1012                             line.node.value = line.amount.toFixed(2);
1013                         }
1014                         this.update_payment_summary();
1015                     },this);
1016                 paymentlines.bind('remove', this.remove_paymentline, this);
1017                 paymentlines.bind('all', this.update_payment_summary, this);
1018
1019             this.old_paymentlines = paymentlines;
1020
1021             if(this.old_orderlines){
1022                 this.old_orderlines.unbind(null,null,this);
1023             }
1024             var orderlines = order.get('orderLines');
1025                 orderlines.bind('all', this.update_payment_summary, this);
1026
1027             this.old_orderlines = orderlines;
1028         },
1029         focus_selected_line: function(){
1030             var line = this.pos.get('selectedOrder').selected_paymentline;
1031             if(line){
1032                 var input = line.node.querySelector('input');
1033                 if(!input){
1034                     return;
1035                 }
1036                 var value = input.value;
1037                 input.focus();
1038
1039                 if(this.numpad_state){
1040                     this.numpad_state.reset();
1041                 }
1042
1043                 if(Number(value) === 0){
1044                     input.value = '';
1045                 }else{
1046                     input.value = value;
1047                     input.select();
1048                 }
1049             }
1050         },
1051         add_paymentline: function(line) {
1052             var list_container = this.el.querySelector('.payment-lines');
1053                 list_container.appendChild(this.render_paymentline(line));
1054             
1055             if(this.numpad_state){
1056                 this.numpad_state.reset();
1057             }
1058         },
1059         render_paymentline: function(line){
1060             var el_html  = openerp.qweb.render('Paymentline',{widget: this, line: line});
1061                 el_html  = _.str.trim(el_html);
1062
1063             var el_node  = document.createElement('tbody');
1064                 el_node.innerHTML = el_html;
1065                 el_node = el_node.childNodes[0];
1066                 el_node.line = line;
1067                 el_node.querySelector('.paymentline-delete')
1068                     .addEventListener('click', this.line_delete_handler);
1069                 el_node.addEventListener('click', this.line_click_handler);
1070                 el_node.querySelector('input')
1071                     .addEventListener('keyup', this.line_change_handler);
1072
1073             line.node = el_node;
1074
1075             return el_node;
1076         },
1077         rerender_paymentline: function(line){
1078             var old_node = line.node;
1079             var new_node = this.render_paymentline(line);
1080             
1081             old_node.parentNode.replaceChild(new_node,old_node);
1082         },
1083         remove_paymentline: function(line){
1084             line.node.parentNode.removeChild(line.node);
1085             line.node = undefined;
1086         },
1087         renderElement: function(){
1088             this._super();
1089
1090             var paymentlines   = this.pos.get('selectedOrder').get('paymentLines').models;
1091             var list_container = this.el.querySelector('.payment-lines');
1092
1093             for(var i = 0; i < paymentlines.length; i++){
1094                 list_container.appendChild(this.render_paymentline(paymentlines[i]));
1095             }
1096             
1097             this.update_payment_summary();
1098         },
1099         update_payment_summary: function() {
1100             var currentOrder = this.pos.get('selectedOrder');
1101             var paidTotal = currentOrder.getPaidTotal();
1102             var dueTotal = currentOrder.getTotalTaxIncluded();
1103             var remaining = dueTotal > paidTotal ? dueTotal - paidTotal : 0;
1104             var change = paidTotal > dueTotal ? paidTotal - dueTotal : 0;
1105
1106             this.$('.payment-due-total').html(this.format_currency(dueTotal));
1107             this.$('.payment-paid-total').html(this.format_currency(paidTotal));
1108             this.$('.payment-remaining').html(this.format_currency(remaining));
1109             this.$('.payment-change').html(this.format_currency(change));
1110             if(currentOrder.selected_orderline === undefined){
1111                 remaining = 1;  // What is this ? 
1112             }
1113                 
1114             if(this.pos_widget.action_bar){
1115                 this.pos_widget.action_bar.set_button_disabled('validation', !this.is_paid());
1116                 this.pos_widget.action_bar.set_button_disabled('invoice', !this.is_paid());
1117             }
1118         },
1119         is_paid: function(){
1120             var currentOrder = this.pos.get('selectedOrder');
1121             return (currentOrder.getTotalTaxIncluded() < 0.000001 
1122                    || currentOrder.getPaidTotal() + 0.000001 >= currentOrder.getTotalTaxIncluded());
1123
1124         },
1125         validate_order: function(options) {
1126             var self = this;
1127             options = options || {};
1128
1129             var currentOrder = this.pos.get('selectedOrder');
1130
1131             if(!this.is_paid()){
1132                 return;
1133             }
1134
1135             if(    this.pos.config.iface_cashdrawer 
1136                 && this.pos.get('selectedOrder').get('paymentLines').find( function(pl){ 
1137                            return pl.cashregister.journal.type === 'cash'; 
1138                    })){
1139                     this.pos.proxy.open_cashbox();
1140             }
1141
1142             if(options.invoice){
1143                 // deactivate the validation button while we try to send the order
1144                 this.pos_widget.action_bar.set_button_disabled('validation',true);
1145                 this.pos_widget.action_bar.set_button_disabled('invoice',true);
1146
1147                 var invoiced = this.pos.push_and_invoice_order(currentOrder);
1148
1149                 invoiced.fail(function(error){
1150                     if(error === 'error-no-client'){
1151                         self.pos_widget.screen_selector.show_popup('error-no-client');
1152                     }else{
1153                         self.pos_widget.screen_selector.show_popup('error-invoice-transfer');
1154                     }
1155                     self.pos_widget.action_bar.set_button_disabled('validation',false);
1156                     self.pos_widget.action_bar.set_button_disabled('invoice',false);
1157                 });
1158
1159                 invoiced.done(function(){
1160                     self.pos_widget.action_bar.set_button_disabled('validation',false);
1161                     self.pos_widget.action_bar.set_button_disabled('invoice',false);
1162                     self.pos.get('selectedOrder').destroy();
1163                 });
1164
1165             }else{
1166                 this.pos.push_order(currentOrder) 
1167                 if(this.pos.config.iface_print_via_proxy){
1168                     var receipt = currentOrder.export_for_printing();
1169                     this.pos.proxy.print_receipt(QWeb.render('XmlReceipt',{
1170                         receipt: receipt
1171                     }));
1172                     this.pos.get('selectedOrder').destroy();    //finish order and go back to scan screen
1173                 }else{
1174                     this.pos_widget.screen_selector.set_current_screen(this.next_screen);
1175                 }
1176             }
1177
1178             // hide onscreen (iOS) keyboard 
1179             setTimeout(function(){
1180                 document.activeElement.blur();
1181                 $("input").blur();
1182             },250);
1183         },
1184         enable_numpad: function(){
1185             this.disable_numpad();  //ensure we don't register the callbacks twice
1186             this.numpad_state = this.pos_widget.numpad.state;
1187             if(this.numpad_state){
1188                 this.numpad_state.reset();
1189                 this.numpad_state.changeMode('payment');
1190                 this.numpad_state.bind('set_value',   this.set_value, this);
1191                 this.numpad_state.bind('change:mode', this.set_mode_back_to_payment, this);
1192             }
1193                     
1194         },
1195         disable_numpad: function(){
1196             if(this.numpad_state){
1197                 this.numpad_state.unbind('set_value',  this.set_value);
1198                 this.numpad_state.unbind('change:mode',this.set_mode_back_to_payment);
1199             }
1200         },
1201         set_mode_back_to_payment: function() {
1202                 this.numpad_state.set({mode: 'payment'});
1203         },
1204         set_value: function(val) {
1205             var selected_line =this.pos.get('selectedOrder').selected_paymentline;
1206             if(selected_line){
1207                 selected_line.set_amount(val);
1208                 selected_line.node.querySelector('input').value = selected_line.amount.toFixed(2);
1209             }
1210         },
1211     });
1212 }