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