[MERGE] forward port of branch 8.0 up to 591e329
[odoo/odoo.git] / addons / point_of_sale / static / src / js / widgets.js
1 function openerp_pos_widgets(instance, module){ //module is instance.point_of_sale
2     var QWeb = instance.web.qweb;
3         var _t = instance.web._t;
4
5     module.DomCache = instance.web.Class.extend({
6         init: function(options){
7             options = options || {};
8             this.max_size = options.max_size || 2000;
9
10             this.cache = {};
11             this.access_time = {};
12             this.size = 0;
13         },
14         cache_node: function(key,node){
15             var cached = this.cache[key];
16             this.cache[key] = node;
17             this.access_time[key] = new Date().getTime();
18             if(!cached){
19                 this.size++;
20                 while(this.size >= this.max_size){
21                     var oldest_key = null;
22                     var oldest_time = new Date().getTime();
23                     for(var key in this.cache){
24                         var time = this.access_time[key];
25                         if(time <= oldest_time){
26                             oldest_time = time;
27                             oldest_key  = key;
28                         }
29                     }
30                     if(oldest_key){
31                         delete this.cache[oldest_key];
32                         delete this.access_time[oldest_key];
33                     }
34                     this.size--;
35                 }
36             }
37             return node;
38         },
39         get_node: function(key){
40             var cached = this.cache[key];
41             if(cached){
42                 this.access_time[key] = new Date().getTime();
43             }
44             return cached;
45         },
46     });
47
48     module.NumpadWidget = module.PosBaseWidget.extend({
49         template:'NumpadWidget',
50         init: function(parent, options) {
51             this._super(parent);
52             this.state = new module.NumpadState();
53             window.numpadstate = this.state;
54             var self = this;
55         },
56         start: function() {
57             this.state.bind('change:mode', this.changedMode, this);
58             this.changedMode();
59             this.$el.find('.numpad-backspace').click(_.bind(this.clickDeleteLastChar, this));
60             this.$el.find('.numpad-minus').click(_.bind(this.clickSwitchSign, this));
61             this.$el.find('.number-char').click(_.bind(this.clickAppendNewChar, this));
62             this.$el.find('.mode-button').click(_.bind(this.clickChangeMode, this));
63         },
64         clickDeleteLastChar: function() {
65             return this.state.deleteLastChar();
66         },
67         clickSwitchSign: function() {
68             return this.state.switchSign();
69         },
70         clickAppendNewChar: function(event) {
71             var newChar;
72             newChar = event.currentTarget.innerText || event.currentTarget.textContent;
73             return this.state.appendNewChar(newChar);
74         },
75         clickChangeMode: function(event) {
76             var newMode = event.currentTarget.attributes['data-mode'].nodeValue;
77             return this.state.changeMode(newMode);
78         },
79         changedMode: function() {
80             var mode = this.state.get('mode');
81             $('.selected-mode').removeClass('selected-mode');
82             $(_.str.sprintf('.mode-button[data-mode="%s"]', mode), this.$el).addClass('selected-mode');
83         },
84     });
85
86     // The 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 if (ss.get_current_screen() !== 'receipt'){
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.ProductCategoriesWidget = module.PosBaseWidget.extend({
297         template: 'ProductCategoriesWidget',
298         init: function(parent, options){
299             var self = this;
300             this._super(parent,options);
301             this.product_type = options.product_type || 'all';  // 'all' | 'weightable'
302             this.onlyWeightable = options.onlyWeightable || false;
303             this.category = this.pos.root_category;
304             this.breadcrumb = [];
305             this.subcategories = [];
306             this.product_list_widget = options.product_list_widget || null;
307             this.category_cache = new module.DomCache();
308             this.set_category();
309             
310             this.switch_category_handler = function(event){
311                 self.set_category(self.pos.db.get_category_by_id(Number(this.dataset['categoryId'])));
312                 self.renderElement();
313             };
314             
315             this.clear_search_handler = function(event){
316                 self.clear_search();
317             };
318
319             var search_timeout  = null;
320             this.search_handler = function(event){
321                 clearTimeout(search_timeout);
322
323                 var query = this.value;
324
325                 search_timeout = setTimeout(function(){
326                     self.perform_search(self.category, query, event.which === 13);
327                 },70);
328             };
329         },
330
331         // changes the category. if undefined, sets to root category
332         set_category : function(category){
333             var db = this.pos.db;
334             if(!category){
335                 this.category = db.get_category_by_id(db.root_category_id);
336             }else{
337                 this.category = category;
338             }
339             this.breadcrumb = [];
340             var ancestors_ids = db.get_category_ancestors_ids(this.category.id);
341             for(var i = 1; i < ancestors_ids.length; i++){
342                 this.breadcrumb.push(db.get_category_by_id(ancestors_ids[i]));
343             }
344             if(this.category.id !== db.root_category_id){
345                 this.breadcrumb.push(this.category);
346             }
347             this.subcategories = db.get_category_by_id(db.get_category_childs_ids(this.category.id));
348         },
349
350         get_image_url: function(category){
351             return window.location.origin + '/web/binary/image?model=pos.category&field=image_medium&id='+category.id;
352         },
353
354         render_category: function( category, with_image ){
355             var cached = this.category_cache.get_node(category.id);
356             if(!cached){
357                 if(with_image){
358                     var image_url = this.get_image_url(category);
359                     var category_html = QWeb.render('CategoryButton',{ 
360                             widget:  this, 
361                             category: category, 
362                             image_url: this.get_image_url(category),
363                         });
364                         category_html = _.str.trim(category_html);
365                     var category_node = document.createElement('div');
366                         category_node.innerHTML = category_html;
367                         category_node = category_node.childNodes[0];
368                 }else{
369                     var category_html = QWeb.render('CategorySimpleButton',{ 
370                             widget:  this, 
371                             category: category, 
372                         });
373                         category_html = _.str.trim(category_html);
374                     var category_node = document.createElement('div');
375                         category_node.innerHTML = category_html;
376                         category_node = category_node.childNodes[0];
377                 }
378                 this.category_cache.cache_node(category.id,category_node);
379                 return category_node;
380             }
381             return cached; 
382         },
383
384         replace: function($target){
385             this.renderElement();
386             var target = $target[0];
387             target.parentNode.replaceChild(this.el,target);
388         },
389
390         renderElement: function(){
391             var self = this;
392
393             var el_str  = openerp.qweb.render(this.template, {widget: this});
394             var el_node = document.createElement('div');
395                 el_node.innerHTML = el_str;
396                 el_node = el_node.childNodes[1];
397
398             if(this.el && this.el.parentNode){
399                 this.el.parentNode.replaceChild(el_node,this.el);
400             }
401
402             this.el = el_node;
403
404             var hasimages = false;  //if none of the subcategories have images, we don't display buttons with icons
405             for(var i = 0; i < this.subcategories.length; i++){
406                 if(this.subcategories[i].image){
407                     hasimages = true;
408                     break;
409                 }
410             }
411
412             var list_container = el_node.querySelector('.category-list');
413             if (list_container) { 
414                 if (!hasimages) {
415                     list_container.classList.add('simple');
416                 } else {
417                     list_container.classList.remove('simple');
418                 }
419                 for(var i = 0, len = this.subcategories.length; i < len; i++){
420                     list_container.appendChild(this.render_category(this.subcategories[i],hasimages));
421                 };
422             }
423
424             var buttons = el_node.querySelectorAll('.js-category-switch');
425             for(var i = 0; i < buttons.length; i++){
426                 buttons[i].addEventListener('click',this.switch_category_handler);
427             }
428
429             var products = this.pos.db.get_product_by_category(this.category.id);
430             this.product_list_widget.set_product_list(products);
431
432             this.el.querySelector('.searchbox input').addEventListener('keyup',this.search_handler);
433
434             this.el.querySelector('.search-clear').addEventListener('click',this.clear_search_handler);
435
436             if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
437                 this.pos_widget.onscreen_keyboard.connect($(this.el.querySelector('.searchbox input')));
438             }
439         },
440         
441         // resets the current category to the root category
442         reset_category: function(){
443             this.set_category();
444             this.renderElement();
445         },
446
447         // empties the content of the search box
448         clear_search: function(){
449             var products = this.pos.db.get_product_by_category(this.category.id);
450             this.product_list_widget.set_product_list(products);
451             var input = this.el.querySelector('.searchbox input');
452                 input.value = '';
453                 input.focus();
454         },
455         perform_search: function(category, query, buy_result){
456             if(query){
457                 var products = this.pos.db.search_product_in_category(category.id,query)
458                 if(buy_result && products.length === 1){
459                         this.pos.get('selectedOrder').addProduct(products[0]);
460                         this.clear_search();
461                 }else{
462                     this.product_list_widget.set_product_list(products);
463                 }
464             }else{
465                 var products = this.pos.db.get_product_by_category(this.category.id);
466                 this.product_list_widget.set_product_list(products);
467             }
468         },
469
470     });
471
472     module.ProductListWidget = module.PosBaseWidget.extend({
473         template:'ProductListWidget',
474         init: function(parent, options) {
475             var self = this;
476             this._super(parent,options);
477             this.model = options.model;
478             this.productwidgets = [];
479             this.weight = options.weight || 0;
480             this.show_scale = options.show_scale || false;
481             this.next_screen = options.next_screen || false;
482
483             this.click_product_handler = function(event){
484                 var product = self.pos.db.get_product_by_id(this.dataset['productId']);
485                 options.click_product_action(product);
486             };
487
488             this.product_list = options.product_list || [];
489             this.product_cache = new module.DomCache();
490         },
491         set_product_list: function(product_list){
492             this.product_list = product_list;
493             this.renderElement();
494         },
495         get_product_image_url: function(product){
496             return window.location.origin + '/web/binary/image?model=product.product&field=image_medium&id='+product.id;
497         },
498         replace: function($target){
499             this.renderElement();
500             var target = $target[0];
501             target.parentNode.replaceChild(this.el,target);
502         },
503
504         render_product: function(product){
505             var cached = this.product_cache.get_node(product.id);
506             if(!cached){
507                 var image_url = this.get_product_image_url(product);
508                 var product_html = QWeb.render('Product',{ 
509                         widget:  this, 
510                         product: product, 
511                         image_url: this.get_product_image_url(product),
512                     });
513                 var product_node = document.createElement('div');
514                 product_node.innerHTML = product_html;
515                 product_node = product_node.childNodes[1];
516                 this.product_cache.cache_node(product.id,product_node);
517                 return product_node;
518             }
519             return cached;
520         },
521
522         renderElement: function() {
523             var self = this;
524
525             // this._super()
526             var el_str  = openerp.qweb.render(this.template, {widget: this});
527             var el_node = document.createElement('div');
528                 el_node.innerHTML = el_str;
529                 el_node = el_node.childNodes[1];
530
531             if(this.el && this.el.parentNode){
532                 this.el.parentNode.replaceChild(el_node,this.el);
533             }
534             this.el = el_node;
535
536             var list_container = el_node.querySelector('.product-list');
537             for(var i = 0, len = this.product_list.length; i < len; i++){
538                 var product_node = this.render_product(this.product_list[i]);
539                 product_node.addEventListener('click',this.click_product_handler);
540                 list_container.appendChild(product_node);
541             };
542         },
543     });
544
545     module.UsernameWidget = module.PosBaseWidget.extend({
546         template: 'UsernameWidget',
547         init: function(parent, options){
548             var options = options || {};
549             this._super(parent,options);
550             this.mode = options.mode || 'cashier';
551         },
552         set_user_mode: function(mode){
553             this.mode = mode;
554             this.refresh();
555         },
556         refresh: function(){
557             this.renderElement();
558         },
559         get_name: function(){
560             var user;
561             if(this.mode === 'cashier'){
562                 user = this.pos.cashier || this.pos.user;
563             }else{
564                 user = this.pos.get('selectedOrder').get_client()  || this.pos.user;
565             }
566             if(user){
567                 return user.name;
568             }else{
569                 return "";
570             }
571         },
572     });
573
574     module.HeaderButtonWidget = module.PosBaseWidget.extend({
575         template: 'HeaderButtonWidget',
576         init: function(parent, options){
577             options = options || {};
578             this._super(parent, options);
579             this.action = options.action;
580             this.label   = options.label;
581         },
582         renderElement: function(){
583             var self = this;
584             this._super();
585             if(this.action){
586                 this.$el.click(function(){
587                     self.action();
588                 });
589             }
590         },
591         show: function(){ this.$el.removeClass('oe_hidden'); },
592         hide: function(){ this.$el.addClass('oe_hidden'); },
593     });
594
595     // The debug widget lets the user control and monitor the hardware and software status
596     // without the use of the proxy
597     module.DebugWidget = module.PosBaseWidget.extend({
598         template: "DebugWidget",
599         eans:{
600             admin_badge:  '0410100000006',
601             client_badge: '0420200000004',
602             invalid_ean:  '1232456',
603             soda_33cl:    '5449000000996',
604             oranges_kg:   '2100002031410',
605             lemon_price:  '2301000001560',
606             unknown_product: '9900000000004',
607         },
608         events:[
609             'open_cashbox',
610             'print_receipt',
611             'scale_read',
612         ],
613         minimized: false,
614         init: function(parent,options){
615             this._super(parent,options);
616             var self = this;
617             
618             this.minimized = false;
619
620             // for dragging the debug widget around
621             this.dragging  = false;
622             this.dragpos = {x:0, y:0};
623
624             function eventpos(event){
625                 if(event.touches && event.touches[0]){
626                     return {x: event.touches[0].screenX, y: event.touches[0].screenY};
627                 }else{
628                     return {x: event.screenX, y: event.screenY};
629                 }
630             }
631
632             this.dragend_handler = function(event){
633                 self.dragging = false;
634             };
635             this.dragstart_handler = function(event){
636                 self.dragging = true;
637                 self.dragpos = eventpos(event);
638             };
639             this.dragmove_handler = function(event){
640                 if(self.dragging){
641                     var top = this.offsetTop;
642                     var left = this.offsetLeft;
643                     var pos  = eventpos(event);
644                     var dx   = pos.x - self.dragpos.x; 
645                     var dy   = pos.y - self.dragpos.y; 
646
647                     self.dragpos = pos;
648
649                     this.style.right = 'auto';
650                     this.style.bottom = 'auto';
651                     this.style.left = left + dx + 'px';
652                     this.style.top  = top  + dy + 'px';
653                 }
654                 event.preventDefault();
655                 event.stopPropagation();
656             };
657         },
658         start: function(){
659             var self = this;
660
661             this.el.addEventListener('mouseleave', this.dragend_handler);
662             this.el.addEventListener('mouseup',    this.dragend_handler);
663             this.el.addEventListener('touchend',   this.dragend_handler);
664             this.el.addEventListener('touchcancel',this.dragend_handler);
665             this.el.addEventListener('mousedown',  this.dragstart_handler);
666             this.el.addEventListener('touchstart', this.dragstart_handler);
667             this.el.addEventListener('mousemove',  this.dragmove_handler);
668             this.el.addEventListener('touchmove',  this.dragmove_handler);
669
670             this.$('.toggle').click(function(){
671                 var content = self.$('.content');
672                 var bg      = self.$el;
673                 if(!self.minimized){
674                     content.animate({'height':'0'},200);
675                 }else{
676                     content.css({'height':'auto'});
677                 }
678                 self.minimized = !self.minimized;
679             });
680             this.$('.button.set_weight').click(function(){
681                 var kg = Number(self.$('input.weight').val());
682                 if(!isNaN(kg)){
683                     self.pos.proxy.debug_set_weight(kg);
684                 }
685             });
686             this.$('.button.reset_weight').click(function(){
687                 self.$('input.weight').val('');
688                 self.pos.proxy.debug_reset_weight();
689             });
690             this.$('.button.custom_ean').click(function(){
691                 var ean = self.pos.barcode_reader.sanitize_ean(self.$('input.ean').val() || '0');
692                 self.$('input.ean').val(ean);
693                 self.pos.barcode_reader.scan(ean);
694             });
695             this.$('.button.reference').click(function(){
696                 self.pos.barcode_reader.scan(self.$('input.ean').val());
697             });
698             this.$('.button.show_orders').click(function(){
699                 self.pos.pos_widget.screen_selector.show_popup('unsent-orders');
700             });
701             this.$('.button.delete_orders').click(function(){
702                 self.pos.pos_widget.screen_selector.show_popup('confirm',{
703                     message: _t('Delete Unsent Orders ?'),
704                     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.'),
705                     confirm: function(){
706                         self.pos.db.remove_all_orders();
707                         self.pos.set({synch: { state:'connected', pending: 0 }});
708                     },
709                 });
710             });
711             _.each(this.eans, function(ean, name){
712                 self.$('.button.'+name).click(function(){
713                     self.$('input.ean').val(ean);
714                     self.pos.barcode_reader.scan(ean);
715                 });
716             });
717             _.each(this.events, function(name){
718                 self.pos.proxy.add_notification(name,function(){
719                     self.$('.event.'+name).stop().clearQueue().css({'background-color':'#6CD11D'}); 
720                     self.$('.event.'+name).animate({'background-color':'#1E1E1E'},2000);
721                 });
722             });
723         },
724     });
725
726 // ---------- Main Point of Sale Widget ----------
727
728     module.StatusWidget = module.PosBaseWidget.extend({
729         status: ['connected','connecting','disconnected','warning'],
730         set_status: function(status,msg){
731             var self = this;
732             for(var i = 0; i < this.status.length; i++){
733                 this.$('.js_'+this.status[i]).addClass('oe_hidden');
734             }
735             this.$('.js_'+status).removeClass('oe_hidden');
736             
737             if(msg){
738                 this.$('.js_msg').removeClass('oe_hidden').html(msg);
739             }else{
740                 this.$('.js_msg').addClass('oe_hidden').html('');
741             }
742         },
743     });
744
745     // this is used to notify the user that data is being synchronized on the network
746     module.SynchNotificationWidget = module.StatusWidget.extend({
747         template: 'SynchNotificationWidget',
748         start: function(){
749             var self = this;
750             this.pos.bind('change:synch', function(pos,synch){
751                 self.set_status(synch.state, synch.pending);
752             });
753             this.$el.click(function(){
754                 self.pos.push_order();
755             });
756         },
757     });
758
759     // this is used to notify the user if the pos is connected to the proxy
760     module.ProxyStatusWidget = module.StatusWidget.extend({
761         template: 'ProxyStatusWidget',
762         set_smart_status: function(status){
763             if(status.status === 'connected'){
764                 var warning = false;
765                 var msg = ''
766                 if(this.pos.config.iface_scan_via_proxy){
767                     var scanner = status.drivers.scanner ? status.drivers.scanner.status : false;
768                     if( scanner != 'connected' && scanner != 'connecting'){
769                         warning = true;
770                         msg += _t('Scanner');
771                     }
772                 }
773                 if( this.pos.config.iface_print_via_proxy || 
774                     this.pos.config.iface_cashdrawer ){
775                     var printer = status.drivers.escpos ? status.drivers.escpos.status : false;
776                     if( printer != 'connected' && printer != 'connecting'){
777                         warning = true;
778                         msg = msg ? msg + ' & ' : msg;
779                         msg += _t('Printer');
780                     }
781                 }
782                 if( this.pos.config.iface_electronic_scale ){
783                     var scale = status.drivers.scale ? status.drivers.scale.status : false;
784                     if( scale != 'connected' && scale != 'connecting' ){
785                         warning = true;
786                         msg = msg ? msg + ' & ' : msg;
787                         msg += _t('Scale');
788                     }
789                 }
790                 msg = msg ? msg + ' ' + _t('Offline') : msg;
791                 this.set_status(warning ? 'warning' : 'connected', msg);
792             }else{
793                 this.set_status(status.status,'');
794             }
795         },
796         start: function(){
797             var self = this;
798             
799             this.set_smart_status(this.pos.proxy.get('status'));
800
801             this.pos.proxy.on('change:status',this,function(eh,status){ //FIXME remove duplicate changes 
802                 self.set_smart_status(status.newValue);
803             });
804
805             this.$el.click(function(){
806                 self.pos.connect_to_proxy();
807             });
808         },
809     });
810
811
812     // The PosWidget is the main widget that contains all other widgets in the PointOfSale.
813     // It is mainly composed of :
814     // - a header, containing the list of orders
815     // - a leftpane, containing the list of bought products (orderlines) 
816     // - a rightpane, containing the screens (see pos_screens.js)
817     // - popups
818     // - an onscreen keyboard
819     // a screen_selector which controls the switching between screens and the showing/closing of popups
820
821     module.PosWidget = module.PosBaseWidget.extend({
822         template: 'PosWidget',
823         init: function() { 
824             this._super(arguments[0],{});
825
826             this.pos = new module.PosModel(this.session,{pos_widget:this});
827             this.pos_widget = this; //So that pos_widget's childs have pos_widget set automatically
828
829             this.numpad_visible = true;
830             this.leftpane_visible = true;
831             this.leftpane_width   = '440px';
832             this.cashier_controls_visible = true;
833
834             FastClick.attach(document.body);
835
836         },
837
838         disable_rubberbanding: function(){
839             // prevent the pos body from being scrollable. 
840             document.body.addEventListener('touchmove',function(event){
841                 var node = event.target;
842                 while(node){
843                     if(node.classList && node.classList.contains('touch-scrollable')){
844                         return;
845                     }
846                     node = node.parentNode;
847                 }
848                 event.preventDefault();
849             });
850         },
851
852         start: function() {
853             var self = this;
854             return self.pos.ready.done(function() {
855                 // remove default webclient handlers that induce click delay
856                 $(document).off();
857                 $(window).off();
858                 $('html').off();
859                 $('body').off();
860                 $(self.$el).parent().off();
861                 $('document').off();
862                 $('.oe_web_client').off();
863                 $('.openerp_webclient_container').off();
864
865                 self.renderElement();
866                 
867                 self.$('.neworder-button').click(function(){
868                     self.pos.add_new_order();
869                 });
870
871                 self.$('.deleteorder-button').click(function(){
872                     if( !self.pos.get('selectedOrder').is_empty() ){
873                         self.screen_selector.show_popup('confirm',{
874                             message: _t('Destroy Current Order ?'),
875                             comment: _t('You will lose any data associated with the current order'),
876                             confirm: function(){
877                                 self.pos.delete_current_order();
878                             },
879                         });
880                     }else{
881                         self.pos.delete_current_order();
882                     }
883                 });
884                 
885                 //when a new order is created, add an order button widget
886                 self.pos.get('orders').bind('add', function(new_order){
887                     var new_order_button = new module.OrderButtonWidget(null, {
888                         order: new_order,
889                         pos: self.pos
890                     });
891                     new_order_button.appendTo(this.$('.orders'));
892                     new_order_button.selectOrder();
893                 }, self);
894
895                 self.pos.add_new_order();
896
897                 self.build_widgets();
898
899                 if(self.pos.config.iface_big_scrollbars){
900                     self.$el.addClass('big-scrollbars');
901                 }
902
903                 self.screen_selector.set_default_screen();
904
905                 self.pos.barcode_reader.connect();
906
907                 instance.webclient.set_content_full_screen(true);
908
909                 if(self.pos.config.iface_fullscreen && document.body.webkitRequestFullscreen && (
910                     window.screen.availWidth  > window.innerWidth ||
911                     window.screen.availHeight > window.innerHeight    )){
912                     self.screen_selector.show_popup('fullscreen');
913                 }
914                 self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').addClass('oe_hidden');});
915
916                 self.pos.push_order();
917
918             }).fail(function(err){   // error when loading models data from the backend
919                 self.loading_error(err);
920             });
921         },
922         loading_error: function(err){
923             var self = this;
924
925             var message = err.message;
926             var comment = err.stack;
927
928             if(err.message === 'XmlHttpRequestError '){
929                 message = 'Network Failure (XmlHttpRequestError)';
930                 comment = 'The Point of Sale could not be loaded due to a network problem.\n Please check your internet connection.';
931             }else if(err.message === 'OpenERP Server Error'){
932                 message = err.data.message;
933                 comment = err.data.debug;
934             }
935
936             if( typeof comment !== 'string' ){
937                 comment = 'Traceback not available.';
938             }
939
940             var popup = $(QWeb.render('ErrorTracebackPopupWidget',{
941                 widget: { message: message, comment: comment },
942             }));
943
944             popup.find('.button').click(function(){
945                 self.close();
946             });
947
948             popup.css({ zindex: 9001 });
949
950             popup.appendTo(this.$el);
951         },
952         loading_progress: function(fac){
953             this.$('.loader .loader-feedback').removeClass('oe_hidden');
954             this.$('.loader .progress').css({'width': ''+Math.floor(fac*100)+'%'});
955         },
956         loading_message: function(msg,progress){
957             this.$('.loader .loader-feedback').removeClass('oe_hidden');
958             this.$('.loader .message').text(msg);
959             if(typeof progress !== 'undefined'){
960                 this.loading_progress(progress);
961             }
962         },
963         loading_skip: function(callback){
964             if(callback){
965                 this.$('.loader .loader-feedback').removeClass('oe_hidden');
966                 this.$('.loader .button.skip').removeClass('oe_hidden');
967                 this.$('.loader .button.skip').off('click');
968                 this.$('.loader .button.skip').click(callback);
969             }else{
970                 this.$('.loader .button.skip').addClass('oe_hidden');
971             }
972         },
973         // This method instantiates all the screens, widgets, etc. If you want to add new screens change the
974         // startup screen, etc, override this method.
975         build_widgets: function() {
976             var self = this;
977
978             // --------  Screens ---------
979
980             this.product_screen = new module.ProductScreenWidget(this,{});
981             this.product_screen.appendTo(this.$('.screens'));
982
983             this.receipt_screen = new module.ReceiptScreenWidget(this, {});
984             this.receipt_screen.appendTo(this.$('.screens'));
985
986             this.payment_screen = new module.PaymentScreenWidget(this, {});
987             this.payment_screen.appendTo(this.$('.screens'));
988
989             this.clientlist_screen = new module.ClientListScreenWidget(this, {});
990             this.clientlist_screen.appendTo(this.$('.screens'));
991
992             this.scale_screen = new module.ScaleScreenWidget(this,{});
993             this.scale_screen.appendTo(this.$('.screens'));
994
995
996             // --------  Popups ---------
997
998             this.error_popup = new module.ErrorPopupWidget(this, {});
999             this.error_popup.appendTo(this.$el);
1000
1001             this.error_barcode_popup = new module.ErrorBarcodePopupWidget(this, {});
1002             this.error_barcode_popup.appendTo(this.$el);
1003
1004             this.error_traceback_popup = new module.ErrorTracebackPopupWidget(this,{});
1005             this.error_traceback_popup.appendTo(this.$el);
1006
1007             this.confirm_popup = new module.ConfirmPopupWidget(this,{});
1008             this.confirm_popup.appendTo(this.$el);
1009
1010             this.fullscreen_popup = new module.FullscreenPopup(this,{});
1011             this.fullscreen_popup.appendTo(this.$el);
1012
1013             this.unsent_orders_popup = new module.UnsentOrdersPopupWidget(this,{});
1014             this.unsent_orders_popup.appendTo(this.$el);
1015
1016             // --------  Misc ---------
1017
1018             this.close_button = new module.HeaderButtonWidget(this,{
1019                 label: _t('Close'),
1020                 action: function(){ 
1021                     var self = this;
1022                     if (!this.confirmed) {
1023                         this.$el.addClass('confirm');
1024                         this.$el.text(_t('Confirm'));
1025                         this.confirmed = setTimeout(function(){
1026                             self.$el.removeClass('confirm');
1027                             self.$el.text(_t('Close'));
1028                             self.confirmed = false;
1029                         },2000);
1030                     } else {
1031                         clearTimeout(this.confirmed);
1032                         this.pos_widget.close();
1033                     }
1034                 },
1035             });
1036             this.close_button.appendTo(this.$('.pos-rightheader'));
1037
1038             this.notification = new module.SynchNotificationWidget(this,{});
1039             this.notification.appendTo(this.$('.pos-rightheader'));
1040
1041             if(this.pos.config.use_proxy){
1042                 this.proxy_status = new module.ProxyStatusWidget(this,{});
1043                 this.proxy_status.appendTo(this.$('.pos-rightheader'));
1044             }
1045
1046             this.username   = new module.UsernameWidget(this,{});
1047             this.username.replace(this.$('.placeholder-UsernameWidget'));
1048
1049             this.actionpad = new module.ActionpadWidget(this, {});
1050             this.actionpad.replace(this.$('.placeholder-ActionpadWidget'));
1051
1052             this.numpad = new module.NumpadWidget(this);
1053             this.numpad.replace(this.$('.placeholder-NumpadWidget'));
1054
1055             this.order_widget = new module.OrderWidget(this, {});
1056             this.order_widget.replace(this.$('.placeholder-OrderWidget'));
1057
1058             this.onscreen_keyboard = new module.OnscreenKeyboardWidget(this, {
1059                 'keyboard_model': 'simple'
1060             });
1061             this.onscreen_keyboard.replace(this.$('.placeholder-OnscreenKeyboardWidget'));
1062
1063             // --------  Screen Selector ---------
1064
1065             this.screen_selector = new module.ScreenSelector({
1066                 pos: this.pos,
1067                 screen_set:{
1068                     'products': this.product_screen,
1069                     'payment' : this.payment_screen,
1070                     'scale':    this.scale_screen,
1071                     'receipt' : this.receipt_screen,
1072                     'clientlist': this.clientlist_screen,
1073                 },
1074                 popup_set:{
1075                     'error': this.error_popup,
1076                     'error-barcode': this.error_barcode_popup,
1077                     'error-traceback': this.error_traceback_popup,
1078                     'confirm': this.confirm_popup,
1079                     'fullscreen': this.fullscreen_popup,
1080                     'unsent-orders': this.unsent_orders_popup,
1081                 },
1082                 default_screen: 'products',
1083                 default_mode: 'cashier',
1084             });
1085
1086             if(this.pos.debug){
1087                 this.debug_widget = new module.DebugWidget(this);
1088                 this.debug_widget.appendTo(this.$('.pos-content'));
1089             }
1090
1091             this.disable_rubberbanding();
1092
1093         },
1094
1095         changed_pending_operations: function () {
1096             var self = this;
1097             this.synch_notification.on_change_nbr_pending(self.pos.get('nbr_pending_operations').length);
1098         },
1099         // shows or hide the numpad and related controls like the paypad.
1100         set_numpad_visible: function(visible){
1101             if(visible !== this.numpad_visible){
1102                 this.numpad_visible = visible;
1103                 if(visible){
1104                     this.numpad.show();
1105                     this.actionpad.show();
1106                 }else{
1107                     this.numpad.hide();
1108                     this.actionpad.hide();
1109                 }
1110             }
1111         },
1112         //shows or hide the leftpane (contains the list of orderlines, the numpad, the paypad, etc.)
1113         set_leftpane_visible: function(visible){
1114             if(visible !== this.leftpane_visible){
1115                 this.leftpane_visible = visible;
1116                 if(visible){
1117                     this.$('.pos-leftpane').removeClass('oe_hidden');
1118                     this.$('.rightpane').css({'left':this.leftpane_width});
1119                 }else{
1120                     this.$('.pos-leftpane').addClass('oe_hidden');
1121                     this.$('.rightpane').css({'left':'0px'});
1122                 }
1123             }
1124         },
1125         close: function() {
1126             var self = this;
1127
1128             function close(){
1129                 self.pos.push_order().then(function(){
1130                     return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_client_pos_menu']], ['res_id']).pipe(function(res) {
1131                         window.location = '/web#action=' + res[0]['res_id'];
1132                     },function(err,event) {
1133                         event.preventDefault();
1134                         self.screen_selector.show_popup('error',{
1135                             'message': _t('Could not close the point of sale.'),
1136                             'comment': _t('Your internet connection is probably down.'),
1137                         });
1138                         self.close_button.renderElement();
1139                     });
1140                 });
1141             }
1142
1143             var draft_order = _.find( self.pos.get('orders').models, function(order){
1144                 return order.get('orderLines').length !== 0 && order.get('paymentLines').length === 0;
1145             });
1146             if(draft_order){
1147                 if (confirm(_t("Pending orders will be lost.\nAre you sure you want to leave this session?"))) {
1148                     close();
1149                 }
1150             }else{
1151                 close();
1152             }
1153         },
1154         destroy: function() {
1155             this.pos.destroy();
1156             instance.webclient.set_content_full_screen(false);
1157             this._super();
1158         }
1159     });
1160 }