[MERGE] forward port of branch 8.0 up to 591e329
[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
215         // this method shows the screen and sets up all the widget related to this screen. Extend this method
216         // if you want to alter the behavior of the screen.
217         show: function(){
218             var self = this;
219
220             this.hidden = false;
221             if(this.$el){
222                 this.$el.removeClass('oe_hidden');
223             }
224
225             var self = this;
226
227             this.pos_widget.set_numpad_visible(this.show_numpad);
228             this.pos_widget.set_leftpane_visible(this.show_leftpane);
229
230             this.pos_widget.username.set_user_mode(this.pos_widget.screen_selector.get_user_mode());
231
232             this.pos.barcode_reader.set_action_callback({
233                 'cashier': self.barcode_cashier_action ? function(code){ self.barcode_cashier_action(code); } : undefined ,
234                 'product': self.barcode_product_action ? function(code){ self.barcode_product_action(code); } : undefined ,
235                 'client' : self.barcode_client_action ?  function(code){ self.barcode_client_action(code);  } : undefined ,
236                 'discount': self.barcode_discount_action ? function(code){ self.barcode_discount_action(code); } : undefined,
237                 'error'   : self.barcode_error_action ?  function(code){ self.barcode_error_action(code);   } : undefined,
238             });
239         },
240
241         // this method is called when the screen is closed to make place for a new screen. this is a good place
242         // to put your cleanup stuff as it is guaranteed that for each show() there is one and only one close()
243         close: function(){
244             if(this.pos.barcode_reader){
245                 this.pos.barcode_reader.reset_action_callbacks();
246             }
247         },
248
249         // this methods hides the screen. It's not a good place to put your cleanup stuff as it is called on the
250         // POS initialization.
251         hide: function(){
252             this.hidden = true;
253             if(this.$el){
254                 this.$el.addClass('oe_hidden');
255             }
256         },
257
258         // we need this because some screens re-render themselves when they are hidden
259         // (due to some events, or magic, or both...)  we must make sure they remain hidden.
260         // the good solution would probably be to make them not re-render themselves when they
261         // are hidden. 
262         renderElement: function(){
263             this._super();
264             if(this.hidden){
265                 if(this.$el){
266                     this.$el.addClass('oe_hidden');
267                 }
268             }
269         },
270     });
271
272     module.PopUpWidget = module.PosBaseWidget.extend({
273         show: function(){
274             if(this.$el){
275                 this.$el.removeClass('oe_hidden');
276             }
277         },
278         /* called before hide, when a popup is closed */
279         close: function(){
280         },
281         /* hides the popup. keep in mind that this is called in the initialization pass of the 
282          * pos instantiation, so you don't want to do anything fancy in here */
283         hide: function(){
284             if(this.$el){
285                 this.$el.addClass('oe_hidden');
286             }
287         },
288     });
289
290     module.FullscreenPopup = module.PopUpWidget.extend({
291         template:'FullscreenPopupWidget',
292         show: function(){
293             var self = this;
294             this._super();
295             this.renderElement();
296             this.$('.button.fullscreen').off('click').click(function(){
297                 window.document.body.webkitRequestFullscreen();
298                 self.pos_widget.screen_selector.close_popup();
299             });
300             this.$('.button.cancel').off('click').click(function(){
301                 self.pos_widget.screen_selector.close_popup();
302             });
303         },
304         ismobile: function(){
305             return typeof window.orientation !== 'undefined'; 
306         }
307     });
308
309
310     module.ErrorPopupWidget = module.PopUpWidget.extend({
311         template:'ErrorPopupWidget',
312         show: function(options){
313             options = options || {};
314             var self = this;
315             this._super();
316
317             $('body').append('<audio src="/point_of_sale/static/src/sounds/error.wav" autoplay="true"></audio>');
318
319             this.message = options.message || _t('Error');
320             this.comment = options.comment || '';
321
322             this.renderElement();
323
324             this.pos.barcode_reader.save_callbacks();
325             this.pos.barcode_reader.reset_action_callbacks();
326
327             this.$('.footer .button').click(function(){
328                 self.pos_widget.screen_selector.close_popup();
329                 if ( options.confirm ) {
330                     options.confirm.call(self);
331                 }
332             });
333         },
334         close:function(){
335             this._super();
336             this.pos.barcode_reader.restore_callbacks();
337         },
338     });
339
340     module.ErrorTracebackPopupWidget = module.ErrorPopupWidget.extend({
341         template:'ErrorTracebackPopupWidget',
342     });
343
344     module.ErrorBarcodePopupWidget = module.ErrorPopupWidget.extend({
345         template:'ErrorBarcodePopupWidget',
346         show: function(barcode){
347             this.barcode = barcode;
348             this._super();
349         },
350     });
351
352     module.ConfirmPopupWidget = module.PopUpWidget.extend({
353         template: 'ConfirmPopupWidget',
354         show: function(options){
355             var self = this;
356             this._super();
357
358             this.message = options.message || '';
359             this.comment = options.comment || '';
360             this.renderElement();
361             
362             this.$('.button.cancel').click(function(){
363                 self.pos_widget.screen_selector.close_popup();
364                 if( options.cancel ){
365                     options.cancel.call(self);
366                 }
367             });
368
369             this.$('.button.confirm').click(function(){
370                 self.pos_widget.screen_selector.close_popup();
371                 if( options.confirm ){
372                     options.confirm.call(self);
373                 }
374             });
375         },
376     });
377
378     module.ErrorInvoiceTransferPopupWidget = module.ErrorPopupWidget.extend({
379         template: 'ErrorInvoiceTransferPopupWidget',
380     });
381
382     module.UnsentOrdersPopupWidget = module.PopUpWidget.extend({
383         template: 'UnsentOrdersPopupWidget',
384         show: function(options){
385             var self = this;
386             this._super(options);
387             this.renderElement();
388             this.$('.button.confirm').click(function(){
389                 self.pos_widget.screen_selector.close_popup();
390             });
391         },
392     });
393
394     module.ScaleScreenWidget = module.ScreenWidget.extend({
395         template:'ScaleScreenWidget',
396
397         next_screen: 'products',
398         previous_screen: 'products',
399
400         show_leftpane:   false,
401
402         show: function(){
403             this._super();
404             var self = this;
405             var queue = this.pos.proxy_queue;
406
407             this.set_weight(0);
408             this.renderElement();
409
410             this.hotkey_handler = function(event){
411                 if(event.which === 13){
412                     self.order_product();
413                     self.pos_widget.screen_selector.set_current_screen(self.next_screen);
414                 }else if(event.which === 27){
415                     self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
416                 }
417             };
418
419             $('body').on('keyup',this.hotkey_handler);
420
421             this.$('.back').click(function(){
422                 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
423             });
424
425             this.$('.next,.buy-product').click(function(){
426                 self.order_product();
427                 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
428             });
429
430             queue.schedule(function(){
431                 return self.pos.proxy.scale_read().then(function(weight){
432                     self.set_weight(weight.weight);
433                 });
434             },{duration:50, repeat: true});
435
436         },
437         get_product: function(){
438             var ss = this.pos_widget.screen_selector;
439             if(ss){
440                 return ss.get_current_screen_param('product');
441             }else{
442                 return undefined;
443             }
444         },
445         order_product: function(){
446             this.pos.get('selectedOrder').addProduct(this.get_product(),{ quantity: this.weight });
447         },
448         get_product_name: function(){
449             var product = this.get_product();
450             return (product ? product.display_name : undefined) || 'Unnamed Product';
451         },
452         get_product_price: function(){
453             var product = this.get_product();
454             return (product ? product.price : 0) || 0;
455         },
456         set_weight: function(weight){
457             this.weight = weight;
458             this.$('.weight').text(this.get_product_weight_string());
459             this.$('.computed-price').text(this.get_computed_price_string());
460         },
461         get_product_weight_string: function(){
462             var product = this.get_product();
463             var defaultstr = (this.weight || 0).toFixed(3) + ' Kg';
464             if(!product || !this.pos){
465                 return defaultstr;
466             }
467             var unit_id = product.uom_id;
468             if(!unit_id){
469                 return defaultstr;
470             }
471             var unit = this.pos.units_by_id[unit_id[0]];
472             var weight = round_pr(this.weight || 0, unit.rounding);
473             var weightstr = weight.toFixed(Math.ceil(Math.log(1.0/unit.rounding) / Math.log(10) ));
474                 weightstr += ' Kg';
475             return weightstr;
476         },
477         get_computed_price_string: function(){
478             return this.format_currency(this.get_product_price() * this.weight);
479         },
480         close: function(){
481             var self = this;
482             this._super();
483             $('body').off('keyup',this.hotkey_handler);
484
485             this.pos.proxy_queue.clear();
486         },
487     });
488
489     module.ProductScreenWidget = module.ScreenWidget.extend({
490         template:'ProductScreenWidget',
491
492         show_numpad:     true,
493         show_leftpane:   true,
494
495         start: function(){ //FIXME this should work as renderElement... but then the categories aren't properly set. explore why
496             var self = this;
497
498             this.product_list_widget = new module.ProductListWidget(this,{
499                 click_product_action: function(product){
500                     if(product.to_weight && self.pos.config.iface_electronic_scale){
501                         self.pos_widget.screen_selector.set_current_screen('scale',{product: product});
502                     }else{
503                         self.pos.get('selectedOrder').addProduct(product);
504                     }
505                 },
506                 product_list: this.pos.db.get_product_by_category(0)
507             });
508             this.product_list_widget.replace(this.$('.placeholder-ProductListWidget'));
509
510             this.product_categories_widget = new module.ProductCategoriesWidget(this,{
511                 product_list_widget: this.product_list_widget,
512             });
513             this.product_categories_widget.replace(this.$('.placeholder-ProductCategoriesWidget'));
514         },
515
516         show: function(){
517             this._super();
518             var self = this;
519
520             this.product_categories_widget.reset_category();
521
522             this.pos_widget.order_widget.set_editable(true);
523         },
524
525         close: function(){
526             this._super();
527
528             this.pos_widget.order_widget.set_editable(false);
529
530             if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
531                 this.pos_widget.onscreen_keyboard.hide();
532             }
533         },
534     });
535
536     module.ClientListScreenWidget = module.ScreenWidget.extend({
537         template: 'ClientListScreenWidget',
538
539         init: function(parent, options){
540             this._super(parent, options);
541             this.partner_cache = new module.DomCache();
542         },
543
544         show_leftpane: false,
545
546         auto_back: true,
547
548         show: function(){
549             var self = this;
550             this._super();
551
552             this.renderElement();
553             this.details_visible = false;
554             this.old_client = this.pos.get('selectedOrder').get('client');
555             this.new_client = this.old_client;
556
557             this.$('.back').click(function(){
558                 self.pos_widget.screen_selector.back();
559             });
560
561             this.$('.next').click(function(){
562                 self.save_changes();
563                 self.pos_widget.screen_selector.back();
564             });
565
566             this.$('.new-customer').click(function(){
567                 self.display_client_details('edit',{
568                     'country_id': self.pos.company.country_id,
569                 });
570             });
571
572             var partners = this.pos.db.get_partners_sorted(1000);
573             this.render_list(partners);
574             
575             this.reload_partners();
576
577             if( this.old_client ){
578                 this.display_client_details('show',this.old_client,0);
579             }
580
581             this.$('.client-list-contents').delegate('.client-line','click',function(event){
582                 self.line_select(event,$(this),parseInt($(this).data('id')));
583             });
584
585             var search_timeout = null;
586
587             if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
588                 this.pos_widget.onscreen_keyboard.connect(this.$('.searchbox input'));
589             }
590
591             this.$('.searchbox input').on('keyup',function(event){
592                 clearTimeout(search_timeout);
593
594                 var query = this.value;
595
596                 search_timeout = setTimeout(function(){
597                     self.perform_search(query,event.which === 13);
598                 },70);
599             });
600
601             this.$('.searchbox .search-clear').click(function(){
602                 self.clear_search();
603             });
604         },
605         barcode_client_action: function(code){
606             if (this.editing_client) {
607                 this.$('.detail.barcode').val(code.code);
608             } else if (this.pos.db.get_partner_by_ean13(code.code)) {
609                 this.display_client_details('show',this.pos.db.get_partner_by_ean13(code.code));
610             }
611         },
612         perform_search: function(query, associate_result){
613             if(query){
614                 var customers = this.pos.db.search_partner(query);
615                 this.display_client_details('hide');
616                 if ( associate_result && customers.length === 1){
617                     this.new_client = customers[0];
618                     this.save_changes();
619                     this.pos_widget.screen_selector.back();
620                 }
621                 this.render_list(customers);
622             }else{
623                 var customers = this.pos.db.get_partners_sorted();
624                 this.render_list(customers);
625             }
626         },
627         clear_search: function(){
628             var customers = this.pos.db.get_partners_sorted(1000);
629             this.render_list(customers);
630             this.$('.searchbox input')[0].value = '';
631             this.$('.searchbox input').focus();
632         },
633         render_list: function(partners){
634             var contents = this.$el[0].querySelector('.client-list-contents');
635             contents.innerHTML = "";
636             for(var i = 0, len = Math.min(partners.length,1000); i < len; i++){
637                 var partner    = partners[i];
638                 var clientline = this.partner_cache.get_node(partner.id);
639                 if(!clientline){
640                     var clientline_html = QWeb.render('ClientLine',{widget: this, partner:partners[i]});
641                     var clientline = document.createElement('tbody');
642                     clientline.innerHTML = clientline_html;
643                     clientline = clientline.childNodes[1];
644                     this.partner_cache.cache_node(partner.id,clientline);
645                 }
646                 if( partners === this.new_client ){
647                     clientline.classList.add('highlight');
648                 }else{
649                     clientline.classList.remove('highlight');
650                 }
651                 contents.appendChild(clientline);
652             }
653         },
654         save_changes: function(){
655             if( this.has_client_changed() ){
656                 this.pos.get('selectedOrder').set_client(this.new_client);
657             }
658         },
659         has_client_changed: function(){
660             if( this.old_client && this.new_client ){
661                 return this.old_client.id !== this.new_client.id;
662             }else{
663                 return !!this.old_client !== !!this.new_client;
664             }
665         },
666         toggle_save_button: function(){
667             var $button = this.$('.button.next');
668             if (this.editing_client) {
669                 $button.addClass('oe_hidden');
670                 return;
671             } else if( this.new_client ){
672                 if( !this.old_client){
673                     $button.text(_t('Set Customer'));
674                 }else{
675                     $button.text(_t('Change Customer'));
676                 }
677             }else{
678                 $button.text(_t('Deselect Customer'));
679             }
680             $button.toggleClass('oe_hidden',!this.has_client_changed());
681         },
682         line_select: function(event,$line,id){
683             var partner = this.pos.db.get_partner_by_id(id);
684             this.$('.client-list .lowlight').removeClass('lowlight');
685             if ( $line.hasClass('highlight') ){
686                 $line.removeClass('highlight');
687                 $line.addClass('lowlight');
688                 this.display_client_details('hide',partner);
689                 this.new_client = null;
690                 this.toggle_save_button();
691             }else{
692                 this.$('.client-list .highlight').removeClass('highlight');
693                 $line.addClass('highlight');
694                 var y = event.pageY - $line.parent().offset().top
695                 this.display_client_details('show',partner,y);
696                 this.new_client = partner;
697                 this.toggle_save_button();
698             }
699         },
700         partner_icon_url: function(id){
701             return '/web/binary/image?model=res.partner&id='+id+'&field=image_small';
702         },
703
704         // ui handle for the 'edit selected customer' action
705         edit_client_details: function(partner) {
706             this.display_client_details('edit',partner);
707         },
708
709         // ui handle for the 'cancel customer edit changes' action
710         undo_client_details: function(partner) {
711             if (!partner.id) {
712                 this.display_client_details('hide');
713             } else {
714                 this.display_client_details('show',partner);
715             }
716         },
717
718         // what happens when we save the changes on the client edit form -> we fetch the fields, sanitize them,
719         // send them to the backend for update, and call saved_client_details() when the server tells us the
720         // save was successfull.
721         save_client_details: function(partner) {
722             var self = this;
723             
724             var fields = {}
725             this.$('.client-details-contents .detail').each(function(idx,el){
726                 fields[el.name] = el.value;
727             });
728
729             if (!fields.name) {
730                 this.pos_widget.screen_selector.show_popup('error',{
731                     message: _t('A Customer Name Is Required'),
732                 });
733                 return;
734             }
735             
736             if (this.uploaded_picture) {
737                 fields.image = this.uploaded_picture;
738             }
739
740             fields.id           = partner.id || false;
741             fields.country_id   = fields.country_id || false;
742             fields.ean13        = fields.ean13 ? this.pos.barcode_reader.sanitize_ean(fields.ean13) : false; 
743
744             new instance.web.Model('res.partner').call('create_from_ui',[fields]).then(function(partner_id){
745                 self.saved_client_details(partner_id);
746             },function(err,event){
747                 event.preventDefault();
748                 self.pos_widget.screen_selector.show_popup('error',{
749                     'message':_t('Error: Could not Save Changes'),
750                     'comment':_t('Your Internet connection is probably down.'),
751                 });
752             });
753         },
754         
755         // what happens when we've just pushed modifications for a partner of id partner_id
756         saved_client_details: function(partner_id){
757             var self = this;
758             this.reload_partners().then(function(){
759                 var partner = self.pos.db.get_partner_by_id(partner_id);
760                 if (partner) {
761                     self.new_client = partner;
762                     self.toggle_save_button();
763                     self.display_client_details('show',partner);
764                 } else {
765                     // should never happen, because create_from_ui must return the id of the partner it
766                     // has created, and reload_partner() must have loaded the newly created partner. 
767                     self.display_client_details('hide');
768                 }
769             });
770         },
771
772         // resizes an image, keeping the aspect ratio intact,
773         // the resize is useful to avoid sending 12Mpixels jpegs
774         // over a wireless connection.
775         resize_image_to_dataurl: function(img, maxwidth, maxheight, callback){
776             img.onload = function(){
777                 var png = new Image();
778                 var canvas = document.createElement('canvas');
779                 var ctx    = canvas.getContext('2d');
780                 var ratio  = 1;
781
782                 if (img.width > maxwidth) {
783                     ratio = maxwidth / img.width;
784                 }
785                 if (img.height * ratio > maxheight) {
786                     ratio = maxheight / img.height;
787                 }
788                 var width  = Math.floor(img.width * ratio);
789                 var height = Math.floor(img.height * ratio);
790
791                 canvas.width  = width;
792                 canvas.height = height;
793                 ctx.drawImage(img,0,0,width,height);
794
795                 var dataurl = canvas.toDataURL();
796                 callback(dataurl);
797             }
798         },
799
800         // Loads and resizes a File that contains an image.
801         // callback gets a dataurl in case of success.
802         load_image_file: function(file, callback){
803             var self = this;
804             if (!file.type.match(/image.*/)) {
805                 this.pos_widget.screen_selector.show_popup('error',{
806                     message:_t('Unsupported File Format'),
807                     comment:_t('Only web-compatible Image formats such as .png or .jpeg are supported'),
808                 });
809                 return;
810             }
811             
812             var reader = new FileReader();
813             reader.onload = function(event){
814                 var dataurl = event.target.result;
815                 var img     = new Image();
816                 img.src = dataurl;
817                 self.resize_image_to_dataurl(img,800,600,callback);
818             }
819             reader.onerror = function(){
820                 self.pos_widget.screen_selector.show_popup('error',{
821                     message:_t('Could Not Read Image'),
822                     comment:_t('The provided file could not be read due to an unknown error'),
823                 });
824             };
825             reader.readAsDataURL(file);
826         },
827
828         // This fetches partner changes on the server, and in case of changes, 
829         // rerenders the affected views
830         reload_partners: function(){
831             var self = this;
832             return this.pos.load_new_partners().then(function(){
833                 self.render_list(self.pos.db.get_partners_sorted(1000));
834                 
835                 // update the currently assigned client if it has been changed in db.
836                 var curr_client = self.pos.get_order().get_client();
837                 if (curr_client) {
838                     self.pos.get_order().set_client(self.pos.db.get_partner_by_id(curr_client.id));
839                 }
840             });
841         },
842
843         // Shows,hides or edit the customer details box :
844         // visibility: 'show', 'hide' or 'edit'
845         // partner:    the partner object to show or edit
846         // clickpos:   the height of the click on the list (in pixel), used
847         //             to maintain consistent scroll.
848         display_client_details: function(visibility,partner,clickpos){
849             var self = this;
850             var contents = this.$('.client-details-contents');
851             var parent   = this.$('.client-list').parent();
852             var scroll   = parent.scrollTop();
853             var height   = contents.height();
854
855             contents.off('click','.button.edit'); 
856             contents.off('click','.button.save'); 
857             contents.off('click','.button.undo'); 
858             contents.on('click','.button.edit',function(){ self.edit_client_details(partner); });
859             contents.on('click','.button.save',function(){ self.save_client_details(partner); });
860             contents.on('click','.button.undo',function(){ self.undo_client_details(partner); });
861             this.editing_client = false;
862             this.uploaded_picture = null;
863
864             if(visibility === 'show'){
865                 contents.empty();
866                 contents.append($(QWeb.render('ClientDetails',{widget:this,partner:partner})));
867
868                 var new_height   = contents.height();
869
870                 if(!this.details_visible){
871                     if(clickpos < scroll + new_height + 20 ){
872                         parent.scrollTop( clickpos - 20 );
873                     }else{
874                         parent.scrollTop(parent.scrollTop() + new_height);
875                     }
876                 }else{
877                     parent.scrollTop(parent.scrollTop() - height + new_height);
878                 }
879
880                 this.details_visible = true;
881                 this.toggle_save_button();
882             } else if (visibility === 'edit') {
883                 this.editing_client = true;
884                 contents.empty();
885                 contents.append($(QWeb.render('ClientDetailsEdit',{widget:this,partner:partner})));
886                 this.toggle_save_button();
887
888                 contents.find('.image-uploader').on('change',function(){
889                     self.load_image_file(event.target.files[0],function(res){
890                         if (res) {
891                             contents.find('.client-picture img, .client-picture .fa').remove();
892                             contents.find('.client-picture').append("<img src='"+res+"'>");
893                             contents.find('.detail.picture').remove();
894                             self.uploaded_picture = res;
895                         }
896                     });
897                 });
898             } else if (visibility === 'hide') {
899                 contents.empty();
900                 if( height > scroll ){
901                     contents.css({height:height+'px'});
902                     contents.animate({height:0},400,function(){
903                         contents.css({height:''});
904                     });
905                 }else{
906                     parent.scrollTop( parent.scrollTop() - height);
907                 }
908                 this.details_visible = false;
909                 this.toggle_save_button();
910             }
911         },
912         close: function(){
913             this._super();
914         },
915     });
916
917     module.ReceiptScreenWidget = module.ScreenWidget.extend({
918         template: 'ReceiptScreenWidget',
919         show_numpad:     false,
920         show_leftpane:   false,
921
922         show: function(){
923             this._super();
924             var self = this;
925
926             this.refresh();
927
928             if (!this.pos.get('selectedOrder')._printed) {
929                 this.print();
930             }
931
932             // The problem is that in chrome the print() is asynchronous and doesn't
933             // execute until all rpc are finished. So it conflicts with the rpc used
934             // to send the orders to the backend, and the user is able to go to the next 
935             // screen before the printing dialog is opened. The problem is that what's 
936             // printed is whatever is in the page when the dialog is opened and not when it's called,
937             // and so you end up printing the product list instead of the receipt... 
938             //
939             // Fixing this would need a re-architecturing
940             // of the code to postpone sending of orders after printing.
941             //
942             // But since the print dialog also blocks the other asynchronous calls, the
943             // button enabling in the setTimeout() is blocked until the printing dialog is 
944             // closed. But the timeout has to be big enough or else it doesn't work
945             // 2 seconds is the same as the default timeout for sending orders and so the dialog
946             // should have appeared before the timeout... so yeah that's not ultra reliable. 
947
948             this.lock_screen(true);  
949             setTimeout(function(){
950                 self.lock_screen(false);  
951             }, 2000);
952         },
953         lock_screen: function(locked) {
954             this._locked = locked;
955             if (locked) {
956                 this.$('.next').removeClass('highlight');
957             } else {
958                 this.$('.next').addClass('highlight');
959             }
960         },
961         print: function() {
962             this.pos.get('selectedOrder')._printed = true;
963             window.print();
964         },
965         finish_order: function() {
966             if (!this._locked) {
967                 this.pos.get_order().finalize();
968             }
969         },
970         renderElement: function() {
971             var self = this;
972             this._super();
973             this.$('.next').click(function(){
974                 self.finish_order();
975             });
976             this.$('.button.print').click(function(){
977                 self.print();
978             });
979         },
980         refresh: function() {
981             var order = this.pos.get_order();
982             this.$('.pos-receipt-container').html(QWeb.render('PosTicket',{
983                     widget:this,
984                     order: order,
985                     orderlines: order.get('orderLines').models,
986                     paymentlines: order.get('paymentLines').models,
987                 }));
988         },
989     });
990
991     module.PaymentScreenWidget = module.ScreenWidget.extend({
992         template:      'PaymentScreenWidget',
993         back_screen:   'product',
994         next_screen:   'receipt',
995         show_leftpane: false,
996         show_numpad:   false,
997         init: function(parent, options) {
998             var self = this;
999             this._super(parent, options);
1000
1001             this.pos.bind('change:selectedOrder',function(){
1002                     this.renderElement();
1003                     this.watch_order_changes();
1004                 },this);
1005             this.watch_order_changes();
1006
1007             this.inputbuffer = "";
1008             this.firstinput  = true;
1009             this.keyboard_handler = function(event){
1010                 var key = '';
1011                 if ( event.keyCode === 13 ) {         // Enter
1012                     self.validate_order();
1013                 } else if ( event.keyCode === 190 ) { // Dot
1014                     key = '.';
1015                 } else if ( event.keyCode === 46 ) {  // Delete
1016                     key = 'CLEAR';
1017                 } else if ( event.keyCode === 8 ) {   // Backspace 
1018                     key = 'BACKSPACE';
1019                     event.preventDefault(); // Prevents history back nav
1020                 } else if ( event.keyCode >= 48 && event.keyCode <= 57 ){       // Numbers
1021                     key = '' + (event.keyCode - 48);
1022                 } else if ( event.keyCode >= 96 && event.keyCode <= 105 ){      // Numpad Numbers
1023                     key = '' + (event.keyCode - 96);
1024                 } else if ( event.keyCode === 189 || event.keyCode === 109 ) {  // Minus
1025                     key = '-';
1026                 } else if ( event.keyCode === 107 ) { // Plus
1027                     key = '+';
1028                 }
1029
1030                 self.payment_input(key);
1031
1032             };
1033         },
1034         // resets the current input buffer
1035         reset_input: function(){
1036             var line = this.pos.get_order().selected_paymentline;
1037             this.firstinput  = true;
1038             if (line) {
1039                 this.inputbuffer = this.format_currency_no_symbol(line.get_amount());
1040             } else {
1041                 this.inputbuffer = "";
1042             }
1043         },
1044         // handle both keyboard and numpad input. Accepts
1045         // a string that represents the key pressed.
1046         payment_input: function(input) {
1047             var oldbuf = this.inputbuffer.slice(0);
1048
1049             if (input === '.') {
1050                 if (this.firstinput) {
1051                     this.inputbuffer = "0.";
1052                 }else if (!this.inputbuffer.length || this.inputbuffer === '-') {
1053                     this.inputbuffer += "0.";
1054                 } else if (this.inputbuffer.indexOf('.') < 0){
1055                     this.inputbuffer = this.inputbuffer + '.';
1056                 }
1057             } else if (input === 'CLEAR') {
1058                 this.inputbuffer = ""; 
1059             } else if (input === 'BACKSPACE') { 
1060                 this.inputbuffer = this.inputbuffer.substring(0,this.inputbuffer.length - 1);
1061             } else if (input === '+') {
1062                 if ( this.inputbuffer[0] === '-' ) {
1063                     this.inputbuffer = this.inputbuffer.substring(1,this.inputbuffer.length);
1064                 }
1065             } else if (input === '-') {
1066                 if ( this.inputbuffer[0] === '-' ) {
1067                     this.inputbuffer = this.inputbuffer.substring(1,this.inputbuffer.length);
1068                 } else {
1069                     this.inputbuffer = '-' + this.inputbuffer;
1070                 }
1071             } else if (input[0] === '+' && !isNaN(parseFloat(input))) {
1072                 this.inputbuffer = '' + ((parseFloat(this.inputbuffer) || 0) + parseFloat(input));
1073             } else if (!isNaN(parseInt(input))) {
1074                 if (this.firstinput) {
1075                     this.inputbuffer = '' + input;
1076                 } else {
1077                     this.inputbuffer += input;
1078                 }
1079             }
1080
1081             this.firstinput = false;
1082
1083             if (this.inputbuffer !== oldbuf) {
1084                 var order = this.pos.get_order();
1085                 if (order.selected_paymentline) {
1086                     order.selected_paymentline.set_amount(parseFloat(this.inputbuffer));
1087                     this.order_changes();
1088                     this.render_paymentlines();
1089                     this.$('.paymentline.selected .edit').text(this.inputbuffer);
1090                 }
1091             }
1092         },
1093         click_numpad: function(button) {
1094             this.payment_input(button.data('action'));
1095         },
1096         render_numpad: function() {
1097             var self = this;
1098             var numpad = $(QWeb.render('PaymentScreen-Numpad', { widget:this }));
1099             numpad.on('click','button',function(){
1100                 self.click_numpad($(this));
1101             });
1102             return numpad;
1103         },
1104         click_delete_paymentline: function(cid){
1105             var lines = this.pos.get_order().get('paymentLines').models;
1106             for ( var i = 0; i < lines.length; i++ ) {
1107                 if (lines[i].cid === cid) {
1108                     this.pos.get_order().removePaymentline(lines[i]);
1109                     this.reset_input();
1110                     this.render_paymentlines();
1111                     return;
1112                 }
1113             }
1114         },
1115         click_paymentline: function(cid){
1116             var lines = this.pos.get_order().get('paymentLines').models;
1117             for ( var i = 0; i < lines.length; i++ ) {
1118                 if (lines[i].cid === cid) {
1119                     this.pos.get_order().selectPaymentline(lines[i]);
1120                     this.reset_input();
1121                     this.render_paymentlines();
1122                     return;
1123                 }
1124             }
1125         },
1126         render_paymentlines: function() {
1127             var self  = this;
1128             var order = this.pos.get_order();
1129             var lines = order.get('paymentLines').models;
1130
1131             this.$('.paymentlines-container').empty();
1132             var lines = $(QWeb.render('PaymentScreen-Paymentlines', { 
1133                 widget: this, 
1134                 order: order,
1135                 paymentlines: lines,
1136             }));
1137
1138             lines.on('click','.delete-button',function(){
1139                 self.click_delete_paymentline($(this).data('cid'));
1140             });
1141
1142             lines.on('click','.paymentline',function(){
1143                 self.click_paymentline($(this).data('cid'));
1144             });
1145                 
1146             lines.appendTo(this.$('.paymentlines-container'));
1147         },
1148         click_paymentmethods: function(id) {
1149             var cashregister = null;
1150             for ( var i = 0; i < this.pos.cashregisters.length; i++ ) {
1151                 if ( this.pos.cashregisters[i].journal_id[0] === id ){
1152                     cashregister = this.pos.cashregisters[i];
1153                     break;
1154                 }
1155             }
1156             this.pos.get_order().addPaymentline( cashregister );
1157             this.reset_input();
1158             this.render_paymentlines();
1159         },
1160         render_paymentmethods: function() {
1161             var self = this;
1162             var methods = $(QWeb.render('PaymentScreen-Paymentmethods', { widget:this }));
1163                 methods.on('click','.paymentmethod',function(){
1164                     self.click_paymentmethods($(this).data('id'));
1165                 });
1166             return methods;
1167         },
1168         click_invoice: function(){
1169             var order = this.pos.get_order();
1170             order.set_to_invoice(!order.is_to_invoice());
1171             if (order.is_to_invoice()) {
1172                 this.$('.js_invoice').addClass('highlight');
1173             } else {
1174                 this.$('.js_invoice').removeClass('highlight');
1175             }
1176         },
1177         renderElement: function() {
1178             var self = this;
1179             this._super();
1180
1181             var numpad = this.render_numpad();
1182             numpad.appendTo(this.$('.payment-numpad'));
1183
1184             var methods = this.render_paymentmethods();
1185             methods.appendTo(this.$('.paymentmethods-container'));
1186
1187             this.render_paymentlines();
1188
1189             this.$('.back').click(function(){
1190                 self.pos_widget.screen_selector.back();
1191             });
1192
1193             this.$('.next').click(function(){
1194                 self.validate_order();
1195             });
1196
1197             this.$('.js_invoice').click(function(){
1198                 self.click_invoice();
1199             });
1200
1201         },
1202         show: function(){
1203             this.pos.get_order().clean_empty_paymentlines();
1204             this.reset_input();
1205             this.render_paymentlines();
1206             this.order_changes();
1207             window.document.body.addEventListener('keydown',this.keyboard_handler);
1208             this._super();
1209         },
1210         hide: function(){
1211             window.document.body.removeEventListener('keydown',this.keyboard_handler);
1212             this._super();
1213         },
1214         // sets up listeners to watch for order changes
1215         watch_order_changes: function() {
1216             var self = this;
1217             var order = this.pos.get_order();
1218             if(this.old_order){
1219                 this.old_order.unbind(null,null,this);
1220             }
1221             order.bind('all',function(){
1222                 self.order_changes();
1223             });
1224             this.old_order = order;
1225         },
1226         // called when the order is changed, used to show if
1227         // the order is paid or not
1228         order_changes: function(){
1229             var self = this;
1230             var order = this.pos.get_order();
1231             if (order.isPaid()) {
1232                 self.$('.next').addClass('highlight');
1233             }else{
1234                 self.$('.next').removeClass('highlight');
1235             }
1236         },
1237         // Check if the order is paid, then sends it to the backend,
1238         // and complete the sale process
1239         validate_order: function() {
1240             var self = this;
1241
1242             var order = this.pos.get_order();
1243
1244             if(order.get('orderLines').models.length === 0){
1245                 this.pos_widget.screen_selector.show_popup('error',{
1246                     'message': _t('Empty Order'),
1247                     'comment': _t('There must be at least one product in your order before it can be validated'),
1248                 });
1249                 return;
1250             }
1251
1252             if (!order.isPaid() || this.invoicing) {
1253                 return;
1254             }
1255
1256             // The exact amount must be paid if there is no cash payment method defined.
1257             if (Math.abs(order.getTotalTaxIncluded() - order.getPaidTotal()) > 0.00001) {
1258                 var cash = false;
1259                 for (var i = 0; i < this.pos.cashregisters.length; i++) {
1260                     cash = cash || (this.pos.cashregisters[i].journal.type === 'cash');
1261                 }
1262                 if (!cash) {
1263                     this.pos_widget.screen_selector.show_popup('error',{
1264                         message: _t('Cannot return change without a cash payment method'),
1265                         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'),
1266                     });
1267                     return;
1268                 }
1269             }
1270
1271             if (order.isPaidWithCash() && this.pos.config.iface_cashdrawer) { 
1272             
1273                     this.pos.proxy.open_cashbox();
1274             }
1275
1276             if (order.is_to_invoice()) {
1277                 var invoiced = this.pos.push_and_invoice_order(order);
1278                 this.invoicing = true;
1279
1280                 invoiced.fail(function(error){
1281                     self.invoicing = false;
1282                     if (error === 'error-no-client') {
1283                         self.pos_widget.screen_selector.show_popup('confirm',{
1284                             message: _t('Please select the Customer'),
1285                             comment: _t('You need to select the customer before you can invoice an order.'),
1286                             confirm: function(){
1287                                 self.pos_widget.screen_selector.set_current_screen('clientlist');
1288                             },
1289                         });
1290                     } else {
1291                         self.pos_widget.screen_selector.show_popup('error',{
1292                             message: _t('The order could not be sent'),
1293                             comment: _t('Check your internet connection and try again.'),
1294                         });
1295                     }
1296                 });
1297
1298                 invoiced.done(function(){
1299                     self.invoicing = false;
1300                     order.finalize();
1301                 });
1302             } else {
1303                 this.pos.push_order(order) 
1304                 if (this.pos.config.iface_print_via_proxy) {
1305                     var receipt = currentOrder.export_for_printing();
1306                     this.pos.proxy.print_receipt(QWeb.render('XmlReceipt',{
1307                         receipt: receipt, widget: self,
1308                     }));
1309                     order.finalize();    //finish order and go back to scan screen
1310                 } else {
1311                     this.pos_widget.screen_selector.set_current_screen(this.next_screen);
1312                 }
1313             }
1314         },
1315     });
1316
1317 }