[IMP] point_of_sale: some code simplifications
[odoo/odoo.git] / addons / point_of_sale / static / src / js / pos_widgets.js
1 function openerp_pos_widgets(instance, module){ //module is instance.point_of_sale
2     var QWeb = instance.web.qweb;
3
4     module.NumpadWidget = instance.web.Widget.extend({
5         template:'NumpadWidget',
6         init: function(parent, options) {
7             this._super(parent);
8             this.state = new module.NumpadState();
9         },
10         start: function() {
11             this.state.bind('change:mode', this.changedMode, this);
12             this.changedMode();
13             this.$element.find('button#numpad-backspace').click(_.bind(this.clickDeleteLastChar, this));
14             this.$element.find('button#numpad-minus').click(_.bind(this.clickSwitchSign, this));
15             this.$element.find('button.number-char').click(_.bind(this.clickAppendNewChar, this));
16             this.$element.find('button.mode-button').click(_.bind(this.clickChangeMode, this));
17         },
18         clickDeleteLastChar: function() {
19             return this.state.deleteLastChar();
20         },
21         clickSwitchSign: function() {
22             return this.state.switchSign();
23         },
24         clickAppendNewChar: function(event) {
25             var newChar;
26             newChar = event.currentTarget.innerText || event.currentTarget.textContent;
27             return this.state.appendNewChar(newChar);
28         },
29         clickChangeMode: function(event) {
30             var newMode = event.currentTarget.attributes['data-mode'].nodeValue;
31             return this.state.changeMode(newMode);
32         },
33         changedMode: function() {
34             var mode = this.state.get('mode');
35             $('.selected-mode').removeClass('selected-mode');
36             $(_.str.sprintf('.mode-button[data-mode="%s"]', mode), this.$element).addClass('selected-mode');
37         },
38     });
39
40     // The paypad allows to select the payment method (cashRegisters) 
41     // used to pay the order.
42     module.PaypadWidget = module.PosBaseWidget.extend({
43         template: 'PaypadWidget',
44         renderElement: function() {
45             var self = this;
46             this._super();
47             console.log('PaypadWidget:',this);
48
49             this.pos.get('cashRegisters').each(function(cashRegister) {
50                 var button = new module.PaypadButtonWidget(self,{
51                     pos: self.pos,
52                     pos_widget : self.pos_widget,
53                     cashRegister: cashRegister,
54                 });
55                 button.appendTo(self.$element);
56             });
57         }
58     });
59
60     module.PaypadButtonWidget = module.PosBaseWidget.extend({
61         template: 'PaypadButtonWidget',
62         init: function(parent, options){
63             this._super(parent, options);
64             this.cashRegister = options.cashRegister;
65         },
66         renderElement: function() {
67             var self = this;
68             this._super();
69
70             this.$element.click(function(){
71                 if (self.pos.get('selectedOrder').get('screen') === 'receipt'){  //TODO Why ?
72                     console.log('TODO should not get there...?');
73                     return;
74                 }
75                 self.pos.get('selectedOrder').addPaymentLine(self.cashRegister);
76                 self.pos_widget.screen_selector.set_current_screen('payment');
77             });
78         },
79     });
80
81 // ---------- "Shopping Carts" ----------
82
83     module.OrderlineWidget = module.PosBaseWidget.extend({
84         template: 'OrderlineWidget',
85         init: function(parent, options) {
86             this._super(parent,options);
87             this.model = options.model;
88             this.model.bind('change', _.bind( function() {
89                 this.refresh();
90             }, this));
91             this.model.bind('remove', _.bind( function() {
92                 this.$element.remove();
93             }, this));
94             this.order = options.order;
95         },
96         start: function() {
97             this.$element.click(_.bind(this.clickHandler, this));
98             this.refresh();
99         },
100         clickHandler: function() {
101             this.select();
102         },
103         renderElement: function() {
104             this._super();
105             this.select();
106         },
107         refresh: function() {
108             this.renderElement();
109             var heights = _.map(this.$element.prevAll(), function(el) {return $(el).outerHeight();});
110             heights.push($('#current-order thead').outerHeight());
111             var position = _.reduce(heights, function(memo, num){ return memo + num; }, 0);
112             $('#current-order').scrollTop(position);
113         },
114         select: function() {
115             $('tr.selected').removeClass('selected');
116             this.$element.addClass('selected');
117             this.order.selected = this.model;
118             this.on_selected();
119         },
120         on_selected: function() {},
121     });
122
123     module.OrderWidget = module.PosBaseWidget.extend({
124         template:'OrderWidget',
125         init: function(parent, options) {
126             this._super(parent,options);
127             console.log('OrderWidget init:',options)
128             this.set_numpad_state(options.numpadState);
129             this.pos.bind('change:selectedOrder', this.change_selected_order, this);
130             this.bind_orderline_events();
131         },
132         set_numpad_state: function(numpadState) {
133                 if (this.numpadState) {
134                         this.numpadState.unbind('set_value', this.set_value);
135                 }
136                 this.numpadState = numpadState;
137                 if (this.numpadState) {
138                         this.numpadState.bind('set_value', this.set_value, this);
139                         this.numpadState.reset();
140                 }
141         },
142         set_value: function(val) {
143                 var param = {};
144                 param[this.numpadState.get('mode')] = val;
145                 var order = this.pos.get('selectedOrder');
146                 if (order.get('orderLines').length !== 0) {
147                    order.selected.set(param);
148                 } else {
149                     this.pos.get('selectedOrder').destroy();
150                 }
151         },
152         change_selected_order: function() {
153             this.currentOrderLines.unbind();
154             this.bind_orderline_events();
155             this.renderElement();
156         },
157         bind_orderline_events: function() {
158             this.currentOrderLines = (this.pos.get('selectedOrder')).get('orderLines');
159             this.currentOrderLines.bind('add', this.add_line, this);
160             this.currentOrderLines.bind('remove', this.renderElement, this);
161         },
162         add_line: function(newLine) {
163             var line = new module.OrderlineWidget(null, {
164                     model: newLine,
165                     pos: this.pos,
166                     order: this.pos.get('selectedOrder')
167             });
168             line.on_selected.add(_.bind(this.selected_line, this));
169             this.selected_line();
170             line.appendTo(this.$element.find('#current-order-content'));
171             this.update_summary();
172         },
173         selected_line: function() {
174                 var reset = false;
175                 if (this.currentSelected !== this.pos.get('selectedOrder').selected) {
176                         reset = true;
177                 }
178                 this.currentSelected = this.pos.get('selectedOrder').selected;
179                 if (reset && this.numpadState)
180                         this.numpadState.reset();
181             this.update_summary();
182         },
183         renderElement: function() {
184             this._super();
185             var $content = this.$element.find('#current-order-content');
186             $content.empty();
187             this.currentOrderLines.each(_.bind( function(orderLine) {
188                 var line = new module.OrderlineWidget(null, {
189                         model: orderLine,
190                         order: this.pos.get('selectedOrder')
191                 });
192                 line.on_selected.add(_.bind(this.selected_line, this));
193                 line.appendTo($content);
194             }, this));
195             this.update_summary();
196         },
197         update_summary: function() {
198             var currentOrder, tax, total, totalTaxExcluded;
199             currentOrder = this.pos.get('selectedOrder');
200             total = currentOrder.getTotal();
201             totalTaxExcluded = currentOrder.getTotalTaxExcluded();
202             tax = currentOrder.getTax();
203             this.pos_widget.action_bar.set_total_value(Math.round(total*100)/100);
204             $('#subtotal').html(totalTaxExcluded.toFixed(2)).hide().fadeIn();
205             $('#tax').html(tax.toFixed(2)).hide().fadeIn();
206             $('#total').html(total.toFixed(2)).hide().fadeIn();
207         },
208     });
209
210 // ---------- Product Screen ----------
211
212
213     module.ProductWidget = module.PosBaseWidget.extend({
214         template: 'ProductWidget',
215         init: function(parent, options) {
216             this._super(parent,options);
217             this.model = options.model;
218             this.model.attributes.weight = options.weight || undefined;
219             this.next_screen = options.next_screen || undefined;
220         },
221         addToOrder: function(event) {
222             /* Preserve the category URL */
223             event.preventDefault();
224             return (this.pos.get('selectedOrder')).addProduct(this.model);
225         },
226         set_weight: function(weight){
227             this.model.attributes.weight = weight;
228             this.renderElement();
229         },
230         set_next_screen: function(screen){
231             this.next_screen = screen;
232         },
233         renderElement: function() {
234             this._super();
235             var self = this;
236             $("a", this.$element).click(function(e){
237                 self.addToOrder(e);
238                 if(self.next_screen){
239                     self.pos_widget.screen_selector.set_current_screen(self.next_screen);    //FIXME There ought to be a better way to do this ...
240                 }
241             });
242         },
243     });
244
245     module.PaymentlineWidget = module.PosBaseWidget.extend({
246         template: 'PaymentlineWidget',
247         init: function(parent, options) {
248             this._super(parent,options);
249             this.payment_line = options.payment_line;
250             this.payment_line.bind('change', this.changedAmount, this);
251         },
252         on_delete: function() {},
253         changeAmount: function(event) {
254             var newAmount;
255             newAmount = event.currentTarget.value;
256             if (newAmount && !isNaN(newAmount)) {
257                 this.amount = parseFloat(newAmount);
258                 this.payment_line.set({
259                     amount: this.amount,
260                 });
261             }
262         },
263         changedAmount: function() {
264                 if (this.amount !== this.payment_line.get('amount'))
265                         this.renderElement();
266         },
267         renderElement: function() {
268             this.name =   this.payment_line.get('journal_id')[1];
269             this._super();
270             $('input', this.$element).keyup(_.bind(this.changeAmount, this));
271             $('.delete-payment-line', this.$element).click(this.on_delete);
272         },
273     });
274
275     module.OrderButtonWidget = module.PosBaseWidget.extend({
276         template:'OrderButtonWidget',
277         init: function(parent, options) {
278             this._super(parent,options);
279             this.order = options.order;
280             this.order.bind('destroy', _.bind( function() {
281                 this.destroy();
282             }, this));
283             this.pos.bind('change:selectedOrder', _.bind( function(pos) {
284                 var selectedOrder;
285                 selectedOrder = pos.get('selectedOrder');
286                 if (this.order === selectedOrder) {
287                     this.setButtonSelected();
288                 }
289             }, this));
290         },
291         start: function() {
292             $('button.select-order', this.$element).click(_.bind(this.selectOrder, this));
293             $('button.close-order', this.$element).click(_.bind(this.closeOrder, this));
294         },
295         selectOrder: function(event) {
296             this.pos.set({
297                 selectedOrder: this.order
298             });
299         },
300         setButtonSelected: function() {
301             $('.selected-order').removeClass('selected-order');
302             this.$element.addClass('selected-order');
303         },
304         closeOrder: function(event) {
305             this.order.destroy();
306         },
307     });
308
309     module.ActionButtonWidget = instance.web.Widget.extend({
310         template:'ActionButtonWidget',
311         init: function(parent, options){
312             this._super(parent, options);
313             this.label = options.label || 'button';
314             this.rightalign = options.rightalign || false;
315             this.click_action = options.click;
316             if(options.icon){
317                 this.icon = options.icon;
318                 this.template = 'ActionButtonWidgetWithIcon';
319             }
320         },
321         start: function(){
322             if(this.click_action){
323                 this.$element.click(_.bind(this.click_action, this));
324             }
325         },
326     });
327
328     module.ActionBarWidget = instance.web.Widget.extend({
329         template:'ActionBarWidget',
330         init: function(parent, options){
331             this._super(parent,options);
332             this.button_list = [];
333             this.fake_buttons  = {};
334             this.visibility = {};
335             this.total_visibility = true;
336             this.help_visibility  = true;
337             this.logout_visibility  = true;
338             this.close_visibility  = true;
339         },
340         set_element_visible: function(element, visible, action){
341             if(visible != this.visibility[element]){
342                 this.visibility[element] = visible;
343                 if(visible){
344                     this.$('.'+element).show();
345                 }else{
346                     this.$('.'+element).hide();
347                 }
348             }
349             if(visible && action){
350                 this.$('.'+element).off('click').click(action);
351             }
352         },
353         set_total_value: function(value){
354             this.$('.value').html(value);
355         },
356         destroy_buttons:function(){
357             for(var i = 0; i < this.button_list.length; i++){
358                 this.button_list[i].destroy();
359             }
360             this.button_list = [];
361             return this;
362         },
363         add_new_button: function(button_options){
364             if(arguments.length == 1){
365                 var button = new module.ActionButtonWidget(this,button_options);
366                 this.button_list.push(button);
367                 button.appendTo($('.pos-actionbar-button-list'));
368                 return button;
369             }else{
370                 for(var i = 0; i < arguments.length; i++){
371                     this.add_new_button(arguments[i]);
372                 }
373             }
374             return undefined;
375         },
376     });
377
378     module.ProductCategoriesWidget = module.PosBaseWidget.extend({
379         template: 'ProductCategoriesWidget',
380         init: function(parent, options){
381             var self = this;
382             this._super(parent,options);
383             this.product_type = options.product_type || 'all';  // 'all' | 'weightable'
384             this.onlyWeightable = options.onlyWeightable || false;
385             this.category = this.pos.root_category;
386             this.breadcrumb = [];
387             this.subcategories = [];
388             this.set_category();
389         },
390
391         start: function(){
392             this.search_and_categories();
393         },
394
395         // changes the category. if undefined, sets to root category
396         set_category : function(category){
397             if(!category){
398                 this.category = this.pos.root_category;
399             }else{
400                 this.category = category;
401             }
402             this.breadcrumb = [];
403             for(var i = 1; i < this.category.ancestors.length; i++){
404                 this.breadcrumb.push(this.category.ancestors[i]);
405             }
406             if(this.category !== this.pos.root_category){
407                 this.breadcrumb.push(this.category);
408             }
409             if(this.product_type === 'weightable'){
410                 this.subcategories = [];
411                 for(var i = 0; i < this.category.childrens.length; i++){
412                     if(this.category.childrens[i].weightable_product_list.length > 0){
413                         this.subcategories.push( this.category.childrens[i]);
414                     }
415                 }
416             }else{
417                 this.subcategories = this.category.childrens || [];
418             }
419         },
420
421         renderElement: function(){
422             var self = this;
423             this._super();
424             this.$element.find(".oe-pos-categories-list a").click(function(event){
425                 var id = $(event.target).data("category-id");
426                 var category = self.pos.categories_by_id[id];
427                 self.set_category(category);
428                 self.renderElement();
429                 self.search_and_categories(category);
430             });
431         },
432         
433         set_product_type: function(type){       // 'all' | 'weightable'
434             this.product_type = type;
435             this.reset_category();
436         },
437
438         // resets the current category to the root category
439         reset_category: function(){
440             this.set_category();
441             this.renderElement();
442             this.search_and_categories();
443         },
444
445         // filters the products, and sets up the search callbacks
446         search_and_categories: function(category){
447             var self = this;
448             
449             var all_products = this.pos.get('product_list');
450             var all_packages = this.pos.get('product.packaging');
451
452             // find all products belonging to the current category
453             var products = [];
454             if(this.product_type === 'weightable'){
455                 products = all_products.filter( function(product){
456                     return self.category.weightable_product_set[product.id];
457                 });
458             }else{
459                 products = all_products.filter( function(product){
460                     return self.category.product_set[product.id];
461                 });
462             }
463
464             // product lists watch for reset events on 'products' to re-render. 
465             // FIXME that means all productlist widget re-render... even the hidden ones ! 
466             this.pos.get('products').reset(products);
467             
468             // find all the products whose name match the query in the searchbox
469             this.$('.searchbox input').keyup(function(){
470                 var results, search_str;
471                 search_str = $(this).val().toLowerCase();
472                 if(search_str){
473                     results = products.filter( function(p){
474                         return p.name.toLowerCase().indexOf(search_str) != -1 || 
475                                (p.ean13 && p.ean13.indexOf(search_str) != -1);
476                     });
477                     self.$element.find('.search-clear').fadeIn();
478                 }else{
479                     results = products;
480                     self.$element.find('.search-clear').fadeOut();
481                 }
482                 self.pos.get('products').reset(results);
483             });
484             this.$('.searchbox input').click(function(){
485             });
486
487             //reset the search when clicking on reset
488             this.$('.search-clear').click(function(){
489                 self.pos.get('products').reset(products);
490                 self.$('.searchbox input').val('').focus();
491                 self.$('.search-clear').fadeOut();
492             });
493         },
494     });
495
496     module.ProductListWidget = module.ScreenWidget.extend({
497         template:'ProductListWidget',
498         init: function(parent, options) {
499             var self = this;
500             this._super(parent,options);
501             this.model = options.model;
502             this.product_list = [];
503             this.weight = options.weight || 0;
504             this.show_scale = options.show_scale || false;
505             this.next_screen = options.next_screen || false;
506
507             this.pos.get('products').bind('reset', function(){
508                 self.renderElement();
509             });
510         },
511         set_weight: function(weight){
512             for(var i = 0; i < this.product_list.length; i++){
513                 this.product_list[i].set_weight(weight);
514             }
515         },
516         set_next_screen: function(screen){
517             for(var i = 0; i < this.product_list.length; i++){
518                 this.product_list[i].set_next_screen(screen);
519             }
520         },
521         renderElement: function() {
522             var self = this;
523             this._super();
524             this.product_list = []; 
525             this.pos.get('products')
526                 .chain()
527                 .map(function(product) {
528                     var product = new module.ProductWidget(self, {
529                             model: product,
530                             weight: self.weight,
531                     })
532                     self.product_list.push(product);
533                     return product;
534                 })
535                 .invoke('appendTo', this.$element);
536         },
537     });
538
539 // ---------- OnScreen Keyboard Widget ----------
540
541     // A Widget that displays an onscreen keyboard.
542     // There are two options when creating the widget :
543     // 
544     // * 'keyboard_model' : 'simple' | 'full' (default) 
545     //   The 'full' emulates a PC keyboard, while 'simple' emulates an 'android' one.
546     //
547     // * 'input_selector  : (default: '.searchbox input') 
548     //   defines the dom element that the keyboard will write to.
549     // 
550     // The widget is initially hidden. It can be shown with this.show(), and is 
551     // automatically shown when the input_selector gets focused.
552
553     module.OnscreenKeyboardWidget = instance.web.Widget.extend({
554         template: 'OnscreenKeyboardSimple', 
555         init: function(parent, options){
556             var self = this;
557             this._super(parent,options);
558             options = options || {};
559
560             this.keyboard_model = options.keyboard_model || 'full';
561             if(this.keyboard_model === 'full'){
562                 this.template = 'OnscreenKeyboardFull';
563             }
564
565             this.input_selector = options.input_selector || '.searchbox input';
566
567             //show the keyboard when the input zone is clicked.
568             $(this.input_selector).focus(function(){self.show();});
569
570             //Keyboard state
571             this.capslock = false;
572             this.shift    = false;
573             this.numlock  = false;
574         },
575         
576         connect : function(){
577             var self = this;
578             $(this.input_selector).focus(function(){self.show();});
579         },
580
581         // Write a character to the input zone
582         writeCharacter: function(character){
583             var $input = $(this.input_selector);
584             $input[0].value += character;
585             $input.keydown();
586             $input.keyup();
587         },
588         
589         // Sends a 'return' character to the input zone. TODO
590         sendReturn: function(){
591         },
592         
593         // Removes the last character from the input zone.
594         deleteCharacter: function(){
595             var $input = $(this.input_selector);
596             var input_value = $input[0].value;
597             $input[0].value = input_value.substr(0, input_value.length - 1);
598             $input.keydown();
599             $input.keyup();
600         },
601         
602         // Clears the content of the input zone.
603         deleteAllCharacters: function(){
604             var $input = $(this.input_selector);
605             $input[0].value = "";
606             $input.keydown();
607             $input.keyup();
608         },
609
610         // Makes the keyboard show and slide from the bottom of the screen.
611         show:  function(){
612             $('.keyboard_frame').show().animate({'height':'235px'}, 500, 'swing');
613         },
614         
615         // Makes the keyboard hide by sliding to the bottom of the screen.
616         hide:  function(){
617             var self = this;
618             var frame = $('.keyboard_frame');
619             frame.animate({'height':'0'}, 500, 'swing', function(){ frame.hide(); self.reset(); });
620         },
621         
622         //What happens when the shift key is pressed : toggle case, remove capslock
623         toggleShift: function(){
624             $('.letter').toggleClass('uppercase');
625             $('.symbol span').toggle();
626             
627             self.shift = (self.shift === true) ? false : true;
628             self.capslock = false;
629         },
630         
631         //what happens when capslock is pressed : toggle case, set capslock
632         toggleCapsLock: function(){
633             $('.letter').toggleClass('uppercase');
634             self.capslock = true;
635         },
636         
637         //What happens when numlock is pressed : toggle symbols and numlock label 
638         toggleNumLock: function(){
639             $('.symbol span').toggle();
640             $('.numlock span').toggle();
641             self.numlock = (self.numlock === true ) ? false : true;
642         },
643
644         //After a key is pressed, shift is disabled. 
645         removeShift: function(){
646             if (self.shift === true) {
647                 $('.symbol span').toggle();
648                 if (this.capslock === false) $('.letter').toggleClass('uppercase');
649                 
650                 self.shift = false;
651             }
652         },
653
654         // Resets the keyboard to its original state; capslock: false, shift: false, numlock: false
655         reset: function(){
656             if(this.shift){
657                 this.toggleShift();
658             }
659             if(this.capslock){
660                 this.toggleCapsLock();
661             }
662             if(this.numlock){
663                 this.toggleNumLock();
664             }
665         },
666
667         //called after the keyboard is in the DOM, sets up the key bindings.
668         start: function(){
669             var self = this;
670
671             //this.show();
672
673
674             $('.close_button').click(function(){ 
675                 self.deleteAllCharacters();
676                 self.hide(); 
677             });
678
679             // Keyboard key click handling
680             $('.keyboard li').click(function(){
681                 
682                 var $this = $(this),
683                     character = $this.html(); // If it's a lowercase letter, nothing happens to this variable
684                 
685                 if ($this.hasClass('left-shift') || $this.hasClass('right-shift')) {
686                     self.toggleShift();
687                     return false;
688                 }
689                 
690                 if ($this.hasClass('capslock')) {
691                     self.toggleCapsLock();
692                     return false;
693                 }
694                 
695                 if ($this.hasClass('delete')) {
696                     self.deleteCharacter();
697                     return false;
698                 }
699
700                 if ($this.hasClass('numlock')){
701                     self.toggleNumLock();
702                     return false;
703                 }
704                 
705                 // Special characters
706                 if ($this.hasClass('symbol')) character = $('span:visible', $this).html();
707                 if ($this.hasClass('space')) character = ' ';
708                 if ($this.hasClass('tab')) character = "\t";
709                 if ($this.hasClass('return')) character = "\n";
710                 
711                 // Uppercase letter
712                 if ($this.hasClass('uppercase')) character = character.toUpperCase();
713                 
714                 // Remove shift once a key is clicked.
715                 self.removeShift();
716
717                 self.writeCharacter(character);
718             });
719         },
720     });
721
722 // ---------- Main Point of Sale Widget ----------
723
724     // this is used to notify the user that data is being synchronized on the network
725     module.SynchNotificationWidget = instance.web.Widget.extend({
726         template: "SynchNotificationWidget",
727         init: function(parent) {
728             this._super(parent);
729             this.nbr_pending = 0;
730         },
731         renderElement: function() {
732             this._super();
733             $('.oe_pos_synch-notification-button', this.$element).click(this.on_synch);
734         },
735         on_change_nbr_pending: function(nbr_pending) {
736             this.nbr_pending = nbr_pending;
737             this.renderElement();
738         },
739         on_synch: function() {}
740     });
741
742     // The PosWidget is the main widget that contains all other widgets in the PointOfSale.
743     // It is mainly composed of :
744     // - a header, containing the list of orders
745     // - a leftpane, containing the list of bought products (orderlines) 
746     // - a rightpane, containing the screens (see pos_screens.js)
747     // - an actionbar on the bottom, containing various action buttons
748     // - popups
749     // - an onscreen keyboard
750     // a screen_selector which controls the switching between screens and the showing/closing of popups
751
752     module.PosWidget = module.PosBaseWidget.extend({
753         template: 'PosWidget',
754         init: function() { 
755             console.log('PosArguments:',arguments);
756             this._super(arguments[0],{});
757             
758             this.pos = new module.PosModel(this.session);
759             window.pos = this.pos;
760             window.pos_widget = this;
761             this.pos_widget = this; //So that pos_widget's childs have pos_widget set automatically
762
763             this.numpad_visible = true;
764             this.leftpane_visible = true;
765             this.leftpane_width   = '440px';
766             this.cashier_controls_visible = true;
767         },
768       
769         start: function() {
770             var self = this;
771             return self.pos.ready.then(function() {
772                 self.build_currency_template();
773                 self.renderElement();
774                 self.synch_notification = new module.SynchNotificationWidget(this);
775                 self.synch_notification.replace($('.placeholder-SynchNotificationWidget', self.$element));
776                 self.synch_notification.on_synch.add(_.bind(self.pos.flush, self.pos));
777                 
778                 self.pos.bind('change:nbr_pending_operations', self.changed_pending_operations, self);
779                 self.changed_pending_operations();
780                 
781                 self.$element.find("#loggedas button").click(function() {
782                     self.try_close();
783                 });
784                 
785                 self.$('button#neworder-button').click(_.bind(self.create_new_order, self));
786                 
787                 //when a new order is created, add an order button widget
788                 self.pos.get('orders').bind('add', function(new_order){
789                     var new_order_button = new module.OrderButtonWidget(null, {
790                         order: new_order,
791                         pos: self.pos
792                     });
793                     new_order_button.appendTo($('#orders'));
794                     new_order_button.selectOrder();
795                 }, self);
796
797                 self.pos.get('orders').add(new module.Order({ pos: self.pos }));
798
799                 self.build_widgets();
800
801                 instance.webclient.set_content_full_screen(true);
802
803                 if (!self.pos.get('pos_session')) {
804                     self.screen_selector.show_popup('error',
805                         'Sorry, we could not create a user session');
806                 //}else if (!self.pos.get('bank_statements') || self.pos.get('bank_statements').length === 0){
807                 //    self.screen_selector.show_popup('error',
808                 //        'Sorry, we could not find any accounting journals in the configuration');
809                 }else if(!self.pos.get('pos_config')){
810                     self.screen_selector.show_popup('error',
811                         'Sorry, we could not find any PoS Configuration for this session');
812                 }
813             
814                 $('.loader').animate({opacity:0},3000,'swing',function(){$('.loader').hide();});
815                 $('.loader img').hide();
816
817             },function(){   // error when loading models data from the backend
818                 $('.loader img').hide();
819                 return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_session_opening']], ['res_id'])
820                     .pipe( _.bind(function(res){
821                         return instance.connection.rpc('/web/action/load', {'action_id': res[0]['res_id']})
822                             .pipe(_.bind(function(result){
823                                 var action = result.result;
824                                 this.do_action(action);
825                             }, this));
826                     }, self));
827             });
828         },
829
830         build_widgets: function() {
831
832             // --------  Screens ---------
833
834             this.search_product_screen = new module.SearchProductScreenWidget(this,{});
835             this.search_product_screen.appendTo($('#rightpane'));
836
837             this.scan_product_screen = new module.ScanProductScreenWidget(this,{});
838             this.scan_product_screen.appendTo($('#rightpane'));
839
840             this.receipt_screen = new module.ReceiptScreenWidget(this, {});
841             this.receipt_screen.appendTo($('#rightpane'));
842
843             this.payment_screen = new module.PaymentScreenWidget(this, {});
844             this.payment_screen.appendTo($('#rightpane'));
845
846             this.welcome_screen = new module.WelcomeScreenWidget(this,{});
847             this.welcome_screen.appendTo($('#rightpane'));
848
849             this.client_payment_screen = new module.ClientPaymentScreenWidget(this, {});
850             this.client_payment_screen.appendTo($('#rightpane'));
851
852             this.scale_invite_screen = new module.ScaleInviteScreenWidget(this, {});
853             this.scale_invite_screen.appendTo($('#rightpane'));
854
855             this.scale_product_screen = new module.ScaleProductScreenWidget(this, {});
856             this.scale_product_screen.appendTo($('#rightpane'));
857
858             // --------  Popups ---------
859
860             this.help_popup = new module.HelpPopupWidget(this, {});
861             this.help_popup.appendTo($('.point-of-sale'));
862
863             this.receipt_popup = new module.ReceiptPopupWidget(this, {});
864             this.receipt_popup.appendTo($('.point-of-sale'));
865
866             this.error_popup = new module.ErrorPopupWidget(this, {});
867             this.error_popup.appendTo($('.point-of-sale'));
868
869             this.error_product_popup = new module.ErrorProductNotRecognizedPopupWidget(this, {});
870             this.error_product_popup.appendTo($('.point-of-sale'));
871
872             this.error_session_popup = new module.ErrorNoSessionPopupWidget(this, {});
873             this.error_session_popup.appendTo($('.point-of-sale'));
874
875             // --------  Misc ---------
876
877             this.action_bar = new module.ActionBarWidget(this);
878             this.action_bar.appendTo($(".point-of-sale #content"));
879
880             this.paypad = new module.PaypadWidget(this, {});
881             this.paypad.replace($('#placeholder-PaypadWidget'));
882
883             this.numpad = new module.NumpadWidget(this);
884             this.numpad.replace($('#placeholder-NumpadWidget'));
885
886             this.order_widget = new module.OrderWidget(this, {});
887             this.order_widget.replace($('#placeholder-OrderWidget'));
888
889             this.onscreen_keyboard = new module.OnscreenKeyboardWidget(this, {
890                 'keyboard_model': 'simple'
891             });
892             this.onscreen_keyboard.appendTo($(".point-of-sale #content")); 
893             
894             // --------  Screen Selector ---------
895
896             this.screen_selector = new module.ScreenSelector({
897                 pos: this.pos,
898                 screen_set:{
899                     'products': this.search_product_screen,
900                     'scan': this.scan_product_screen,
901                     'payment' : this.payment_screen,
902                     'client_payment' : this.client_payment_screen,
903                     'scale_invite' : this.scale_invite_screen,
904                     'scale_product' : this.scale_product_screen,
905                     'receipt' : this.receipt_screen,
906                     'welcome' : this.welcome_screen,
907                 },
908                 popup_set:{
909                     'help': this.help_popup,
910                     'error': this.error_popup,
911                     'error-product': this.error_product_popup,
912                     'error-session': this.error_session_popup,
913                     'receipt': this.receipt_popup,
914                 },
915                 default_client_screen: 'welcome',
916                 default_cashier_screen: 'products',
917                 default_mode: this.pos.use_selfcheckout ?  'client' : 'cashier',
918             });
919             this.screen_selector.set_default_screen();
920
921             window.screen_selector = this.screen_selector; //DEBUG
922
923             this.pos.barcode_reader.connect();
924             
925         },
926
927         //FIXME this method is probably not at the right place ... 
928         scan_product: function(parsed_ean){
929             var selectedOrder = this.pos.get('selectedOrder');
930             var scannedProductModel = this.get_product_by_ean(parsed_ean);
931             if (!scannedProductModel){
932                 return false;
933             } else {
934                 selectedOrder.addProduct(new module.Product(scannedProductModel));
935                 return true;
936             }
937         },
938
939         // returns a product that has a packaging with an EAN matching to provided parsed ean . 
940         // returns undefined if no such product is found.
941         get_product_by_ean: function(parsed_ean) {
942             var allProducts = this.pos.get('product_list');
943             var allPackages = this.pos.get('product.packaging');
944             var scannedProductModel = undefined;
945
946             if (parsed_ean.type === 'price') {
947                 var itemCode = parsed_ean.id;
948                 console.log('price! id:',itemCode);
949                 var scannedPackaging = _.detect(allPackages, function(pack) { 
950                     return pack.ean && pack.ean.substring(0,7) === itemCode;
951                 });
952                 if (scannedPackaging) {
953                     console.log('found matching package, finding matching product...');
954                     scannedProductModel = _.detect(allProducts, function(pc) { return pc.id === scannedPackaging.product_id[0];});
955                 }else{
956                     console.log('matching package not found, finding matching product...');
957                     scannedProductModel = _.detect(allProducts, function(pc) { return pc.ean13  && (pc.ean13.substring(0,7) === parsed_ean.id);});   
958                 }
959                 if(scannedProductModel){
960                     scannedProductModel.list_price = parsed_ean.value;
961                 }
962             } else if (parsed_ean.type === 'weight') {
963                 var weight = parsed_ean.value;
964                 var itemCode = parsed_ean.id;
965                 var scannedPackaging = _.detect(allPackages, function(pack) { 
966                     return pack.ean  && pack.ean.substring(0,7) === itemCode;
967                 });
968                 if (scannedPackaging){
969                     console.log('found matching package, finding matching product...');
970                     scannedProductModel = _.detect(allProducts, function(pc) { return pc.id === scannedPackaging.product_id[0];});
971                 }else{
972                     console.log('matching package not found, finding matching product...');
973                     scannedProductModel = _.detect(allProducts, function(pc) { return pc.ean13  && (pc.ean13.substring(0,7) === parsed_ean.id);});   
974                 }
975                 if(scannedProductModel){
976                     scannedProductModel.list_price *= weight;
977                     scannedProductModel.name += ' - ' + weight + ' Kg.';
978                 }
979             } else if(parsed_ean.type === 'unit'){
980                 scannedProductModel = _.detect(allProducts, function(pc) { return pc.ean13 === parsed_ean.ean;});   //TODO DOES NOT SCALE
981             }
982             return scannedProductModel;
983         },
984         // creates a new order, and add it to the list of orders.
985         create_new_order: function() {
986             var new_order;
987             new_order = new module.Order({ pos: this.pos });
988             this.pos.get('orders').add(new_order);
989             this.pos.set({ selectedOrder: new_order });
990         },
991         changed_pending_operations: function () {
992             var self = this;
993             this.synch_notification.on_change_nbr_pending(self.pos.get('nbr_pending_operations').length);
994         },
995         // shows or hide the numpad and related controls like the paypad.
996         set_numpad_visible: function(visible){
997             if(visible != this.numpad_visible){
998                 this.numpad_visible = visible;
999                 if(visible){
1000                     $('#numpad').show();
1001                     $('#paypad').show();
1002                     $('#current-order').css({'bottom':'271px'});
1003                 }else{
1004                     $('#numpad').hide();
1005                     $('#paypad').hide();
1006                     $('#current-order').css({'bottom':'0px'});
1007                 }
1008             }
1009         },
1010         //shows or hide the leftpane (contains the list of orderlines, the numpad, the paypad, etc.)
1011         set_leftpane_visible: function(visible){
1012             if(visible != this.leftpane_visible){
1013                 this.leftpane_visible = visible;
1014                 if(visible){
1015                     $('#leftpane').show().animate({'width':this.leftpane_width},500,'swing');
1016                     $('#rightpane').animate({'left':this.leftpane_width},500,'swing');
1017                 }else{
1018                     var leftpane = $('#leftpane');
1019                     $('#leftpane').animate({'width':'0px'},500,'swing', function(){ leftpane.hide(); });
1020                     $('#rightpane').animate({'left':'0px'},500,'swing');
1021                 }
1022             }
1023         },
1024         //shows or hide the controls in the PosWidget that are specific to the cashier ( Orders, close button, etc. ) 
1025         set_cashier_controls_visible: function(visible){
1026             if(visible != this.cashier_controls_visible){
1027                 this.cashier_controls_visible = visible;
1028                 if(visible){
1029                     $('#loggedas').show();
1030                     $('#rightheader').show();
1031                 }else{
1032                     $('#loggedas').hide();
1033                     $('#rightheader').hide();
1034                 }
1035             }
1036         },
1037         try_close: function() {
1038             var self = this;
1039             self.pos.flush().then(_.bind(function() {
1040                 var close = _.bind(this.close, this);
1041                 if (self.pos.get('nbr_pending_operations').length > 0) {
1042                     var confirm = false;
1043                     $(QWeb.render('PosCloseWarning')).dialog({
1044                         resizable: false,
1045                         height:160,
1046                         modal: true,
1047                         title: "Warning",
1048                         buttons: {
1049                             "Yes": function() {
1050                                 confirm = true;
1051                                 $( this ).dialog( "close" );
1052                             },
1053                             "No": function() {
1054                                 $( this ).dialog( "close" );
1055                             }
1056                         },
1057                         close: function() {
1058                             if (confirm){
1059                                 close();
1060                             }
1061                         }
1062                     });
1063                 } else {
1064                     close();
1065                 }
1066             }, this));
1067         },
1068         close: function() {
1069             this.pos.barcode_reader.disconnect();
1070             return new session.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_close_statement']], ['res_id']).pipe(
1071                     _.bind(function(res) {
1072                 return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) {
1073                     var action = result.result;
1074                     action.context = _.extend(action.context || {}, {'cancel_action': {type: 'ir.actions.client', tag: 'default_home'}});
1075                     this.do_action(action);
1076                 }, this));
1077             }, this));
1078         },
1079         destroy: function() {
1080             instance.webclient.set_content_full_screen(false);
1081             self.pos = undefined;
1082             this._super();
1083         }
1084     });
1085 }