[IMP] point_of_sale: initial commit for the fullscreen mode.
[odoo/odoo.git] / addons / point_of_sale / static / src / js / widgets.js
1 function openerp_pos_widgets(instance, module){ //module is instance.point_of_sale
2     var QWeb = instance.web.qweb;
3         var _t = instance.web._t;
4
5     module.DomCache = instance.web.Class.extend({
6         init: function(options){
7             options = options || {};
8             this.max_size = options.max_size || 2000;
9
10             this.cache = {};
11             this.access_time = {};
12             this.size = 0;
13         },
14         cache_node: function(key,node){
15             var cached = this.cache[key];
16             this.cache[key] = node;
17             this.access_time[key] = new Date().getTime();
18             if(!cached){
19                 this.size++;
20                 while(this.size >= this.max_size){
21                     var oldest_key = null;
22                     var oldest_time = new Date().getTime();
23                     for(var key in this.cache){
24                         var time = this.access_time[key];
25                         if(time <= oldest_time){
26                             oldest_time = time;
27                             oldest_key  = key;
28                         }
29                     }
30                     if(oldestKey){
31                         delete this.cache[oldest_key];
32                         delete this.access_time[oldest_key];
33                     }
34                     this.size--;
35                 }
36             }
37             return node;
38         },
39         get_node: function(key){
40             var cached = this.cache[key];
41             if(cached){
42                 this.access_time[key] = new Date().getTime();
43             }
44             return cached;
45         },
46     });
47
48     module.NumpadWidget = module.PosBaseWidget.extend({
49         template:'NumpadWidget',
50         init: function(parent, options) {
51             this._super(parent);
52             this.state = new module.NumpadState();
53             window.numpadstate = this.state;
54             var self = this;
55         },
56         start: function() {
57             this.state.bind('change:mode', this.changedMode, this);
58             this.changedMode();
59             this.$el.find('.numpad-backspace').click(_.bind(this.clickDeleteLastChar, this));
60             this.$el.find('.numpad-minus').click(_.bind(this.clickSwitchSign, this));
61             this.$el.find('.number-char').click(_.bind(this.clickAppendNewChar, this));
62             this.$el.find('.mode-button').click(_.bind(this.clickChangeMode, this));
63         },
64         clickDeleteLastChar: function() {
65             return this.state.deleteLastChar();
66         },
67         clickSwitchSign: function() {
68             return this.state.switchSign();
69         },
70         clickAppendNewChar: function(event) {
71             var newChar;
72             newChar = event.currentTarget.innerText || event.currentTarget.textContent;
73             return this.state.appendNewChar(newChar);
74         },
75         clickChangeMode: function(event) {
76             var newMode = event.currentTarget.attributes['data-mode'].nodeValue;
77             return this.state.changeMode(newMode);
78         },
79         changedMode: function() {
80             var mode = this.state.get('mode');
81             $('.selected-mode').removeClass('selected-mode');
82             $(_.str.sprintf('.mode-button[data-mode="%s"]', mode), this.$el).addClass('selected-mode');
83         },
84     });
85
86     // The paypad allows to select the payment method (cashregisters) 
87     // used to pay the order.
88     module.PaypadWidget = module.PosBaseWidget.extend({
89         template: 'PaypadWidget',
90         renderElement: function() {
91             var self = this;
92             this._super();
93
94             _.each(this.pos.cashregisters,function(cashregister) {
95                 var button = new module.PaypadButtonWidget(self,{
96                     pos: self.pos,
97                     pos_widget : self.pos_widget,
98                     cashregister: cashregister,
99                 });
100                 button.appendTo(self.$el);
101             });
102         }
103     });
104
105     module.PaypadButtonWidget = module.PosBaseWidget.extend({
106         template: 'PaypadButtonWidget',
107         init: function(parent, options){
108             this._super(parent, options);
109             this.cashregister = options.cashregister;
110         },
111         renderElement: function() {
112             var self = this;
113             this._super();
114
115             this.$el.click(function(){
116                 if (self.pos.get('selectedOrder').get('screen') === 'receipt'){  //TODO Why ?
117                     console.warn('TODO should not get there...?');
118                     return;
119                 }
120                 self.pos.get('selectedOrder').addPaymentline(self.cashregister);
121                 self.pos_widget.screen_selector.set_current_screen('payment');
122             });
123         },
124     });
125
126     module.OrderWidget = module.PosBaseWidget.extend({
127         template:'OrderWidget',
128         init: function(parent, options) {
129             var self = this;
130             this._super(parent,options);
131             this.editable = false;
132             this.pos.bind('change:selectedOrder', this.change_selected_order, this);
133             this.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{
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             for(var i = 0, len = this.subcategories.length; i < len; i++){
523                 list_container.appendChild(this.render_category(this.subcategories[i],hasimages));
524             };
525
526             var buttons = el_node.querySelectorAll('.js-category-switch');
527             for(var i = 0; i < buttons.length; i++){
528                 buttons[i].addEventListener('click',this.switch_category_handler);
529             }
530
531             var products = this.pos.db.get_product_by_category(this.category.id);
532             this.product_list_widget.set_product_list(products);
533
534             this.el.querySelector('.searchbox input').addEventListener('keyup',this.search_handler);
535
536             this.el.querySelector('.search-clear').addEventListener('click',this.clear_search_handler);
537
538             if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
539                 this.pos_widget.onscreen_keyboard.connect($(this.el.querySelector('.searchbox input')));
540             }
541         },
542         
543         // resets the current category to the root category
544         reset_category: function(){
545             this.set_category();
546             this.renderElement();
547         },
548
549         // empties the content of the search box
550         clear_search: function(){
551             var products = this.pos.db.get_product_by_category(this.category.id);
552             this.product_list_widget.set_product_list(products);
553             var input = this.el.querySelector('.searchbox input');
554                 input.value = '';
555                 input.focus();
556         },
557         perform_search: function(category, query, buy_result){
558             if(query){
559                 var products = this.pos.db.search_product_in_category(category.id,query)
560                 if(buy_result && products.length === 1){
561                         this.pos.get('selectedOrder').addProduct(products[0]);
562                         this.clear_search();
563                 }else{
564                     this.product_list_widget.set_product_list(products);
565                 }
566             }else{
567                 var products = this.pos.db.get_product_by_category(this.category.id);
568                 this.product_list_widget.set_product_list(products);
569             }
570         },
571
572     });
573
574     module.ProductListWidget = module.PosBaseWidget.extend({
575         template:'ProductListWidget',
576         init: function(parent, options) {
577             var self = this;
578             this._super(parent,options);
579             this.model = options.model;
580             this.productwidgets = [];
581             this.weight = options.weight || 0;
582             this.show_scale = options.show_scale || false;
583             this.next_screen = options.next_screen || false;
584
585             this.click_product_handler = function(event){
586                 var product = self.pos.db.get_product_by_id(this.dataset['productId']);
587                 options.click_product_action(product);
588             };
589
590             this.product_list = options.product_list || [];
591             this.product_cache = new module.DomCache();
592         },
593         set_product_list: function(product_list){
594             this.product_list = product_list;
595             this.renderElement();
596         },
597         get_product_image_url: function(product){
598             return window.location.origin + '/web/binary/image?model=product.product&field=image_medium&id='+product.id;
599         },
600         replace: function($target){
601             this.renderElement();
602             var target = $target[0];
603             target.parentNode.replaceChild(this.el,target);
604         },
605
606         render_product: function(product){
607             var cached = this.product_cache.get_node(product.id);
608             if(!cached){
609                 var image_url = this.get_product_image_url(product);
610                 var product_html = QWeb.render('Product',{ 
611                         widget:  this, 
612                         product: product, 
613                         image_url: this.get_product_image_url(product),
614                     });
615                 var product_node = document.createElement('div');
616                 product_node.innerHTML = product_html;
617                 product_node = product_node.childNodes[1];
618                 this.product_cache.cache_node(product.id,product_node);
619                 return product_node;
620             }
621             return cached;
622         },
623
624         renderElement: function() {
625             var self = this;
626
627             // this._super()
628             var el_str  = openerp.qweb.render(this.template, {widget: this});
629             var el_node = document.createElement('div');
630                 el_node.innerHTML = el_str;
631                 el_node = el_node.childNodes[1];
632
633             if(this.el && this.el.parentNode){
634                 this.el.parentNode.replaceChild(el_node,this.el);
635             }
636             this.el = el_node;
637
638             var list_container = el_node.querySelector('.product-list');
639             for(var i = 0, len = this.product_list.length; i < len; i++){
640                 var product_node = this.render_product(this.product_list[i]);
641                 product_node.addEventListener('click',this.click_product_handler);
642                 list_container.appendChild(product_node);
643             };
644         },
645     });
646
647     module.UsernameWidget = module.PosBaseWidget.extend({
648         template: 'UsernameWidget',
649         init: function(parent, options){
650             var options = options || {};
651             this._super(parent,options);
652             this.mode = options.mode || 'cashier';
653         },
654         set_user_mode: function(mode){
655             this.mode = mode;
656             this.refresh();
657         },
658         refresh: function(){
659             this.renderElement();
660         },
661         get_name: function(){
662             var user;
663             if(this.mode === 'cashier'){
664                 user = this.pos.cashier || this.pos.user;
665             }else{
666                 user = this.pos.get('selectedOrder').get_client()  || this.pos.user;
667             }
668             if(user){
669                 return user.name;
670             }else{
671                 return "";
672             }
673         },
674     });
675
676     module.HeaderButtonWidget = module.PosBaseWidget.extend({
677         template: 'HeaderButtonWidget',
678         init: function(parent, options){
679             options = options || {};
680             this._super(parent, options);
681             this.action = options.action;
682             this.label   = options.label;
683         },
684         renderElement: function(){
685             var self = this;
686             this._super();
687             if(this.action){
688                 this.$el.click(function(){
689                     self.action();
690                 });
691             }
692         },
693         show: function(){ this.$el.removeClass('oe_hidden'); },
694         hide: function(){ this.$el.addClass('oe_hidden'); },
695     });
696
697     // The debug widget lets the user control and monitor the hardware and software status
698     // without the use of the proxy
699     module.DebugWidget = module.PosBaseWidget.extend({
700         template: "DebugWidget",
701         eans:{
702             admin_badge:  '0410100000006',
703             client_badge: '0420200000004',
704             invalid_ean:  '1232456',
705             soda_33cl:    '5449000000996',
706             oranges_kg:   '2100002031410',
707             lemon_price:  '2301000001560',
708             unknown_product: '9900000000004',
709         },
710         events:[
711             'open_cashbox',
712             'print_receipt',
713             'scale_read',
714         ],
715         minimized: false,
716         init: function(parent,options){
717             this._super(parent,options);
718             var self = this;
719             
720             this.minimized = false;
721
722             // for dragging the debug widget around
723             this.dragging  = false;
724             this.dragpos = {x:0, y:0};
725
726             function eventpos(event){
727                 if(event.touches && event.touches[0]){
728                     return {x: event.touches[0].screenX, y: event.touches[0].screenY};
729                 }else{
730                     return {x: event.screenX, y: event.screenY};
731                 }
732             }
733
734             this.dragend_handler = function(event){
735                 self.dragging = false;
736             };
737             this.dragstart_handler = function(event){
738                 self.dragging = true;
739                 self.dragpos = eventpos(event);
740             };
741             this.dragmove_handler = function(event){
742                 if(self.dragging){
743                     var top = this.offsetTop;
744                     var left = this.offsetLeft;
745                     var pos  = eventpos(event);
746                     var dx   = pos.x - self.dragpos.x; 
747                     var dy   = pos.y - self.dragpos.y; 
748
749                     self.dragpos = pos;
750
751                     this.style.right = 'auto';
752                     this.style.bottom = 'auto';
753                     this.style.left = left + dx + 'px';
754                     this.style.top  = top  + dy + 'px';
755                 }
756                 event.preventDefault();
757                 event.stopPropagation();
758             };
759         },
760         start: function(){
761             var self = this;
762
763             this.el.addEventListener('mouseleave', this.dragend_handler);
764             this.el.addEventListener('mouseup',    this.dragend_handler);
765             this.el.addEventListener('touchend',   this.dragend_handler);
766             this.el.addEventListener('touchcancel',this.dragend_handler);
767             this.el.addEventListener('mousedown',  this.dragstart_handler);
768             this.el.addEventListener('touchstart', this.dragstart_handler);
769             this.el.addEventListener('mousemove',  this.dragmove_handler);
770             this.el.addEventListener('touchmove',  this.dragmove_handler);
771
772             this.$('.toggle').click(function(){
773                 var content = self.$('.content');
774                 var bg      = self.$el;
775                 if(!self.minimized){
776                     content.animate({'height':'0'},200);
777                 }else{
778                     content.css({'height':'auto'});
779                 }
780                 self.minimized = !self.minimized;
781             });
782             this.$('.button.set_weight').click(function(){
783                 var kg = Number(self.$('input.weight').val());
784                 if(!isNaN(kg)){
785                     self.pos.proxy.debug_set_weight(kg);
786                 }
787             });
788             this.$('.button.reset_weight').click(function(){
789                 self.$('input.weight').val('');
790                 self.pos.proxy.debug_reset_weight();
791             });
792             this.$('.button.custom_ean').click(function(){
793                 var ean = self.pos.barcode_reader.sanitize_ean(self.$('input.ean').val() || '0');
794                 self.$('input.ean').val(ean);
795                 self.pos.barcode_reader.scan(ean);
796             });
797             this.$('.button.reference').click(function(){
798                 self.pos.barcode_reader.scan(self.$('input.ean').val());
799             });
800             _.each(this.eans, function(ean, name){
801                 self.$('.button.'+name).click(function(){
802                     self.$('input.ean').val(ean);
803                     self.pos.barcode_reader.scan(ean);
804                 });
805             });
806             _.each(this.events, function(name){
807                 self.pos.proxy.add_notification(name,function(){
808                     self.$('.event.'+name).stop().clearQueue().css({'background-color':'#6CD11D'}); 
809                     self.$('.event.'+name).animate({'background-color':'#1E1E1E'},2000);
810                 });
811             });
812         },
813     });
814
815 // ---------- Main Point of Sale Widget ----------
816
817     module.StatusWidget = module.PosBaseWidget.extend({
818         status: ['connected','connecting','disconnected','warning'],
819         set_status: function(status,msg){
820             var self = this;
821             for(var i = 0; i < this.status.length; i++){
822                 this.$('.js_'+this.status[i]).addClass('oe_hidden');
823             }
824             this.$('.js_'+status).removeClass('oe_hidden');
825             
826             if(msg){
827                 this.$('.js_msg').removeClass('oe_hidden').html(msg);
828             }else{
829                 this.$('.js_msg').addClass('oe_hidden').html('');
830             }
831         },
832     });
833
834     // this is used to notify the user that data is being synchronized on the network
835     module.SynchNotificationWidget = module.StatusWidget.extend({
836         template: 'SynchNotificationWidget',
837         start: function(){
838             var self = this;
839             this.pos.bind('change:synch', function(pos,synch){
840                 self.set_status(synch.state, synch.pending);
841             });
842             this.$el.click(function(){
843                 self.pos.flush();
844             });
845         },
846     });
847
848     // this is used to notify the user if the pos is connected to the proxy
849     module.ProxyStatusWidget = module.StatusWidget.extend({
850         template: 'ProxyStatusWidget',
851         set_smart_status: function(status){
852             if(status.status === 'connected'){
853                 var warning = false;
854                 var msg = ''
855                 if(this.pos.config.iface_scan_via_proxy){
856                     var scanner = status.drivers.scanner ? status.drivers.scanner.status : false;
857                     if( scanner != 'connected' && scanner != 'connecting'){
858                         warning = true;
859                         msg += _t('Scanner');
860                     }
861                 }
862                 if( this.pos.config.iface_print_via_proxy || 
863                     this.pos.config.iface_cashdrawer ){
864                     var printer = status.drivers.escpos ? status.drivers.escpos.status : false;
865                     if( printer != 'connected' && printer != 'connecting'){
866                         warning = true;
867                         msg = msg ? msg + ' & ' : msg;
868                         msg += _t('Printer');
869                     }
870                 }
871                 if( this.pos.config.iface_electronic_scale ){
872                     var scale = status.drivers.scale ? status.drivers.scale.status : false;
873                     if( scale != 'connected' && scale != 'connecting' ){
874                         warning = true;
875                         msg = msg ? msg + ' & ' : msg;
876                         msg += _t('Scale');
877                     }
878                 }
879                 msg = msg ? msg + ' ' + _t('Offline') : msg;
880                 this.set_status(warning ? 'warning' : 'connected', msg);
881             }else{
882                 this.set_status(status.status,'');
883             }
884         },
885         start: function(){
886             var self = this;
887             
888             this.set_smart_status(this.pos.proxy.get('status'));
889
890             this.pos.proxy.on('change:status',this,function(eh,status){ //FIXME remove duplicate changes 
891                 self.set_smart_status(status.newValue);
892             });
893
894             this.$el.click(function(){
895                 self.pos.connect_to_proxy();
896             });
897         },
898     });
899
900
901     // The PosWidget is the main widget that contains all other widgets in the PointOfSale.
902     // It is mainly composed of :
903     // - a header, containing the list of orders
904     // - a leftpane, containing the list of bought products (orderlines) 
905     // - a rightpane, containing the screens (see pos_screens.js)
906     // - an actionbar on the bottom, containing various action buttons
907     // - popups
908     // - an onscreen keyboard
909     // a screen_selector which controls the switching between screens and the showing/closing of popups
910
911     module.PosWidget = module.PosBaseWidget.extend({
912         template: 'PosWidget',
913         init: function() { 
914             this._super(arguments[0],{});
915
916             this.pos = new module.PosModel(this.session,{pos_widget:this});
917             this.pos_widget = this; //So that pos_widget's childs have pos_widget set automatically
918
919             this.numpad_visible = true;
920             this.leftpane_visible = true;
921             this.leftpane_width   = '440px';
922             this.cashier_controls_visible = true;
923
924             FastClick.attach(document.body);
925
926         },
927
928         disable_rubberbanding: function(){
929             // prevent the pos body from being scrollable. 
930             document.body.addEventListener('touchmove',function(event){
931                 var node = event.target;
932                 while(node){
933                     if(node.classList && node.classList.contains('touch-scrollable')){
934                         return;
935                     }
936                     node = node.parentNode;
937                 }
938                 event.preventDefault();
939             });
940         },
941
942         start: function() {
943             var self = this;
944             return self.pos.ready.done(function() {
945                 // remove default webclient handlers that induce click delay
946                 $(document).off();
947                 $(window).off();
948                 $('html').off();
949                 $('body').off();
950                 $(self.$el).parent().off();
951                 $('document').off();
952                 $('.oe_web_client').off();
953                 $('.openerp_webclient_container').off();
954
955                 self.build_currency_template();
956                 self.renderElement();
957                 
958                 self.$('.neworder-button').click(function(){
959                     self.pos.add_new_order();
960                 });
961
962                 self.$('.deleteorder-button').click(function(){
963                     if( !self.pos.get('selectedOrder').is_empty() ){
964                         self.screen_selector.show_popup('confirm',{
965                             message: _t('Destroy Current Order ?'),
966                             comment: _t('You will lose any data associated with the current order'),
967                             confirm: function(){
968                                 self.pos.delete_current_order();
969                             },
970                         });
971                     }else{
972                         self.pos.delete_current_order();
973                     }
974                 });
975                 
976                 //when a new order is created, add an order button widget
977                 self.pos.get('orders').bind('add', function(new_order){
978                     var new_order_button = new module.OrderButtonWidget(null, {
979                         order: new_order,
980                         pos: self.pos
981                     });
982                     new_order_button.appendTo(this.$('.orders'));
983                     new_order_button.selectOrder();
984                 }, self);
985
986                 self.pos.add_new_order();
987
988                 self.build_widgets();
989
990                 if(self.pos.config.iface_big_scrollbars){
991                     self.$el.addClass('big-scrollbars');
992                 }
993
994                 self.screen_selector.set_default_screen();
995
996                 self.pos.barcode_reader.connect();
997
998                 instance.webclient.set_content_full_screen(true);
999
1000                 if (!self.pos.session) {
1001                     self.screen_selector.show_popup('error', 'Sorry, we could not create a user session');
1002                 }else if(!self.pos.config){
1003                     self.screen_selector.show_popup('error', 'Sorry, we could not find any PoS Configuration for this session');
1004                 }else if(self.pos.config.iface_fullscreen && document.body.webkitRequestFullscreen && (
1005                     window.screen.availWidth  > window.innerWidth ||
1006                     window.screen.availHeight > window.innerHeight    )){
1007                     self.screen_selector.show_popup('fullscreen');
1008                 }
1009             
1010                 self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').addClass('oe_hidden');});
1011
1012                 self.pos.flush();
1013
1014             }).fail(function(){   // error when loading models data from the backend
1015                 return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_session_opening']], ['res_id'])
1016                     .pipe( _.bind(function(res){
1017                         return instance.session.rpc('/web/action/load', {'action_id': res[0]['res_id']})
1018                             .pipe(_.bind(function(result){
1019                                 var action = result.result;
1020                                 this.do_action(action);
1021                             }, this));
1022                     }, self));
1023             });
1024         },
1025         loading_progress: function(fac){
1026             this.$('.loader .loader-feedback').removeClass('oe_hidden');
1027             this.$('.loader .progress').css({'width': ''+Math.floor(fac*100)+'%'});
1028         },
1029         loading_message: function(msg,progress){
1030             this.$('.loader .loader-feedback').removeClass('oe_hidden');
1031             this.$('.loader .message').text(msg);
1032             if(typeof progress !== 'undefined'){
1033                 this.loading_progress(progress);
1034             }
1035         },
1036         loading_skip: function(callback){
1037             if(callback){
1038                 this.$('.loader .loader-feedback').removeClass('oe_hidden');
1039                 this.$('.loader .button.skip').removeClass('oe_hidden');
1040                 this.$('.loader .button.skip').off('click');
1041                 this.$('.loader .button.skip').click(callback);
1042             }else{
1043                 this.$('.loader .button.skip').addClass('oe_hidden');
1044             }
1045         },
1046         // This method instantiates all the screens, widgets, etc. If you want to add new screens change the
1047         // startup screen, etc, override this method.
1048         build_widgets: function() {
1049             var self = this;
1050
1051             // --------  Screens ---------
1052
1053             this.product_screen = new module.ProductScreenWidget(this,{});
1054             this.product_screen.appendTo(this.$('.screens'));
1055
1056             this.receipt_screen = new module.ReceiptScreenWidget(this, {});
1057             this.receipt_screen.appendTo(this.$('.screens'));
1058
1059             this.payment_screen = new module.PaymentScreenWidget(this, {});
1060             this.payment_screen.appendTo(this.$('.screens'));
1061
1062             this.clientlist_screen = new module.ClientListScreenWidget(this, {});
1063             this.clientlist_screen.appendTo(this.$('.screens'));
1064
1065             this.scale_screen = new module.ScaleScreenWidget(this,{});
1066             this.scale_screen.appendTo(this.$('.screens'));
1067
1068
1069             // --------  Popups ---------
1070
1071             this.error_popup = new module.ErrorPopupWidget(this, {});
1072             this.error_popup.appendTo(this.$el);
1073
1074             this.error_barcode_popup = new module.ErrorBarcodePopupWidget(this, {});
1075             this.error_barcode_popup.appendTo(this.$el);
1076
1077             this.error_session_popup = new module.ErrorSessionPopupWidget(this, {});
1078             this.error_session_popup.appendTo(this.$el);
1079
1080             this.choose_receipt_popup = new module.ChooseReceiptPopupWidget(this, {});
1081             this.choose_receipt_popup.appendTo(this.$el);
1082
1083             this.error_no_client_popup = new module.ErrorNoClientPopupWidget(this, {});
1084             this.error_no_client_popup.appendTo(this.$el);
1085
1086             this.error_invoice_transfer_popup = new module.ErrorInvoiceTransferPopupWidget(this, {});
1087             this.error_invoice_transfer_popup.appendTo(this.$el);
1088
1089             this.confirm_popup = new module.ConfirmPopupWidget(this,{});
1090             this.confirm_popup.appendTo(this.$el);
1091
1092             this.fullscreen_popup = new module.FullscreenPopup(this,{});
1093             this.fullscreen_popup.appendTo(this.$el);
1094
1095             // --------  Misc ---------
1096
1097             this.close_button = new module.HeaderButtonWidget(this,{
1098                 label: _t('Close'),
1099                 action: function(){ self.close(); },
1100             });
1101             this.close_button.appendTo(this.$('.pos-rightheader'));
1102
1103             this.notification = new module.SynchNotificationWidget(this,{});
1104             this.notification.appendTo(this.$('.pos-rightheader'));
1105
1106             if(this.pos.config.use_proxy){
1107                 this.proxy_status = new module.ProxyStatusWidget(this,{});
1108                 this.proxy_status.appendTo(this.$('.pos-rightheader'));
1109             }
1110
1111             this.username   = new module.UsernameWidget(this,{});
1112             this.username.replace(this.$('.placeholder-UsernameWidget'));
1113
1114             this.action_bar = new module.ActionBarWidget(this);
1115             this.action_bar.replace(this.$(".placeholder-RightActionBar"));
1116
1117             this.paypad = new module.PaypadWidget(this, {});
1118             this.paypad.replace(this.$('.placeholder-PaypadWidget'));
1119
1120             this.numpad = new module.NumpadWidget(this);
1121             this.numpad.replace(this.$('.placeholder-NumpadWidget'));
1122
1123             this.order_widget = new module.OrderWidget(this, {});
1124             this.order_widget.replace(this.$('.placeholder-OrderWidget'));
1125
1126             this.onscreen_keyboard = new module.OnscreenKeyboardWidget(this, {
1127                 'keyboard_model': 'simple'
1128             });
1129             this.onscreen_keyboard.replace(this.$('.placeholder-OnscreenKeyboardWidget'));
1130
1131             // --------  Screen Selector ---------
1132
1133             this.screen_selector = new module.ScreenSelector({
1134                 pos: this.pos,
1135                 screen_set:{
1136                     'products': this.product_screen,
1137                     'payment' : this.payment_screen,
1138                     'scale':    this.scale_screen,
1139                     'receipt' : this.receipt_screen,
1140                     'clientlist': this.clientlist_screen,
1141                 },
1142                 popup_set:{
1143                     'error': this.error_popup,
1144                     'error-barcode': this.error_barcode_popup,
1145                     'error-session': this.error_session_popup,
1146                     'choose-receipt': this.choose_receipt_popup,
1147                     'error-no-client': this.error_no_client_popup,
1148                     'error-invoice-transfer': this.error_invoice_transfer_popup,
1149                     'confirm': this.confirm_popup,
1150                     'fullscreen': this.fullscreen_popup,
1151                 },
1152                 default_screen: 'products',
1153                 default_mode: 'cashier',
1154             });
1155
1156             if(this.pos.debug){
1157                 this.debug_widget = new module.DebugWidget(this);
1158                 this.debug_widget.appendTo(this.$('.pos-content'));
1159             }
1160
1161             this.disable_rubberbanding();
1162
1163         },
1164
1165         changed_pending_operations: function () {
1166             var self = this;
1167             this.synch_notification.on_change_nbr_pending(self.pos.get('nbr_pending_operations').length);
1168         },
1169         // shows or hide the numpad and related controls like the paypad.
1170         set_numpad_visible: function(visible){
1171             if(visible !== this.numpad_visible){
1172                 this.numpad_visible = visible;
1173                 if(visible){
1174                     this.numpad.show();
1175                     this.paypad.show();
1176                 }else{
1177                     this.numpad.hide();
1178                     this.paypad.hide();
1179                 }
1180             }
1181         },
1182         //shows or hide the leftpane (contains the list of orderlines, the numpad, the paypad, etc.)
1183         set_leftpane_visible: function(visible){
1184             if(visible !== this.leftpane_visible){
1185                 this.leftpane_visible = visible;
1186                 if(visible){
1187                     this.$('.pos-leftpane').removeClass('oe_hidden');
1188                     this.$('.rightpane').css({'left':this.leftpane_width});
1189                 }else{
1190                     this.$('.pos-leftpane').addClass('oe_hidden');
1191                     this.$('.rightpane').css({'left':'0px'});
1192                 }
1193             }
1194         },
1195         close: function() {
1196             var self = this;
1197
1198             function close(){
1199                 return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_client_pos_menu']], ['res_id']).pipe(function(res) {
1200                     window.location = '/web#action=' + res[0]['res_id'];
1201                 });
1202             }
1203
1204             var draft_order = _.find( self.pos.get('orders').models, function(order){
1205                 return order.get('orderLines').length !== 0 && order.get('paymentLines').length === 0;
1206             });
1207             if(draft_order){
1208                 if (confirm(_t("Pending orders will be lost.\nAre you sure you want to leave this session?"))) {
1209                     return close();
1210                 }
1211             }else{
1212                 return close();
1213             }
1214         },
1215         destroy: function() {
1216             this.pos.destroy();
1217             instance.webclient.set_content_full_screen(false);
1218             this._super();
1219         }
1220     });
1221 }