1 function openerp_pos_widgets(instance, module){ //module is instance.point_of_sale
2 var QWeb = instance.web.qweb;
3 var _t = instance.web._t;
5 module.DomCache = instance.web.Class.extend({
6 init: function(options){
7 options = options || {};
8 this.max_size = options.max_size || 2000;
11 this.access_time = {};
14 cache_node: function(key,node){
15 var cached = this.cache[key];
16 this.cache[key] = node;
17 this.access_time[key] = new Date().getTime();
20 while(this.size >= this.max_size){
21 var oldest_key = null;
22 var oldest_time = new Date().getTime();
23 for(var key in this.cache){
24 var time = this.access_time[key];
25 if(time <= oldest_time){
31 delete this.cache[oldest_key];
32 delete this.access_time[oldest_key];
39 get_node: function(key){
40 var cached = this.cache[key];
42 this.access_time[key] = new Date().getTime();
48 module.NumpadWidget = module.PosBaseWidget.extend({
49 template:'NumpadWidget',
50 init: function(parent, options) {
52 this.state = new module.NumpadState();
53 window.numpadstate = this.state;
57 this.state.bind('change:mode', this.changedMode, this);
59 this.$el.find('.numpad-backspace').click(_.bind(this.clickDeleteLastChar, this));
60 this.$el.find('.numpad-minus').click(_.bind(this.clickSwitchSign, this));
61 this.$el.find('.number-char').click(_.bind(this.clickAppendNewChar, this));
62 this.$el.find('.mode-button').click(_.bind(this.clickChangeMode, this));
64 clickDeleteLastChar: function() {
65 return this.state.deleteLastChar();
67 clickSwitchSign: function() {
68 return this.state.switchSign();
70 clickAppendNewChar: function(event) {
72 newChar = event.currentTarget.innerText || event.currentTarget.textContent;
73 return this.state.appendNewChar(newChar);
75 clickChangeMode: function(event) {
76 var newMode = event.currentTarget.attributes['data-mode'].nodeValue;
77 return this.state.changeMode(newMode);
79 changedMode: function() {
80 var mode = this.state.get('mode');
81 $('.selected-mode').removeClass('selected-mode');
82 $(_.str.sprintf('.mode-button[data-mode="%s"]', mode), this.$el).addClass('selected-mode');
86 // The paypad allows to select the payment method (cashregisters)
87 // used to pay the order.
88 module.PaypadWidget = module.PosBaseWidget.extend({
89 template: 'PaypadWidget',
90 renderElement: function() {
94 _.each(this.pos.cashregisters,function(cashregister) {
95 var button = new module.PaypadButtonWidget(self,{
97 pos_widget : self.pos_widget,
98 cashregister: cashregister,
100 button.appendTo(self.$el);
105 module.PaypadButtonWidget = module.PosBaseWidget.extend({
106 template: 'PaypadButtonWidget',
107 init: function(parent, options){
108 this._super(parent, options);
109 this.cashregister = options.cashregister;
111 renderElement: function() {
115 this.$el.click(function(){
116 if (self.pos.get('selectedOrder').get('screen') === 'receipt'){ //TODO Why ?
117 console.warn('TODO should not get there...?');
120 self.pos.get('selectedOrder').addPaymentline(self.cashregister);
121 self.pos_widget.screen_selector.set_current_screen('payment');
126 module.OrderWidget = module.PosBaseWidget.extend({
127 template:'OrderWidget',
128 init: function(parent, options) {
130 this._super(parent,options);
131 this.editable = false;
132 this.pos.bind('change:selectedOrder', this.change_selected_order, this);
133 this.line_click_handler = function(event){
137 self.pos.get('selectedOrder').selectLine(this.orderline);
138 self.pos_widget.numpad.state.reset();
140 this.client_change_handler = function(event){
141 self.update_summary();
143 this.bind_order_events();
145 enable_numpad: function(){
146 this.disable_numpad(); //ensure we don't register the callbacks twice
147 this.numpad_state = this.pos_widget.numpad.state;
148 if(this.numpad_state){
149 this.numpad_state.reset();
150 this.numpad_state.bind('set_value', this.set_value, this);
154 disable_numpad: function(){
155 if(this.numpad_state){
156 this.numpad_state.unbind('set_value', this.set_value);
157 this.numpad_state.reset();
160 set_editable: function(editable){
161 this.editable = editable;
163 this.enable_numpad();
165 this.disable_numpad();
166 this.pos.get('selectedOrder').deselectLine();
169 set_value: function(val) {
170 var order = this.pos.get('selectedOrder');
171 if (this.editable && order.getSelectedLine()) {
172 var mode = this.numpad_state.get('mode');
173 if( mode === 'quantity'){
174 order.getSelectedLine().set_quantity(val);
175 }else if( mode === 'discount'){
176 order.getSelectedLine().set_discount(val);
177 }else if( mode === 'price'){
178 order.getSelectedLine().set_unit_price(val);
182 change_selected_order: function() {
183 this.bind_order_events();
184 this.renderElement();
186 bind_order_events: function() {
188 var order = this.pos.get('selectedOrder');
189 order.unbind('change:client', this.client_change_handler);
190 order.bind('change:client', this.client_change_handler);
192 var lines = order.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});
245 var el_node = document.createElement('div');
246 el_node.innerHTML = _.str.trim(el_str);
247 el_node = el_node.childNodes[0];
250 var list_container = el_node.querySelector('.orderlines');
251 for(var i = 0, len = orderlines.length; i < len; i++){
252 var orderline = this.render_orderline(orderlines[i]);
253 list_container.appendChild(orderline);
256 if(this.el && this.el.parentNode){
257 this.el.parentNode.replaceChild(el_node,this.el);
260 this.update_summary();
263 this.el.querySelector('.order-scroller').scrollTop = 100 * orderlines.length;
266 update_summary: function(){
267 var order = this.pos.get('selectedOrder');
268 var total = order ? order.getTotalTaxIncluded() : 0;
269 var taxes = order ? total - order.getTotalTaxExcluded() : 0;
271 this.el.querySelector('.summary .total > .value').textContent = this.format_currency(total);
272 this.el.querySelector('.summary .total .subentry .value').textContent = this.format_currency(taxes);
277 module.OrderButtonWidget = module.PosBaseWidget.extend({
278 template:'OrderButtonWidget',
279 init: function(parent, options) {
280 this._super(parent,options);
283 this.order = options.order;
284 this.order.bind('destroy',this.destroy, this );
285 this.order.bind('change', this.renderElement, this );
286 this.pos.bind('change:selectedOrder', this.renderElement,this );
288 renderElement:function(){
289 this.selected = ( this.pos.get('selectedOrder') === this.order )
292 this.$el.click(function(){
293 if( self.pos.get('selectedOrder') === self.order ){
294 var ss = self.pos.pos_widget.screen_selector;
295 if(ss.get_current_screen() === 'clientlist'){
298 ss.set_current_screen('clientlist');
305 this.$el.addClass('selected');
308 selectOrder: function(event) {
310 selectedOrder: this.order
314 this.order.unbind('destroy', this.destroy, this);
315 this.order.unbind('change', this.renderElement, this);
316 this.pos.unbind('change:selectedOrder', this.renderElement, this);
321 module.ActionButtonWidget = instance.web.Widget.extend({
322 template:'ActionButtonWidget',
323 icon_template:'ActionButtonWidgetWithIcon',
324 init: function(parent, options){
325 this._super(parent, options);
326 this.label = options.label || 'button';
327 this.rightalign = options.rightalign || false;
328 this.click_action = options.click;
329 this.disabled = options.disabled || false;
331 this.icon = options.icon;
332 this.template = this.icon_template;
335 set_disabled: function(disabled){
336 if(this.disabled != disabled){
337 this.disabled = !!disabled;
338 this.renderElement();
341 renderElement: function(){
343 if(this.click_action && !this.disabled){
344 this.$el.click(_.bind(this.click_action, this));
349 module.ActionBarWidget = instance.web.Widget.extend({
350 template:'ActionBarWidget',
351 init: function(parent, options){
352 this._super(parent,options);
353 this.button_list = [];
355 this.visibility = {};
357 set_element_visible: function(element, visible, action){
358 if(visible != this.visibility[element]){
359 this.visibility[element] = !!visible;
361 this.$('.'+element).removeClass('oe_hidden');
363 this.$('.'+element).addClass('oe_hidden');
366 if(visible && action){
367 this.action[element] = action;
368 this.$('.'+element).off('click').click(action);
371 set_button_disabled: function(name, disabled){
372 var b = this.buttons[name];
374 b.set_disabled(disabled);
377 destroy_buttons:function(){
378 for(var i = 0; i < this.button_list.length; i++){
379 this.button_list[i].destroy();
381 this.button_list = [];
385 get_button_count: function(){
386 return this.button_list.length;
388 add_new_button: function(button_options){
389 var button = new module.ActionButtonWidget(this,button_options);
390 this.button_list.push(button);
391 if(button_options.name){
392 this.buttons[button_options.name] = button;
394 button.appendTo(this.$('.pos-actionbar-button-list'));
398 this.$el.removeClass('oe_hidden');
401 this.$el.addClass('oe_hidden');
405 module.ProductCategoriesWidget = module.PosBaseWidget.extend({
406 template: 'ProductCategoriesWidget',
407 init: function(parent, options){
409 this._super(parent,options);
410 this.product_type = options.product_type || 'all'; // 'all' | 'weightable'
411 this.onlyWeightable = options.onlyWeightable || false;
412 this.category = this.pos.root_category;
413 this.breadcrumb = [];
414 this.subcategories = [];
415 this.product_list_widget = options.product_list_widget || null;
416 this.category_cache = new module.DomCache();
419 this.switch_category_handler = function(event){
420 self.set_category(self.pos.db.get_category_by_id(Number(this.dataset['categoryId'])));
421 self.renderElement();
424 this.clear_search_handler = function(event){
428 var search_timeout = null;
429 this.search_handler = function(event){
430 clearTimeout(search_timeout);
432 var query = this.value;
434 search_timeout = setTimeout(function(){
435 self.perform_search(self.category, query, event.which === 13);
440 // changes the category. if undefined, sets to root category
441 set_category : function(category){
442 var db = this.pos.db;
444 this.category = db.get_category_by_id(db.root_category_id);
446 this.category = category;
448 this.breadcrumb = [];
449 var ancestors_ids = db.get_category_ancestors_ids(this.category.id);
450 for(var i = 1; i < ancestors_ids.length; i++){
451 this.breadcrumb.push(db.get_category_by_id(ancestors_ids[i]));
453 if(this.category.id !== db.root_category_id){
454 this.breadcrumb.push(this.category);
456 this.subcategories = db.get_category_by_id(db.get_category_childs_ids(this.category.id));
459 get_image_url: function(category){
460 return window.location.origin + '/web/binary/image?model=pos.category&field=image_medium&id='+category.id;
463 render_category: function( category, with_image ){
464 var cached = this.category_cache.get_node(category.id);
467 var image_url = this.get_image_url(category);
468 var category_html = QWeb.render('CategoryButton',{
471 image_url: this.get_image_url(category),
473 category_html = _.str.trim(category_html);
474 var category_node = document.createElement('div');
475 category_node.innerHTML = category_html;
476 category_node = category_node.childNodes[0];
478 var category_html = QWeb.render('CategorySimpleButton',{
482 category_html = _.str.trim(category_html);
483 var category_node = document.createElement('div');
484 category_node.innerHTML = category_html;
485 category_node = category_node.childNodes[0];
487 this.category_cache.cache_node(category.id,category_node);
488 return category_node;
493 replace: function($target){
494 this.renderElement();
495 var target = $target[0];
496 target.parentNode.replaceChild(this.el,target);
499 renderElement: function(){
502 var el_str = openerp.qweb.render(this.template, {widget: this});
503 var el_node = document.createElement('div');
504 el_node.innerHTML = el_str;
505 el_node = el_node.childNodes[1];
507 if(this.el && this.el.parentNode){
508 this.el.parentNode.replaceChild(el_node,this.el);
513 var hasimages = false; //if none of the subcategories have images, we don't display buttons with icons
514 for(var i = 0; i < this.subcategories.length; i++){
515 if(this.subcategories[i].image){
521 var list_container = el_node.querySelector('.category-list');
522 for(var i = 0, len = this.subcategories.length; i < len; i++){
523 list_container.appendChild(this.render_category(this.subcategories[i],hasimages));
526 var buttons = el_node.querySelectorAll('.js-category-switch');
527 for(var i = 0; i < buttons.length; i++){
528 buttons[i].addEventListener('click',this.switch_category_handler);
531 var products = this.pos.db.get_product_by_category(this.category.id);
532 this.product_list_widget.set_product_list(products);
534 this.el.querySelector('.searchbox input').addEventListener('keyup',this.search_handler);
536 this.el.querySelector('.search-clear').addEventListener('click',this.clear_search_handler);
538 if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
539 this.pos_widget.onscreen_keyboard.connect($(this.el.querySelector('.searchbox input')));
543 // resets the current category to the root category
544 reset_category: function(){
546 this.renderElement();
549 // empties the content of the search box
550 clear_search: function(){
551 var products = this.pos.db.get_product_by_category(this.category.id);
552 this.product_list_widget.set_product_list(products);
553 var input = this.el.querySelector('.searchbox input');
557 perform_search: function(category, query, buy_result){
559 var products = this.pos.db.search_product_in_category(category.id,query)
560 if(buy_result && products.length === 1){
561 this.pos.get('selectedOrder').addProduct(products[0]);
564 this.product_list_widget.set_product_list(products);
567 var products = this.pos.db.get_product_by_category(this.category.id);
568 this.product_list_widget.set_product_list(products);
574 module.ProductListWidget = module.PosBaseWidget.extend({
575 template:'ProductListWidget',
576 init: function(parent, options) {
578 this._super(parent,options);
579 this.model = options.model;
580 this.productwidgets = [];
581 this.weight = options.weight || 0;
582 this.show_scale = options.show_scale || false;
583 this.next_screen = options.next_screen || false;
585 this.click_product_handler = function(event){
586 var product = self.pos.db.get_product_by_id(this.dataset['productId']);
587 options.click_product_action(product);
590 this.product_list = options.product_list || [];
591 this.product_cache = new module.DomCache();
593 set_product_list: function(product_list){
594 this.product_list = product_list;
595 this.renderElement();
597 get_product_image_url: function(product){
598 return window.location.origin + '/web/binary/image?model=product.product&field=image_medium&id='+product.id;
600 replace: function($target){
601 this.renderElement();
602 var target = $target[0];
603 target.parentNode.replaceChild(this.el,target);
606 render_product: function(product){
607 var cached = this.product_cache.get_node(product.id);
609 var image_url = this.get_product_image_url(product);
610 var product_html = QWeb.render('Product',{
613 image_url: this.get_product_image_url(product),
615 var product_node = document.createElement('div');
616 product_node.innerHTML = product_html;
617 product_node = product_node.childNodes[1];
618 this.product_cache.cache_node(product.id,product_node);
624 renderElement: function() {
628 var el_str = openerp.qweb.render(this.template, {widget: this});
629 var el_node = document.createElement('div');
630 el_node.innerHTML = el_str;
631 el_node = el_node.childNodes[1];
633 if(this.el && this.el.parentNode){
634 this.el.parentNode.replaceChild(el_node,this.el);
638 var list_container = el_node.querySelector('.product-list');
639 for(var i = 0, len = this.product_list.length; i < len; i++){
640 var product_node = this.render_product(this.product_list[i]);
641 product_node.addEventListener('click',this.click_product_handler);
642 list_container.appendChild(product_node);
647 module.UsernameWidget = module.PosBaseWidget.extend({
648 template: 'UsernameWidget',
649 init: function(parent, options){
650 var options = options || {};
651 this._super(parent,options);
652 this.mode = options.mode || 'cashier';
654 set_user_mode: function(mode){
659 this.renderElement();
661 get_name: function(){
663 if(this.mode === 'cashier'){
664 user = this.pos.cashier || this.pos.user;
666 user = this.pos.get('selectedOrder').get_client() || this.pos.user;
676 module.HeaderButtonWidget = module.PosBaseWidget.extend({
677 template: 'HeaderButtonWidget',
678 init: function(parent, options){
679 options = options || {};
680 this._super(parent, options);
681 this.action = options.action;
682 this.label = options.label;
684 renderElement: function(){
688 this.$el.click(function(){
693 show: function(){ this.$el.removeClass('oe_hidden'); },
694 hide: function(){ this.$el.addClass('oe_hidden'); },
697 // The debug widget lets the user control and monitor the hardware and software status
698 // without the use of the proxy
699 module.DebugWidget = module.PosBaseWidget.extend({
700 template: "DebugWidget",
702 admin_badge: '0410100000006',
703 client_badge: '0420200000004',
704 invalid_ean: '1232456',
705 soda_33cl: '5449000000996',
706 oranges_kg: '2100002031410',
707 lemon_price: '2301000001560',
708 unknown_product: '9900000000004',
716 init: function(parent,options){
717 this._super(parent,options);
720 this.minimized = false;
722 // for dragging the debug widget around
723 this.dragging = false;
724 this.dragpos = {x:0, y:0};
726 function eventpos(event){
727 if(event.touches && event.touches[0]){
728 return {x: event.touches[0].screenX, y: event.touches[0].screenY};
730 return {x: event.screenX, y: event.screenY};
734 this.dragend_handler = function(event){
735 self.dragging = false;
737 this.dragstart_handler = function(event){
738 self.dragging = true;
739 self.dragpos = eventpos(event);
741 this.dragmove_handler = function(event){
743 var top = this.offsetTop;
744 var left = this.offsetLeft;
745 var pos = eventpos(event);
746 var dx = pos.x - self.dragpos.x;
747 var dy = pos.y - self.dragpos.y;
751 this.style.right = 'auto';
752 this.style.bottom = 'auto';
753 this.style.left = left + dx + 'px';
754 this.style.top = top + dy + 'px';
756 event.preventDefault();
757 event.stopPropagation();
763 this.el.addEventListener('mouseleave', this.dragend_handler);
764 this.el.addEventListener('mouseup', this.dragend_handler);
765 this.el.addEventListener('touchend', this.dragend_handler);
766 this.el.addEventListener('touchcancel',this.dragend_handler);
767 this.el.addEventListener('mousedown', this.dragstart_handler);
768 this.el.addEventListener('touchstart', this.dragstart_handler);
769 this.el.addEventListener('mousemove', this.dragmove_handler);
770 this.el.addEventListener('touchmove', this.dragmove_handler);
772 this.$('.toggle').click(function(){
773 var content = self.$('.content');
776 content.animate({'height':'0'},200);
778 content.css({'height':'auto'});
780 self.minimized = !self.minimized;
782 this.$('.button.set_weight').click(function(){
783 var kg = Number(self.$('input.weight').val());
785 self.pos.proxy.debug_set_weight(kg);
788 this.$('.button.reset_weight').click(function(){
789 self.$('input.weight').val('');
790 self.pos.proxy.debug_reset_weight();
792 this.$('.button.custom_ean').click(function(){
793 var ean = self.pos.barcode_reader.sanitize_ean(self.$('input.ean').val() || '0');
794 self.$('input.ean').val(ean);
795 self.pos.barcode_reader.scan(ean);
797 this.$('.button.reference').click(function(){
798 self.pos.barcode_reader.scan(self.$('input.ean').val());
800 this.$('.button.show_orders').click(function(){
801 self.pos.pos_widget.screen_selector.show_popup('unsent-orders');
803 this.$('.button.delete_orders').click(function(){
804 self.pos.pos_widget.screen_selector.show_popup('confirm',{
805 message: _t('Delete Unsent Orders ?'),
806 comment: _t('This operation will permanently destroy all unsent orders from the local storage. You will lose all the data. This operation cannot be undone.'),
808 self.pos.db.remove_all_orders();
809 self.pos.set({synch: { state:'connected', pending: 0 }});
813 _.each(this.eans, function(ean, name){
814 self.$('.button.'+name).click(function(){
815 self.$('input.ean').val(ean);
816 self.pos.barcode_reader.scan(ean);
819 _.each(this.events, function(name){
820 self.pos.proxy.add_notification(name,function(){
821 self.$('.event.'+name).stop().clearQueue().css({'background-color':'#6CD11D'});
822 self.$('.event.'+name).animate({'background-color':'#1E1E1E'},2000);
828 // ---------- Main Point of Sale Widget ----------
830 module.StatusWidget = module.PosBaseWidget.extend({
831 status: ['connected','connecting','disconnected','warning'],
832 set_status: function(status,msg){
834 for(var i = 0; i < this.status.length; i++){
835 this.$('.js_'+this.status[i]).addClass('oe_hidden');
837 this.$('.js_'+status).removeClass('oe_hidden');
840 this.$('.js_msg').removeClass('oe_hidden').html(msg);
842 this.$('.js_msg').addClass('oe_hidden').html('');
847 // this is used to notify the user that data is being synchronized on the network
848 module.SynchNotificationWidget = module.StatusWidget.extend({
849 template: 'SynchNotificationWidget',
852 this.pos.bind('change:synch', function(pos,synch){
853 self.set_status(synch.state, synch.pending);
855 this.$el.click(function(){
856 self.pos.push_order();
861 // this is used to notify the user if the pos is connected to the proxy
862 module.ProxyStatusWidget = module.StatusWidget.extend({
863 template: 'ProxyStatusWidget',
864 set_smart_status: function(status){
865 if(status.status === 'connected'){
868 if(this.pos.config.iface_scan_via_proxy){
869 var scanner = status.drivers.scanner ? status.drivers.scanner.status : false;
870 if( scanner != 'connected' && scanner != 'connecting'){
872 msg += _t('Scanner');
875 if( this.pos.config.iface_print_via_proxy ||
876 this.pos.config.iface_cashdrawer ){
877 var printer = status.drivers.escpos ? status.drivers.escpos.status : false;
878 if( printer != 'connected' && printer != 'connecting'){
880 msg = msg ? msg + ' & ' : msg;
881 msg += _t('Printer');
884 if( this.pos.config.iface_electronic_scale ){
885 var scale = status.drivers.scale ? status.drivers.scale.status : false;
886 if( scale != 'connected' && scale != 'connecting' ){
888 msg = msg ? msg + ' & ' : msg;
892 msg = msg ? msg + ' ' + _t('Offline') : msg;
893 this.set_status(warning ? 'warning' : 'connected', msg);
895 this.set_status(status.status,'');
901 this.set_smart_status(this.pos.proxy.get('status'));
903 this.pos.proxy.on('change:status',this,function(eh,status){ //FIXME remove duplicate changes
904 self.set_smart_status(status.newValue);
907 this.$el.click(function(){
908 self.pos.connect_to_proxy();
914 // The PosWidget is the main widget that contains all other widgets in the PointOfSale.
915 // It is mainly composed of :
916 // - a header, containing the list of orders
917 // - a leftpane, containing the list of bought products (orderlines)
918 // - a rightpane, containing the screens (see pos_screens.js)
919 // - an actionbar on the bottom, containing various action buttons
921 // - an onscreen keyboard
922 // a screen_selector which controls the switching between screens and the showing/closing of popups
924 module.PosWidget = module.PosBaseWidget.extend({
925 template: 'PosWidget',
927 this._super(arguments[0],{});
929 this.pos = new module.PosModel(this.session,{pos_widget:this});
930 this.pos_widget = this; //So that pos_widget's childs have pos_widget set automatically
932 this.numpad_visible = true;
933 this.leftpane_visible = true;
934 this.leftpane_width = '440px';
935 this.cashier_controls_visible = true;
937 FastClick.attach(document.body);
941 disable_rubberbanding: function(){
942 // prevent the pos body from being scrollable.
943 document.body.addEventListener('touchmove',function(event){
944 var node = event.target;
946 if(node.classList && node.classList.contains('touch-scrollable')){
949 node = node.parentNode;
951 event.preventDefault();
957 return self.pos.ready.done(function() {
958 // remove default webclient handlers that induce click delay
963 $(self.$el).parent().off();
965 $('.oe_web_client').off();
966 $('.openerp_webclient_container').off();
968 self.build_currency_template();
969 self.renderElement();
971 self.$('.neworder-button').click(function(){
972 self.pos.add_new_order();
975 self.$('.deleteorder-button').click(function(){
976 if( !self.pos.get('selectedOrder').is_empty() ){
977 self.screen_selector.show_popup('confirm',{
978 message: _t('Destroy Current Order ?'),
979 comment: _t('You will lose any data associated with the current order'),
981 self.pos.delete_current_order();
985 self.pos.delete_current_order();
989 //when a new order is created, add an order button widget
990 self.pos.get('orders').bind('add', function(new_order){
991 var new_order_button = new module.OrderButtonWidget(null, {
995 new_order_button.appendTo(this.$('.orders'));
996 new_order_button.selectOrder();
999 self.pos.add_new_order();
1001 self.build_widgets();
1003 if(self.pos.config.iface_big_scrollbars){
1004 self.$el.addClass('big-scrollbars');
1007 self.screen_selector.set_default_screen();
1009 self.pos.barcode_reader.connect();
1011 instance.webclient.set_content_full_screen(true);
1013 if (!self.pos.session) {
1014 self.screen_selector.show_popup('error', 'Sorry, we could not create a user session');
1015 }else if(!self.pos.config){
1016 self.screen_selector.show_popup('error', 'Sorry, we could not find any PoS Configuration for this session');
1019 self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').addClass('oe_hidden');});
1021 self.pos.push_order();
1023 }).fail(function(err){ // error when loading models data from the backend
1024 self.loading_error(err);
1027 loading_error: function(err){
1030 var message = err.message;
1031 var comment = err.stack;
1033 if(err.message === 'XmlHttpRequestError '){
1034 message = 'Network Failure (XmlHttpRequestError)';
1035 comment = 'The Point of Sale could not be loaded due to a network problem.\n Please check your internet connection.';
1036 }else if(err.message === 'OpenERP Server Error'){
1037 message = err.data.message;
1038 comment = err.data.debug;
1041 if( typeof comment !== 'string' ){
1042 comment = 'Traceback not available.';
1045 var popup = $(QWeb.render('ErrorTracebackPopupWidget',{
1046 widget: { message: message, comment: comment },
1049 popup.find('.button').click(function(){
1053 popup.css({ zindex: 9001 });
1055 popup.appendTo(this.$el);
1057 loading_progress: function(fac){
1058 this.$('.loader .loader-feedback').removeClass('oe_hidden');
1059 this.$('.loader .progress').css({'width': ''+Math.floor(fac*100)+'%'});
1061 loading_message: function(msg,progress){
1062 this.$('.loader .loader-feedback').removeClass('oe_hidden');
1063 this.$('.loader .message').text(msg);
1064 if(typeof progress !== 'undefined'){
1065 this.loading_progress(progress);
1068 loading_skip: function(callback){
1070 this.$('.loader .loader-feedback').removeClass('oe_hidden');
1071 this.$('.loader .button.skip').removeClass('oe_hidden');
1072 this.$('.loader .button.skip').off('click');
1073 this.$('.loader .button.skip').click(callback);
1075 this.$('.loader .button.skip').addClass('oe_hidden');
1078 // This method instantiates all the screens, widgets, etc. If you want to add new screens change the
1079 // startup screen, etc, override this method.
1080 build_widgets: function() {
1083 // -------- Screens ---------
1085 this.product_screen = new module.ProductScreenWidget(this,{});
1086 this.product_screen.appendTo(this.$('.screens'));
1088 this.receipt_screen = new module.ReceiptScreenWidget(this, {});
1089 this.receipt_screen.appendTo(this.$('.screens'));
1091 this.payment_screen = new module.PaymentScreenWidget(this, {});
1092 this.payment_screen.appendTo(this.$('.screens'));
1094 this.clientlist_screen = new module.ClientListScreenWidget(this, {});
1095 this.clientlist_screen.appendTo(this.$('.screens'));
1097 this.scale_screen = new module.ScaleScreenWidget(this,{});
1098 this.scale_screen.appendTo(this.$('.screens'));
1101 // -------- Popups ---------
1103 this.error_popup = new module.ErrorPopupWidget(this, {});
1104 this.error_popup.appendTo(this.$el);
1106 this.error_barcode_popup = new module.ErrorBarcodePopupWidget(this, {});
1107 this.error_barcode_popup.appendTo(this.$el);
1109 this.error_session_popup = new module.ErrorSessionPopupWidget(this, {});
1110 this.error_session_popup.appendTo(this.$el);
1112 this.choose_receipt_popup = new module.ChooseReceiptPopupWidget(this, {});
1113 this.choose_receipt_popup.appendTo(this.$el);
1115 this.error_no_client_popup = new module.ErrorNoClientPopupWidget(this, {});
1116 this.error_no_client_popup.appendTo(this.$el);
1118 this.error_invoice_transfer_popup = new module.ErrorInvoiceTransferPopupWidget(this, {});
1119 this.error_invoice_transfer_popup.appendTo(this.$el);
1121 this.error_traceback_popup = new module.ErrorTracebackPopupWidget(this,{});
1122 this.error_traceback_popup.appendTo(this.$el);
1124 this.confirm_popup = new module.ConfirmPopupWidget(this,{});
1125 this.confirm_popup.appendTo(this.$el);
1127 this.unsent_orders_popup = new module.UnsentOrdersPopupWidget(this,{});
1128 this.unsent_orders_popup.appendTo(this.$el);
1130 // -------- Misc ---------
1132 this.close_button = new module.HeaderButtonWidget(this,{
1136 if (!this.confirmed) {
1137 this.$el.addClass('confirm');
1138 this.$el.text(_t('Confirm'));
1139 this.confirmed = setTimeout(function(){
1140 self.$el.removeClass('confirm');
1141 self.$el.text(_t('Close'));
1142 self.confirmed = false;
1145 clearTimeout(this.confirmed);
1146 this.pos_widget.close();
1150 this.close_button.appendTo(this.$('.pos-rightheader'));
1152 this.notification = new module.SynchNotificationWidget(this,{});
1153 this.notification.appendTo(this.$('.pos-rightheader'));
1155 if(this.pos.config.use_proxy){
1156 this.proxy_status = new module.ProxyStatusWidget(this,{});
1157 this.proxy_status.appendTo(this.$('.pos-rightheader'));
1160 this.username = new module.UsernameWidget(this,{});
1161 this.username.replace(this.$('.placeholder-UsernameWidget'));
1163 this.action_bar = new module.ActionBarWidget(this);
1164 this.action_bar.replace(this.$(".placeholder-RightActionBar"));
1166 this.paypad = new module.PaypadWidget(this, {});
1167 this.paypad.replace(this.$('.placeholder-PaypadWidget'));
1169 this.numpad = new module.NumpadWidget(this);
1170 this.numpad.replace(this.$('.placeholder-NumpadWidget'));
1172 this.order_widget = new module.OrderWidget(this, {});
1173 this.order_widget.replace(this.$('.placeholder-OrderWidget'));
1175 this.onscreen_keyboard = new module.OnscreenKeyboardWidget(this, {
1176 'keyboard_model': 'simple'
1178 this.onscreen_keyboard.replace(this.$('.placeholder-OnscreenKeyboardWidget'));
1180 // -------- Screen Selector ---------
1182 this.screen_selector = new module.ScreenSelector({
1185 'products': this.product_screen,
1186 'payment' : this.payment_screen,
1187 'scale': this.scale_screen,
1188 'receipt' : this.receipt_screen,
1189 'clientlist': this.clientlist_screen,
1192 'error': this.error_popup,
1193 'error-barcode': this.error_barcode_popup,
1194 'error-session': this.error_session_popup,
1195 'choose-receipt': this.choose_receipt_popup,
1196 'error-no-client': this.error_no_client_popup,
1197 'error-invoice-transfer': this.error_invoice_transfer_popup,
1198 'error-traceback': this.error_traceback_popup,
1199 'confirm': this.confirm_popup,
1200 'unsent-orders': this.unsent_orders_popup,
1202 default_screen: 'products',
1203 default_mode: 'cashier',
1207 this.debug_widget = new module.DebugWidget(this);
1208 this.debug_widget.appendTo(this.$('.pos-content'));
1211 this.disable_rubberbanding();
1215 changed_pending_operations: function () {
1217 this.synch_notification.on_change_nbr_pending(self.pos.get('nbr_pending_operations').length);
1219 // shows or hide the numpad and related controls like the paypad.
1220 set_numpad_visible: function(visible){
1221 if(visible !== this.numpad_visible){
1222 this.numpad_visible = visible;
1232 //shows or hide the leftpane (contains the list of orderlines, the numpad, the paypad, etc.)
1233 set_leftpane_visible: function(visible){
1234 if(visible !== this.leftpane_visible){
1235 this.leftpane_visible = visible;
1237 this.$('.pos-leftpane').removeClass('oe_hidden');
1238 this.$('.rightpane').css({'left':this.leftpane_width});
1240 this.$('.pos-leftpane').addClass('oe_hidden');
1241 this.$('.rightpane').css({'left':'0px'});
1249 return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_client_pos_menu']], ['res_id']).pipe(function(res) {
1250 window.location = '/web#action=' + res[0]['res_id'];
1254 var draft_order = _.find( self.pos.get('orders').models, function(order){
1255 return order.get('orderLines').length !== 0 && order.get('paymentLines').length === 0;
1258 if (confirm(_t("Pending orders will be lost.\nAre you sure you want to leave this session?"))) {
1265 destroy: function() {
1267 instance.webclient.set_content_full_screen(false);