1 function openerp_pos_widgets(instance, module){ //module is instance.point_of_sale
2 var QWeb = instance.web.qweb,
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
9 module.ImageCache = instance.web.Class.extend({
10 init: function(options){
11 options = options || {};
12 this.max_size = options.max_size || 500;
15 this.access_time = {};
18 get_image_uncached: function(url){
19 var img = new Image();
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];
27 this.access_time[url] = (new Date()).getTime();
30 var img = new Image();
32 while(this.size >= this.max_size){
34 var oldestTime = (new Date()).getTime();
35 for(var url in this.cache){
36 var time = this.access_time[url];
37 if(time <= oldestTime){
43 delete this.cache[oldestUrl];
44 delete this.access_time[oldestUrl];
48 this.cache[url] = img;
49 this.access_time[url] = (new Date()).getTime();
56 module.NumpadWidget = module.PosBaseWidget.extend({
57 template:'NumpadWidget',
58 init: function(parent, options) {
60 this.state = new module.NumpadState();
61 window.numpadstate = this.state;
65 this.state.bind('change:mode', this.changedMode, this);
67 this.$el.find('.numpad-backspace').click(_.bind(this.clickDeleteLastChar, this));
68 this.$el.find('.numpad-minus').click(_.bind(this.clickSwitchSign, this));
69 this.$el.find('.number-char').click(_.bind(this.clickAppendNewChar, this));
70 this.$el.find('.mode-button').click(_.bind(this.clickChangeMode, this));
72 clickDeleteLastChar: function() {
73 return this.state.deleteLastChar();
75 clickSwitchSign: function() {
76 return this.state.switchSign();
78 clickAppendNewChar: function(event) {
80 newChar = event.currentTarget.innerText || event.currentTarget.textContent;
81 return this.state.appendNewChar(newChar);
83 clickChangeMode: function(event) {
84 var newMode = event.currentTarget.attributes['data-mode'].nodeValue;
85 return this.state.changeMode(newMode);
87 changedMode: function() {
88 var mode = this.state.get('mode');
89 $('.selected-mode').removeClass('selected-mode');
90 $(_.str.sprintf('.mode-button[data-mode="%s"]', mode), this.$el).addClass('selected-mode');
94 // The paypad allows to select the payment method (cashregisters)
95 // used to pay the order.
96 module.PaypadWidget = module.PosBaseWidget.extend({
97 template: 'PaypadWidget',
98 renderElement: function() {
102 _.each(this.pos.cashregisters,function(cashregister) {
103 var button = new module.PaypadButtonWidget(self,{
105 pos_widget : self.pos_widget,
106 cashregister: cashregister,
108 button.appendTo(self.$el);
113 module.PaypadButtonWidget = module.PosBaseWidget.extend({
114 template: 'PaypadButtonWidget',
115 init: function(parent, options){
116 this._super(parent, options);
117 this.cashregister = options.cashregister;
119 renderElement: function() {
123 this.$el.click(function(){
124 if (self.pos.get('selectedOrder').get('screen') === 'receipt'){ //TODO Why ?
125 console.warn('TODO should not get there...?');
128 self.pos.get('selectedOrder').addPaymentline(self.cashregister);
129 self.pos_widget.screen_selector.set_current_screen('payment');
134 module.OrderWidget = module.PosBaseWidget.extend({
135 template:'OrderWidget',
136 init: function(parent, options) {
138 this._super(parent,options);
139 this.editable = false;
140 this.pos.bind('change:selectedOrder', this.change_selected_order, this);
141 this.bind_orderline_events();
142 this.line_click_handler = function(event){
146 self.pos.get('selectedOrder').selectLine(this.orderline);
147 self.pos_widget.numpad.state.reset();
150 enable_numpad: function(){
151 this.disable_numpad(); //ensure we don't register the callbacks twice
152 this.numpad_state = this.pos_widget.numpad.state;
153 if(this.numpad_state){
154 this.numpad_state.reset();
155 this.numpad_state.bind('set_value', this.set_value, this);
159 disable_numpad: function(){
160 if(this.numpad_state){
161 this.numpad_state.unbind('set_value', this.set_value);
162 this.numpad_state.reset();
165 set_editable: function(editable){
166 this.editable = editable;
168 this.enable_numpad();
170 this.disable_numpad();
171 this.pos.get('selectedOrder').deselectLine();
174 set_value: function(val) {
175 var order = this.pos.get('selectedOrder');
176 if (this.editable && order.getSelectedLine()) {
177 var mode = this.numpad_state.get('mode');
178 if( mode === 'quantity'){
179 order.getSelectedLine().set_quantity(val);
180 }else if( mode === 'discount'){
181 order.getSelectedLine().set_discount(val);
182 }else if( mode === 'price'){
183 order.getSelectedLine().set_unit_price(val);
187 change_selected_order: function() {
188 this.bind_orderline_events();
189 this.renderElement();
191 bind_orderline_events: function() {
192 var lines = this.pos.get('selectedOrder').get('orderLines');
194 lines.bind('add', function(){
195 this.numpad_state.reset();
196 this.renderElement(true);
198 lines.bind('remove', function(line){
199 this.remove_orderline(line);
200 this.numpad_state.reset();
201 this.update_summary();
203 lines.bind('change', function(line){
204 this.rerender_orderline(line);
205 this.update_summary();
208 render_orderline: function(orderline){
209 var el_str = openerp.qweb.render('Orderline',{widget:this, line:orderline});
210 var el_node = document.createElement('div');
211 el_node.innerHTML = _.str.trim(el_str);
212 el_node = el_node.childNodes[0];
213 el_node.orderline = orderline;
214 el_node.addEventListener('click',this.line_click_handler);
216 orderline.node = el_node;
219 remove_orderline: function(order_line){
220 if(this.pos.get('selectedOrder').get('orderLines').length === 0){
221 this.renderElement();
223 order_line.node.parentNode.removeChild(order_line.node);
226 rerender_orderline: function(order_line){
227 var node = order_line.node;
228 var replacement_line = this.render_orderline(order_line);
229 node.parentNode.replaceChild(replacement_line,node);
231 // overriding the openerp framework replace method for performance reasons
232 replace: function($target){
233 this.renderElement();
234 var target = $target[0];
235 target.parentNode.replaceChild(this.el,target);
237 renderElement: function(scrollbottom){
238 this.pos_widget.numpad.state.reset();
240 var order = this.pos.get('selectedOrder');
241 var orderlines = order.get('orderLines').models;
243 var el_str = openerp.qweb.render('OrderWidget',{widget:this, order:order, orderlines:orderlines});
244 var el_node = document.createElement('div');
245 el_node.innerHTML = _.str.trim(el_str);
246 el_node = el_node.childNodes[0];
248 var list_container = el_node.querySelector('.orderlines');
249 for(var i = 0, len = orderlines.length; i < len; i++){
250 var orderline = this.render_orderline(orderlines[i]);
251 list_container.appendChild(orderline);
254 if(this.el && this.el.parentNode){
255 this.el.parentNode.replaceChild(el_node,this.el);
258 this.update_summary();
261 this.el.querySelector('.order-scroller').scrollTop = 100 * orderlines.length;
264 update_summary: function(){
265 var order = this.pos.get('selectedOrder');
266 var total = order ? order.getTotalTaxIncluded() : 0;
267 var taxes = order ? total - order.getTotalTaxExcluded() : 0;
269 this.el.querySelector('.summary .total > .value').innerText = this.format_currency(total);
270 this.el.querySelector('.summary .total .subentry .value').innerText = this.format_currency(taxes);
274 module.OrderButtonWidget = module.PosBaseWidget.extend({
275 template:'OrderButtonWidget',
276 init: function(parent, options) {
277 this._super(parent,options);
280 this.order = options.order;
281 this.order.bind('destroy',this.destroy, this );
282 this.order.bind('change', this.renderElement, this );
283 this.pos.bind('change:selectedOrder', this.renderElement,this );
285 renderElement:function(){
288 this.$el.click(function(){
291 if( this.order === this.pos.get('selectedOrder') ){
292 this.$el.addClass('selected');
295 selectOrder: function(event) {
297 selectedOrder: this.order
301 this.order.unbind('destroy', this.destroy, this);
302 this.order.unbind('change', this.renderElement, this);
303 this.pos.unbind('change:selectedOrder', this.renderElement, this);
308 module.ActionButtonWidget = instance.web.Widget.extend({
309 template:'ActionButtonWidget',
310 icon_template:'ActionButtonWidgetWithIcon',
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 this.disabled = options.disabled || false;
318 this.icon = options.icon;
319 this.template = this.icon_template;
322 set_disabled: function(disabled){
323 if(this.disabled != disabled){
324 this.disabled = !!disabled;
325 this.renderElement();
328 renderElement: function(){
330 if(this.click_action && !this.disabled){
331 this.$el.click(_.bind(this.click_action, this));
336 module.ActionBarWidget = instance.web.Widget.extend({
337 template:'ActionBarWidget',
338 init: function(parent, options){
339 this._super(parent,options);
340 this.button_list = [];
342 this.visibility = {};
344 set_element_visible: function(element, visible, action){
345 if(visible != this.visibility[element]){
346 this.visibility[element] = !!visible;
348 this.$('.'+element).removeClass('oe_hidden');
350 this.$('.'+element).addClass('oe_hidden');
353 if(visible && action){
354 this.action[element] = action;
355 this.$('.'+element).off('click').click(action);
358 set_button_disabled: function(name, disabled){
359 var b = this.buttons[name];
361 b.set_disabled(disabled);
364 destroy_buttons:function(){
365 for(var i = 0; i < this.button_list.length; i++){
366 this.button_list[i].destroy();
368 this.button_list = [];
372 get_button_count: function(){
373 return this.button_list.length;
375 add_new_button: function(button_options){
376 var button = new module.ActionButtonWidget(this,button_options);
377 this.button_list.push(button);
378 if(button_options.name){
379 this.buttons[button_options.name] = button;
381 button.appendTo(this.$('.pos-actionbar-button-list'));
385 this.$el.removeClass('oe_hidden');
388 this.$el.addClass('oe_hidden');
392 module.ProductCategoriesWidget = module.PosBaseWidget.extend({
393 template: 'ProductCategoriesWidget',
394 init: function(parent, options){
396 this._super(parent,options);
397 this.product_type = options.product_type || 'all'; // 'all' | 'weightable'
398 this.onlyWeightable = options.onlyWeightable || false;
399 this.category = this.pos.root_category;
400 this.breadcrumb = [];
401 this.subcategories = [];
402 this.product_list_widget = options.product_list_widget || null;
403 this.category_cache = {};
406 this.switch_category_handler = function(event){
407 self.set_category(self.pos.db.get_category_by_id(Number(this.dataset['categoryId'])));
408 self.renderElement();
411 this.clear_search_handler = function(event){
415 var search_timeout = null;
416 this.search_handler = function(event){
417 clearTimeout(search_timeout);
419 var query = this.value;
421 search_timeout = setTimeout(function(){
422 self.perform_search(self.category, query, event.which === 13);
427 // changes the category. if undefined, sets to root category
428 set_category : function(category){
429 var db = this.pos.db;
431 this.category = db.get_category_by_id(db.root_category_id);
433 this.category = category;
435 this.breadcrumb = [];
436 var ancestors_ids = db.get_category_ancestors_ids(this.category.id);
437 for(var i = 1; i < ancestors_ids.length; i++){
438 this.breadcrumb.push(db.get_category_by_id(ancestors_ids[i]));
440 if(this.category.id !== db.root_category_id){
441 this.breadcrumb.push(this.category);
443 this.subcategories = db.get_category_by_id(db.get_category_childs_ids(this.category.id));
446 get_image_url: function(category){
447 return window.location.origin + '/web/binary/image?model=pos.category&field=image_medium&id='+category.id;
450 render_category: function( category, with_image ){
451 if(!this.category_cache[category.id]){
454 var image_url = this.get_image_url(category);
455 var category_html = QWeb.render('CategoryButton',{
459 category_html = _.str.trim(category_html);
460 var category_node = document.createElement('div');
461 category_node.innerHTML = category_html;
462 category_node = category_node.childNodes[0];
463 var img = category_node.querySelector('img');
464 img.parentNode.replaceChild(this.pos_widget.image_cache.get_image(image_url),img);
466 var category_html = QWeb.render('CategorySimpleButton',{
470 category_html = _.str.trim(category_html);
471 var category_node = document.createElement('div');
472 category_node.innerHTML = category_html;
473 category_node = category_node.childNodes[0];
475 this.category_cache[category.id] = category_node;
477 return this.category_cache[category.id];
480 replace: function($target){
481 this.renderElement();
482 var target = $target[0];
483 target.parentNode.replaceChild(this.el,target);
486 renderElement: function(){
489 var el_str = openerp.qweb.render(this.template, {widget: this});
490 var el_node = document.createElement('div');
491 el_node.innerHTML = el_str;
492 el_node = el_node.childNodes[1];
494 if(this.el && this.el.parentNode){
495 this.el.parentNode.replaceChild(el_node,this.el);
500 var hasimages = false; //if none of the subcategories have images, we don't display buttons with icons
501 for(var i = 0; i < this.subcategories.length; i++){
502 if(this.subcategories[i].image){
508 var list_container = el_node.querySelector('.category-list');
509 for(var i = 0, len = this.subcategories.length; i < len; i++){
510 list_container.appendChild(this.render_category(this.subcategories[i],hasimages));
513 var buttons = el_node.querySelectorAll('.js-category-switch');
514 for(var i = 0; i < buttons.length; i++){
515 buttons[i].addEventListener('click',this.switch_category_handler);
518 var products = this.pos.db.get_product_by_category(this.category.id);
519 this.product_list_widget.set_product_list(products);
521 this.el.querySelector('.searchbox input').addEventListener('keyup',this.search_handler);
523 this.el.querySelector('.search-clear').addEventListener('click',this.clear_search_handler);
525 if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
526 this.pos_widget.onscreen_keyboard.connect($(this.el.querySelector('.searchbox input')));
530 // resets the current category to the root category
531 reset_category: function(){
533 this.renderElement();
536 // empties the content of the search box
537 clear_search: function(){
538 var products = this.pos.db.get_product_by_category(this.category.id);
539 this.product_list_widget.set_product_list(products);
540 var input = this.el.querySelector('.searchbox input');
544 perform_search: function(category, query, buy_result){
546 var products = this.pos.db.search_product_in_category(category.id,query)
547 if(buy_result && products.length === 1){
548 this.pos.get('selectedOrder').addProduct(products[0]);
551 this.product_list_widget.set_product_list(products);
554 var products = this.pos.db.get_product_by_category(this.category.id);
555 this.product_list_widget.set_product_list(products);
561 module.ProductListWidget = module.PosBaseWidget.extend({
562 template:'ProductListWidget',
563 init: function(parent, options) {
565 this._super(parent,options);
566 this.model = options.model;
567 this.productwidgets = [];
568 this.weight = options.weight || 0;
569 this.show_scale = options.show_scale || false;
570 this.next_screen = options.next_screen || false;
572 this.click_product_handler = function(event){
573 var product = self.pos.db.get_product_by_id(this.dataset['productId']);
574 options.click_product_action(product);
577 this.product_list = options.product_list || [];
578 this.product_cache = {};
580 set_product_list: function(product_list){
581 this.product_list = product_list;
582 this.renderElement();
584 get_product_image_url: function(product){
585 return window.location.origin + '/web/binary/image?model=product.product&field=image_medium&id='+product.id;
587 replace: function($target){
588 this.renderElement();
589 var target = $target[0];
590 target.parentNode.replaceChild(this.el,target);
593 render_product: function(product){
594 if(!this.product_cache[product.id]){
595 var image_url = this.get_product_image_url(product);
596 var product_html = QWeb.render('Product',{
600 var product_node = document.createElement('div');
601 product_node.innerHTML = product_html;
602 product_node = product_node.childNodes[1];
603 var img = product_node.querySelector('img');
604 img.parentNode.replaceChild(this.pos_widget.image_cache.get_image(image_url),img);
605 this.product_cache[product.id] = product_node;
607 return this.product_cache[product.id];
610 renderElement: function() {
614 var el_str = openerp.qweb.render(this.template, {widget: this});
615 var el_node = document.createElement('div');
616 el_node.innerHTML = el_str;
617 el_node = el_node.childNodes[1];
619 if(this.el && this.el.parentNode){
620 this.el.parentNode.replaceChild(el_node,this.el);
624 var list_container = el_node.querySelector('.product-list');
625 for(var i = 0, len = this.product_list.length; i < len; i++){
626 var product_node = this.render_product(this.product_list[i]);
627 product_node.addEventListener('click',this.click_product_handler);
628 list_container.appendChild(product_node);
633 module.UsernameWidget = module.PosBaseWidget.extend({
634 template: 'UsernameWidget',
635 init: function(parent, options){
636 var options = options || {};
637 this._super(parent,options);
638 this.mode = options.mode || 'cashier';
640 set_user_mode: function(mode){
645 this.renderElement();
647 get_name: function(){
649 if(this.mode === 'cashier'){
650 user = this.pos.cashier || this.pos.user;
652 user = this.pos.get('selectedOrder').get_client() || this.pos.user;
662 module.HeaderButtonWidget = module.PosBaseWidget.extend({
663 template: 'HeaderButtonWidget',
664 init: function(parent, options){
665 options = options || {};
666 this._super(parent, options);
667 this.action = options.action;
668 this.label = options.label;
670 renderElement: function(){
674 this.$el.click(function(){
679 show: function(){ this.$el.removeClass('oe_hidden'); },
680 hide: function(){ this.$el.addClass('oe_hidden'); },
683 // The debug widget lets the user control and monitor the hardware and software status
684 // without the use of the proxy
685 module.DebugWidget = module.PosBaseWidget.extend({
686 template: "DebugWidget",
688 admin_badge: '0410100000006',
689 client_badge: '0420200000004',
690 invalid_ean: '1232456',
691 soda_33cl: '5449000000996',
692 oranges_kg: '2100002031410',
693 lemon_price: '2301000001560',
694 unknown_product: '9900000000004',
698 'scan_item_error_unrecognized',
707 init: function(parent,options){
708 this._super(parent,options);
711 this.minimized = false;
713 // for dragging the debug widget around
714 this.dragging = false;
715 this.dragpos = {x:0, y:0};
717 function eventpos(event){
718 if(event.touches && event.touches[0]){
719 return {x: event.touches[0].screenX, y: event.touches[0].screenY};
721 return {x: event.screenX, y: event.screenY};
725 this.dragend_handler = function(event){
726 self.dragging = false;
728 this.dragstart_handler = function(event){
729 self.dragging = true;
730 self.dragpos = eventpos(event);
732 this.dragmove_handler = function(event){
734 var top = this.offsetTop;
735 var left = this.offsetLeft;
736 var pos = eventpos(event);
737 var dx = pos.x - self.dragpos.x;
738 var dy = pos.y - self.dragpos.y;
742 this.style.right = 'auto';
743 this.style.bottom = 'auto';
744 this.style.left = left + dx + 'px';
745 this.style.top = top + dy + 'px';
747 event.preventDefault();
748 event.stopPropagation();
754 this.el.addEventListener('mouseleave', this.dragend_handler);
755 this.el.addEventListener('mouseup', this.dragend_handler);
756 this.el.addEventListener('touchend', this.dragend_handler);
757 this.el.addEventListener('touchcancel',this.dragend_handler);
758 this.el.addEventListener('mousedown', this.dragstart_handler);
759 this.el.addEventListener('touchstart', this.dragstart_handler);
760 this.el.addEventListener('mousemove', this.dragmove_handler);
761 this.el.addEventListener('touchmove', this.dragmove_handler);
763 this.$('.toggle').click(function(){
764 var content = self.$('.content');
767 content.animate({'height':'0'},200);
769 content.css({'height':'auto'});
771 self.minimized = !self.minimized;
773 this.$('.button.accept_payment').click(function(){
774 self.pos.proxy.debug_accept_payment();
776 this.$('.button.reject_payment').click(function(){
777 self.pos.proxy.debug_reject_payment();
779 this.$('.button.set_weight').click(function(){
780 var kg = Number(self.$('input.weight').val());
782 self.pos.proxy.debug_set_weight(kg);
785 this.$('.button.reset_weight').click(function(){
786 self.$('input.weight').val('');
787 self.pos.proxy.debug_reset_weight();
789 this.$('.button.custom_ean').click(function(){
790 var ean = self.pos.barcode_reader.sanitize_ean(self.$('input.ean').val() || '0');
791 self.$('input.ean').val(ean);
792 self.pos.barcode_reader.scan('ean13',ean);
794 this.$('.button.reference').click(function(){
795 self.pos.barcode_reader.scan('reference',self.$('input.ean').val());
797 _.each(this.eans, function(ean, name){
798 self.$('.button.'+name).click(function(){
799 self.$('input.ean').val(ean);
800 self.pos.barcode_reader.scan('ean13',ean);
803 _.each(this.events, function(name){
804 self.pos.proxy.add_notification(name,function(){
805 self.$('.event.'+name).stop().clearQueue().css({'background-color':'#6CD11D'});
806 self.$('.event.'+name).animate({'background-color':'#1E1E1E'},2000);
809 self.pos.proxy.add_notification('help_needed',function(){
810 self.$('.status.help_needed').addClass('on');
812 self.pos.proxy.add_notification('help_canceled',function(){
813 self.$('.status.help_needed').removeClass('on');
815 self.pos.proxy.add_notification('transaction_start',function(){
816 self.$('.status.transaction').addClass('on');
818 self.pos.proxy.add_notification('transaction_end',function(){
819 self.$('.status.transaction').removeClass('on');
821 self.pos.proxy.add_notification('weighting_start',function(){
822 self.$('.status.weighting').addClass('on');
824 self.pos.proxy.add_notification('weighting_end',function(){
825 self.$('.status.weighting').removeClass('on');
830 // ---------- Main Point of Sale Widget ----------
832 // this is used to notify the user that data is being synchronized on the network
833 module.SynchNotificationWidget = module.PosBaseWidget.extend({
834 template: "SynchNotificationWidget",
835 init: function(parent, options){
836 options = options || {};
837 this._super(parent, options);
839 renderElement: function() {
842 this.$el.click(function(){
848 this.pos.bind('change:nbr_pending_operations', function(){
849 self.renderElement();
852 get_nbr_pending: function(){
853 return this.pos.get('nbr_pending_operations');
858 // The PosWidget is the main widget that contains all other widgets in the PointOfSale.
859 // It is mainly composed of :
860 // - a header, containing the list of orders
861 // - a leftpane, containing the list of bought products (orderlines)
862 // - a rightpane, containing the screens (see pos_screens.js)
863 // - an actionbar on the bottom, containing various action buttons
865 // - an onscreen keyboard
866 // a screen_selector which controls the switching between screens and the showing/closing of popups
868 module.PosWidget = module.PosBaseWidget.extend({
869 template: 'PosWidget',
871 this._super(arguments[0],{});
873 instance.web.blockUI();
875 this.pos = new module.PosModel(this.session);
876 this.pos.pos_widget = this;
877 this.pos_widget = this; //So that pos_widget's childs have pos_widget set automatically
879 this.numpad_visible = true;
880 this.left_action_bar_visible = true;
881 this.leftpane_visible = true;
882 this.leftpane_width = '440px';
883 this.cashier_controls_visible = true;
884 this.image_cache = new module.ImageCache(); // for faster products image display
886 FastClick.attach(document.body);
890 disable_rubberbanding: function(){
891 // prevent the pos body from being scrollable.
892 document.body.addEventListener('touchmove',function(event){
893 var node = event.target;
895 if(node.classList && node.classList.contains('touch-scrollable')){
898 node = node.parentNode;
900 event.preventDefault();
906 return self.pos.ready.done(function() {
907 $('.oe_tooltip').remove(); // remove tooltip from the start session button
909 // remove default webclient handlers that induce click delay
914 $(self.$el).parent().off();
916 $('.oe_web_client').off();
917 $('.openerp_webclient_container').off();
919 self.build_currency_template();
920 self.renderElement();
922 self.$('.neworder-button').click(function(){
923 self.pos.add_new_order();
926 self.$('.deleteorder-button').click(function(){
927 self.pos.delete_current_order();
930 //when a new order is created, add an order button widget
931 self.pos.get('orders').bind('add', function(new_order){
932 var new_order_button = new module.OrderButtonWidget(null, {
936 new_order_button.appendTo(this.$('.orders'));
937 new_order_button.selectOrder();
940 self.pos.add_new_order();
942 self.build_widgets();
944 if(self.pos.config.iface_big_scrollbars){
945 self.$el.addClass('big-scrollbars');
948 self.screen_selector.set_default_screen();
950 self.pos.barcode_reader.connect();
952 instance.webclient.set_content_full_screen(true);
954 if (!self.pos.session) {
955 self.screen_selector.show_popup('error', 'Sorry, we could not create a user session');
956 }else if(!self.pos.config){
957 self.screen_selector.show_popup('error', 'Sorry, we could not find any PoS Configuration for this session');
960 instance.web.unblockUI();
961 self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').addClass('oe_hidden');});
965 }).fail(function(){ // error when loading models data from the backend
966 instance.web.unblockUI();
967 return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_session_opening']], ['res_id'])
968 .pipe( _.bind(function(res){
969 return instance.session.rpc('/web/action/load', {'action_id': res[0]['res_id']})
970 .pipe(_.bind(function(result){
971 var action = result.result;
972 this.do_action(action);
978 // This method instantiates all the screens, widgets, etc. If you want to add new screens change the
979 // startup screen, etc, override this method.
980 build_widgets: function() {
983 // -------- Screens ---------
985 this.product_screen = new module.ProductScreenWidget(this,{});
986 this.product_screen.appendTo(this.$('.screens'));
988 this.receipt_screen = new module.ReceiptScreenWidget(this, {});
989 this.receipt_screen.appendTo(this.$('.screens'));
991 this.payment_screen = new module.PaymentScreenWidget(this, {});
992 this.payment_screen.appendTo(this.$('.screens'));
994 this.welcome_screen = new module.WelcomeScreenWidget(this,{});
995 this.welcome_screen.appendTo(this.$('.screens'));
997 this.client_payment_screen = new module.ClientPaymentScreenWidget(this, {});
998 this.client_payment_screen.appendTo(this.$('.screens'));
1000 this.scale_invite_screen = new module.ScaleInviteScreenWidget(this, {});
1001 this.scale_invite_screen.appendTo(this.$('.screens'));
1003 this.scale_screen = new module.ScaleScreenWidget(this,{});
1004 this.scale_screen.appendTo(this.$('.screens'));
1006 // -------- Popups ---------
1008 this.help_popup = new module.HelpPopupWidget(this, {});
1009 this.help_popup.appendTo(this.$el);
1011 this.error_popup = new module.ErrorPopupWidget(this, {});
1012 this.error_popup.appendTo(this.$el);
1014 this.error_product_popup = new module.ProductErrorPopupWidget(this, {});
1015 this.error_product_popup.appendTo(this.$el);
1017 this.error_session_popup = new module.ErrorSessionPopupWidget(this, {});
1018 this.error_session_popup.appendTo(this.$el);
1020 this.choose_receipt_popup = new module.ChooseReceiptPopupWidget(this, {});
1021 this.choose_receipt_popup.appendTo(this.$el);
1023 this.error_negative_price_popup = new module.ErrorNegativePricePopupWidget(this, {});
1024 this.error_negative_price_popup.appendTo(this.$el);
1026 this.error_no_client_popup = new module.ErrorNoClientPopupWidget(this, {});
1027 this.error_no_client_popup.appendTo(this.$el);
1029 this.error_invoice_transfer_popup = new module.ErrorInvoiceTransferPopupWidget(this, {});
1030 this.error_invoice_transfer_popup.appendTo(this.$el);
1032 // -------- Misc ---------
1034 this.notification = new module.SynchNotificationWidget(this,{});
1035 this.notification.appendTo(this.$('.pos-rightheader'));
1037 this.username = new module.UsernameWidget(this,{});
1038 this.username.replace(this.$('.placeholder-UsernameWidget'));
1040 this.action_bar = new module.ActionBarWidget(this);
1041 this.action_bar.replace(this.$(".placeholder-RightActionBar"));
1043 this.left_action_bar = new module.ActionBarWidget(this);
1044 this.left_action_bar.replace(this.$('.placeholder-LeftActionBar'));
1046 this.paypad = new module.PaypadWidget(this, {});
1047 this.paypad.replace(this.$('.placeholder-PaypadWidget'));
1049 this.numpad = new module.NumpadWidget(this);
1050 this.numpad.replace(this.$('.placeholder-NumpadWidget'));
1052 this.order_widget = new module.OrderWidget(this, {});
1053 this.order_widget.replace(this.$('.placeholder-OrderWidget'));
1055 this.onscreen_keyboard = new module.OnscreenKeyboardWidget(this, {
1056 'keyboard_model': 'simple'
1058 this.onscreen_keyboard.replace(this.$('.placeholder-OnscreenKeyboardWidget'));
1060 this.close_button = new module.HeaderButtonWidget(this,{
1062 action: function(){ self.close(); },
1064 this.close_button.appendTo(this.$('.pos-rightheader'));
1066 this.client_button = new module.HeaderButtonWidget(this,{
1067 label: _t('Self-Checkout'),
1068 action: function(){ self.screen_selector.set_user_mode('client'); },
1070 this.client_button.appendTo(this.$('.pos-rightheader'));
1073 // -------- Screen Selector ---------
1075 this.screen_selector = new module.ScreenSelector({
1078 'products': this.product_screen,
1079 'payment' : this.payment_screen,
1080 'client_payment' : this.client_payment_screen,
1081 'scale_invite' : this.scale_invite_screen,
1082 'scale': this.scale_screen,
1083 'receipt' : this.receipt_screen,
1084 'welcome' : this.welcome_screen,
1087 'help': this.help_popup,
1088 'error': this.error_popup,
1089 'error-product': this.error_product_popup,
1090 'error-session': this.error_session_popup,
1091 'error-negative-price': this.error_negative_price_popup,
1092 'choose-receipt': this.choose_receipt_popup,
1093 'error-no-client': this.error_no_client_popup,
1094 'error-invoice-transfer': this.error_invoice_transfer_popup,
1096 default_client_screen: 'welcome',
1097 default_cashier_screen: 'products',
1098 default_mode: this.pos.config.iface_self_checkout ? 'client' : 'cashier',
1102 this.debug_widget = new module.DebugWidget(this);
1103 this.debug_widget.appendTo(this.$('.pos-content'));
1106 this.disable_rubberbanding();
1110 changed_pending_operations: function () {
1112 this.synch_notification.on_change_nbr_pending(self.pos.get('nbr_pending_operations').length);
1114 // shows or hide the numpad and related controls like the paypad.
1115 set_numpad_visible: function(visible){
1116 if(visible !== this.numpad_visible){
1117 this.numpad_visible = visible;
1119 this.set_left_action_bar_visible(false);
1128 set_left_action_bar_visible: function(visible){
1129 if(visible !== this.left_action_bar_visible){
1130 this.left_action_bar_visible = visible;
1132 this.set_numpad_visible(false);
1133 this.left_action_bar.show();
1135 this.left_action_bar.hide();
1139 //shows or hide the leftpane (contains the list of orderlines, the numpad, the paypad, etc.)
1140 set_leftpane_visible: function(visible){
1141 if(visible !== this.leftpane_visible){
1142 this.leftpane_visible = visible;
1144 this.$('.pos-leftpane').removeClass('oe_hidden').animate({'width':this.leftpane_width},500,'swing');
1145 this.$('.pos-rightpane').animate({'left':this.leftpane_width},500,'swing');
1147 var leftpane = this.$('.pos-leftpane');
1148 leftpane.animate({'width':'0px'},500,'swing', function(){ leftpane.addClass('oe_hidden'); });
1149 this.$('.pos-rightpane').animate({'left':'0px'},500,'swing');
1153 //shows or hide the controls in the PosWidget that are specific to the cashier ( Orders, close button, etc. )
1154 set_cashier_controls_visible: function(visible){
1155 if(visible !== this.cashier_controls_visible){
1156 this.cashier_controls_visible = visible;
1158 this.$('.pos-rightheader').removeClass('oe_hidden');
1160 this.$('.pos-rightheader').addClass('oe_hidden');
1168 return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_client_pos_menu']], ['res_id']).pipe(
1169 _.bind(function(res) {
1170 return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) {
1171 var action = result;
1172 action.context = _.extend(action.context || {}, {'cancel_action': {type: 'ir.actions.client', tag: 'reload'}});
1173 this.do_action(action);
1178 var draft_order = _.find( self.pos.get('orders').models, function(order){
1179 return order.get('orderLines').length !== 0 && order.get('paymentLines').length === 0;
1182 if (confirm(_t("Pending orders will be lost.\nAre you sure you want to leave this session?"))) {
1189 destroy: function() {
1191 instance.webclient.set_content_full_screen(false);