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