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