[WIP] point_of_sale: mobile ui optimisations
[odoo/odoo.git] / addons / point_of_sale / static / src / js / widgets.js
1 function openerp_pos_widgets(instance, module){ //module is instance.point_of_sale
2     var QWeb = instance.web.qweb,
3         _t = instance.web._t;
4
5     // The ImageCache is used to hide the latency of the application cache on-disk access in chrome 
6     // that causes annoying flickering on product pictures. Why the hell a simple access to
7     // the application cache involves such latency is beyond me, hopefully one day this can be
8     // removed.
9     module.ImageCache   = instance.web.Class.extend({
10         init: function(options){
11             options = options || {};
12             this.max_size = options.max_size || 500;
13
14             this.cache = {};
15             this.access_time = {};
16             this.size = 0;
17         },
18         get_image_uncached: function(url){
19             var img =  new Image();
20             img.src = url;
21             return img;
22         },
23         // returns a DOM Image object from an url, and cache the last 500 (by default) results
24         get_image: function(url){
25             var cached = this.cache[url];
26             if(cached){
27                 this.access_time[url] = (new Date()).getTime();
28                 return cached;
29             }else{
30                 var img = new Image();
31                 img.src = url;
32                 while(this.size >= this.max_size){
33                     var oldestUrl = null;
34                     var oldestTime = (new Date()).getTime();
35                     for(var url in this.cache){
36                         var time = this.access_time[url];
37                         if(time <= oldestTime){
38                             oldestTime = time;
39                             oldestUrl  = url;
40                         }
41                     }
42                     if(oldestUrl){
43                         delete this.cache[oldestUrl];
44                         delete this.access_time[oldestUrl];
45                     }
46                     this.size--;
47                 }
48                 this.cache[url] = img;
49                 this.access_time[url] = (new Date()).getTime();
50                 this.size++;
51                 return img;
52             }
53         },
54     });
55
56     module.NumpadWidget = module.PosBaseWidget.extend({
57         template:'NumpadWidget',
58         init: function(parent, options) {
59             this._super(parent);
60             this.state = new module.NumpadState();
61         },
62         start: function() {
63             this.state.bind('change:mode', this.changedMode, this);
64             this.changedMode();
65             this.$el.find('.numpad-backspace').click(_.bind(this.clickDeleteLastChar, this));
66             this.$el.find('.numpad-minus').click(_.bind(this.clickSwitchSign, this));
67             this.$el.find('.number-char').click(_.bind(this.clickAppendNewChar, this));
68             this.$el.find('.mode-button').click(_.bind(this.clickChangeMode, this));
69         },
70         clickDeleteLastChar: function() {
71             return this.state.deleteLastChar();
72         },
73         clickSwitchSign: function() {
74             return this.state.switchSign();
75         },
76         clickAppendNewChar: function(event) {
77             var newChar;
78             newChar = event.currentTarget.innerText || event.currentTarget.textContent;
79             return this.state.appendNewChar(newChar);
80         },
81         clickChangeMode: function(event) {
82             var newMode = event.currentTarget.attributes['data-mode'].nodeValue;
83             return this.state.changeMode(newMode);
84         },
85         changedMode: function() {
86             var mode = this.state.get('mode');
87             $('.selected-mode').removeClass('selected-mode');
88             $(_.str.sprintf('.mode-button[data-mode="%s"]', mode), this.$el).addClass('selected-mode');
89         },
90     });
91
92     // The paypad allows to select the payment method (cashRegisters) 
93     // used to pay the order.
94     module.PaypadWidget = module.PosBaseWidget.extend({
95         template: 'PaypadWidget',
96         renderElement: function() {
97             var self = this;
98             this._super();
99
100             this.pos.get('cashRegisters').each(function(cashRegister) {
101                 var button = new module.PaypadButtonWidget(self,{
102                     pos: self.pos,
103                     pos_widget : self.pos_widget,
104                     cashRegister: cashRegister,
105                 });
106                 button.appendTo(self.$el);
107             });
108         }
109     });
110
111     module.PaypadButtonWidget = module.PosBaseWidget.extend({
112         template: 'PaypadButtonWidget',
113         init: function(parent, options){
114             this._super(parent, options);
115             this.cashRegister = options.cashRegister;
116         },
117         renderElement: function() {
118             var self = this;
119             this._super();
120
121             this.$el.click(function(){
122                 if (self.pos.get('selectedOrder').get('screen') === 'receipt'){  //TODO Why ?
123                     console.warn('TODO should not get there...?');
124                     return;
125                 }
126                 self.pos.get('selectedOrder').addPaymentLine(self.cashRegister);
127                 self.pos_widget.screen_selector.set_current_screen('payment');
128             });
129         },
130     });
131
132     module.OrderlineWidget = module.PosBaseWidget.extend({
133         template: 'OrderlineWidget',
134         init: function(parent, options) {
135             this._super(parent,options);
136
137             this.model = options.model;
138             this.order = options.order;
139
140             this.model.bind('change', this.refresh, this);
141         },
142         renderElement: function() {
143             var self = this;
144             this._super();
145             this.$el.click(function(){
146                 self.order.selectLine(self.model);
147                 self.trigger('order_line_selected');
148             });
149             if(this.model.is_selected()){
150                 this.$el.addClass('selected');
151             }
152         },
153         refresh: function(){
154             this.renderElement();
155             this.trigger('order_line_refreshed');
156         },
157         destroy: function(){
158             this.model.unbind('change',this.refresh,this);
159             this._super();
160         },
161     });
162     
163     module.OrderWidget = module.PosBaseWidget.extend({
164         template:'OrderWidget',
165         init: function(parent, options) {
166             this._super(parent,options);
167             this.display_mode = options.display_mode || 'numpad';   // 'maximized' | 'actionbar' | 'numpad'
168             this.set_numpad_state(options.numpadState);
169             this.pos.bind('change:selectedOrder', this.change_selected_order, this);
170             this.bind_orderline_events();
171             this.orderlinewidgets = [];
172         },
173         set_numpad_state: function(numpadState) {
174                 if (this.numpadState) {
175                         this.numpadState.unbind('set_value', this.set_value);
176                 }
177                 this.numpadState = numpadState;
178                 if (this.numpadState) {
179                         this.numpadState.bind('set_value', this.set_value, this);
180                         this.numpadState.reset();
181                 }
182         },
183         set_value: function(val) {
184                 var order = this.pos.get('selectedOrder');
185                 if (order.get('orderLines').length !== 0) {
186                 var mode = this.numpadState.get('mode');
187                 if( mode === 'quantity'){
188                     order.getSelectedLine().set_quantity(val);
189                 }else if( mode === 'discount'){
190                     order.getSelectedLine().set_discount(val);
191                 }else if( mode === 'price'){
192                     order.getSelectedLine().set_unit_price(val);
193                 }
194                 }
195         },
196         change_selected_order: function() {
197             this.currentOrderLines.unbind();
198             this.bind_orderline_events();
199             this.renderElement();
200         },
201         bind_orderline_events: function() {
202             this.currentOrderLines = (this.pos.get('selectedOrder')).get('orderLines');
203             this.currentOrderLines.bind('add', function(){ this.renderElement(true);}, this);
204             this.currentOrderLines.bind('remove', this.renderElement, this);
205         },
206         update_numpad: function() {
207             this.selected_line = this.pos.get('selectedOrder').getSelectedLine();
208             if (this.numpadState)
209                 this.numpadState.reset();
210         },
211         renderElement: function(goto_bottom) {
212             var self = this;
213             var scroller = this.$('.order-scroller')[0];
214             var scrollbottom = true;
215             var scrollTop = 0;
216             /*if(scroller){
217                 var overflow_bottom = scroller.scrollHeight - scroller.clientHeight;
218                 scrollTop = scroller.scrollTop;
219                 if( !goto_bottom && scrollTop < 0.9 * overflow_bottom){
220                     scrollbottom = false;
221                 }
222             }*/
223             this._super();
224
225             // freeing subwidgets
226             
227             for(var i = 0, len = this.orderlinewidgets.length; i < len; i++){
228                 this.orderlinewidgets[i].destroy();
229             }
230             this.orderlinewidgets = [];
231
232             var $content = this.$('.orderlines');
233             this.currentOrderLines.each(_.bind( function(orderLine) {
234                 var line = new module.OrderlineWidget(this, {
235                         model: orderLine,
236                         order: this.pos.get('selectedOrder'),
237                 });
238                 line.on('order_line_selected', self, self.update_numpad);
239                 line.on('order_line_refreshed', self, self.update_summary);
240                 line.appendTo($content);
241                 self.orderlinewidgets.push(line);
242             }, this));
243             this.update_numpad();
244             this.update_summary();
245
246             scroller = this.$('.order-scroller')[0];
247             if(scroller){
248                 //scroller.scrollTop = 1000000;
249                 /*
250                 if(scrollbottom){
251                     scroller.scrollTop = scroller.scrollHeight - scroller.clientHeight;
252                 }else{
253                     scroller.scrollTop = scrollTop;
254                 }*/
255             }
256         },
257         update_summary: function(){
258             var order = this.pos.get('selectedOrder');
259             var total     = order ? order.getTotalTaxIncluded() : 0;
260             var taxes     = order ? total - order.getTotalTaxExcluded() : 0;
261             this.$('.summary .total > .value').html(this.format_currency(total));
262             this.$('.summary .total .subentry .value').html(this.format_currency(taxes));
263         },
264         set_display_mode: function(mode){
265             if(this.display_mode !== mode){
266                 this.display_mode = mode;
267                 this.renderElement();
268             }
269         },
270     });
271
272
273     module.PaymentlineWidget = module.PosBaseWidget.extend({
274         template: 'PaymentlineWidget',
275         init: function(parent, options) {
276             this._super(parent,options);
277             this.payment_line = options.payment_line;
278             this.payment_line.bind('change', this.changedAmount, this);
279         },
280         changeAmount: function(event) {
281             var newAmount = event.currentTarget.value;
282             var amount = parseFloat(newAmount);
283             if(!isNaN(amount)){
284                 this.amount = amount;
285                 this.payment_line.set_amount(amount);
286             }
287         },
288         checkAmount: function(e){
289             if (e.which !== 0 && e.charCode !== 0) {
290                 if(isNaN(String.fromCharCode(e.charCode))){
291                     return (String.fromCharCode(e.charCode) === "." && e.currentTarget.value.toString().split(".").length < 2)?true:false;
292                 }
293             }
294             return true
295         },
296         changedAmount: function() {
297                 if (this.amount !== this.payment_line.get_amount()){
298                         this.renderElement();
299             }
300         },
301         renderElement: function() {
302             var self = this;
303             this.name =   this.payment_line.get_cashregister().get('journal_id')[1];
304             this._super();
305             this.$('input').keypress(_.bind(this.checkAmount, this))
306                         .keyup(function(event){
307                 self.changeAmount(event);
308             });
309             this.$('.delete-payment-line').click(function() {
310                 self.trigger('delete_payment_line', self);
311             });
312         },
313         focus: function(){
314             var val = this.$('input')[0].value;
315             this.$('input')[0].focus();
316             this.$('input')[0].value = val;
317             this.$('input')[0].select();
318         },
319     });
320
321     module.OrderButtonWidget = module.PosBaseWidget.extend({
322         template:'OrderButtonWidget',
323         init: function(parent, options) {
324             this._super(parent,options);
325             var self = this;
326
327             this.order = options.order;
328             this.order.bind('destroy',this.destroy, this );
329             this.order.bind('change', this.renderElement, this );
330             this.pos.bind('change:selectedOrder', this.renderElement,this );
331         },
332         renderElement:function(){
333             this._super();
334             var self = this;
335             this.$el.click(function(){ 
336                 self.selectOrder();
337             });
338             if( this.order === this.pos.get('selectedOrder') ){
339                 this.$el.addClass('selected-order');
340             }
341         },
342         selectOrder: function(event) {
343             this.pos.set({
344                 selectedOrder: this.order
345             });
346         },
347         destroy: function(){
348             this.order.unbind('destroy', this.destroy, this);
349             this.order.unbind('change',  this.renderElement, this);
350             this.pos.unbind('change:selectedOrder', this.renderElement, this);
351             this._super();
352         },
353     });
354
355     module.ActionButtonWidget = instance.web.Widget.extend({
356         template:'ActionButtonWidget',
357         icon_template:'ActionButtonWidgetWithIcon',
358         init: function(parent, options){
359             this._super(parent, options);
360             this.label = options.label || 'button';
361             this.rightalign = options.rightalign || false;
362             this.click_action = options.click;
363             this.disabled = options.disabled || false;
364             if(options.icon){
365                 this.icon = options.icon;
366                 this.template = this.icon_template;
367             }
368         },
369         set_disabled: function(disabled){
370             if(this.disabled != disabled){
371                 this.disabled = !!disabled;
372                 this.renderElement();
373             }
374         },
375         renderElement: function(){
376             this._super();
377             if(this.click_action && !this.disabled){
378                 this.$el.click(_.bind(this.click_action, this));
379             }
380         },
381     });
382
383     module.ActionBarWidget = instance.web.Widget.extend({
384         template:'ActionBarWidget',
385         init: function(parent, options){
386             this._super(parent,options);
387             this.button_list = [];
388             this.buttons = {};
389             this.visibility = {};
390         },
391         set_element_visible: function(element, visible, action){
392             if(visible != this.visibility[element]){
393                 this.visibility[element] = !!visible;
394                 if(visible){
395                     this.$('.'+element).removeClass('oe_hidden');
396                 }else{
397                     this.$('.'+element).addClass('oe_hidden');
398                 }
399             }
400             if(visible && action){
401                 this.action[element] = action;
402                 this.$('.'+element).off('click').click(action);
403             }
404         },
405         set_button_disabled: function(name, disabled){
406             var b = this.buttons[name];
407             if(b){
408                 b.set_disabled(disabled);
409             }
410         },
411         destroy_buttons:function(){
412             for(var i = 0; i < this.button_list.length; i++){
413                 this.button_list[i].destroy();
414             }
415             this.button_list = [];
416             this.buttons = {};
417             return this;
418         },
419         get_button_count: function(){
420             return this.button_list.length;
421         },
422         add_new_button: function(button_options){
423             var button = new module.ActionButtonWidget(this,button_options);
424             this.button_list.push(button);
425             if(button_options.name){
426                 this.buttons[button_options.name] = button;
427             }
428             button.appendTo(this.$('.pos-actionbar-button-list'));
429             return button;
430         },
431         show:function(){
432             this.$el.removeClass('oe_hidden');
433         },
434         hide:function(){
435             this.$el.addClass('oe_hidden');
436         },
437     });
438
439     module.CategoryButton = module.PosBaseWidget.extend({
440     });
441     module.ProductCategoriesWidget = module.PosBaseWidget.extend({
442         template: 'ProductCategoriesWidget',
443         init: function(parent, options){
444             var self = this;
445             this._super(parent,options);
446             this.product_type = options.product_type || 'all';  // 'all' | 'weightable'
447             this.onlyWeightable = options.onlyWeightable || false;
448             this.category = this.pos.root_category;
449             this.breadcrumb = [];
450             this.subcategories = [];
451             this.set_category();
452         },
453
454         // changes the category. if undefined, sets to root category
455         set_category : function(category){
456             var db = this.pos.db;
457             if(!category){
458                 this.category = db.get_category_by_id(db.root_category_id);
459             }else{
460                 this.category = category;
461             }
462             this.breadcrumb = [];
463             var ancestors_ids = db.get_category_ancestors_ids(this.category.id);
464             for(var i = 1; i < ancestors_ids.length; i++){
465                 this.breadcrumb.push(db.get_category_by_id(ancestors_ids[i]));
466             }
467             if(this.category.id !== db.root_category_id){
468                 this.breadcrumb.push(this.category);
469             }
470             this.subcategories = db.get_category_by_id(db.get_category_childs_ids(this.category.id));
471         },
472
473         get_image_url: function(category){
474             return instance.session.url('/web/binary/image', {model: 'pos.category', field: 'image_medium', id: category.id});
475         },
476
477         renderElement: function(){
478             var self = this;
479             this._super();
480
481             var hasimages = false;  //if none of the subcategories have images, we don't display buttons with icons
482             _.each(this.subcategories, function(category){
483                 if(category.image){
484                     hasimages = true;
485                 }
486             });
487
488             _.each(this.subcategories, function(category){
489                 if(hasimages){
490                     var button = QWeb.render('CategoryButton',{category:category});
491                     var button = _.str.trim(button);
492                     var button = $(button);
493                     button.find('img').replaceWith(self.pos_widget.image_cache.get_image(self.get_image_url(category)));
494                 }else{
495                     var button = QWeb.render('CategorySimpleButton',{category:category});
496                     button = _.str.trim(button);    // we remove whitespace between buttons to fix spacing
497                     var button = $(button);
498                 }
499
500                 button.appendTo(this.$('.category-list')).click(function(event){
501                     var id = category.id;
502                     var cat = self.pos.db.get_category_by_id(id);
503                     self.set_category(cat);
504                     self.renderElement();
505                 });
506             });
507             // breadcrumb click actions
508             this.$(".oe-pos-categories-list a").click(function(event){
509                 var id = $(event.target).data("category-id");
510                 var category = self.pos.db.get_category_by_id(id);
511                 self.set_category(category);
512                 self.renderElement();
513             });
514
515             this.search_and_categories();
516
517             if(this.pos.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
518                 this.pos_widget.onscreen_keyboard.connect(this.$('.searchbox input'));
519             }
520         },
521         
522         set_product_type: function(type){       // 'all' | 'weightable'
523             this.product_type = type;
524             this.reset_category();
525         },
526
527         // resets the current category to the root category
528         reset_category: function(){
529             this.set_category();
530             this.renderElement();
531         },
532
533         // empties the content of the search box
534         clear_search: function(){
535             var products = this.pos.db.get_product_by_category(this.category.id);
536             this.pos.get('products').reset(products);
537             this.$('.searchbox input').val('').focus();
538             this.$('.search-clear').fadeOut();
539         },
540
541         // filters the products, and sets up the search callbacks
542         search_and_categories: function(category){
543             var self = this;
544
545             // find all products belonging to the current category
546             var products = this.pos.db.get_product_by_category(this.category.id);
547             self.pos.get('products').reset(products);
548
549
550             var searchtimeout = null;
551             // filter the products according to the search string
552             this.$('.searchbox input').keyup(function(event){
553
554                 clearTimeout(searchtimeout);
555
556                 var query = $(this).val().toLowerCase();
557                 
558                 searchtimeout = setTimeout(function(){
559                     if(query){
560                         if(event.which === 13){
561                             if( self.pos.get('products').size() === 1 ){
562                                 self.pos.get('selectedOrder').addProduct(self.pos.get('products').at(0));
563                                 self.clear_search();
564                             }
565                         }else{
566                             var products = self.pos.db.search_product_in_category(self.category.id, query);
567                             self.pos.get('products').reset(products);
568                             self.$('.search-clear').fadeIn();
569                         }
570                     }else{
571                         var products = self.pos.db.get_product_by_category(self.category.id);
572                         self.pos.get('products').reset(products);
573                         self.$('.search-clear').fadeOut();
574                     }
575                 },200);
576             });
577
578             //reset the search when clicking on reset
579             this.$('.search-clear').click(function(){
580                 self.clear_search();
581             });
582         },
583     });
584
585     module.ProductListWidget = module.ScreenWidget.extend({
586         template:'ProductListWidget',
587         init: function(parent, options) {
588             var self = this;
589             this._super(parent,options);
590             this.model = options.model;
591             this.productwidgets = [];
592             this.weight = options.weight || 0;
593             this.show_scale = options.show_scale || false;
594             this.next_screen = options.next_screen || false;
595             this.click_product_action = options.click_product_action;
596
597             this.pos.get('products').bind('reset', function(){
598                 self.renderElement();
599             });
600         },
601         renderElement: function() {
602             var self = this;
603             this._super();
604
605             var products = this.pos.get('products').models || [];
606
607             _.each(products,function(product,i){
608                 var $product = $(QWeb.render('Product',{ widget:self, product: products[i] }));
609                 $product.find('img').replaceWith(self.pos_widget.image_cache.get_image(products[i].get_image_url()));
610                 $product.appendTo(self.$('.product-list'));
611             });
612             this.$el.delegate('a','click',function(){ 
613                 self.click_product_action(new module.Product(self.pos.db.get_product_by_id(+$(this).data('product-id')))); 
614             });
615
616         },
617     });
618
619     module.UsernameWidget = module.PosBaseWidget.extend({
620         template: 'UsernameWidget',
621         init: function(parent, options){
622             var options = options || {};
623             this._super(parent,options);
624             this.mode = options.mode || 'cashier';
625         },
626         set_user_mode: function(mode){
627             this.mode = mode;
628             this.refresh();
629         },
630         refresh: function(){
631             this.renderElement();
632         },
633         get_name: function(){
634             var user;
635             if(this.mode === 'cashier'){
636                 user = this.pos.get('cashier') || this.pos.get('user');
637             }else{
638                 user = this.pos.get('selectedOrder').get_client()  || this.pos.get('user');
639             }
640             if(user){
641                 return user.name;
642             }else{
643                 return "";
644             }
645         },
646     });
647
648     module.HeaderButtonWidget = module.PosBaseWidget.extend({
649         template: 'HeaderButtonWidget',
650         init: function(parent, options){
651             options = options || {};
652             this._super(parent, options);
653             this.action = options.action;
654             this.label   = options.label;
655         },
656         renderElement: function(){
657             var self = this;
658             this._super();
659             if(this.action){
660                 this.$el.click(function(){
661                     self.action();
662                 });
663             }
664         },
665         show: function(){ this.$el.removeClass('oe_hidden'); },
666         hide: function(){ this.$el.addClass('oe_hidden'); },
667     });
668
669     // The debug widget lets the user control and monitor the hardware and software status
670     // without the use of the proxy
671     module.DebugWidget = module.PosBaseWidget.extend({
672         template: "DebugWidget",
673         eans:{
674             admin_badge:  '0410100000006',
675             client_badge: '0420200000004',
676             invalid_ean:  '1232456',
677             soda_33cl:    '5449000000996',
678             oranges_kg:   '2100002031410',
679             lemon_price:  '2301000001560',
680             unknown_product: '9900000000004',
681         },
682         events:[
683             'scan_item_success',
684             'scan_item_error_unrecognized',
685             'payment_request',
686             'open_cashbox',
687             'print_receipt',
688             'print_pdf_invoice',
689             'weighting_read_kg',
690             'payment_status',
691         ],
692         minimized: false,
693         start: function(){
694             var self = this;
695
696             this.$el.draggable();
697             this.$('.toggle').click(function(){
698                 var content = self.$('.content');
699                 var bg      = self.$el;
700                 if(!self.minimized){
701                     content.animate({'height':'0'},200);
702                 }else{
703                     content.css({'height':'auto'});
704                 }
705                 self.minimized = !self.minimized;
706             });
707             this.$('.button.accept_payment').click(function(){
708                 self.pos.proxy.debug_accept_payment();
709             });
710             this.$('.button.reject_payment').click(function(){
711                 self.pos.proxy.debug_reject_payment();
712             });
713             this.$('.button.set_weight').click(function(){
714                 var kg = Number(self.$('input.weight').val());
715                 if(!isNaN(kg)){
716                     self.pos.proxy.debug_set_weight(kg);
717                 }
718             });
719             this.$('.button.reset_weight').click(function(){
720                 self.$('input.weight').val('');
721                 self.pos.proxy.debug_reset_weight();
722             });
723             this.$('.button.custom_ean').click(function(){
724                 var ean = self.pos.barcode_reader.sanitize_ean(self.$('input.ean').val() || '0');
725                 self.$('input.ean').val(ean);
726                 self.pos.barcode_reader.scan('ean13',ean);
727             });
728             this.$('.button.reference').click(function(){
729                 self.pos.barcode_reader.scan('reference',self.$('input.ean').val());
730             });
731             _.each(this.eans, function(ean, name){
732                 self.$('.button.'+name).click(function(){
733                     self.$('input.ean').val(ean);
734                     self.pos.barcode_reader.scan('ean13',ean);
735                 });
736             });
737             _.each(this.events, function(name){
738                 self.pos.proxy.add_notification(name,function(){
739                     self.$('.event.'+name).stop().clearQueue().css({'background-color':'#6CD11D'}); 
740                     self.$('.event.'+name).animate({'background-color':'#1E1E1E'},2000);
741                 });
742             });
743             self.pos.proxy.add_notification('help_needed',function(){
744                 self.$('.status.help_needed').addClass('on');
745             });
746             self.pos.proxy.add_notification('help_canceled',function(){
747                 self.$('.status.help_needed').removeClass('on');
748             });
749             self.pos.proxy.add_notification('transaction_start',function(){
750                 self.$('.status.transaction').addClass('on');
751             });
752             self.pos.proxy.add_notification('transaction_end',function(){
753                 self.$('.status.transaction').removeClass('on');
754             });
755             self.pos.proxy.add_notification('weighting_start',function(){
756                 self.$('.status.weighting').addClass('on');
757             });
758             self.pos.proxy.add_notification('weighting_end',function(){
759                 self.$('.status.weighting').removeClass('on');
760             });
761         },
762     });
763
764 // ---------- Main Point of Sale Widget ----------
765
766     // this is used to notify the user that data is being synchronized on the network
767     module.SynchNotificationWidget = module.PosBaseWidget.extend({
768         template: "SynchNotificationWidget",
769         init: function(parent, options){
770             options = options || {};
771             this._super(parent, options);
772         },
773         renderElement: function() {
774             var self = this;
775             this._super();
776             this.$el.click(function(){
777                 self.pos.flush();
778             });
779         },
780         start: function(){
781             var self = this;
782             this.pos.bind('change:nbr_pending_operations', function(){
783                 self.renderElement();
784             });
785         },
786         get_nbr_pending: function(){
787             return this.pos.get('nbr_pending_operations');
788         },
789     });
790
791
792     // The PosWidget is the main widget that contains all other widgets in the PointOfSale.
793     // It is mainly composed of :
794     // - a header, containing the list of orders
795     // - a leftpane, containing the list of bought products (orderlines) 
796     // - a rightpane, containing the screens (see pos_screens.js)
797     // - an actionbar on the bottom, containing various action buttons
798     // - popups
799     // - an onscreen keyboard
800     // a screen_selector which controls the switching between screens and the showing/closing of popups
801
802     module.PosWidget = module.PosBaseWidget.extend({
803         template: 'PosWidget',
804         init: function() { 
805             this._super(arguments[0],{});
806
807             instance.web.blockUI(); 
808
809             this.pos = new module.PosModel(this.session);
810             this.pos.pos_widget = this;
811             this.pos_widget = this; //So that pos_widget's childs have pos_widget set automatically
812
813             this.numpad_visible = true;
814             this.left_action_bar_visible = true;
815             this.leftpane_visible = true;
816             this.leftpane_width   = '440px';
817             this.cashier_controls_visible = true;
818             this.image_cache = new module.ImageCache(); // for faster products image display
819
820             FastClick.attach(document.body);
821
822         },
823       
824         start: function() {
825             var self = this;
826             return self.pos.ready.done(function() {
827                 $('.oe_tooltip').remove();  // remove tooltip from the start session button
828
829                 self.build_currency_template();
830                 self.renderElement();
831                 
832                 self.$('.neworder-button').click(function(){
833                     self.pos.add_new_order();
834                 });
835
836                 self.$('.deleteorder-button').click(function(){
837                     self.pos.delete_current_order();
838                 });
839                 
840                 $('body').on('keyup',function(event){
841                     if(event.which === 13){
842                         self.set_fullscreen();
843                     }
844                 });
845                 
846                 //when a new order is created, add an order button widget
847                 self.pos.get('orders').bind('add', function(new_order){
848                     var new_order_button = new module.OrderButtonWidget(null, {
849                         order: new_order,
850                         pos: self.pos
851                     });
852                     new_order_button.appendTo(this.$('.orders'));
853                     new_order_button.selectOrder();
854                 }, self);
855
856                 self.pos.add_new_order();
857
858                 self.build_widgets();
859
860                 if(self.pos.iface_big_scrollbars){
861                     self.$el.addClass('big-scrollbars');
862                 }
863
864                 self.screen_selector.set_default_screen();
865
866
867                 self.pos.barcode_reader.connect();
868
869                 if(!$('#oe-fullscreenwidget-viewport').length){
870                     $('head').append('<meta id="oe-pos-viewport" name="viewport" content=" width=1024px; user-scalable=no;">');
871                     $('head').append('<meta id="oe-pos-apple-mobile" name="apple-mobile-web-capable" content="yes">');
872                 }
873
874                 $('.oe_leftbar').addClass('oe_hidden');
875
876                 instance.webclient.set_content_full_screen(true);
877
878                 if (!self.pos.get('pos_session')) {
879                     self.screen_selector.show_popup('error', 'Sorry, we could not create a user session');
880                 }else if(!self.pos.get('pos_config')){
881                     self.screen_selector.show_popup('error', 'Sorry, we could not find any PoS Configuration for this session');
882                 }
883             
884                 instance.web.unblockUI();
885                 self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').addClass('oe_hidden');});
886
887                 self.pos.flush();
888
889             }).fail(function(){   // error when loading models data from the backend
890                 instance.web.unblockUI();
891                 return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_session_opening']], ['res_id'])
892                     .pipe( _.bind(function(res){
893                         return instance.session.rpc('/web/action/load', {'action_id': res[0]['res_id']})
894                             .pipe(_.bind(function(result){
895                                 var action = result.result;
896                                 this.do_action(action);
897                             }, this));
898                     }, self));
899             });
900         },
901         
902         // This method instantiates all the screens, widgets, etc. If you want to add new screens change the
903         // startup screen, etc, override this method.
904         build_widgets: function() {
905             var self = this;
906
907             // --------  Screens ---------
908
909             this.product_screen = new module.ProductScreenWidget(this,{});
910             this.product_screen.appendTo(this.$('.screens'));
911
912             this.receipt_screen = new module.ReceiptScreenWidget(this, {});
913             this.receipt_screen.appendTo(this.$('.screens'));
914
915             this.payment_screen = new module.PaymentScreenWidget(this, {});
916             this.payment_screen.appendTo(this.$('.screens'));
917
918             this.welcome_screen = new module.WelcomeScreenWidget(this,{});
919             this.welcome_screen.appendTo(this.$('.screens'));
920
921             this.client_payment_screen = new module.ClientPaymentScreenWidget(this, {});
922             this.client_payment_screen.appendTo(this.$('.screens'));
923
924             this.scale_invite_screen = new module.ScaleInviteScreenWidget(this, {});
925             this.scale_invite_screen.appendTo(this.$('.screens'));
926
927             this.scale_screen = new module.ScaleScreenWidget(this,{});
928             this.scale_screen.appendTo(this.$('.screens'));
929
930             // --------  Popups ---------
931
932             this.help_popup = new module.HelpPopupWidget(this, {});
933             this.help_popup.appendTo(this.$el);
934
935             this.error_popup = new module.ErrorPopupWidget(this, {});
936             this.error_popup.appendTo(this.$el);
937
938             this.error_product_popup = new module.ProductErrorPopupWidget(this, {});
939             this.error_product_popup.appendTo(this.$el);
940
941             this.error_session_popup = new module.ErrorSessionPopupWidget(this, {});
942             this.error_session_popup.appendTo(this.$el);
943
944             this.choose_receipt_popup = new module.ChooseReceiptPopupWidget(this, {});
945             this.choose_receipt_popup.appendTo(this.$el);
946
947             this.error_negative_price_popup = new module.ErrorNegativePricePopupWidget(this, {});
948             this.error_negative_price_popup.appendTo(this.$el);
949
950             this.error_no_client_popup = new module.ErrorNoClientPopupWidget(this, {});
951             this.error_no_client_popup.appendTo(this.$el);
952
953             this.error_invoice_transfer_popup = new module.ErrorInvoiceTransferPopupWidget(this, {});
954             this.error_invoice_transfer_popup.appendTo(this.$el);
955
956             // --------  Misc ---------
957
958             this.notification = new module.SynchNotificationWidget(this,{});
959             this.notification.appendTo(this.$('.pos-rightheader'));
960
961             this.username   = new module.UsernameWidget(this,{});
962             this.username.replace(this.$('.placeholder-UsernameWidget'));
963
964             this.action_bar = new module.ActionBarWidget(this);
965             this.action_bar.replace(this.$(".placeholder-RightActionBar"));
966
967             this.left_action_bar = new module.ActionBarWidget(this);
968             this.left_action_bar.replace(this.$('.placeholder-LeftActionBar'));
969
970             this.paypad = new module.PaypadWidget(this, {});
971             this.paypad.replace(this.$('.placeholder-PaypadWidget'));
972
973             this.numpad = new module.NumpadWidget(this);
974             this.numpad.replace(this.$('.placeholder-NumpadWidget'));
975
976             this.order_widget = new module.OrderWidget(this, {});
977             this.order_widget.replace(this.$('.placeholder-OrderWidget'));
978
979             this.onscreen_keyboard = new module.OnscreenKeyboardWidget(this, {
980                 'keyboard_model': 'simple'
981             });
982             this.onscreen_keyboard.replace(this.$('.placeholder-OnscreenKeyboardWidget'));
983
984             this.close_button = new module.HeaderButtonWidget(this,{
985                 label: _t('Close'),
986                 action: function(){ self.close(); },
987             });
988             this.close_button.appendTo(this.$('.pos-rightheader'));
989
990             this.client_button = new module.HeaderButtonWidget(this,{
991                 label: _t('Self-Checkout'),
992                 action: function(){ self.screen_selector.set_user_mode('client'); },
993             });
994             this.client_button.appendTo(this.$('.pos-rightheader'));
995
996             
997             // --------  Screen Selector ---------
998
999             this.screen_selector = new module.ScreenSelector({
1000                 pos: this.pos,
1001                 screen_set:{
1002                     'products': this.product_screen,
1003                     'payment' : this.payment_screen,
1004                     'client_payment' : this.client_payment_screen,
1005                     'scale_invite' : this.scale_invite_screen,
1006                     'scale':    this.scale_screen,
1007                     'receipt' : this.receipt_screen,
1008                     'welcome' : this.welcome_screen,
1009                 },
1010                 popup_set:{
1011                     'help': this.help_popup,
1012                     'error': this.error_popup,
1013                     'error-product': this.error_product_popup,
1014                     'error-session': this.error_session_popup,
1015                     'error-negative-price': this.error_negative_price_popup,
1016                     'choose-receipt': this.choose_receipt_popup,
1017                     'error-no-client': this.error_no_client_popup,
1018                     'error-invoice-transfer': this.error_invoice_transfer_popup,
1019                 },
1020                 default_client_screen: 'welcome',
1021                 default_cashier_screen: 'products',
1022                 default_mode: this.pos.iface_self_checkout ?  'client' : 'cashier',
1023             });
1024
1025             if(this.pos.debug){
1026                 this.debug_widget = new module.DebugWidget(this);
1027                 this.debug_widget.appendTo(this.$('.pos-content'));
1028             }
1029         },
1030
1031         changed_pending_operations: function () {
1032             var self = this;
1033             this.synch_notification.on_change_nbr_pending(self.pos.get('nbr_pending_operations').length);
1034         },
1035         // shows or hide the numpad and related controls like the paypad.
1036         set_numpad_visible: function(visible){
1037             if(visible !== this.numpad_visible){
1038                 this.numpad_visible = visible;
1039                 if(visible){
1040                     this.set_left_action_bar_visible(false);
1041                     this.numpad.show();
1042                     this.paypad.show();
1043                     this.order_widget.set_display_mode('numpad');
1044                 }else{
1045                     this.numpad.hide();
1046                     this.paypad.hide();
1047                     if(this.order_widget.display_mode === 'numpad'){
1048                         this.order_widget.set_display_mode('maximized');
1049                     }
1050                 }
1051             }
1052         },
1053         set_left_action_bar_visible: function(visible){
1054             if(visible !== this.left_action_bar_visible){
1055                 this.left_action_bar_visible = visible;
1056                 if(visible){
1057                     this.set_numpad_visible(false);
1058                     this.left_action_bar.show();
1059                     this.order_widget.set_display_mode('actionbar');
1060                 }else{
1061                     this.left_action_bar.hide();
1062                     if(this.order_widget.display_mode === 'actionbar'){
1063                         this.order_widget.set_display_mode('maximized');
1064                     }
1065                 }
1066             }
1067         },
1068         set_fullscreen: function(){
1069             if(this.el.webkitRequestFullscreen){
1070                 this.el.webkitRequestFullscreen();
1071             }
1072         },
1073         //shows or hide the leftpane (contains the list of orderlines, the numpad, the paypad, etc.)
1074         set_leftpane_visible: function(visible){
1075             if(visible !== this.leftpane_visible){
1076                 this.leftpane_visible = visible;
1077                 if(visible){
1078                     this.$('.pos-leftpane').removeClass('oe_hidden').animate({'width':this.leftpane_width},500,'swing');
1079                     this.$('.pos-rightpane').animate({'left':this.leftpane_width},500,'swing');
1080                 }else{
1081                     var leftpane = this.$('.pos-leftpane');
1082                     leftpane.animate({'width':'0px'},500,'swing', function(){ leftpane.addClass('oe_hidden'); });
1083                     this.$('.pos-rightpane').animate({'left':'0px'},500,'swing');
1084                 }
1085             }
1086         },
1087         //shows or hide the controls in the PosWidget that are specific to the cashier ( Orders, close button, etc. ) 
1088         set_cashier_controls_visible: function(visible){
1089             if(visible !== this.cashier_controls_visible){
1090                 this.cashier_controls_visible = visible;
1091                 if(visible){
1092                     this.$('.pos-rightheader').removeClass('oe_hidden');
1093                 }else{
1094                     this.$('.pos-rightheader').addClass('oe_hidden');
1095                 }
1096             }
1097         },
1098         close: function() {
1099             var self = this;
1100
1101             function close(){
1102                 return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_client_pos_menu']], ['res_id']).pipe(
1103                         _.bind(function(res) {
1104                     return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) {
1105                         var action = result;
1106                         action.context = _.extend(action.context || {}, {'cancel_action': {type: 'ir.actions.client', tag: 'reload'}});
1107                         //self.destroy();
1108                         this.do_action(action);
1109                     }, this));
1110                 }, self));
1111             }
1112
1113             var draft_order = _.find( self.pos.get('orders').models, function(order){
1114                 return order.get('orderLines').length !== 0 && order.get('paymentLines').length === 0;
1115             });
1116             if(draft_order){
1117                 if (confirm(_t("Pending orders will be lost.\nAre you sure you want to leave this session?"))) {
1118                     return close();
1119                 }
1120             }else{
1121                 return close();
1122             }
1123         },
1124         destroy: function() {
1125             this.pos.destroy();
1126             instance.webclient.set_content_full_screen(false);
1127             $('.oe_leftbar').removeClass('oe_hidden');
1128             $('#oe-pos-viewport').remove();
1129             $('#oe-pos-apple-mobile').remove();
1130             this._super();
1131         }
1132     });
1133 }