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