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