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