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