cbd6779f33c54eaec94977ff477fff1e7b4d5703
[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.weighting_start()
530             },{ important: true });
531             
532             queue.schedule(function(){
533                 return self.pos.proxy.weighting_read_kg().then(function(weight){
534                     self.set_weight(weight);
535                 });
536             },{duration:50, repeat: true});
537
538         },
539         renderElement: function(){
540             var self = this;
541             this._super();
542             this.$('.product-picture').click(function(){
543                 self.order_product();
544                 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
545             });
546         },
547         get_product: function(){
548             var ss = this.pos_widget.screen_selector;
549             if(ss){
550                 return ss.get_current_screen_param('product');
551             }else{
552                 return undefined;
553             }
554         },
555         order_product: function(){
556             this.pos.get('selectedOrder').addProduct(this.get_product(),{ quantity: this.weight });
557         },
558         get_product_name: function(){
559             var product = this.get_product();
560             return (product ? product.name : undefined) || 'Unnamed Product';
561         },
562         get_product_price: function(){
563             var product = this.get_product();
564             return (product ? product.price : 0) || 0;
565         },
566         set_weight: function(weight){
567             this.weight = weight;
568             this.$('.js-weight').text(this.get_product_weight_string());
569         },
570         get_product_weight_string: function(){
571             return (this.weight || 0).toFixed(3) + ' Kg';
572         },
573         get_product_image_url: function(){
574             var product = this.get_product();
575             if(product){
576                 return window.location.origin + '/web/binary/image?model=product.product&field=image_medium&id='+product.id;
577             }else{
578                 return "";
579             }
580         },
581         close: function(){
582             var self = this;
583             this._super();
584             $('body').off('keyup',this.hotkey_handler);
585
586             this.pos.proxy_queue.clear();
587             this.pos.proxy_queue.schedule(function(){
588                 self.pos.proxy.weighting_end();
589             },{ important: true });
590         },
591     });
592
593
594     module.ClientPaymentScreenWidget =  module.ScreenWidget.extend({
595         template:'ClientPaymentScreenWidget',
596
597         next_screen: 'welcome',
598         previous_screen: 'products',
599
600         show: function(){
601             this._super();
602             var self = this;
603            
604             this.queue = new module.JobQueue();
605             this.canceled = false;
606             this.paid     = false;
607
608             // initiates the connection to the payment terminal and starts the update requests
609             this.start = function(){
610                 var def = new $.Deferred();
611                 self.pos.proxy.payment_request(self.pos.get('selectedOrder').getDueLeft())
612                     .done(function(ack){
613                         if(ack === 'ok'){
614                             self.queue.schedule(self.update);
615                         }else if(ack.indexOf('error') === 0){
616                             console.error('cannot make payment. TODO');
617                         }else{
618                             console.error('unknown payment request return value:',ack);
619                         }
620                         def.resolve();
621                     });
622                 return def;
623             };
624             
625             // gets updated status from the payment terminal and performs the appropriate consequences
626             this.update = function(){
627                 var def = new $.Deferred();
628                 if(self.canceled){
629                     return def.resolve();
630                 }
631                 self.pos.proxy.payment_status()
632                     .done(function(status){
633                         if(status.status === 'paid'){
634
635                             var currentOrder = self.pos.get('selectedOrder');
636                             
637                             //we get the first cashregister marked as self-checkout
638                             var selfCheckoutRegisters = [];
639                             for(var i = 0; i < self.pos.cashregisters.length; i++){
640                                 var cashregister = self.pos.cashregisters[i];
641                                 if(cashregister.self_checkout_payment_method){
642                                     selfCheckoutRegisters.push(cashregister);
643                                 }
644                             }
645
646                             var cashregister = selfCheckoutRegisters[0] || self.pos.cashregisters[0];
647                             currentOrder.addPaymentline(cashregister);
648                             self.pos.push_order(currentOrder)
649                             currentOrder.destroy();
650                             self.pos.proxy.transaction_end();
651                             self.pos_widget.screen_selector.set_current_screen(self.next_screen);
652                             self.paid = true;
653                         }else if(status.status.indexOf('error') === 0){
654                             console.error('error in payment request. TODO');
655                         }else if(status.status === 'waiting'){
656                             self.queue.schedule(self.update,200);
657                         }else{
658                             console.error('unknown status value:',status.status);
659                         }
660                         def.resolve();
661                     });
662                 return def;
663             }
664             
665             // cancels a payment.
666             this.cancel = function(){
667                 if(!self.paid && !self.canceled){
668                     self.canceled = true;
669                     self.pos.proxy.payment_cancel();
670                     self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
671                     self.queue.clear();
672                 }
673                 return (new $.Deferred()).resolve();
674             }
675             
676             if(this.pos.get('selectedOrder').getDueLeft() <= 0){
677                 this.pos_widget.screen_selector.show_popup('error-negative-price');
678             }else{
679                 this.queue.schedule(this.start);
680             }
681
682             this.add_action_button({
683                     label: _t('Back'),
684                     icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
685                     click: function(){  
686                        self.queue.schedule(self.cancel);
687                        self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
688                     }
689                 });
690         },
691         close: function(){
692             if(this.queue){
693                 this.queue.schedule(this.cancel);
694             }
695             //TODO CANCEL
696             this._super();
697         },
698     });
699
700     module.WelcomeScreenWidget = module.ScreenWidget.extend({
701         template:'WelcomeScreenWidget',
702
703         next_screen: 'products',
704
705         show_numpad:     false,
706         show_leftpane:   false,
707         start: function(){
708             this._super();
709             $('.goodbye-message').click(function(){
710                 $(this).addClass('oe_hidden');
711             });
712         },
713
714         barcode_product_action: function(code){
715             this.pos.proxy.transaction_start();
716             this._super(code);
717         },
718
719         barcode_client_action: function(code){
720             this.pos.proxy.transaction_start();
721             this._super(code);
722             $('.goodbye-message').addClass('oe_hidden');
723             this.pos_widget.screen_selector.show_popup('choose-receipt');
724         },
725         
726         show: function(){
727             this._super();
728             var self = this;
729
730             this.add_action_button({
731                     label: _t('Help'),
732                     icon: '/point_of_sale/static/src/img/icons/png48/help.png',
733                     click: function(){ 
734                         $('.goodbye-message').css({opacity:1}).addClass('oe_hidden');
735                         self.help_button_action();
736                     },
737                 });
738
739             $('.goodbye-message').css({opacity:1}).removeClass('oe_hidden');
740             setTimeout(function(){
741                 $('.goodbye-message').animate({opacity:0},500,'swing',function(){$('.goodbye-message').addClass('oe_hidden');});
742             },5000);
743         },
744     });
745     
746     module.ProductScreenWidget = module.ScreenWidget.extend({
747         template:'ProductScreenWidget',
748
749         scale_screen: 'scale',
750         client_scale_screen : 'scale_invite',
751         client_next_screen:  'client_payment',
752
753         show_numpad:     true,
754         show_leftpane:   true,
755
756         start: function(){ //FIXME this should work as renderElement... but then the categories aren't properly set. explore why
757             var self = this;
758
759             this.product_list_widget = new module.ProductListWidget(this,{
760                 click_product_action: function(product){
761                     if(product.to_weight && self.pos.config.iface_electronic_scale){
762                         self.pos_widget.screen_selector.set_current_screen( self.cashier_mode ? self.scale_screen : self.client_scale_screen, {product: product});
763                     }else{
764                         self.pos.get('selectedOrder').addProduct(product);
765                     }
766                 },
767                 product_list: this.pos.db.get_product_by_category(0)
768             });
769             this.product_list_widget.replace($('.placeholder-ProductListWidget'));
770
771             this.product_categories_widget = new module.ProductCategoriesWidget(this,{
772                 product_list_widget: this.product_list_widget,
773             });
774             this.product_categories_widget.replace($('.placeholder-ProductCategoriesWidget'));
775         },
776
777         show: function(){
778             this._super();
779             var self = this;
780
781             this.product_categories_widget.reset_category();
782
783             this.pos_widget.order_widget.set_editable(true);
784
785             if(this.pos_widget.screen_selector.current_mode === 'client'){ 
786                 this.add_action_button({
787                         label: _t('Pay'),
788                         icon: '/point_of_sale/static/src/img/icons/png48/go-next.png',
789                         click: function(){  
790                             self.pos_widget.screen_selector.set_current_screen(self.client_next_screen);
791                         }
792                     });
793             }
794         },
795
796         close: function(){
797             this._super();
798
799             this.pos_widget.order_widget.set_editable(false);
800
801             if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
802                 this.pos_widget.onscreen_keyboard.hide();
803             }
804         },
805     });
806
807     module.ReceiptScreenWidget = module.ScreenWidget.extend({
808         template: 'ReceiptScreenWidget',
809
810         show_numpad:     true,
811         show_leftpane:   true,
812
813         show: function(){
814             this._super();
815             var self = this;
816
817             var print_button = this.add_action_button({
818                     label: _t('Print'),
819                     icon: '/point_of_sale/static/src/img/icons/png48/printer.png',
820                     click: function(){ self.print(); },
821                 });
822
823             var finish_button = this.add_action_button({
824                     label: _t('Next Order'),
825                     icon: '/point_of_sale/static/src/img/icons/png48/go-next.png',
826                     click: function() { self.finishOrder(); },
827                 });
828
829             this.refresh();
830             this.print();
831
832             //
833             // The problem is that in chrome the print() is asynchronous and doesn't
834             // execute until all rpc are finished. So it conflicts with the rpc used
835             // to send the orders to the backend, and the user is able to go to the next 
836             // screen before the printing dialog is opened. The problem is that what's 
837             // printed is whatever is in the page when the dialog is opened and not when it's called,
838             // and so you end up printing the product list instead of the receipt... 
839             //
840             // Fixing this would need a re-architecturing
841             // of the code to postpone sending of orders after printing.
842             //
843             // But since the print dialog also blocks the other asynchronous calls, the
844             // button enabling in the setTimeout() is blocked until the printing dialog is 
845             // closed. But the timeout has to be big enough or else it doesn't work
846             // 2 seconds is the same as the default timeout for sending orders and so the dialog
847             // should have appeared before the timeout... so yeah that's not ultra reliable. 
848
849             finish_button.set_disabled(true);   
850             setTimeout(function(){
851                 finish_button.set_disabled(false);
852             }, 2000);
853         },
854         print: function() {
855             window.print();
856         },
857         finishOrder: function() {
858             this.pos.get('selectedOrder').destroy();
859         },
860         refresh: function() {
861             var order = this.pos.get('selectedOrder');
862             $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{
863                     widget:this,
864                     order: order,
865                     orderlines: order.get('orderLines').models,
866                     paymentlines: order.get('paymentLines').models,
867                 }));
868         },
869         close: function(){
870             this._super();
871         }
872     });
873
874     module.PaymentScreenWidget = module.ScreenWidget.extend({
875         template: 'PaymentScreenWidget',
876         back_screen: 'products',
877         next_screen: 'receipt',
878         init: function(parent, options) {
879             var self = this;
880             this._super(parent,options);
881
882             this.pos.bind('change:selectedOrder',function(){
883                     this.bind_events();
884                     this.renderElement();
885                 },this);
886
887             this.bind_events();
888
889             this.line_delete_handler = function(event){
890                 var node = this;
891                 while(node && !node.classList.contains('paymentline')){
892                     node = node.parentNode;
893                 }
894                 if(node){
895                     self.pos.get('selectedOrder').removePaymentline(node.line)   
896                 }
897                 event.stopPropagation();
898             };
899
900             this.line_change_handler = function(event){
901                 var node = this;
902                 while(node && !node.classList.contains('paymentline')){
903                     node = node.parentNode;
904                 }
905                 if(node){
906                     node.line.set_amount(this.value);
907                 }
908                 
909             };
910
911             this.line_click_handler = function(event){
912                 var node = this;
913                 while(node && !node.classList.contains('paymentline')){
914                     node = node.parentNode;
915                 }
916                 if(node){
917                     self.pos.get('selectedOrder').selectPaymentline(node.line);
918                 }
919             };
920
921             this.hotkey_handler = function(event){
922                 if(event.which === 13){
923                     self.validate_order();
924                 }else if(event.which === 27){
925                     self.back();
926                 }
927             };
928
929         },
930         show: function(){
931             this._super();
932             var self = this;
933             
934             this.enable_numpad();
935             this.focus_selected_line();
936             
937             document.body.addEventListener('keyup', this.hotkey_handler);
938             
939
940
941             this.add_action_button({
942                     label: _t('Back'),
943                     icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
944                     click: function(){  
945                         self.back();
946                     },
947                 });
948
949             this.add_action_button({
950                     label: _t('Validate'),
951                     name: 'validation',
952                     icon: '/point_of_sale/static/src/img/icons/png48/validate.png',
953                     click: function(){
954                         self.validate_order();
955                     },
956                 });
957            
958             if( this.pos.config.iface_invoicing ){
959                 this.add_action_button({
960                         label: 'Invoice',
961                         name: 'invoice',
962                         icon: '/point_of_sale/static/src/img/icons/png48/invoice.png',
963                         click: function(){
964                             self.validate_order({invoice: true});
965                         },
966                     });
967             }
968
969             if( this.pos.config.iface_cashdrawer ){
970                 this.add_action_button({
971                         label: _t('Cash'),
972                         name: 'cashbox',
973                         icon: '/point_of_sale/static/src/img/open-cashbox.png',
974                         click: function(){
975                             self.pos.proxy.open_cashbox();
976                         },
977                     });
978             }
979
980             this.update_payment_summary();
981
982         },
983         close: function(){
984             this._super();
985             this.disable_numpad();
986             document.body.removeEventListener('keyup',this.hotkey_handler);
987         },
988         remove_empty_lines: function(){
989             var order = this.pos.get('selectedOrder');
990             var lines = order.get('paymentLines').models.slice(0);
991             for(var i = 0; i < lines.length; i++){ 
992                 var line = lines[i];
993                 if(line.get_amount() === 0){
994                     order.removePaymentline(line);
995                 }
996             }
997         },
998         back: function() {
999             this.remove_empty_lines();
1000             this.pos_widget.screen_selector.set_current_screen(this.back_screen);
1001         },
1002         bind_events: function() {
1003             if(this.old_order){
1004                 this.old_order.unbind(null,null,this);
1005             }
1006             var order = this.pos.get('selectedOrder');
1007                 order.bind('change:selected_paymentline',this.focus_selected_line,this);
1008
1009             this.old_order = order;
1010
1011             if(this.old_paymentlines){
1012                 this.old_paymentlines.unbind(null,null,this);
1013             }
1014             var paymentlines = order.get('paymentLines');
1015                 paymentlines.bind('add', this.add_paymentline, this);
1016                 paymentlines.bind('change:selected', this.rerender_paymentline, this);
1017                 paymentlines.bind('change:amount', function(line){
1018                         if(!line.selected && line.node){
1019                             line.node.value = line.amount.toFixed(2);
1020                         }
1021                         this.update_payment_summary();
1022                     },this);
1023                 paymentlines.bind('remove', this.remove_paymentline, this);
1024                 paymentlines.bind('all', this.update_payment_summary, this);
1025
1026             this.old_paymentlines = paymentlines;
1027
1028             if(this.old_orderlines){
1029                 this.old_orderlines.unbind(null,null,this);
1030             }
1031             var orderlines = order.get('orderLines');
1032                 orderlines.bind('all', this.update_payment_summary, this);
1033
1034             this.old_orderlines = orderlines;
1035         },
1036         focus_selected_line: function(){
1037             var line = this.pos.get('selectedOrder').selected_paymentline;
1038             if(line){
1039                 var input = line.node.querySelector('input');
1040                 if(!input){
1041                     return;
1042                 }
1043                 var value = input.value;
1044                 input.focus();
1045
1046                 if(this.numpad_state){
1047                     this.numpad_state.reset();
1048                 }
1049
1050                 if(Number(value) === 0){
1051                     input.value = '';
1052                 }else{
1053                     input.value = value;
1054                     input.select();
1055                 }
1056             }
1057         },
1058         add_paymentline: function(line) {
1059             var list_container = this.el.querySelector('.payment-lines');
1060                 list_container.appendChild(this.render_paymentline(line));
1061             
1062             if(this.numpad_state){
1063                 this.numpad_state.reset();
1064             }
1065         },
1066         render_paymentline: function(line){
1067             var el_html  = openerp.qweb.render('Paymentline',{widget: this, line: line});
1068                 el_html  = _.str.trim(el_html);
1069
1070             var el_node  = document.createElement('tbody');
1071                 el_node.innerHTML = el_html;
1072                 el_node = el_node.childNodes[0];
1073                 el_node.line = line;
1074                 el_node.querySelector('.paymentline-delete')
1075                     .addEventListener('click', this.line_delete_handler);
1076                 el_node.addEventListener('click', this.line_click_handler);
1077                 el_node.querySelector('input')
1078                     .addEventListener('keyup', this.line_change_handler);
1079
1080             line.node = el_node;
1081
1082             return el_node;
1083         },
1084         rerender_paymentline: function(line){
1085             var old_node = line.node;
1086             var new_node = this.render_paymentline(line);
1087             
1088             old_node.parentNode.replaceChild(new_node,old_node);
1089         },
1090         remove_paymentline: function(line){
1091             line.node.parentNode.removeChild(line.node);
1092             line.node = undefined;
1093         },
1094         renderElement: function(){
1095             this._super();
1096
1097             var paymentlines   = this.pos.get('selectedOrder').get('paymentLines').models;
1098             var list_container = this.el.querySelector('.payment-lines');
1099
1100             for(var i = 0; i < paymentlines.length; i++){
1101                 list_container.appendChild(this.render_paymentline(paymentlines[i]));
1102             }
1103             
1104             this.update_payment_summary();
1105         },
1106         update_payment_summary: function() {
1107             var currentOrder = this.pos.get('selectedOrder');
1108             var paidTotal = currentOrder.getPaidTotal();
1109             var dueTotal = currentOrder.getTotalTaxIncluded();
1110             var remaining = dueTotal > paidTotal ? dueTotal - paidTotal : 0;
1111             var change = paidTotal > dueTotal ? paidTotal - dueTotal : 0;
1112
1113             this.$('.payment-due-total').html(this.format_currency(dueTotal));
1114             this.$('.payment-paid-total').html(this.format_currency(paidTotal));
1115             this.$('.payment-remaining').html(this.format_currency(remaining));
1116             this.$('.payment-change').html(this.format_currency(change));
1117             if(currentOrder.selected_orderline === undefined){
1118                 remaining = 1;  // What is this ? 
1119             }
1120                 
1121             if(this.pos_widget.action_bar){
1122                 this.pos_widget.action_bar.set_button_disabled('validation', !this.is_paid());
1123                 this.pos_widget.action_bar.set_button_disabled('invoice', !this.is_paid());
1124             }
1125         },
1126         is_paid: function(){
1127             var currentOrder = this.pos.get('selectedOrder');
1128             return (currentOrder.getTotalTaxIncluded() >= 0.000001 
1129                    && currentOrder.getPaidTotal() + 0.000001 >= currentOrder.getTotalTaxIncluded());
1130
1131         },
1132         validate_order: function(options) {
1133             var self = this;
1134             options = options || {};
1135
1136             var currentOrder = this.pos.get('selectedOrder');
1137
1138             if(!this.is_paid()){
1139                 return;
1140             }
1141
1142             if(    this.pos.config.iface_cashdrawer 
1143                 && this.pos.get('selectedOrder').get('paymentLines').find( function(pl){ 
1144                            return pl.cashregister.journal.type === 'cash'; 
1145                    })){
1146                     this.pos.proxy.open_cashbox();
1147             }
1148
1149             if(options.invoice){
1150                 // deactivate the validation button while we try to send the order
1151                 this.pos_widget.action_bar.set_button_disabled('validation',true);
1152                 this.pos_widget.action_bar.set_button_disabled('invoice',true);
1153
1154                 var invoiced = this.pos.push_and_invoice_order(currentOrder);
1155
1156                 invoiced.fail(function(error){
1157                     if(error === 'error-no-client'){
1158                         self.pos_widget.screen_selector.show_popup('error-no-client');
1159                     }else{
1160                         self.pos_widget.screen_selector.show_popup('error-invoice-transfer');
1161                     }
1162                     self.pos_widget.action_bar.set_button_disabled('validation',false);
1163                     self.pos_widget.action_bar.set_button_disabled('invoice',false);
1164                 });
1165
1166                 invoiced.done(function(){
1167                     self.pos_widget.action_bar.set_button_disabled('validation',false);
1168                     self.pos_widget.action_bar.set_button_disabled('invoice',false);
1169                     self.pos.get('selectedOrder').destroy();
1170                 });
1171
1172             }else{
1173                 this.pos.push_order(currentOrder) 
1174                 if(this.pos.config.iface_print_via_proxy){
1175                     this.pos.proxy.print_receipt(currentOrder.export_for_printing());
1176                     this.pos.get('selectedOrder').destroy();    //finish order and go back to scan screen
1177                 }else{
1178                     this.pos_widget.screen_selector.set_current_screen(this.next_screen);
1179                 }
1180             }
1181
1182             // hide onscreen (iOS) keyboard 
1183             setTimeout(function(){
1184                 document.activeElement.blur();
1185                 $("input").blur();
1186             },250);
1187         },
1188         enable_numpad: function(){
1189             this.disable_numpad();  //ensure we don't register the callbacks twice
1190             this.numpad_state = this.pos_widget.numpad.state;
1191             if(this.numpad_state){
1192                 this.numpad_state.reset();
1193                 this.numpad_state.changeMode('payment');
1194                 this.numpad_state.bind('set_value',   this.set_value, this);
1195                 this.numpad_state.bind('change:mode', this.set_mode_back_to_payment, this);
1196             }
1197                     
1198         },
1199         disable_numpad: function(){
1200             if(this.numpad_state){
1201                 this.numpad_state.unbind('set_value',  this.set_value);
1202                 this.numpad_state.unbind('change:mode',this.set_mode_back_to_payment);
1203             }
1204         },
1205         set_mode_back_to_payment: function() {
1206                 this.numpad_state.set({mode: 'payment'});
1207         },
1208         set_value: function(val) {
1209             var selected_line =this.pos.get('selectedOrder').selected_paymentline;
1210             if(selected_line){
1211                 selected_line.set_amount(val);
1212                 selected_line.node.querySelector('input').value = selected_line.amount.toFixed(2);
1213             }
1214         },
1215     });
1216 }