1 openerp.point_of_sale.load_widgets = function load_widgets(instance, module){ //module is instance.point_of_sale
4 var QWeb = instance.web.qweb;
5 var _t = instance.web._t;
7 module.DomCache = instance.web.Class.extend({
8 init: function(options){
9 options = options || {};
10 this.max_size = options.max_size || 2000;
13 this.access_time = {};
16 cache_node: function(key,node){
17 var cached = this.cache[key];
18 this.cache[key] = node;
19 this.access_time[key] = new Date().getTime();
22 while(this.size >= this.max_size){
23 var oldest_key = null;
24 var oldest_time = new Date().getTime();
25 for(var key in this.cache){
26 var time = this.access_time[key];
27 if(time <= oldest_time){
33 delete this.cache[oldest_key];
34 delete this.access_time[oldest_key];
41 get_node: function(key){
42 var cached = this.cache[key];
44 this.access_time[key] = new Date().getTime();
50 module.NumpadWidget = module.PosBaseWidget.extend({
51 template:'NumpadWidget',
52 init: function(parent, options) {
54 this.state = new module.NumpadState();
55 window.numpadstate = this.state;
59 this.state.bind('change:mode', this.changedMode, this);
61 this.$el.find('.numpad-backspace').click(_.bind(this.clickDeleteLastChar, this));
62 this.$el.find('.numpad-minus').click(_.bind(this.clickSwitchSign, this));
63 this.$el.find('.number-char').click(_.bind(this.clickAppendNewChar, this));
64 this.$el.find('.mode-button').click(_.bind(this.clickChangeMode, this));
66 clickDeleteLastChar: function() {
67 return this.state.deleteLastChar();
69 clickSwitchSign: function() {
70 return this.state.switchSign();
72 clickAppendNewChar: function(event) {
74 newChar = event.currentTarget.innerText || event.currentTarget.textContent;
75 return this.state.appendNewChar(newChar);
77 clickChangeMode: function(event) {
78 var newMode = event.currentTarget.attributes['data-mode'].nodeValue;
79 return this.state.changeMode(newMode);
81 changedMode: function() {
82 var mode = this.state.get('mode');
83 $('.selected-mode').removeClass('selected-mode');
84 $(_.str.sprintf('.mode-button[data-mode="%s"]', mode), this.$el).addClass('selected-mode');
88 // The action pad contains the payment button and the customer selection button
89 module.ActionpadWidget = module.PosBaseWidget.extend({
90 template: 'ActionpadWidget',
91 renderElement: function() {
94 this.$('.pay').click(function(){
95 self.pos.pos_widget.screen_selector.set_current_screen('payment');
97 this.$('.set-customer').click(function(){
98 self.pos.pos_widget.screen_selector.set_current_screen('clientlist');
103 module.OrderWidget = module.PosBaseWidget.extend({
104 template:'OrderWidget',
105 init: function(parent, options) {
107 this._super(parent,options);
108 this.editable = false;
109 this.pos.bind('change:selectedOrder', this.change_selected_order, this);
110 this.line_click_handler = function(event){
114 self.pos.get_order().select_orderline(this.orderline);
115 self.pos_widget.numpad.state.reset();
117 this.client_change_handler = function(event){
118 self.update_summary();
120 if (this.pos.get_order()) {
121 this.bind_order_events();
124 enable_numpad: function(){
125 this.disable_numpad(); //ensure we don't register the callbacks twice
126 this.numpad_state = this.pos_widget.numpad.state;
127 if(this.numpad_state){
128 this.numpad_state.reset();
129 this.numpad_state.bind('set_value', this.set_value, this);
133 disable_numpad: function(){
134 if(this.numpad_state){
135 this.numpad_state.unbind('set_value', this.set_value);
136 this.numpad_state.reset();
139 set_editable: function(editable){
140 var order = this.pos.get_order();
142 this.editable = editable;
144 this.enable_numpad();
146 this.disable_numpad();
147 order.deselect_orderline();
151 set_value: function(val) {
152 var order = this.pos.get_order();
153 if (this.editable && order.get_selected_orderline()) {
154 var mode = this.numpad_state.get('mode');
155 if( mode === 'quantity'){
156 order.get_selected_orderline().set_quantity(val);
157 }else if( mode === 'discount'){
158 order.get_selected_orderline().set_discount(val);
159 }else if( mode === 'price'){
160 order.get_selected_orderline().set_unit_price(val);
164 change_selected_order: function() {
165 if (this.pos.get_order()) {
166 this.bind_order_events();
167 this.renderElement();
170 orderline_add: function(){
171 this.numpad_state.reset();
172 this.renderElement('and_scroll_to_bottom');
174 orderline_remove: function(line){
175 this.remove_orderline(line);
176 this.numpad_state.reset();
177 this.update_summary();
179 orderline_change: function(line){
180 this.rerender_orderline(line);
181 this.update_summary();
183 bind_order_events: function() {
184 var order = this.pos.get_order();
185 order.unbind('change:client', this.client_change_handler);
186 order.bind('change:client', this.client_change_handler);
188 var lines = order.orderlines;
189 lines.unbind('add', this.orderline_add, this);
190 lines.bind('add', this.orderline_add, this);
191 lines.unbind('remove', this.orderline_remove, this);
192 lines.bind('remove', this.orderline_remove, this);
193 lines.unbind('change', this.orderline_change, this);
194 lines.bind('change', this.orderline_change, this);
197 render_orderline: function(orderline){
198 var el_str = openerp.qweb.render('Orderline',{widget:this, line:orderline});
199 var el_node = document.createElement('div');
200 el_node.innerHTML = _.str.trim(el_str);
201 el_node = el_node.childNodes[0];
202 el_node.orderline = orderline;
203 el_node.addEventListener('click',this.line_click_handler);
205 orderline.node = el_node;
208 remove_orderline: function(order_line){
209 if(this.pos.get_order().get_orderlines().length === 0){
210 this.renderElement();
212 order_line.node.parentNode.removeChild(order_line.node);
215 rerender_orderline: function(order_line){
216 var node = order_line.node;
217 var replacement_line = this.render_orderline(order_line);
218 node.parentNode.replaceChild(replacement_line,node);
220 // overriding the openerp framework replace method for performance reasons
221 replace: function($target){
222 this.renderElement();
223 var target = $target[0];
224 target.parentNode.replaceChild(this.el,target);
226 renderElement: function(scrollbottom){
227 this.pos_widget.numpad.state.reset();
229 var order = this.pos.get_order();
233 var orderlines = order.get_orderlines();
235 var el_str = openerp.qweb.render('OrderWidget',{widget:this, order:order, orderlines:orderlines});
237 var el_node = document.createElement('div');
238 el_node.innerHTML = _.str.trim(el_str);
239 el_node = el_node.childNodes[0];
242 var list_container = el_node.querySelector('.orderlines');
243 for(var i = 0, len = orderlines.length; i < len; i++){
244 var orderline = this.render_orderline(orderlines[i]);
245 list_container.appendChild(orderline);
248 if(this.el && this.el.parentNode){
249 this.el.parentNode.replaceChild(el_node,this.el);
252 this.update_summary();
255 this.el.querySelector('.order-scroller').scrollTop = 100 * orderlines.length;
258 update_summary: function(){
259 var order = this.pos.get_order();
260 var total = order ? order.get_total_with_tax() : 0;
261 var taxes = order ? total - order.get_total_without_tax() : 0;
263 this.el.querySelector('.summary .total > .value').textContent = this.format_currency(total);
264 this.el.querySelector('.summary .total .subentry .value').textContent = this.format_currency(taxes);
269 module.OrderSelectorWidget = module.PosBaseWidget.extend({
270 template: 'OrderSelectorWidget',
271 init: function(parent, options) {
272 this._super(parent, options);
273 this.pos.get('orders').bind('add remove change',this.renderElement,this);
274 this.pos.bind('change:selectedOrder',this.renderElement,this);
276 get_order_by_uid: function(uid) {
277 var orders = this.pos.get_order_list();
278 for (var i = 0; i < orders.length; i++) {
279 if (orders[i].uid === uid) {
285 order_click_handler: function(event,$el) {
286 var order = this.get_order_by_uid($el.data('uid'));
288 this.pos.set_order(order);
291 neworder_click_handler: function(event, $el) {
292 this.pos.add_new_order();
294 deleteorder_click_handler: function(event, $el) {
296 var order = this.pos.get_order();
299 } else if ( !order.is_empty() ){
300 this.pos_widget.screen_selector.show_popup('confirm',{
301 message: _t('Destroy Current Order ?'),
302 comment: _t('You will lose any data associated with the current order'),
304 self.pos.delete_current_order();
308 this.pos.delete_current_order();
311 renderElement: function(){
314 this.$('.order-button.select-order').click(function(event){
315 self.order_click_handler(event,$(this));
317 this.$('.neworder-button').click(function(event){
318 self.neworder_click_handler(event,$(this));
320 this.$('.deleteorder-button').click(function(event){
321 self.deleteorder_click_handler(event,$(this));
326 module.ProductCategoriesWidget = module.PosBaseWidget.extend({
327 template: 'ProductCategoriesWidget',
328 init: function(parent, options){
330 this._super(parent,options);
331 this.product_type = options.product_type || 'all'; // 'all' | 'weightable'
332 this.onlyWeightable = options.onlyWeightable || false;
333 this.category = this.pos.root_category;
334 this.breadcrumb = [];
335 this.subcategories = [];
336 this.product_list_widget = options.product_list_widget || null;
337 this.category_cache = new module.DomCache();
340 this.switch_category_handler = function(event){
341 self.set_category(self.pos.db.get_category_by_id(Number(this.dataset['categoryId'])));
342 self.renderElement();
345 this.clear_search_handler = function(event){
349 var search_timeout = null;
350 this.search_handler = function(event){
351 clearTimeout(search_timeout);
353 var query = this.value;
355 search_timeout = setTimeout(function(){
356 self.perform_search(self.category, query, event.which === 13);
361 // changes the category. if undefined, sets to root category
362 set_category : function(category){
363 var db = this.pos.db;
365 this.category = db.get_category_by_id(db.root_category_id);
367 this.category = category;
369 this.breadcrumb = [];
370 var ancestors_ids = db.get_category_ancestors_ids(this.category.id);
371 for(var i = 1; i < ancestors_ids.length; i++){
372 this.breadcrumb.push(db.get_category_by_id(ancestors_ids[i]));
374 if(this.category.id !== db.root_category_id){
375 this.breadcrumb.push(this.category);
377 this.subcategories = db.get_category_by_id(db.get_category_childs_ids(this.category.id));
380 get_image_url: function(category){
381 return window.location.origin + '/web/binary/image?model=pos.category&field=image_medium&id='+category.id;
384 render_category: function( category, with_image ){
385 var cached = this.category_cache.get_node(category.id);
388 var image_url = this.get_image_url(category);
389 var category_html = QWeb.render('CategoryButton',{
392 image_url: this.get_image_url(category),
394 category_html = _.str.trim(category_html);
395 var category_node = document.createElement('div');
396 category_node.innerHTML = category_html;
397 category_node = category_node.childNodes[0];
399 var category_html = QWeb.render('CategorySimpleButton',{
403 category_html = _.str.trim(category_html);
404 var category_node = document.createElement('div');
405 category_node.innerHTML = category_html;
406 category_node = category_node.childNodes[0];
408 this.category_cache.cache_node(category.id,category_node);
409 return category_node;
414 replace: function($target){
415 this.renderElement();
416 var target = $target[0];
417 target.parentNode.replaceChild(this.el,target);
420 renderElement: function(){
423 var el_str = openerp.qweb.render(this.template, {widget: this});
424 var el_node = document.createElement('div');
425 el_node.innerHTML = el_str;
426 el_node = el_node.childNodes[1];
428 if(this.el && this.el.parentNode){
429 this.el.parentNode.replaceChild(el_node,this.el);
434 var hasimages = false; //if none of the subcategories have images, we don't display buttons with icons
435 for(var i = 0; i < this.subcategories.length; i++){
436 if(this.subcategories[i].image){
442 var list_container = el_node.querySelector('.category-list');
443 if (list_container) {
445 list_container.classList.add('simple');
447 list_container.classList.remove('simple');
449 for(var i = 0, len = this.subcategories.length; i < len; i++){
450 list_container.appendChild(this.render_category(this.subcategories[i],hasimages));
454 var buttons = el_node.querySelectorAll('.js-category-switch');
455 for(var i = 0; i < buttons.length; i++){
456 buttons[i].addEventListener('click',this.switch_category_handler);
459 var products = this.pos.db.get_product_by_category(this.category.id);
460 this.product_list_widget.set_product_list(products);
462 this.el.querySelector('.searchbox input').addEventListener('keyup',this.search_handler);
464 this.el.querySelector('.search-clear').addEventListener('click',this.clear_search_handler);
466 if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
467 this.pos_widget.onscreen_keyboard.connect($(this.el.querySelector('.searchbox input')));
471 // resets the current category to the root category
472 reset_category: function(){
474 this.renderElement();
477 // empties the content of the search box
478 clear_search: function(){
479 var products = this.pos.db.get_product_by_category(this.category.id);
480 this.product_list_widget.set_product_list(products);
481 var input = this.el.querySelector('.searchbox input');
485 perform_search: function(category, query, buy_result){
487 var products = this.pos.db.search_product_in_category(category.id,query)
488 if(buy_result && products.length === 1){
489 this.pos.get_order().add_product(products[0]);
492 this.product_list_widget.set_product_list(products);
495 var products = this.pos.db.get_product_by_category(this.category.id);
496 this.product_list_widget.set_product_list(products);
502 module.ProductListWidget = module.PosBaseWidget.extend({
503 template:'ProductListWidget',
504 init: function(parent, options) {
506 this._super(parent,options);
507 this.model = options.model;
508 this.productwidgets = [];
509 this.weight = options.weight || 0;
510 this.show_scale = options.show_scale || false;
511 this.next_screen = options.next_screen || false;
513 this.click_product_handler = function(event){
514 var product = self.pos.db.get_product_by_id(this.dataset['productId']);
515 options.click_product_action(product);
518 this.product_list = options.product_list || [];
519 this.product_cache = new module.DomCache();
521 set_product_list: function(product_list){
522 this.product_list = product_list;
523 this.renderElement();
525 get_product_image_url: function(product){
526 return window.location.origin + '/web/binary/image?model=product.product&field=image_medium&id='+product.id;
528 replace: function($target){
529 this.renderElement();
530 var target = $target[0];
531 target.parentNode.replaceChild(this.el,target);
534 render_product: function(product){
535 var cached = this.product_cache.get_node(product.id);
537 var image_url = this.get_product_image_url(product);
538 var product_html = QWeb.render('Product',{
541 image_url: this.get_product_image_url(product),
543 var product_node = document.createElement('div');
544 product_node.innerHTML = product_html;
545 product_node = product_node.childNodes[1];
546 this.product_cache.cache_node(product.id,product_node);
552 renderElement: function() {
556 var el_str = openerp.qweb.render(this.template, {widget: this});
557 var el_node = document.createElement('div');
558 el_node.innerHTML = el_str;
559 el_node = el_node.childNodes[1];
561 if(this.el && this.el.parentNode){
562 this.el.parentNode.replaceChild(el_node,this.el);
566 var list_container = el_node.querySelector('.product-list');
567 for(var i = 0, len = this.product_list.length; i < len; i++){
568 var product_node = this.render_product(this.product_list[i]);
569 product_node.addEventListener('click',this.click_product_handler);
570 list_container.appendChild(product_node);
575 module.UsernameWidget = module.PosBaseWidget.extend({
576 template: 'UsernameWidget',
577 init: function(parent, options){
578 var options = options || {};
579 this._super(parent,options);
581 set_user_mode: function(mode){
583 this.renderElement();
585 renderElement: function(){
589 this.$el.click(function(){
590 self.click_username();
593 click_username: function(){
595 this.pos_widget.select_user({
597 'current_user': this.pos.get_cashier(),
598 'message': _t('Change Cashier'),
599 }).then(function(user){
600 self.pos.set_cashier(user);
601 self.renderElement();
604 get_name: function(){
605 var user = this.pos.cashier || this.pos.user;
614 module.HeaderButtonWidget = module.PosBaseWidget.extend({
615 template: 'HeaderButtonWidget',
616 init: function(parent, options){
617 options = options || {};
618 this._super(parent, options);
619 this.action = options.action;
620 this.label = options.label;
622 renderElement: function(){
626 this.$el.click(function(){
631 show: function(){ this.$el.removeClass('oe_hidden'); },
632 hide: function(){ this.$el.addClass('oe_hidden'); },
635 // The debug widget lets the user control and monitor the hardware and software status
636 // without the use of the proxy
637 module.DebugWidget = module.PosBaseWidget.extend({
638 template: "DebugWidget",
640 admin_badge: '0410100000006',
641 client_badge: '0420200000004',
642 invalid_ean: '1232456',
643 soda_33cl: '5449000000996',
644 oranges_kg: '2100002031410',
645 lemon_price: '2301000001560',
646 unknown_product: '9900000000004',
654 init: function(parent,options){
655 this._super(parent,options);
658 this.minimized = false;
660 // for dragging the debug widget around
661 this.dragging = false;
662 this.dragpos = {x:0, y:0};
664 function eventpos(event){
665 if(event.touches && event.touches[0]){
666 return {x: event.touches[0].screenX, y: event.touches[0].screenY};
668 return {x: event.screenX, y: event.screenY};
672 this.dragend_handler = function(event){
673 self.dragging = false;
675 this.dragstart_handler = function(event){
676 self.dragging = true;
677 self.dragpos = eventpos(event);
679 this.dragmove_handler = function(event){
681 var top = this.offsetTop;
682 var left = this.offsetLeft;
683 var pos = eventpos(event);
684 var dx = pos.x - self.dragpos.x;
685 var dy = pos.y - self.dragpos.y;
689 this.style.right = 'auto';
690 this.style.bottom = 'auto';
691 this.style.left = left + dx + 'px';
692 this.style.top = top + dy + 'px';
694 event.preventDefault();
695 event.stopPropagation();
701 this.el.addEventListener('mouseleave', this.dragend_handler);
702 this.el.addEventListener('mouseup', this.dragend_handler);
703 this.el.addEventListener('touchend', this.dragend_handler);
704 this.el.addEventListener('touchcancel',this.dragend_handler);
705 this.el.addEventListener('mousedown', this.dragstart_handler);
706 this.el.addEventListener('touchstart', this.dragstart_handler);
707 this.el.addEventListener('mousemove', this.dragmove_handler);
708 this.el.addEventListener('touchmove', this.dragmove_handler);
710 this.$('.toggle').click(function(){
711 var content = self.$('.content');
714 content.animate({'height':'0'},200);
716 content.css({'height':'auto'});
718 self.minimized = !self.minimized;
720 this.$('.button.set_weight').click(function(){
721 var kg = Number(self.$('input.weight').val());
723 self.pos.proxy.debug_set_weight(kg);
726 this.$('.button.reset_weight').click(function(){
727 self.$('input.weight').val('');
728 self.pos.proxy.debug_reset_weight();
730 this.$('.button.custom_ean').click(function(){
731 var ean = self.pos.barcode_reader.sanitize_ean(self.$('input.ean').val() || '0');
732 self.$('input.ean').val(ean);
733 self.pos.barcode_reader.scan(ean);
735 this.$('.button.reference').click(function(){
736 self.pos.barcode_reader.scan(self.$('input.ean').val());
738 this.$('.button.show_orders').click(function(){
739 self.pos.pos_widget.screen_selector.show_popup('unsent-orders');
741 this.$('.button.delete_orders').click(function(){
742 self.pos.pos_widget.screen_selector.show_popup('confirm',{
743 message: _t('Delete Unsent Orders ?'),
744 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.'),
746 self.pos.db.remove_all_orders();
747 self.pos.set({synch: { state:'connected', pending: 0 }});
751 this.$('.button.show_unpaid_orders').click(function(){
752 self.pos.pos_widget.screen_selector.show_popup('unpaid-orders');
754 this.$('.button.delete_unpaid_orders').click(function(){
755 self.pos.pos_widget.screen_selector.show_popup('confirm',{
756 message: _t('Delete Unpaid Orders ?'),
757 comment: _t('This operation will permanently destroy all unpaid orders from all sessions that have been put in the local storage. You will lose all the data and exit the point of sale. This operation cannot be undone.'),
759 self.pos.db.remove_all_unpaid_orders();
760 window.location = '/';
764 _.each(this.eans, function(ean, name){
765 self.$('.button.'+name).click(function(){
766 self.$('input.ean').val(ean);
767 self.pos.barcode_reader.scan(ean);
770 _.each(this.events, function(name){
771 self.pos.proxy.add_notification(name,function(){
772 self.$('.event.'+name).stop().clearQueue().css({'background-color':'#6CD11D'});
773 self.$('.event.'+name).animate({'background-color':'#1E1E1E'},2000);
779 // ---------- Main Point of Sale Widget ----------
781 module.StatusWidget = module.PosBaseWidget.extend({
782 status: ['connected','connecting','disconnected','warning'],
783 set_status: function(status,msg){
785 for(var i = 0; i < this.status.length; i++){
786 this.$('.js_'+this.status[i]).addClass('oe_hidden');
788 this.$('.js_'+status).removeClass('oe_hidden');
791 this.$('.js_msg').removeClass('oe_hidden').html(msg);
793 this.$('.js_msg').addClass('oe_hidden').html('');
798 // this is used to notify the user that data is being synchronized on the network
799 module.SynchNotificationWidget = module.StatusWidget.extend({
800 template: 'SynchNotificationWidget',
803 this.pos.bind('change:synch', function(pos,synch){
804 self.set_status(synch.state, synch.pending);
806 this.$el.click(function(){
807 self.pos.push_order();
812 // this is used to notify the user if the pos is connected to the proxy
813 module.ProxyStatusWidget = module.StatusWidget.extend({
814 template: 'ProxyStatusWidget',
815 set_smart_status: function(status){
816 if(status.status === 'connected'){
819 if(this.pos.config.iface_scan_via_proxy){
820 var scanner = status.drivers.scanner ? status.drivers.scanner.status : false;
821 if( scanner != 'connected' && scanner != 'connecting'){
823 msg += _t('Scanner');
826 if( this.pos.config.iface_print_via_proxy ||
827 this.pos.config.iface_cashdrawer ){
828 var printer = status.drivers.escpos ? status.drivers.escpos.status : false;
829 if( printer != 'connected' && printer != 'connecting'){
831 msg = msg ? msg + ' & ' : msg;
832 msg += _t('Printer');
835 if( this.pos.config.iface_electronic_scale ){
836 var scale = status.drivers.scale ? status.drivers.scale.status : false;
837 if( scale != 'connected' && scale != 'connecting' ){
839 msg = msg ? msg + ' & ' : msg;
843 msg = msg ? msg + ' ' + _t('Offline') : msg;
844 this.set_status(warning ? 'warning' : 'connected', msg);
846 this.set_status(status.status,'');
852 this.set_smart_status(this.pos.proxy.get('status'));
854 this.pos.proxy.on('change:status',this,function(eh,status){ //FIXME remove duplicate changes
855 self.set_smart_status(status.newValue);
858 this.$el.click(function(){
859 self.pos.connect_to_proxy();
865 // The PosWidget is the main widget that contains all other widgets in the PointOfSale.
866 // It is mainly composed of :
867 // - a header, containing the list of orders
868 // - a leftpane, containing the list of bought products (orderlines)
869 // - a rightpane, containing the screens (see pos_screens.js)
871 // - an onscreen keyboard
872 // a screen_selector which controls the switching between screens and the showing/closing of popups
874 module.PosWidget = module.PosBaseWidget.extend({
875 template: 'PosWidget',
877 this._super(arguments[0],{});
879 this.pos = new module.PosModel(this.session,{pos_widget:this});
880 this.pos_widget = this; //So that pos_widget's childs have pos_widget set automatically
882 this.numpad_visible = true;
883 this.leftpane_visible = true;
884 this.leftpane_width = '440px';
885 this.cashier_controls_visible = true;
887 FastClick.attach(document.body);
891 disable_rubberbanding: function(){
892 // prevent the pos body from being scrollable.
893 document.body.addEventListener('touchmove',function(event){
894 var node = event.target;
896 if(node.classList && node.classList.contains('touch-scrollable')){
899 node = node.parentNode;
901 event.preventDefault();
907 return self.pos.ready.done(function() {
908 // remove default webclient handlers that induce click delay
913 $(self.$el).parent().off();
915 $('.oe_web_client').off();
916 $('.openerp_webclient_container').off();
918 self.renderElement();
920 self.pos.load_orders();
921 self.pos.set_start_order();
923 self.build_widgets();
925 if(self.pos.config.iface_big_scrollbars){
926 self.$el.addClass('big-scrollbars');
929 self.screen_selector.set_default_screen();
931 self.pos.barcode_reader.connect();
933 instance.webclient.set_content_full_screen(true);
935 if(self.pos.config.iface_fullscreen && document.body.webkitRequestFullscreen && (
936 window.screen.availWidth > window.innerWidth ||
937 window.screen.availHeight > window.innerHeight )){
938 self.screen_selector.show_popup('fullscreen');
942 self.pos.push_order();
944 }).fail(function(err){ // error when loading models data from the backend
945 self.loading_error(err);
948 loading_error: function(err){
951 var message = err.message;
952 var comment = err.stack;
954 if(err.message === 'XmlHttpRequestError '){
955 message = 'Network Failure (XmlHttpRequestError)';
956 comment = 'The Point of Sale could not be loaded due to a network problem.\n Please check your internet connection.';
957 }else if(err.message === 'OpenERP Server Error'){
958 message = err.data.message;
959 comment = err.data.debug;
962 if( typeof comment !== 'string' ){
963 comment = 'Traceback not available.';
966 var popup = $(QWeb.render('ErrorTracebackPopupWidget',{
967 widget: { message: message, comment: comment },
970 popup.find('.button').click(function(){
974 popup.css({ zindex: 9001 });
976 popup.appendTo(this.$el);
978 loading_progress: function(fac){
979 this.$('.loader .loader-feedback').removeClass('oe_hidden');
980 this.$('.loader .progress').removeClass('oe_hidden').css({'width': ''+Math.floor(fac*100)+'%'});
982 loading_message: function(msg,progress){
983 this.$('.loader .loader-feedback').removeClass('oe_hidden');
984 this.$('.loader .message').text(msg);
985 if (typeof progress !== 'undefined') {
986 this.loading_progress(progress);
988 this.$('.loader .progress').addClass('oe_hidden');
991 loading_skip: function(callback){
993 this.$('.loader .loader-feedback').removeClass('oe_hidden');
994 this.$('.loader .button.skip').removeClass('oe_hidden');
995 this.$('.loader .button.skip').off('click');
996 this.$('.loader .button.skip').click(callback);
998 this.$('.loader .button.skip').addClass('oe_hidden');
1001 loading_hide: function(){
1002 this.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').addClass('oe_hidden');});
1004 loading_show: function(){
1005 this.$('.loader').removeClass('oe_hidden').animate({opacity:1},150,'swing');
1008 // A Generic UI that allow to select a user from a list.
1009 // It returns a deferred that resolves with the selected user
1010 // upon success. Several options are available :
1011 // - security: passwords will be asked
1012 // - only_managers: restricts the list to managers
1013 // - current_user: password will not be asked if this
1014 // user is selected.
1015 // - message: The title of the user selection list.
1016 select_user: function(options){
1017 options = options || {};
1019 var def = new $.Deferred();
1022 for (var i = 0; i < this.pos.users.length; i++) {
1023 var user = this.pos.users[i];
1024 if (!options.only_managers || user.role === 'manager') {
1032 this.pos_widget.screen_selector.show_popup('selection',{
1033 'message': options.message || _t('Select User'),
1035 confirm: function(user){ def.resolve(user); },
1036 cancel: function(){ def.reject(); },
1039 return def.then(function(user){
1040 if (options.security && user !== options.current_user && user.pos_security_pin) {
1041 var ret = new $.Deferred();
1043 self.pos_widget.screen_selector.show_popup('password',{
1044 'message': _t('Password'),
1045 confirm: function(password) {
1046 if (password !== user.pos_security_pin) {
1047 this.pos_widget.screen_selector.show_popup('error',{
1048 'message':_t('Password Incorrect'),
1055 cancel: function(){ ret.reject(); },
1065 // checks if the current user (or the user provided) has manager
1066 // access rights. If not, a popup is shown allowing the user to
1067 // temporarily login as an administrator.
1068 // This method returns a defferred, that succeeds with the
1069 // manager user when the login is successfull.
1070 sudo: function(user){
1071 var def = new $.Deferred();
1072 user = user || this.pos.get_cashier();
1074 if (user.role === 'manager') {
1075 return new $.Deferred().resolve(user);
1077 return this.select_user({
1079 only_managers: true,
1080 message: _t('Login as a Manager'),
1085 // This method instantiates all the screens, widgets, etc. If you want to add new screens change the
1086 // startup screen, etc, override this method.
1087 build_widgets: function() {
1090 // -------- Screens ---------
1092 this.product_screen = new module.ProductScreenWidget(this,{});
1093 this.product_screen.appendTo(this.$('.screens'));
1095 this.receipt_screen = new module.ReceiptScreenWidget(this, {});
1096 this.receipt_screen.appendTo(this.$('.screens'));
1098 this.payment_screen = new module.PaymentScreenWidget(this, {});
1099 this.payment_screen.appendTo(this.$('.screens'));
1101 this.clientlist_screen = new module.ClientListScreenWidget(this, {});
1102 this.clientlist_screen.appendTo(this.$('.screens'));
1104 this.scale_screen = new module.ScaleScreenWidget(this,{});
1105 this.scale_screen.appendTo(this.$('.screens'));
1108 // -------- Popups ---------
1110 this.error_popup = new module.ErrorPopupWidget(this, {});
1111 this.error_popup.appendTo(this.$el);
1113 this.error_barcode_popup = new module.ErrorBarcodePopupWidget(this, {});
1114 this.error_barcode_popup.appendTo(this.$el);
1116 this.error_traceback_popup = new module.ErrorTracebackPopupWidget(this,{});
1117 this.error_traceback_popup.appendTo(this.$el);
1119 this.confirm_popup = new module.ConfirmPopupWidget(this,{});
1120 this.confirm_popup.appendTo(this.$el);
1122 this.fullscreen_popup = new module.FullscreenPopup(this,{});
1123 this.fullscreen_popup.appendTo(this.$el);
1125 this.selection_popup = new module.SelectionPopupWidget(this,{});
1126 this.selection_popup.appendTo(this.$el);
1128 this.textinput_popup = new module.TextInputPopupWidget(this,{});
1129 this.textinput_popup.appendTo(this.$el);
1131 this.textarea_popup = new module.TextAreaPopupWidget(this,{});
1132 this.textarea_popup.appendTo(this.$el);
1134 this.number_popup = new module.NumberPopupWidget(this,{});
1135 this.number_popup.appendTo(this.$el);
1137 this.password_popup = new module.PasswordPopupWidget(this,{});
1138 this.password_popup.appendTo(this.$el);
1140 this.unsent_orders_popup = new module.UnsentOrdersPopupWidget(this,{});
1141 this.unsent_orders_popup.appendTo(this.$el);
1143 this.unpaid_orders_popup = new module.UnpaidOrdersPopupWidget(this,{});
1144 this.unpaid_orders_popup.appendTo(this.$el);
1146 // -------- Misc ---------
1148 this.order_selector = new module.OrderSelectorWidget(this,{});
1149 this.order_selector.replace(this.$('.placeholder-OrderSelectorWidget'));
1151 if(this.pos.config.use_proxy){
1152 this.proxy_status = new module.ProxyStatusWidget(this,{});
1153 this.proxy_status.appendTo(this.$('.pos-rightheader'));
1156 this.notification = new module.SynchNotificationWidget(this,{});
1157 this.notification.appendTo(this.$('.pos-rightheader'));
1159 this.close_button = new module.HeaderButtonWidget(this,{
1163 if (!this.confirmed) {
1164 this.$el.addClass('confirm');
1165 this.$el.text(_t('Confirm'));
1166 this.confirmed = setTimeout(function(){
1167 self.$el.removeClass('confirm');
1168 self.$el.text(_t('Close'));
1169 self.confirmed = false;
1172 clearTimeout(this.confirmed);
1173 this.pos_widget.close();
1177 this.close_button.appendTo(this.$('.pos-rightheader'));
1181 this.username = new module.UsernameWidget(this,{});
1182 this.username.replace(this.$('.placeholder-UsernameWidget'));
1184 this.actionpad = new module.ActionpadWidget(this, {});
1185 this.actionpad.replace(this.$('.placeholder-ActionpadWidget'));
1187 this.numpad = new module.NumpadWidget(this);
1188 this.numpad.replace(this.$('.placeholder-NumpadWidget'));
1190 this.order_widget = new module.OrderWidget(this, {});
1191 this.order_widget.replace(this.$('.placeholder-OrderWidget'));
1193 this.onscreen_keyboard = new module.OnscreenKeyboardWidget(this, {
1194 'keyboard_model': 'simple'
1196 this.onscreen_keyboard.replace(this.$('.placeholder-OnscreenKeyboardWidget'));
1198 // -------- Screen Selector ---------
1200 this.screen_selector = new module.ScreenSelector({
1203 'products': this.product_screen,
1204 'payment' : this.payment_screen,
1205 'scale': this.scale_screen,
1206 'receipt' : this.receipt_screen,
1207 'clientlist': this.clientlist_screen,
1210 'error': this.error_popup,
1211 'error-barcode': this.error_barcode_popup,
1212 'error-traceback': this.error_traceback_popup,
1213 'textinput': this.textinput_popup,
1214 'textarea': this.textarea_popup,
1215 'number': this.number_popup,
1216 'password': this.password_popup,
1217 'confirm': this.confirm_popup,
1218 'fullscreen': this.fullscreen_popup,
1219 'selection': this.selection_popup,
1220 'unsent-orders': this.unsent_orders_popup,
1221 'unpaid-orders': this.unpaid_orders_popup,
1223 default_screen: 'products',
1224 default_mode: 'cashier',
1228 this.debug_widget = new module.DebugWidget(this);
1229 this.debug_widget.appendTo(this.$('.pos-content'));
1232 this.disable_rubberbanding();
1236 changed_pending_operations: function () {
1238 this.synch_notification.on_change_nbr_pending(self.pos.get('nbr_pending_operations').length);
1240 // shows or hide the numpad and related controls like the paypad.
1241 set_numpad_visible: function(visible){
1242 if(visible !== this.numpad_visible){
1243 this.numpad_visible = visible;
1246 this.actionpad.show();
1249 this.actionpad.hide();
1253 //shows or hide the leftpane (contains the list of orderlines, the numpad, the paypad, etc.)
1254 set_leftpane_visible: function(visible){
1255 if(visible !== this.leftpane_visible){
1256 this.leftpane_visible = visible;
1258 this.$('.pos-leftpane').removeClass('oe_hidden');
1259 this.$('.rightpane').css({'left':this.leftpane_width});
1261 this.$('.pos-leftpane').addClass('oe_hidden');
1262 this.$('.rightpane').css({'left':'0px'});
1268 self.loading_show();
1269 self.loading_message(_t('Closing ...'));
1271 self.pos.push_order().then(function(){
1272 return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_client_pos_menu']], ['res_id']).pipe(function(res) {
1273 window.location = '/web#action=' + res[0]['res_id'];
1278 destroy: function() {
1280 instance.webclient.set_content_full_screen(false);