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