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 action pads contains the payment button and the customer selection button.
87 module.ActionpadWidget = module.PosBaseWidget.extend({
88 template: 'ActionpadWidget',
89 renderElement: function() {
92 this.$('.pay').click(function(){
93 self.pos.pos_widget.screen_selector.set_current_screen('payment');
95 this.$('.set-customer').click(function(){
96 self.pos.pos_widget.screen_selector.set_current_screen('clientlist');
101 module.OrderWidget = module.PosBaseWidget.extend({
102 template:'OrderWidget',
103 init: function(parent, options) {
105 this._super(parent,options);
106 this.editable = false;
107 this.pos.bind('change:selectedOrder', this.change_selected_order, this);
108 this.line_click_handler = function(event){
112 self.pos.get('selectedOrder').selectLine(this.orderline);
113 self.pos_widget.numpad.state.reset();
115 this.client_change_handler = function(event){
116 self.update_summary();
118 this.bind_order_events();
120 enable_numpad: function(){
121 this.disable_numpad(); //ensure we don't register the callbacks twice
122 this.numpad_state = this.pos_widget.numpad.state;
123 if(this.numpad_state){
124 this.numpad_state.reset();
125 this.numpad_state.bind('set_value', this.set_value, this);
129 disable_numpad: function(){
130 if(this.numpad_state){
131 this.numpad_state.unbind('set_value', this.set_value);
132 this.numpad_state.reset();
135 set_editable: function(editable){
136 this.editable = editable;
138 this.enable_numpad();
140 this.disable_numpad();
141 this.pos.get('selectedOrder').deselectLine();
144 set_value: function(val) {
145 var order = this.pos.get('selectedOrder');
146 if (this.editable && order.getSelectedLine()) {
147 var mode = this.numpad_state.get('mode');
148 if( mode === 'quantity'){
149 order.getSelectedLine().set_quantity(val);
150 }else if( mode === 'discount'){
151 order.getSelectedLine().set_discount(val);
152 }else if( mode === 'price'){
153 order.getSelectedLine().set_unit_price(val);
157 change_selected_order: function() {
158 this.bind_order_events();
159 this.renderElement();
161 bind_order_events: function() {
163 var order = this.pos.get('selectedOrder');
164 order.unbind('change:client', this.client_change_handler);
165 order.bind('change:client', this.client_change_handler);
167 var lines = order.get('orderLines');
169 lines.bind('add', function(){
170 this.numpad_state.reset();
171 this.renderElement(true);
173 lines.bind('remove', function(line){
174 this.remove_orderline(line);
175 this.numpad_state.reset();
176 this.update_summary();
178 lines.bind('change', function(line){
179 this.rerender_orderline(line);
180 this.update_summary();
183 render_orderline: function(orderline){
184 var el_str = openerp.qweb.render('Orderline',{widget:this, line:orderline});
185 var el_node = document.createElement('div');
186 el_node.innerHTML = _.str.trim(el_str);
187 el_node = el_node.childNodes[0];
188 el_node.orderline = orderline;
189 el_node.addEventListener('click',this.line_click_handler);
191 orderline.node = el_node;
194 remove_orderline: function(order_line){
195 if(this.pos.get('selectedOrder').get('orderLines').length === 0){
196 this.renderElement();
198 order_line.node.parentNode.removeChild(order_line.node);
201 rerender_orderline: function(order_line){
202 var node = order_line.node;
203 var replacement_line = this.render_orderline(order_line);
204 node.parentNode.replaceChild(replacement_line,node);
206 // overriding the openerp framework replace method for performance reasons
207 replace: function($target){
208 this.renderElement();
209 var target = $target[0];
210 target.parentNode.replaceChild(this.el,target);
212 renderElement: function(scrollbottom){
213 this.pos_widget.numpad.state.reset();
215 var order = this.pos.get('selectedOrder');
216 var orderlines = order.get('orderLines').models;
218 var el_str = openerp.qweb.render('OrderWidget',{widget:this, order:order, orderlines:orderlines});
220 var el_node = document.createElement('div');
221 el_node.innerHTML = _.str.trim(el_str);
222 el_node = el_node.childNodes[0];
225 var list_container = el_node.querySelector('.orderlines');
226 for(var i = 0, len = orderlines.length; i < len; i++){
227 var orderline = this.render_orderline(orderlines[i]);
228 list_container.appendChild(orderline);
231 if(this.el && this.el.parentNode){
232 this.el.parentNode.replaceChild(el_node,this.el);
235 this.update_summary();
238 this.el.querySelector('.order-scroller').scrollTop = 100 * orderlines.length;
241 update_summary: function(){
242 var order = this.pos.get('selectedOrder');
243 var total = order ? order.getTotalTaxIncluded() : 0;
244 var taxes = order ? total - order.getTotalTaxExcluded() : 0;
246 this.el.querySelector('.summary .total > .value').textContent = this.format_currency(total);
247 this.el.querySelector('.summary .total .subentry .value').textContent = this.format_currency(taxes);
252 module.OrderButtonWidget = module.PosBaseWidget.extend({
253 template:'OrderButtonWidget',
254 init: function(parent, options) {
255 this._super(parent,options);
258 this.order = options.order;
259 this.order.bind('destroy',this.destroy, this );
260 this.order.bind('change', this.renderElement, this );
261 this.pos.bind('change:selectedOrder', this.renderElement,this );
263 renderElement:function(){
264 this.selected = ( this.pos.get('selectedOrder') === this.order )
267 this.$el.click(function(){
268 if( self.pos.get('selectedOrder') === self.order ){
269 var ss = self.pos.pos_widget.screen_selector;
270 if(ss.get_current_screen() === 'clientlist'){
272 }else if (ss.get_current_screen() !== 'receipt'){
273 ss.set_current_screen('clientlist');
280 this.$el.addClass('selected');
283 selectOrder: function(event) {
285 selectedOrder: this.order
289 this.order.unbind('destroy', this.destroy, this);
290 this.order.unbind('change', this.renderElement, this);
291 this.pos.unbind('change:selectedOrder', this.renderElement, this);
296 module.ProductCategoriesWidget = module.PosBaseWidget.extend({
297 template: 'ProductCategoriesWidget',
298 init: function(parent, options){
300 this._super(parent,options);
301 this.product_type = options.product_type || 'all'; // 'all' | 'weightable'
302 this.onlyWeightable = options.onlyWeightable || false;
303 this.category = this.pos.root_category;
304 this.breadcrumb = [];
305 this.subcategories = [];
306 this.product_list_widget = options.product_list_widget || null;
307 this.category_cache = new module.DomCache();
310 this.switch_category_handler = function(event){
311 self.set_category(self.pos.db.get_category_by_id(Number(this.dataset['categoryId'])));
312 self.renderElement();
315 this.clear_search_handler = function(event){
319 var search_timeout = null;
320 this.search_handler = function(event){
321 clearTimeout(search_timeout);
323 var query = this.value;
325 search_timeout = setTimeout(function(){
326 self.perform_search(self.category, query, event.which === 13);
331 // changes the category. if undefined, sets to root category
332 set_category : function(category){
333 var db = this.pos.db;
335 this.category = db.get_category_by_id(db.root_category_id);
337 this.category = category;
339 this.breadcrumb = [];
340 var ancestors_ids = db.get_category_ancestors_ids(this.category.id);
341 for(var i = 1; i < ancestors_ids.length; i++){
342 this.breadcrumb.push(db.get_category_by_id(ancestors_ids[i]));
344 if(this.category.id !== db.root_category_id){
345 this.breadcrumb.push(this.category);
347 this.subcategories = db.get_category_by_id(db.get_category_childs_ids(this.category.id));
350 get_image_url: function(category){
351 return window.location.origin + '/web/binary/image?model=pos.category&field=image_medium&id='+category.id;
354 render_category: function( category, with_image ){
355 var cached = this.category_cache.get_node(category.id);
358 var image_url = this.get_image_url(category);
359 var category_html = QWeb.render('CategoryButton',{
362 image_url: this.get_image_url(category),
364 category_html = _.str.trim(category_html);
365 var category_node = document.createElement('div');
366 category_node.innerHTML = category_html;
367 category_node = category_node.childNodes[0];
369 var category_html = QWeb.render('CategorySimpleButton',{
373 category_html = _.str.trim(category_html);
374 var category_node = document.createElement('div');
375 category_node.innerHTML = category_html;
376 category_node = category_node.childNodes[0];
378 this.category_cache.cache_node(category.id,category_node);
379 return category_node;
384 replace: function($target){
385 this.renderElement();
386 var target = $target[0];
387 target.parentNode.replaceChild(this.el,target);
390 renderElement: function(){
393 var el_str = openerp.qweb.render(this.template, {widget: this});
394 var el_node = document.createElement('div');
395 el_node.innerHTML = el_str;
396 el_node = el_node.childNodes[1];
398 if(this.el && this.el.parentNode){
399 this.el.parentNode.replaceChild(el_node,this.el);
404 var hasimages = false; //if none of the subcategories have images, we don't display buttons with icons
405 for(var i = 0; i < this.subcategories.length; i++){
406 if(this.subcategories[i].image){
412 var list_container = el_node.querySelector('.category-list');
413 if (list_container) {
415 list_container.classList.add('simple');
417 list_container.classList.remove('simple');
419 for(var i = 0, len = this.subcategories.length; i < len; i++){
420 list_container.appendChild(this.render_category(this.subcategories[i],hasimages));
424 var buttons = el_node.querySelectorAll('.js-category-switch');
425 for(var i = 0; i < buttons.length; i++){
426 buttons[i].addEventListener('click',this.switch_category_handler);
429 var products = this.pos.db.get_product_by_category(this.category.id);
430 this.product_list_widget.set_product_list(products);
432 this.el.querySelector('.searchbox input').addEventListener('keyup',this.search_handler);
434 this.el.querySelector('.search-clear').addEventListener('click',this.clear_search_handler);
436 if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
437 this.pos_widget.onscreen_keyboard.connect($(this.el.querySelector('.searchbox input')));
441 // resets the current category to the root category
442 reset_category: function(){
444 this.renderElement();
447 // empties the content of the search box
448 clear_search: function(){
449 var products = this.pos.db.get_product_by_category(this.category.id);
450 this.product_list_widget.set_product_list(products);
451 var input = this.el.querySelector('.searchbox input');
455 perform_search: function(category, query, buy_result){
457 var products = this.pos.db.search_product_in_category(category.id,query)
458 if(buy_result && products.length === 1){
459 this.pos.get('selectedOrder').addProduct(products[0]);
462 this.product_list_widget.set_product_list(products);
465 var products = this.pos.db.get_product_by_category(this.category.id);
466 this.product_list_widget.set_product_list(products);
472 module.ProductListWidget = module.PosBaseWidget.extend({
473 template:'ProductListWidget',
474 init: function(parent, options) {
476 this._super(parent,options);
477 this.model = options.model;
478 this.productwidgets = [];
479 this.weight = options.weight || 0;
480 this.show_scale = options.show_scale || false;
481 this.next_screen = options.next_screen || false;
483 this.click_product_handler = function(event){
484 var product = self.pos.db.get_product_by_id(this.dataset['productId']);
485 options.click_product_action(product);
488 this.product_list = options.product_list || [];
489 this.product_cache = new module.DomCache();
491 set_product_list: function(product_list){
492 this.product_list = product_list;
493 this.renderElement();
495 get_product_image_url: function(product){
496 return window.location.origin + '/web/binary/image?model=product.product&field=image_medium&id='+product.id;
498 replace: function($target){
499 this.renderElement();
500 var target = $target[0];
501 target.parentNode.replaceChild(this.el,target);
504 render_product: function(product){
505 var cached = this.product_cache.get_node(product.id);
507 var image_url = this.get_product_image_url(product);
508 var product_html = QWeb.render('Product',{
511 image_url: this.get_product_image_url(product),
513 var product_node = document.createElement('div');
514 product_node.innerHTML = product_html;
515 product_node = product_node.childNodes[1];
516 this.product_cache.cache_node(product.id,product_node);
522 renderElement: function() {
526 var el_str = openerp.qweb.render(this.template, {widget: this});
527 var el_node = document.createElement('div');
528 el_node.innerHTML = el_str;
529 el_node = el_node.childNodes[1];
531 if(this.el && this.el.parentNode){
532 this.el.parentNode.replaceChild(el_node,this.el);
536 var list_container = el_node.querySelector('.product-list');
537 for(var i = 0, len = this.product_list.length; i < len; i++){
538 var product_node = this.render_product(this.product_list[i]);
539 product_node.addEventListener('click',this.click_product_handler);
540 list_container.appendChild(product_node);
545 module.UsernameWidget = module.PosBaseWidget.extend({
546 template: 'UsernameWidget',
547 init: function(parent, options){
548 var options = options || {};
549 this._super(parent,options);
550 this.mode = options.mode || 'cashier';
552 set_user_mode: function(mode){
557 this.renderElement();
559 get_name: function(){
561 if(this.mode === 'cashier'){
562 user = this.pos.cashier || this.pos.user;
564 user = this.pos.get('selectedOrder').get_client() || this.pos.user;
574 module.HeaderButtonWidget = module.PosBaseWidget.extend({
575 template: 'HeaderButtonWidget',
576 init: function(parent, options){
577 options = options || {};
578 this._super(parent, options);
579 this.action = options.action;
580 this.label = options.label;
582 renderElement: function(){
586 this.$el.click(function(){
591 show: function(){ this.$el.removeClass('oe_hidden'); },
592 hide: function(){ this.$el.addClass('oe_hidden'); },
595 // The debug widget lets the user control and monitor the hardware and software status
596 // without the use of the proxy
597 module.DebugWidget = module.PosBaseWidget.extend({
598 template: "DebugWidget",
600 admin_badge: '0410100000006',
601 client_badge: '0420200000004',
602 invalid_ean: '1232456',
603 soda_33cl: '5449000000996',
604 oranges_kg: '2100002031410',
605 lemon_price: '2301000001560',
606 unknown_product: '9900000000004',
614 init: function(parent,options){
615 this._super(parent,options);
618 this.minimized = false;
620 // for dragging the debug widget around
621 this.dragging = false;
622 this.dragpos = {x:0, y:0};
624 function eventpos(event){
625 if(event.touches && event.touches[0]){
626 return {x: event.touches[0].screenX, y: event.touches[0].screenY};
628 return {x: event.screenX, y: event.screenY};
632 this.dragend_handler = function(event){
633 self.dragging = false;
635 this.dragstart_handler = function(event){
636 self.dragging = true;
637 self.dragpos = eventpos(event);
639 this.dragmove_handler = function(event){
641 var top = this.offsetTop;
642 var left = this.offsetLeft;
643 var pos = eventpos(event);
644 var dx = pos.x - self.dragpos.x;
645 var dy = pos.y - self.dragpos.y;
649 this.style.right = 'auto';
650 this.style.bottom = 'auto';
651 this.style.left = left + dx + 'px';
652 this.style.top = top + dy + 'px';
654 event.preventDefault();
655 event.stopPropagation();
661 this.el.addEventListener('mouseleave', this.dragend_handler);
662 this.el.addEventListener('mouseup', this.dragend_handler);
663 this.el.addEventListener('touchend', this.dragend_handler);
664 this.el.addEventListener('touchcancel',this.dragend_handler);
665 this.el.addEventListener('mousedown', this.dragstart_handler);
666 this.el.addEventListener('touchstart', this.dragstart_handler);
667 this.el.addEventListener('mousemove', this.dragmove_handler);
668 this.el.addEventListener('touchmove', this.dragmove_handler);
670 this.$('.toggle').click(function(){
671 var content = self.$('.content');
674 content.animate({'height':'0'},200);
676 content.css({'height':'auto'});
678 self.minimized = !self.minimized;
680 this.$('.button.set_weight').click(function(){
681 var kg = Number(self.$('input.weight').val());
683 self.pos.proxy.debug_set_weight(kg);
686 this.$('.button.reset_weight').click(function(){
687 self.$('input.weight').val('');
688 self.pos.proxy.debug_reset_weight();
690 this.$('.button.custom_ean').click(function(){
691 var ean = self.pos.barcode_reader.sanitize_ean(self.$('input.ean').val() || '0');
692 self.$('input.ean').val(ean);
693 self.pos.barcode_reader.scan(ean);
695 this.$('.button.reference').click(function(){
696 self.pos.barcode_reader.scan(self.$('input.ean').val());
698 this.$('.button.show_orders').click(function(){
699 self.pos.pos_widget.screen_selector.show_popup('unsent-orders');
701 this.$('.button.delete_orders').click(function(){
702 self.pos.pos_widget.screen_selector.show_popup('confirm',{
703 message: _t('Delete Unsent Orders ?'),
704 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.'),
706 self.pos.db.remove_all_orders();
707 self.pos.set({synch: { state:'connected', pending: 0 }});
711 _.each(this.eans, function(ean, name){
712 self.$('.button.'+name).click(function(){
713 self.$('input.ean').val(ean);
714 self.pos.barcode_reader.scan(ean);
717 _.each(this.events, function(name){
718 self.pos.proxy.add_notification(name,function(){
719 self.$('.event.'+name).stop().clearQueue().css({'background-color':'#6CD11D'});
720 self.$('.event.'+name).animate({'background-color':'#1E1E1E'},2000);
726 // ---------- Main Point of Sale Widget ----------
728 module.StatusWidget = module.PosBaseWidget.extend({
729 status: ['connected','connecting','disconnected','warning'],
730 set_status: function(status,msg){
732 for(var i = 0; i < this.status.length; i++){
733 this.$('.js_'+this.status[i]).addClass('oe_hidden');
735 this.$('.js_'+status).removeClass('oe_hidden');
738 this.$('.js_msg').removeClass('oe_hidden').html(msg);
740 this.$('.js_msg').addClass('oe_hidden').html('');
745 // this is used to notify the user that data is being synchronized on the network
746 module.SynchNotificationWidget = module.StatusWidget.extend({
747 template: 'SynchNotificationWidget',
750 this.pos.bind('change:synch', function(pos,synch){
751 self.set_status(synch.state, synch.pending);
753 this.$el.click(function(){
754 self.pos.push_order();
759 // this is used to notify the user if the pos is connected to the proxy
760 module.ProxyStatusWidget = module.StatusWidget.extend({
761 template: 'ProxyStatusWidget',
762 set_smart_status: function(status){
763 if(status.status === 'connected'){
766 if(this.pos.config.iface_scan_via_proxy){
767 var scanner = status.drivers.scanner ? status.drivers.scanner.status : false;
768 if( scanner != 'connected' && scanner != 'connecting'){
770 msg += _t('Scanner');
773 if( this.pos.config.iface_print_via_proxy ||
774 this.pos.config.iface_cashdrawer ){
775 var printer = status.drivers.escpos ? status.drivers.escpos.status : false;
776 if( printer != 'connected' && printer != 'connecting'){
778 msg = msg ? msg + ' & ' : msg;
779 msg += _t('Printer');
782 if( this.pos.config.iface_electronic_scale ){
783 var scale = status.drivers.scale ? status.drivers.scale.status : false;
784 if( scale != 'connected' && scale != 'connecting' ){
786 msg = msg ? msg + ' & ' : msg;
790 msg = msg ? msg + ' ' + _t('Offline') : msg;
791 this.set_status(warning ? 'warning' : 'connected', msg);
793 this.set_status(status.status,'');
799 this.set_smart_status(this.pos.proxy.get('status'));
801 this.pos.proxy.on('change:status',this,function(eh,status){ //FIXME remove duplicate changes
802 self.set_smart_status(status.newValue);
805 this.$el.click(function(){
806 self.pos.connect_to_proxy();
812 // The PosWidget is the main widget that contains all other widgets in the PointOfSale.
813 // It is mainly composed of :
814 // - a header, containing the list of orders
815 // - a leftpane, containing the list of bought products (orderlines)
816 // - a rightpane, containing the screens (see pos_screens.js)
818 // - an onscreen keyboard
819 // a screen_selector which controls the switching between screens and the showing/closing of popups
821 module.PosWidget = module.PosBaseWidget.extend({
822 template: 'PosWidget',
824 this._super(arguments[0],{});
826 this.pos = new module.PosModel(this.session,{pos_widget:this});
827 this.pos_widget = this; //So that pos_widget's childs have pos_widget set automatically
829 this.numpad_visible = true;
830 this.leftpane_visible = true;
831 this.leftpane_width = '440px';
832 this.cashier_controls_visible = true;
834 FastClick.attach(document.body);
838 disable_rubberbanding: function(){
839 // prevent the pos body from being scrollable.
840 document.body.addEventListener('touchmove',function(event){
841 var node = event.target;
843 if(node.classList && node.classList.contains('touch-scrollable')){
846 node = node.parentNode;
848 event.preventDefault();
854 return self.pos.ready.done(function() {
855 // remove default webclient handlers that induce click delay
860 $(self.$el).parent().off();
862 $('.oe_web_client').off();
863 $('.openerp_webclient_container').off();
865 self.renderElement();
867 self.$('.neworder-button').click(function(){
868 self.pos.add_new_order();
871 self.$('.deleteorder-button').click(function(){
872 if( !self.pos.get('selectedOrder').is_empty() ){
873 self.screen_selector.show_popup('confirm',{
874 message: _t('Destroy Current Order ?'),
875 comment: _t('You will lose any data associated with the current order'),
877 self.pos.delete_current_order();
881 self.pos.delete_current_order();
885 //when a new order is created, add an order button widget
886 self.pos.get('orders').bind('add', function(new_order){
887 var new_order_button = new module.OrderButtonWidget(null, {
891 new_order_button.appendTo(this.$('.orders'));
892 new_order_button.selectOrder();
895 self.pos.add_new_order();
897 self.build_widgets();
899 if(self.pos.config.iface_big_scrollbars){
900 self.$el.addClass('big-scrollbars');
903 self.screen_selector.set_default_screen();
905 self.pos.barcode_reader.connect();
907 instance.webclient.set_content_full_screen(true);
909 if(self.pos.config.iface_fullscreen && document.body.webkitRequestFullscreen && (
910 window.screen.availWidth > window.innerWidth ||
911 window.screen.availHeight > window.innerHeight )){
912 self.screen_selector.show_popup('fullscreen');
914 self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').addClass('oe_hidden');});
916 self.pos.push_order();
918 }).fail(function(err){ // error when loading models data from the backend
919 self.loading_error(err);
922 loading_error: function(err){
925 var message = err.message;
926 var comment = err.stack;
928 if(err.message === 'XmlHttpRequestError '){
929 message = 'Network Failure (XmlHttpRequestError)';
930 comment = 'The Point of Sale could not be loaded due to a network problem.\n Please check your internet connection.';
931 }else if(err.message === 'OpenERP Server Error'){
932 message = err.data.message;
933 comment = err.data.debug;
936 if( typeof comment !== 'string' ){
937 comment = 'Traceback not available.';
940 var popup = $(QWeb.render('ErrorTracebackPopupWidget',{
941 widget: { message: message, comment: comment },
944 popup.find('.button').click(function(){
948 popup.css({ zindex: 9001 });
950 popup.appendTo(this.$el);
952 loading_progress: function(fac){
953 this.$('.loader .loader-feedback').removeClass('oe_hidden');
954 this.$('.loader .progress').css({'width': ''+Math.floor(fac*100)+'%'});
956 loading_message: function(msg,progress){
957 this.$('.loader .loader-feedback').removeClass('oe_hidden');
958 this.$('.loader .message').text(msg);
959 if(typeof progress !== 'undefined'){
960 this.loading_progress(progress);
963 loading_skip: function(callback){
965 this.$('.loader .loader-feedback').removeClass('oe_hidden');
966 this.$('.loader .button.skip').removeClass('oe_hidden');
967 this.$('.loader .button.skip').off('click');
968 this.$('.loader .button.skip').click(callback);
970 this.$('.loader .button.skip').addClass('oe_hidden');
973 // This method instantiates all the screens, widgets, etc. If you want to add new screens change the
974 // startup screen, etc, override this method.
975 build_widgets: function() {
978 // -------- Screens ---------
980 this.product_screen = new module.ProductScreenWidget(this,{});
981 this.product_screen.appendTo(this.$('.screens'));
983 this.receipt_screen = new module.ReceiptScreenWidget(this, {});
984 this.receipt_screen.appendTo(this.$('.screens'));
986 this.payment_screen = new module.PaymentScreenWidget(this, {});
987 this.payment_screen.appendTo(this.$('.screens'));
989 this.clientlist_screen = new module.ClientListScreenWidget(this, {});
990 this.clientlist_screen.appendTo(this.$('.screens'));
992 this.scale_screen = new module.ScaleScreenWidget(this,{});
993 this.scale_screen.appendTo(this.$('.screens'));
996 // -------- Popups ---------
998 this.error_popup = new module.ErrorPopupWidget(this, {});
999 this.error_popup.appendTo(this.$el);
1001 this.error_barcode_popup = new module.ErrorBarcodePopupWidget(this, {});
1002 this.error_barcode_popup.appendTo(this.$el);
1004 this.error_traceback_popup = new module.ErrorTracebackPopupWidget(this,{});
1005 this.error_traceback_popup.appendTo(this.$el);
1007 this.confirm_popup = new module.ConfirmPopupWidget(this,{});
1008 this.confirm_popup.appendTo(this.$el);
1010 this.fullscreen_popup = new module.FullscreenPopup(this,{});
1011 this.fullscreen_popup.appendTo(this.$el);
1013 this.unsent_orders_popup = new module.UnsentOrdersPopupWidget(this,{});
1014 this.unsent_orders_popup.appendTo(this.$el);
1016 // -------- Misc ---------
1018 this.close_button = new module.HeaderButtonWidget(this,{
1022 if (!this.confirmed) {
1023 this.$el.addClass('confirm');
1024 this.$el.text(_t('Confirm'));
1025 this.confirmed = setTimeout(function(){
1026 self.$el.removeClass('confirm');
1027 self.$el.text(_t('Close'));
1028 self.confirmed = false;
1031 clearTimeout(this.confirmed);
1032 this.pos_widget.close();
1036 this.close_button.appendTo(this.$('.pos-rightheader'));
1038 this.notification = new module.SynchNotificationWidget(this,{});
1039 this.notification.appendTo(this.$('.pos-rightheader'));
1041 if(this.pos.config.use_proxy){
1042 this.proxy_status = new module.ProxyStatusWidget(this,{});
1043 this.proxy_status.appendTo(this.$('.pos-rightheader'));
1046 this.username = new module.UsernameWidget(this,{});
1047 this.username.replace(this.$('.placeholder-UsernameWidget'));
1049 this.actionpad = new module.ActionpadWidget(this, {});
1050 this.actionpad.replace(this.$('.placeholder-ActionpadWidget'));
1052 this.numpad = new module.NumpadWidget(this);
1053 this.numpad.replace(this.$('.placeholder-NumpadWidget'));
1055 this.order_widget = new module.OrderWidget(this, {});
1056 this.order_widget.replace(this.$('.placeholder-OrderWidget'));
1058 this.onscreen_keyboard = new module.OnscreenKeyboardWidget(this, {
1059 'keyboard_model': 'simple'
1061 this.onscreen_keyboard.replace(this.$('.placeholder-OnscreenKeyboardWidget'));
1063 // -------- Screen Selector ---------
1065 this.screen_selector = new module.ScreenSelector({
1068 'products': this.product_screen,
1069 'payment' : this.payment_screen,
1070 'scale': this.scale_screen,
1071 'receipt' : this.receipt_screen,
1072 'clientlist': this.clientlist_screen,
1075 'error': this.error_popup,
1076 'error-barcode': this.error_barcode_popup,
1077 'error-traceback': this.error_traceback_popup,
1078 'confirm': this.confirm_popup,
1079 'fullscreen': this.fullscreen_popup,
1080 'unsent-orders': this.unsent_orders_popup,
1082 default_screen: 'products',
1083 default_mode: 'cashier',
1087 this.debug_widget = new module.DebugWidget(this);
1088 this.debug_widget.appendTo(this.$('.pos-content'));
1091 this.disable_rubberbanding();
1095 changed_pending_operations: function () {
1097 this.synch_notification.on_change_nbr_pending(self.pos.get('nbr_pending_operations').length);
1099 // shows or hide the numpad and related controls like the paypad.
1100 set_numpad_visible: function(visible){
1101 if(visible !== this.numpad_visible){
1102 this.numpad_visible = visible;
1105 this.actionpad.show();
1108 this.actionpad.hide();
1112 //shows or hide the leftpane (contains the list of orderlines, the numpad, the paypad, etc.)
1113 set_leftpane_visible: function(visible){
1114 if(visible !== this.leftpane_visible){
1115 this.leftpane_visible = visible;
1117 this.$('.pos-leftpane').removeClass('oe_hidden');
1118 this.$('.rightpane').css({'left':this.leftpane_width});
1120 this.$('.pos-leftpane').addClass('oe_hidden');
1121 this.$('.rightpane').css({'left':'0px'});
1129 self.pos.push_order().then(function(){
1130 return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_client_pos_menu']], ['res_id']).pipe(function(res) {
1131 window.location = '/web#action=' + res[0]['res_id'];
1132 },function(err,event) {
1133 event.preventDefault();
1134 self.screen_selector.show_popup('error',{
1135 'message': _t('Could not close the point of sale.'),
1136 'comment': _t('Your internet connection is probably down.'),
1138 self.close_button.renderElement();
1143 var draft_order = _.find( self.pos.get('orders').models, function(order){
1144 return order.get('orderLines').length !== 0 && order.get('paymentLines').length === 0;
1147 if (confirm(_t("Pending orders will be lost.\nAre you sure you want to leave this session?"))) {
1154 destroy: function() {
1156 instance.webclient.set_content_full_screen(false);