[IMP] point_of_sale: improve the reactivity of the searchbox by timeouting on the...
[odoo/odoo.git] / addons / point_of_sale / static / src / js / widgets.js
1 function openerp_pos_widgets(instance, module){ //module is instance.point_of_sale
2     var QWeb = instance.web.qweb,
3         _t = instance.web._t;
4
5     // The ImageCache is used to hide the latency of the application cache on-disk access in chrome 
6     // that causes annoying flickering on product pictures. Why the hell a simple access to
7     // the application cache involves such latency is beyond me, hopefully one day this can be
8     // removed.
9     module.ImageCache   = instance.web.Class.extend({
10         init: function(options){
11             options = options || {};
12             this.max_size = options.max_size || 500;
13
14             this.cache = {};
15             this.access_time = {};
16             this.size = 0;
17         },
18         get_image_uncached: function(url){
19             var img =  new Image();
20             img.src = url;
21             return img;
22         },
23         // returns a DOM Image object from an url, and cache the last 500 (by default) results
24         get_image: function(url){
25             var cached = this.cache[url];
26             if(cached){
27                 this.access_time[url] = (new Date()).getTime();
28                 return cached;
29             }else{
30                 var img = new Image();
31                 img.src = url;
32                 while(this.size >= this.max_size){
33                     var oldestUrl = null;
34                     var oldestTime = (new Date()).getTime();
35                     for(var url in this.cache){
36                         var time = this.access_time[url];
37                         if(time <= oldestTime){
38                             oldestTime = time;
39                             oldestUrl  = url;
40                         }
41                     }
42                     if(oldestUrl){
43                         delete this.cache[oldestUrl];
44                         delete this.access_time[oldestUrl];
45                     }
46                     this.size--;
47                 }
48                 this.cache[url] = img;
49                 this.access_time[url] = (new Date()).getTime();
50                 this.size++;
51                 return img;
52             }
53         },
54     });
55
56     module.NumpadWidget = module.PosBaseWidget.extend({
57         template:'NumpadWidget',
58         init: function(parent, options) {
59             this._super(parent);
60             this.state = new module.NumpadState();
61         },
62         start: function() {
63             this.state.bind('change:mode', this.changedMode, this);
64             this.changedMode();
65             this.$el.find('.numpad-backspace').click(_.bind(this.clickDeleteLastChar, this));
66             this.$el.find('.numpad-minus').click(_.bind(this.clickSwitchSign, this));
67             this.$el.find('.number-char').click(_.bind(this.clickAppendNewChar, this));
68             this.$el.find('.mode-button').click(_.bind(this.clickChangeMode, this));
69         },
70         clickDeleteLastChar: function() {
71             return this.state.deleteLastChar();
72         },
73         clickSwitchSign: function() {
74             return this.state.switchSign();
75         },
76         clickAppendNewChar: function(event) {
77             var newChar;
78             newChar = event.currentTarget.innerText || event.currentTarget.textContent;
79             return this.state.appendNewChar(newChar);
80         },
81         clickChangeMode: function(event) {
82             var newMode = event.currentTarget.attributes['data-mode'].nodeValue;
83             return this.state.changeMode(newMode);
84         },
85         changedMode: function() {
86             var mode = this.state.get('mode');
87             $('.selected-mode').removeClass('selected-mode');
88             $(_.str.sprintf('.mode-button[data-mode="%s"]', mode), this.$el).addClass('selected-mode');
89         },
90     });
91
92     // The paypad allows to select the payment method (cashRegisters) 
93     // used to pay the order.
94     module.PaypadWidget = module.PosBaseWidget.extend({
95         template: 'PaypadWidget',
96         renderElement: function() {
97             var self = this;
98             this._super();
99
100             this.pos.get('cashRegisters').each(function(cashRegister) {
101                 var button = new module.PaypadButtonWidget(self,{
102                     pos: self.pos,
103                     pos_widget : self.pos_widget,
104                     cashRegister: cashRegister,
105                 });
106                 button.appendTo(self.$el);
107             });
108         }
109     });
110
111     module.PaypadButtonWidget = module.PosBaseWidget.extend({
112         template: 'PaypadButtonWidget',
113         init: function(parent, options){
114             this._super(parent, options);
115             this.cashRegister = options.cashRegister;
116         },
117         renderElement: function() {
118             var self = this;
119             this._super();
120
121             this.$el.click(function(){
122                 if (self.pos.get('selectedOrder').get('screen') === 'receipt'){  //TODO Why ?
123                     console.warn('TODO should not get there...?');
124                     return;
125                 }
126                 self.pos.get('selectedOrder').addPaymentLine(self.cashRegister);
127                 self.pos_widget.screen_selector.set_current_screen('payment');
128             });
129         },
130     });
131
132     module.OrderlineWidget = module.PosBaseWidget.extend({
133         template: 'OrderlineWidget',
134         init: function(parent, options) {
135             this._super(parent,options);
136
137             this.model = options.model;
138             this.order = options.order;
139
140             this.model.bind('change', this.refresh, this);
141         },
142         renderElement: function() {
143             var self = this;
144             this._super();
145             this.$el.click(function(){
146                 self.order.selectLine(self.model);
147                 self.trigger('order_line_selected');
148             });
149             if(this.model.is_selected()){
150                 this.$el.addClass('selected');
151             }
152         },
153         refresh: function(){
154             this.renderElement();
155             this.trigger('order_line_refreshed');
156         },
157         destroy: function(){
158             this.model.unbind('change',this.refresh,this);
159             this._super();
160         },
161     });
162     
163     module.OrderWidget = module.PosBaseWidget.extend({
164         template:'OrderWidget',
165         init: function(parent, options) {
166             this._super(parent,options);
167             this.display_mode = options.display_mode || 'numpad';   // 'maximized' | 'actionbar' | 'numpad'
168             this.set_numpad_state(options.numpadState);
169             this.pos.bind('change:selectedOrder', this.change_selected_order, this);
170             this.bind_orderline_events();
171             this.orderlinewidgets = [];
172         },
173         set_numpad_state: function(numpadState) {
174                 if (this.numpadState) {
175                         this.numpadState.unbind('set_value', this.set_value);
176                 }
177                 this.numpadState = numpadState;
178                 if (this.numpadState) {
179                         this.numpadState.bind('set_value', this.set_value, this);
180                         this.numpadState.reset();
181                 }
182         },
183         set_value: function(val) {
184                 var order = this.pos.get('selectedOrder');
185                 if (order.get('orderLines').length !== 0) {
186                 var mode = this.numpadState.get('mode');
187                 if( mode === 'quantity'){
188                     order.getSelectedLine().set_quantity(val);
189                 }else if( mode === 'discount'){
190                     order.getSelectedLine().set_discount(val);
191                 }else if( mode === 'price'){
192                     order.getSelectedLine().set_unit_price(val);
193                 }
194                 }
195         },
196         change_selected_order: function() {
197             this.currentOrderLines.unbind();
198             this.bind_orderline_events();
199             this.renderElement();
200         },
201         bind_orderline_events: function() {
202             this.currentOrderLines = (this.pos.get('selectedOrder')).get('orderLines');
203             this.currentOrderLines.bind('add', this.renderElement, this);
204             this.currentOrderLines.bind('remove', this.renderElement, this);
205         },
206         update_numpad: function() {
207             this.selected_line = this.pos.get('selectedOrder').getSelectedLine();
208             if (this.numpadState)
209                 this.numpadState.reset();
210         },
211         renderElement: function() {
212             var self = this;
213             this._super();
214
215             // freeing subwidgets
216             
217             if(this.scrollbar){
218                 this.scrollbar.destroy();
219             }
220             for(var i = 0, len = this.orderlinewidgets.length; i < len; i++){
221                 this.orderlinewidgets[i].destroy();
222             }
223             this.orderlinewidgets = [];
224
225             if(this.display_mode === 'maximized'){
226                 $('.point-of-sale .order-container').css({'bottom':'0px'});
227             }else if(this.display_mode === 'actionbar'){
228                 $('.point-of-sale .order-container').css({'bottom':'105px'});
229             }else if(this.display_mode !== 'numpad'){
230                 console.error('ERROR: OrderWidget renderElement(): wrong display_mode:',this.display_mode);
231             }
232
233             var $content = this.$('.orderlines');
234             this.currentOrderLines.each(_.bind( function(orderLine) {
235                 var line = new module.OrderlineWidget(this, {
236                         model: orderLine,
237                         order: this.pos.get('selectedOrder'),
238                 });
239                 line.on('order_line_selected', self, self.update_numpad);
240                 line.on('order_line_refreshed', self, self.update_summary);
241                 line.appendTo($content);
242                 self.orderlinewidgets.push(line);
243             }, this));
244             this.update_numpad();
245             this.update_summary();
246
247             var position = this.scrollbar ? this.scrollbar.get_position() : 0;
248             var at_bottom = this.scrollbar ? this.scrollbar.is_at_bottom() : false;
249             
250             this.scrollbar = new module.ScrollbarWidget(this,{
251                 target_widget:   this,
252                 target_selector: '.order-scroller',
253                 name: 'order',
254                 track_bottom: true,
255                 on_show: function(){
256                     self.$('.order-scroller').css({'width':'89%'},100);
257                 },
258                 on_hide: function(){
259                     self.$('.order-scroller').css({'width':'100%'},100);
260                 },
261             });
262
263             this.scrollbar.replace(this.$('.placeholder-ScrollbarWidget'));
264             this.scrollbar.set_position(position);
265
266             if( at_bottom ){
267                 this.scrollbar.set_position(Number.MAX_VALUE, false);
268             }
269
270         },
271         update_summary: function(){
272             var order = this.pos.get('selectedOrder');
273             var total     = order ? order.getTotalTaxIncluded() : 0;
274             var taxes     = order ? total - order.getTotalTaxExcluded() : 0;
275             this.$('.summary .total > .value').html(this.format_currency(total));
276             this.$('.summary .total .subentry .value').html(this.format_currency(taxes));
277         },
278         set_display_mode: function(mode){
279             if(this.display_mode !== mode){
280                 this.display_mode = mode;
281                 this.renderElement();
282             }
283         },
284     });
285
286
287     module.PaymentlineWidget = module.PosBaseWidget.extend({
288         template: 'PaymentlineWidget',
289         init: function(parent, options) {
290             this._super(parent,options);
291             this.payment_line = options.payment_line;
292             this.payment_line.bind('change', this.changedAmount, this);
293         },
294         changeAmount: function(event) {
295             var newAmount = event.currentTarget.value;
296             var amount = parseFloat(newAmount);
297             if(!isNaN(amount)){
298                 this.amount = amount;
299                 this.payment_line.set_amount(amount);
300             }
301         },
302         checkAmount: function(e){
303             if (e.which !== 0 && e.charCode !== 0) {
304                 if(isNaN(String.fromCharCode(e.charCode))){
305                     return (String.fromCharCode(e.charCode) === "." && e.currentTarget.value.toString().split(".").length < 2)?true:false;
306                 }
307             }
308             return true
309         },
310         changedAmount: function() {
311                 if (this.amount !== this.payment_line.get_amount()){
312                         this.renderElement();
313             }
314         },
315         renderElement: function() {
316             var self = this;
317             this.name =   this.payment_line.get_cashregister().get('journal_id')[1];
318             this._super();
319             this.$('input').keypress(_.bind(this.checkAmount, this))
320                         .keyup(function(event){
321                 self.changeAmount(event);
322             });
323             this.$('.delete-payment-line').click(function() {
324                 self.trigger('delete_payment_line', self);
325             });
326         },
327         focus: function(){
328             var val = this.$('input')[0].value;
329             this.$('input')[0].focus();
330             this.$('input')[0].value = val;
331             this.$('input')[0].select();
332         },
333     });
334
335     module.OrderButtonWidget = module.PosBaseWidget.extend({
336         template:'OrderButtonWidget',
337         init: function(parent, options) {
338             this._super(parent,options);
339             var self = this;
340
341             this.order = options.order;
342             this.order.bind('destroy',this.destroy, this );
343             this.order.bind('change', this.renderElement, this );
344             this.pos.bind('change:selectedOrder', this.renderElement,this );
345         },
346         renderElement:function(){
347             this._super();
348             var self = this;
349             this.$el.click(function(){ 
350                 self.selectOrder();
351             });
352             if( this.order === this.pos.get('selectedOrder') ){
353                 this.$el.addClass('selected-order');
354             }
355         },
356         selectOrder: function(event) {
357             this.pos.set({
358                 selectedOrder: this.order
359             });
360         },
361         destroy: function(){
362             this.order.unbind('destroy', this.destroy, this);
363             this.order.unbind('change',  this.renderElement, this);
364             this.pos.unbind('change:selectedOrder', this.renderElement, this);
365             this._super();
366         },
367     });
368
369     module.ActionButtonWidget = instance.web.Widget.extend({
370         template:'ActionButtonWidget',
371         icon_template:'ActionButtonWidgetWithIcon',
372         init: function(parent, options){
373             this._super(parent, options);
374             this.label = options.label || 'button';
375             this.rightalign = options.rightalign || false;
376             this.click_action = options.click;
377             this.disabled = options.disabled || false;
378             if(options.icon){
379                 this.icon = options.icon;
380                 this.template = this.icon_template;
381             }
382         },
383         set_disabled: function(disabled){
384             if(this.disabled != disabled){
385                 this.disabled = !!disabled;
386                 this.renderElement();
387             }
388         },
389         renderElement: function(){
390             this._super();
391             if(this.click_action && !this.disabled){
392                 this.$el.click(_.bind(this.click_action, this));
393             }
394         },
395     });
396
397     module.ActionBarWidget = instance.web.Widget.extend({
398         template:'ActionBarWidget',
399         init: function(parent, options){
400             this._super(parent,options);
401             this.button_list = [];
402             this.buttons = {};
403             this.visibility = {};
404         },
405         set_element_visible: function(element, visible, action){
406             if(visible != this.visibility[element]){
407                 this.visibility[element] = !!visible;
408                 if(visible){
409                     this.$('.'+element).removeClass('oe_hidden');
410                 }else{
411                     this.$('.'+element).addClass('oe_hidden');
412                 }
413             }
414             if(visible && action){
415                 this.action[element] = action;
416                 this.$('.'+element).off('click').click(action);
417             }
418         },
419         set_button_disabled: function(name, disabled){
420             var b = this.buttons[name];
421             if(b){
422                 b.set_disabled(disabled);
423             }
424         },
425         destroy_buttons:function(){
426             for(var i = 0; i < this.button_list.length; i++){
427                 this.button_list[i].destroy();
428             }
429             this.button_list = [];
430             this.buttons = {};
431             return this;
432         },
433         get_button_count: function(){
434             return this.button_list.length;
435         },
436         add_new_button: function(button_options){
437             var button = new module.ActionButtonWidget(this,button_options);
438             this.button_list.push(button);
439             if(button_options.name){
440                 this.buttons[button_options.name] = button;
441             }
442             button.appendTo(this.$('.pos-actionbar-button-list'));
443             return button;
444         },
445         show:function(){
446             this.$el.removeClass('oe_hidden');
447         },
448         hide:function(){
449             this.$el.addClass('oe_hidden');
450         },
451     });
452
453     module.CategoryButton = module.PosBaseWidget.extend({
454     });
455     module.ProductCategoriesWidget = module.PosBaseWidget.extend({
456         template: 'ProductCategoriesWidget',
457         init: function(parent, options){
458             var self = this;
459             this._super(parent,options);
460             this.product_type = options.product_type || 'all';  // 'all' | 'weightable'
461             this.onlyWeightable = options.onlyWeightable || false;
462             this.category = this.pos.root_category;
463             this.breadcrumb = [];
464             this.subcategories = [];
465             this.set_category();
466         },
467
468         // changes the category. if undefined, sets to root category
469         set_category : function(category){
470             var db = this.pos.db;
471             if(!category){
472                 this.category = db.get_category_by_id(db.root_category_id);
473             }else{
474                 this.category = category;
475             }
476             this.breadcrumb = [];
477             var ancestors_ids = db.get_category_ancestors_ids(this.category.id);
478             for(var i = 1; i < ancestors_ids.length; i++){
479                 this.breadcrumb.push(db.get_category_by_id(ancestors_ids[i]));
480             }
481             if(this.category.id !== db.root_category_id){
482                 this.breadcrumb.push(this.category);
483             }
484             this.subcategories = db.get_category_by_id(db.get_category_childs_ids(this.category.id));
485         },
486
487         get_image_url: function(category){
488             return instance.session.url('/web/binary/image', {model: 'pos.category', field: 'image_medium', id: category.id});
489         },
490
491         renderElement: function(){
492             var self = this;
493             this._super();
494
495             var hasimages = false;  //if none of the subcategories have images, we don't display buttons with icons
496             _.each(this.subcategories, function(category){
497                 if(category.image){
498                     hasimages = true;
499                 }
500             });
501
502             _.each(this.subcategories, function(category){
503                 if(hasimages){
504                     var button = QWeb.render('CategoryButton',{category:category});
505                     var button = _.str.trim(button);
506                     var button = $(button);
507                     button.find('img').replaceWith(self.pos_widget.image_cache.get_image(self.get_image_url(category)));
508                 }else{
509                     var button = QWeb.render('CategorySimpleButton',{category:category});
510                     button = _.str.trim(button);    // we remove whitespace between buttons to fix spacing
511                     var button = $(button);
512                 }
513
514                 button.appendTo(this.$('.category-list')).click(function(event){
515                     var id = category.id;
516                     var cat = self.pos.db.get_category_by_id(id);
517                     self.set_category(cat);
518                     self.renderElement();
519                 });
520             });
521             // breadcrumb click actions
522             this.$(".oe-pos-categories-list a").click(function(event){
523                 var id = $(event.target).data("category-id");
524                 var category = self.pos.db.get_category_by_id(id);
525                 self.set_category(category);
526                 self.renderElement();
527             });
528
529             this.search_and_categories();
530
531             if(this.pos.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
532                 this.pos_widget.onscreen_keyboard.connect(this.$('.searchbox input'));
533             }
534         },
535         
536         set_product_type: function(type){       // 'all' | 'weightable'
537             this.product_type = type;
538             this.reset_category();
539         },
540
541         // resets the current category to the root category
542         reset_category: function(){
543             this.set_category();
544             this.renderElement();
545         },
546
547         // empties the content of the search box
548         clear_search: function(){
549             var products = this.pos.db.get_product_by_category(this.category.id);
550             this.pos.get('products').reset(products);
551             this.$('.searchbox input').val('').focus();
552             this.$('.search-clear').fadeOut();
553         },
554
555         // filters the products, and sets up the search callbacks
556         search_and_categories: function(category){
557             var self = this;
558
559             // find all products belonging to the current category
560             var products = this.pos.db.get_product_by_category(this.category.id);
561             self.pos.get('products').reset(products);
562
563
564             var searchtimeout = null;
565             // filter the products according to the search string
566             this.$('.searchbox input').keyup(function(event){
567                 clearTimeout(searchtimeout);
568
569                 var query = $(this).val().toLowerCase();
570                 
571                 searchtimeout = setTimeout(function(){
572                     if(query){
573                         if(event.which === 13){
574                             if( self.pos.get('products').size() === 1 ){
575                                 self.pos.get('selectedOrder').addProduct(self.pos.get('products').at(0));
576                                 self.clear_search();
577                             }
578                         }else{
579                             var products = self.pos.db.search_product_in_category(self.category.id, query);
580                             self.pos.get('products').reset(products);
581                             self.$('.search-clear').fadeIn();
582                         }
583                     }else{
584                         var products = self.pos.db.get_product_by_category(self.category.id);
585                         self.pos.get('products').reset(products);
586                         self.$('.search-clear').fadeOut();
587                     }
588                 },200);
589             });
590
591             //reset the search when clicking on reset
592             this.$('.search-clear').click(function(){
593                 self.clear_search();
594             });
595         },
596     });
597
598     module.ProductListWidget = module.ScreenWidget.extend({
599         template:'ProductListWidget',
600         init: function(parent, options) {
601             var self = this;
602             this._super(parent,options);
603             this.model = options.model;
604             this.productwidgets = [];
605             this.weight = options.weight || 0;
606             this.show_scale = options.show_scale || false;
607             this.next_screen = options.next_screen || false;
608             this.click_product_action = options.click_product_action;
609
610             this.pos.get('products').bind('reset', function(){
611                 self.renderElement();
612             });
613         },
614         renderElement: function() {
615             var self = this;
616             this._super();
617             
618             if(this.scrollbar){
619                 this.scrollbar.destroy();
620             }
621             var products = this.pos.get('products').models || [];
622             
623             _.each(products,function(product,i){
624                 var $product = $(QWeb.render('Product',{ widget:self, product: products[i] }));
625                 $product.find('img').replaceWith(self.pos_widget.image_cache.get_image(products[i].get_image_url()));
626                 $product.find('a').click(function(){ self.click_product_action(product); });
627                 $product.appendTo(self.$('.product-list'));
628             });
629
630             this.scrollbar = new module.ScrollbarWidget(this,{
631                 target_widget:   this,
632                 target_selector: '.product-list-scroller',
633                 on_show: function(){
634                     self.$('.product-list-scroller').css({'padding-right':'62px'},100);
635                 },
636                 on_hide: function(){
637                     self.$('.product-list-scroller').css({'padding-right':'0px'},100);
638                 },
639             });
640
641             this.scrollbar.replace(this.$('.placeholder-ScrollbarWidget'));
642
643         },
644     });
645
646     module.UsernameWidget = module.PosBaseWidget.extend({
647         template: 'UsernameWidget',
648         init: function(parent, options){
649             var options = options || {};
650             this._super(parent,options);
651             this.mode = options.mode || 'cashier';
652         },
653         set_user_mode: function(mode){
654             this.mode = mode;
655             this.refresh();
656         },
657         refresh: function(){
658             this.renderElement();
659         },
660         get_name: function(){
661             var user;
662             if(this.mode === 'cashier'){
663                 user = this.pos.get('cashier') || this.pos.get('user');
664             }else{
665                 user = this.pos.get('selectedOrder').get_client()  || this.pos.get('user');
666             }
667             if(user){
668                 return user.name;
669             }else{
670                 return "";
671             }
672         },
673     });
674
675     module.HeaderButtonWidget = module.PosBaseWidget.extend({
676         template: 'HeaderButtonWidget',
677         init: function(parent, options){
678             options = options || {};
679             this._super(parent, options);
680             this.action = options.action;
681             this.label   = options.label;
682         },
683         renderElement: function(){
684             var self = this;
685             this._super();
686             if(this.action){
687                 this.$el.click(function(){
688                     self.action();
689                 });
690             }
691         },
692         show: function(){ this.$el.removeClass('oe_hidden'); },
693         hide: function(){ this.$el.addClass('oe_hidden'); },
694     });
695
696     // The debug widget lets the user control and monitor the hardware and software status
697     // without the use of the proxy
698     module.DebugWidget = module.PosBaseWidget.extend({
699         template: "DebugWidget",
700         eans:{
701             admin_badge:  '0410100000006',
702             client_badge: '0420200000004',
703             invalid_ean:  '1232456',
704             soda_33cl:    '5449000000996',
705             oranges_kg:   '2100002031410',
706             lemon_price:  '2301000001560',
707             unknown_product: '9900000000004',
708         },
709         events:[
710             'scan_item_success',
711             'scan_item_error_unrecognized',
712             'payment_request',
713             'open_cashbox',
714             'print_receipt',
715             'print_pdf_invoice',
716             'weighting_read_kg',
717             'payment_status',
718         ],
719         minimized: false,
720         start: function(){
721             var self = this;
722
723             this.$el.draggable();
724             this.$('.toggle').click(function(){
725                 var content = self.$('.content');
726                 var bg      = self.$el;
727                 if(!self.minimized){
728                     content.animate({'height':'0'},200);
729                 }else{
730                     content.css({'height':'auto'});
731                 }
732                 self.minimized = !self.minimized;
733             });
734             this.$('.button.accept_payment').click(function(){
735                 self.pos.proxy.debug_accept_payment();
736             });
737             this.$('.button.reject_payment').click(function(){
738                 self.pos.proxy.debug_reject_payment();
739             });
740             this.$('.button.set_weight').click(function(){
741                 var kg = Number(self.$('input.weight').val());
742                 if(!isNaN(kg)){
743                     self.pos.proxy.debug_set_weight(kg);
744                 }
745             });
746             this.$('.button.reset_weight').click(function(){
747                 self.$('input.weight').val('');
748                 self.pos.proxy.debug_reset_weight();
749             });
750             this.$('.button.custom_ean').click(function(){
751                 var ean = self.pos.barcode_reader.sanitize_ean(self.$('input.ean').val() || '0');
752                 self.$('input.ean').val(ean);
753                 self.pos.barcode_reader.scan('ean13',ean);
754             });
755             this.$('.button.reference').click(function(){
756                 self.pos.barcode_reader.scan('reference',self.$('input.ean').val());
757             });
758             _.each(this.eans, function(ean, name){
759                 self.$('.button.'+name).click(function(){
760                     self.$('input.ean').val(ean);
761                     self.pos.barcode_reader.scan('ean13',ean);
762                 });
763             });
764             _.each(this.events, function(name){
765                 self.pos.proxy.add_notification(name,function(){
766                     self.$('.event.'+name).stop().clearQueue().css({'background-color':'#6CD11D'}); 
767                     self.$('.event.'+name).animate({'background-color':'#1E1E1E'},2000);
768                 });
769             });
770             self.pos.proxy.add_notification('help_needed',function(){
771                 self.$('.status.help_needed').addClass('on');
772             });
773             self.pos.proxy.add_notification('help_canceled',function(){
774                 self.$('.status.help_needed').removeClass('on');
775             });
776             self.pos.proxy.add_notification('transaction_start',function(){
777                 self.$('.status.transaction').addClass('on');
778             });
779             self.pos.proxy.add_notification('transaction_end',function(){
780                 self.$('.status.transaction').removeClass('on');
781             });
782             self.pos.proxy.add_notification('weighting_start',function(){
783                 self.$('.status.weighting').addClass('on');
784             });
785             self.pos.proxy.add_notification('weighting_end',function(){
786                 self.$('.status.weighting').removeClass('on');
787             });
788         },
789     });
790
791 // ---------- Main Point of Sale Widget ----------
792
793     // this is used to notify the user that data is being synchronized on the network
794     module.SynchNotificationWidget = module.PosBaseWidget.extend({
795         template: "SynchNotificationWidget",
796         init: function(parent, options){
797             options = options || {};
798             this._super(parent, options);
799         },
800         renderElement: function() {
801             var self = this;
802             this._super();
803             this.$el.click(function(){
804                 self.pos.flush();
805             });
806         },
807         start: function(){
808             var self = this;
809             this.pos.bind('change:nbr_pending_operations', function(){
810                 self.renderElement();
811             });
812         },
813         get_nbr_pending: function(){
814             return this.pos.get('nbr_pending_operations');
815         },
816     });
817
818
819     // The PosWidget is the main widget that contains all other widgets in the PointOfSale.
820     // It is mainly composed of :
821     // - a header, containing the list of orders
822     // - a leftpane, containing the list of bought products (orderlines) 
823     // - a rightpane, containing the screens (see pos_screens.js)
824     // - an actionbar on the bottom, containing various action buttons
825     // - popups
826     // - an onscreen keyboard
827     // a screen_selector which controls the switching between screens and the showing/closing of popups
828
829     module.PosWidget = module.PosBaseWidget.extend({
830         template: 'PosWidget',
831         init: function() { 
832             this._super(arguments[0],{});
833
834             instance.web.blockUI(); 
835
836             this.pos = new module.PosModel(this.session);
837             this.pos.pos_widget = this;
838             this.pos_widget = this; //So that pos_widget's childs have pos_widget set automatically
839
840             this.numpad_visible = true;
841             this.left_action_bar_visible = true;
842             this.leftpane_visible = true;
843             this.leftpane_width   = '440px';
844             this.cashier_controls_visible = true;
845             this.image_cache = new module.ImageCache(); // for faster products image display
846
847         },
848       
849         start: function() {
850             var self = this;
851             return self.pos.ready.done(function() {
852                 $('.oe_tooltip').remove();  // remove tooltip from the start session button
853
854                 self.build_currency_template();
855                 self.renderElement();
856                 
857                 self.$('.neworder-button').click(function(){
858                     self.pos.add_new_order();
859                 });
860
861                 self.$('.deleteorder-button').click(function(){
862                     self.pos.delete_current_order();
863                 });
864                 
865                 //when a new order is created, add an order button widget
866                 self.pos.get('orders').bind('add', function(new_order){
867                     var new_order_button = new module.OrderButtonWidget(null, {
868                         order: new_order,
869                         pos: self.pos
870                     });
871                     new_order_button.appendTo($('#orders'));
872                     new_order_button.selectOrder();
873                 }, self);
874
875                 self.pos.add_new_order();
876
877                 self.build_widgets();
878
879                 self.screen_selector.set_default_screen();
880
881
882                 self.pos.barcode_reader.connect();
883
884                 instance.webclient.set_content_full_screen(true);
885
886                 if (!self.pos.get('pos_session')) {
887                     self.screen_selector.show_popup('error', 'Sorry, we could not create a user session');
888                 }else if(!self.pos.get('pos_config')){
889                     self.screen_selector.show_popup('error', 'Sorry, we could not find any PoS Configuration for this session');
890                 }
891             
892                 instance.web.unblockUI();
893                 self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').addClass('oe_hidden');});
894
895                 self.pos.flush();
896
897             }).fail(function(){   // error when loading models data from the backend
898                 instance.web.unblockUI();
899                 return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_session_opening']], ['res_id'])
900                     .pipe( _.bind(function(res){
901                         return instance.session.rpc('/web/action/load', {'action_id': res[0]['res_id']})
902                             .pipe(_.bind(function(result){
903                                 var action = result.result;
904                                 this.do_action(action);
905                             }, this));
906                     }, self));
907             });
908         },
909         
910         // This method instantiates all the screens, widgets, etc. If you want to add new screens change the
911         // startup screen, etc, override this method.
912         build_widgets: function() {
913             var self = this;
914
915             // --------  Screens ---------
916
917             this.product_screen = new module.ProductScreenWidget(this,{});
918             this.product_screen.appendTo($('#rightpane'));
919
920             this.receipt_screen = new module.ReceiptScreenWidget(this, {});
921             this.receipt_screen.appendTo($('#rightpane'));
922
923             this.payment_screen = new module.PaymentScreenWidget(this, {});
924             this.payment_screen.appendTo($('#rightpane'));
925
926             this.welcome_screen = new module.WelcomeScreenWidget(this,{});
927             this.welcome_screen.appendTo($('#rightpane'));
928
929             this.client_payment_screen = new module.ClientPaymentScreenWidget(this, {});
930             this.client_payment_screen.appendTo($('#rightpane'));
931
932             this.scale_invite_screen = new module.ScaleInviteScreenWidget(this, {});
933             this.scale_invite_screen.appendTo($('#rightpane'));
934
935             this.scale_screen = new module.ScaleScreenWidget(this,{});
936             this.scale_screen.appendTo($('#rightpane'));
937
938             // --------  Popups ---------
939
940             this.help_popup = new module.HelpPopupWidget(this, {});
941             this.help_popup.appendTo($('.point-of-sale'));
942
943             this.error_popup = new module.ErrorPopupWidget(this, {});
944             this.error_popup.appendTo($('.point-of-sale'));
945
946             this.error_product_popup = new module.ProductErrorPopupWidget(this, {});
947             this.error_product_popup.appendTo($('.point-of-sale'));
948
949             this.error_session_popup = new module.ErrorSessionPopupWidget(this, {});
950             this.error_session_popup.appendTo($('.point-of-sale'));
951
952             this.choose_receipt_popup = new module.ChooseReceiptPopupWidget(this, {});
953             this.choose_receipt_popup.appendTo($('.point-of-sale'));
954
955             this.error_negative_price_popup = new module.ErrorNegativePricePopupWidget(this, {});
956             this.error_negative_price_popup.appendTo($('.point-of-sale'));
957
958             this.error_no_client_popup = new module.ErrorNoClientPopupWidget(this, {});
959             this.error_no_client_popup.appendTo($('.point-of-sale'));
960
961             this.error_invoice_transfer_popup = new module.ErrorInvoiceTransferPopupWidget(this, {});
962             this.error_invoice_transfer_popup.appendTo($('.point-of-sale'));
963
964             // --------  Misc ---------
965
966             this.notification = new module.SynchNotificationWidget(this,{});
967             this.notification.appendTo(this.$('#rightheader'));
968
969             this.username   = new module.UsernameWidget(this,{});
970             this.username.replace(this.$('.placeholder-UsernameWidget'));
971
972             this.action_bar = new module.ActionBarWidget(this);
973             this.action_bar.appendTo($(".point-of-sale #rightpane"));
974
975             this.left_action_bar = new module.ActionBarWidget(this);
976             this.left_action_bar.appendTo($(".point-of-sale #leftpane"));
977
978             this.paypad = new module.PaypadWidget(this, {});
979             this.paypad.replace($('#placeholder-PaypadWidget'));
980
981             this.numpad = new module.NumpadWidget(this);
982             this.numpad.replace($('#placeholder-NumpadWidget'));
983
984             this.order_widget = new module.OrderWidget(this, {});
985             this.order_widget.replace($('#placeholder-OrderWidget'));
986
987             this.onscreen_keyboard = new module.OnscreenKeyboardWidget(this, {
988                 'keyboard_model': 'simple'
989             });
990             this.onscreen_keyboard.appendTo($(".point-of-sale #content")); 
991
992             this.close_button = new module.HeaderButtonWidget(this,{
993                 label: _t('Close'),
994                 action: function(){ self.close(); },
995             });
996             this.close_button.appendTo(this.$('#rightheader'));
997
998             this.client_button = new module.HeaderButtonWidget(this,{
999                 label: _t('Self-Checkout'),
1000                 action: function(){ self.screen_selector.set_user_mode('client'); },
1001             });
1002             this.client_button.appendTo(this.$('#rightheader'));
1003
1004             
1005             // --------  Screen Selector ---------
1006
1007             this.screen_selector = new module.ScreenSelector({
1008                 pos: this.pos,
1009                 screen_set:{
1010                     'products': this.product_screen,
1011                     'payment' : this.payment_screen,
1012                     'client_payment' : this.client_payment_screen,
1013                     'scale_invite' : this.scale_invite_screen,
1014                     'scale':    this.scale_screen,
1015                     'receipt' : this.receipt_screen,
1016                     'welcome' : this.welcome_screen,
1017                 },
1018                 popup_set:{
1019                     'help': this.help_popup,
1020                     'error': this.error_popup,
1021                     'error-product': this.error_product_popup,
1022                     'error-session': this.error_session_popup,
1023                     'error-negative-price': this.error_negative_price_popup,
1024                     'choose-receipt': this.choose_receipt_popup,
1025                     'error-no-client': this.error_no_client_popup,
1026                     'error-invoice-transfer': this.error_invoice_transfer_popup,
1027                 },
1028                 default_client_screen: 'welcome',
1029                 default_cashier_screen: 'products',
1030                 default_mode: this.pos.iface_self_checkout ?  'client' : 'cashier',
1031             });
1032
1033             if(this.pos.debug){
1034                 this.debug_widget = new module.DebugWidget(this);
1035                 this.debug_widget.appendTo(this.$('#content'));
1036             }
1037         },
1038
1039         changed_pending_operations: function () {
1040             var self = this;
1041             this.synch_notification.on_change_nbr_pending(self.pos.get('nbr_pending_operations').length);
1042         },
1043         // shows or hide the numpad and related controls like the paypad.
1044         set_numpad_visible: function(visible){
1045             if(visible !== this.numpad_visible){
1046                 this.numpad_visible = visible;
1047                 if(visible){
1048                     this.set_left_action_bar_visible(false);
1049                     this.numpad.show();
1050                     this.paypad.show();
1051                     this.order_widget.set_display_mode('numpad');
1052                 }else{
1053                     this.numpad.hide();
1054                     this.paypad.hide();
1055                     if(this.order_widget.display_mode === 'numpad'){
1056                         this.order_widget.set_display_mode('maximized');
1057                     }
1058                 }
1059             }
1060         },
1061         set_left_action_bar_visible: function(visible){
1062             if(visible !== this.left_action_bar_visible){
1063                 this.left_action_bar_visible = visible;
1064                 if(visible){
1065                     this.set_numpad_visible(false);
1066                     this.left_action_bar.show();
1067                     this.order_widget.set_display_mode('actionbar');
1068                 }else{
1069                     this.left_action_bar.hide();
1070                     if(this.order_widget.display_mode === 'actionbar'){
1071                         this.order_widget.set_display_mode('maximized');
1072                     }
1073                 }
1074             }
1075         },
1076
1077         //shows or hide the leftpane (contains the list of orderlines, the numpad, the paypad, etc.)
1078         set_leftpane_visible: function(visible){
1079             if(visible !== this.leftpane_visible){
1080                 this.leftpane_visible = visible;
1081                 if(visible){
1082                     $('#leftpane').removeClass('oe_hidden').animate({'width':this.leftpane_width},500,'swing');
1083                     $('#rightpane').animate({'left':this.leftpane_width},500,'swing');
1084                 }else{
1085                     var leftpane = $('#leftpane');
1086                     $('#leftpane').animate({'width':'0px'},500,'swing', function(){ leftpane.addClass('oe_hidden'); });
1087                     $('#rightpane').animate({'left':'0px'},500,'swing');
1088                 }
1089             }
1090         },
1091         //shows or hide the controls in the PosWidget that are specific to the cashier ( Orders, close button, etc. ) 
1092         set_cashier_controls_visible: function(visible){
1093             if(visible !== this.cashier_controls_visible){
1094                 this.cashier_controls_visible = visible;
1095                 if(visible){
1096                     $('#loggedas').removeClass('oe_hidden');
1097                     $('#rightheader').removeClass('oe_hidden');
1098                 }else{
1099                     $('#loggedas').addClass('oe_hidden');
1100                     $('#rightheader').addClass('oe_hidden');
1101                 }
1102             }
1103         },
1104         close: function() {
1105             var self = this;
1106
1107             function close(){
1108                 return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_client_pos_menu']], ['res_id']).pipe(
1109                         _.bind(function(res) {
1110                     return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) {
1111                         var action = result;
1112                         action.context = _.extend(action.context || {}, {'cancel_action': {type: 'ir.actions.client', tag: 'reload'}});
1113                         //self.destroy();
1114                         this.do_action(action);
1115                     }, this));
1116                 }, self));
1117             }
1118
1119             var draft_order = _.find( self.pos.get('orders').models, function(order){
1120                 return order.get('orderLines').length !== 0 && order.get('paymentLines').length === 0;
1121             });
1122             if(draft_order){
1123                 if (confirm(_t("Pending orders will be lost.\nAre you sure you want to leave this session?"))) {
1124                     return close();
1125                 }
1126             }else{
1127                 return close();
1128             }
1129         },
1130         destroy: function() {
1131             this.pos.destroy();
1132             instance.webclient.set_content_full_screen(false);
1133             this._super();
1134         }
1135     });
1136 }