// this file contains the screens definitions. Screens are the // content of the right pane of the pos, containing the main functionalities. // screens are contained in the PosWidget, in pos_widget.js // all screens are present in the dom at all time, but only one is shown at the // same time. // // transition between screens is made possible by the use of the screen_selector, // which is responsible of hiding and showing the screens, as well as maintaining // the state of the screens between different orders. // // all screens inherit from ScreenWidget. the only addition from the base widgets // are show() and hide() which shows and hides the screen but are also used to // bind and unbind actions on widgets and devices. The screen_selector guarantees // that only one screen is shown at the same time and that show() is called after all // hide()s function openerp_pos_screens(instance, module){ //module is instance.point_of_sale var QWeb = instance.web.qweb, _t = instance.web._t; var round_pr = instance.web.round_precision module.ScreenSelector = instance.web.Class.extend({ init: function(options){ this.pos = options.pos; this.screen_set = options.screen_set || {}; this.popup_set = options.popup_set || {}; this.default_screen = options.default_screen; this.current_popup = null; this.current_mode = options.default_mode || 'cashier'; this.current_screen = null; for(screen_name in this.screen_set){ this.screen_set[screen_name].hide(); } for(popup_name in this.popup_set){ this.popup_set[popup_name].hide(); } this.pos.get('selectedOrder').set_screen_data({ 'screen': this.default_screen, }); this.pos.bind('change:selectedOrder', this.load_saved_screen, this); }, add_screen: function(screen_name, screen){ screen.hide(); this.screen_set[screen_name] = screen; return this; }, show_popup: function(name,options){ if(this.current_popup){ this.close_popup(); } this.current_popup = this.popup_set[name]; this.current_popup.show(options); }, close_popup: function(){ if(this.current_popup){ this.current_popup.close(); this.current_popup.hide(); this.current_popup = null; } }, load_saved_screen: function(){ this.close_popup(); var selectedOrder = this.pos.get('selectedOrder'); // FIXME : this changing screen behaviour is sometimes confusing ... this.set_current_screen(selectedOrder.get_screen_data('screen') || this.default_screen,null,'refresh'); //this.set_current_screen(this.default_screen,null,'refresh'); }, set_user_mode: function(user_mode){ if(user_mode !== this.current_mode){ this.close_popup(); this.current_mode = user_mode; this.load_saved_screen(); } }, get_user_mode: function(){ return this.current_mode; }, set_current_screen: function(screen_name,params,refresh){ var screen = this.screen_set[screen_name]; if(!screen){ console.error("ERROR: set_current_screen("+screen_name+") : screen not found"); } this.close_popup(); var order = this.pos.get('selectedOrder'); var old_screen_name = order.get_screen_data('screen'); order.set_screen_data('screen',screen_name); if(params){ order.set_screen_data('params',params); } if( screen_name !== old_screen_name ){ order.set_screen_data('previous-screen',old_screen_name); } if ( refresh || screen !== this.current_screen){ if(this.current_screen){ this.current_screen.close(); this.current_screen.hide(); } this.current_screen = screen; this.current_screen.show(); } }, get_current_screen: function(){ return this.pos.get('selectedOrder').get_screen_data('screen') || this.default_screen; }, back: function(){ var previous = this.pos.get('selectedOrder').get_screen_data('previous-screen'); if(previous){ this.set_current_screen(previous); } }, get_current_screen_param: function(param){ var params = this.pos.get('selectedOrder').get_screen_data('params'); return params ? params[param] : undefined; }, set_default_screen: function(){ this.set_current_screen(this.default_screen); }, }); module.ScreenWidget = module.PosBaseWidget.extend({ show_numpad: true, show_leftpane: true, init: function(parent,options){ this._super(parent,options); this.hidden = false; }, help_button_action: function(){ this.pos_widget.screen_selector.show_popup('help'); }, barcode_product_screen: 'products', //if defined, this screen will be loaded when a product is scanned hotkeys_handlers: {}, // what happens when a product is scanned : // it will add the product to the order and go to barcode_product_screen. barcode_product_action: function(code){ var self = this; if(self.pos.scan_product(code)){ if(self.barcode_product_screen){ self.pos_widget.screen_selector.set_current_screen(self.barcode_product_screen); } }else{ self.pos_widget.screen_selector.show_popup('error-barcode',code.code); } }, // what happens when a cashier id barcode is scanned. // the default behavior is the following : // - if there's a user with a matching ean, put it as the active 'cashier', go to cashier mode, and return true // - else : do nothing and return false. You probably want to extend this to show and appropriate error popup... barcode_cashier_action: function(code){ var users = this.pos.users; for(var i = 0, len = users.length; i < len; i++){ if(users[i].ean13 === code.code){ this.pos.cashier = users[i]; this.pos_widget.username.refresh(); return true; } } this.pos_widget.screen_selector.show_popup('error-barcode',code.code); return false; }, // what happens when a client id barcode is scanned. // the default behavior is the following : // - if there's a user with a matching ean, put it as the active 'client' and return true // - else : return false. barcode_client_action: function(code){ var partner = this.pos.db.get_partner_by_ean13(code.code); if(partner){ this.pos.get('selectedOrder').set_client(partner); this.pos_widget.username.refresh(); return true; } this.pos_widget.screen_selector.show_popup('error-barcode',code.code); return false; }, // what happens when a discount barcode is scanned : the default behavior // is to set the discount on the last order. barcode_discount_action: function(code){ var last_orderline = this.pos.get('selectedOrder').getLastOrderline(); if(last_orderline){ last_orderline.set_discount(code.value) } }, // What happens when an invalid barcode is scanned : shows an error popup. barcode_error_action: function(code){ this.pos_widget.screen_selector.show_popup('error-barcode',code.code); }, // shows an action bar on the screen. The actionbar is automatically shown when you add a button // with add_action_button() show_action_bar: function(){ this.pos_widget.action_bar.show(); }, // hides the action bar. The actionbar is automatically hidden when it is empty hide_action_bar: function(){ this.pos_widget.action_bar.hide(); }, // adds a new button to the action bar. The button definition takes three parameters, all optional : // - label: the text below the button // - icon: a small icon that will be shown // - click: a callback that will be executed when the button is clicked. // the method returns a reference to the button widget, and automatically show the actionbar. add_action_button: function(button_def){ this.show_action_bar(); return this.pos_widget.action_bar.add_new_button(button_def); }, // this method shows the screen and sets up all the widget related to this screen. Extend this method // if you want to alter the behavior of the screen. show: function(){ var self = this; this.hidden = false; if(this.$el){ this.$el.removeClass('oe_hidden'); } if(this.pos_widget.action_bar.get_button_count() > 0){ this.show_action_bar(); }else{ this.hide_action_bar(); } var self = this; this.pos_widget.set_numpad_visible(this.show_numpad); this.pos_widget.set_leftpane_visible(this.show_leftpane); this.pos_widget.username.set_user_mode(this.pos_widget.screen_selector.get_user_mode()); this.pos.barcode_reader.set_action_callback({ 'cashier': self.barcode_cashier_action ? function(code){ self.barcode_cashier_action(code); } : undefined , 'product': self.barcode_product_action ? function(code){ self.barcode_product_action(code); } : undefined , 'client' : self.barcode_client_action ? function(code){ self.barcode_client_action(code); } : undefined , 'discount': self.barcode_discount_action ? function(code){ self.barcode_discount_action(code); } : undefined, 'error' : self.barcode_error_action ? function(code){ self.barcode_error_action(code); } : undefined, }); }, // this method is called when the screen is closed to make place for a new screen. this is a good place // to put your cleanup stuff as it is guaranteed that for each show() there is one and only one close() close: function(){ if(this.pos.barcode_reader){ this.pos.barcode_reader.reset_action_callbacks(); } this.pos_widget.action_bar.destroy_buttons(); }, // this methods hides the screen. It's not a good place to put your cleanup stuff as it is called on the // POS initialization. hide: function(){ this.hidden = true; if(this.$el){ this.$el.addClass('oe_hidden'); } }, // we need this because some screens re-render themselves when they are hidden // (due to some events, or magic, or both...) we must make sure they remain hidden. // the good solution would probably be to make them not re-render themselves when they // are hidden. renderElement: function(){ this._super(); if(this.hidden){ if(this.$el){ this.$el.addClass('oe_hidden'); } } }, }); module.PopUpWidget = module.PosBaseWidget.extend({ show: function(){ if(this.$el){ this.$el.removeClass('oe_hidden'); } }, /* called before hide, when a popup is closed */ close: function(){ }, /* hides the popup. keep in mind that this is called in the initialization pass of the * pos instantiation, so you don't want to do anything fancy in here */ hide: function(){ if(this.$el){ this.$el.addClass('oe_hidden'); } }, }); module.ErrorPopupWidget = module.PopUpWidget.extend({ template:'ErrorPopupWidget', show: function(options){ options = options || {}; var self = this; this._super(); $('body').append(''); this.message = options.message || _t('Error'); this.comment = options.comment || ''; this.renderElement(); this.pos.barcode_reader.save_callbacks(); this.pos.barcode_reader.reset_action_callbacks(); this.$('.footer .button').click(function(){ self.pos_widget.screen_selector.close_popup(); if ( options.confirm ) { options.confirm.call(self); } }); }, close:function(){ this._super(); this.pos.barcode_reader.restore_callbacks(); }, }); module.ErrorTracebackPopupWidget = module.ErrorPopupWidget.extend({ template:'ErrorTracebackPopupWidget', }); module.ErrorBarcodePopupWidget = module.ErrorPopupWidget.extend({ template:'ErrorBarcodePopupWidget', show: function(barcode){ this.barcode = barcode; this._super(); }, }); module.ConfirmPopupWidget = module.PopUpWidget.extend({ template: 'ConfirmPopupWidget', show: function(options){ var self = this; this._super(); this.message = options.message || ''; this.comment = options.comment || ''; this.renderElement(); this.$('.button.cancel').click(function(){ self.pos_widget.screen_selector.close_popup(); if( options.cancel ){ options.cancel.call(self); } }); this.$('.button.confirm').click(function(){ self.pos_widget.screen_selector.close_popup(); if( options.confirm ){ options.confirm.call(self); } }); }, }); module.ErrorNoClientPopupWidget = module.ErrorPopupWidget.extend({ template: 'ErrorNoClientPopupWidget', }); module.ErrorInvoiceTransferPopupWidget = module.ErrorPopupWidget.extend({ template: 'ErrorInvoiceTransferPopupWidget', }); module.UnsentOrdersPopupWidget = module.PopUpWidget.extend({ template: 'UnsentOrdersPopupWidget', show: function(options){ var self = this; this._super(options); this.renderElement(); this.$('.button.confirm').click(function(){ self.pos_widget.screen_selector.close_popup(); }); }, }); module.ScaleScreenWidget = module.ScreenWidget.extend({ template:'ScaleScreenWidget', next_screen: 'products', previous_screen: 'products', show_leftpane: false, show: function(){ this._super(); var self = this; var queue = this.pos.proxy_queue; this.set_weight(0); this.renderElement(); this.hotkey_handler = function(event){ if(event.which === 13){ self.order_product(); self.pos_widget.screen_selector.set_current_screen(self.next_screen); }else if(event.which === 27){ self.pos_widget.screen_selector.set_current_screen(self.previous_screen); } }; $('body').on('keyup',this.hotkey_handler); this.$('.back').click(function(){ self.pos_widget.screen_selector.set_current_screen(self.previous_screen); }); this.$('.next,.buy-product').click(function(){ self.order_product(); self.pos_widget.screen_selector.set_current_screen(self.next_screen); }); queue.schedule(function(){ return self.pos.proxy.scale_read().then(function(weight){ self.set_weight(weight.weight); }); },{duration:50, repeat: true}); }, get_product: function(){ var ss = this.pos_widget.screen_selector; if(ss){ return ss.get_current_screen_param('product'); }else{ return undefined; } }, order_product: function(){ this.pos.get('selectedOrder').addProduct(this.get_product(),{ quantity: this.weight }); }, get_product_name: function(){ var product = this.get_product(); return (product ? product.display_name : undefined) || 'Unnamed Product'; }, get_product_price: function(){ var product = this.get_product(); return (product ? product.price : 0) || 0; }, set_weight: function(weight){ this.weight = weight; this.$('.weight').text(this.get_product_weight_string()); this.$('.computed-price').text(this.get_computed_price_string()); }, get_product_weight_string: function(){ var product = this.get_product(); var defaultstr = (this.weight || 0).toFixed(3) + ' Kg'; if(!product || !this.pos){ return defaultstr; } var unit_id = product.uom_id; if(!unit_id){ return defaultstr; } var unit = this.pos.units_by_id[unit_id[0]]; var weight = round_pr(this.weight || 0, unit.rounding); var weightstr = weight.toFixed(Math.ceil(Math.log(1.0/unit.rounding) / Math.log(10) )); weightstr += ' Kg'; return weightstr; }, get_computed_price_string: function(){ return this.format_currency(this.get_product_price() * this.weight); }, close: function(){ var self = this; this._super(); $('body').off('keyup',this.hotkey_handler); this.pos.proxy_queue.clear(); }, }); module.ProductScreenWidget = module.ScreenWidget.extend({ template:'ProductScreenWidget', show_numpad: true, show_leftpane: true, start: function(){ //FIXME this should work as renderElement... but then the categories aren't properly set. explore why var self = this; this.product_list_widget = new module.ProductListWidget(this,{ click_product_action: function(product){ if(product.to_weight && self.pos.config.iface_electronic_scale){ self.pos_widget.screen_selector.set_current_screen('scale',{product: product}); }else{ self.pos.get('selectedOrder').addProduct(product); } }, product_list: this.pos.db.get_product_by_category(0) }); this.product_list_widget.replace(this.$('.placeholder-ProductListWidget')); this.product_categories_widget = new module.ProductCategoriesWidget(this,{ product_list_widget: this.product_list_widget, }); this.product_categories_widget.replace(this.$('.placeholder-ProductCategoriesWidget')); }, show: function(){ this._super(); var self = this; this.product_categories_widget.reset_category(); this.pos_widget.order_widget.set_editable(true); }, close: function(){ this._super(); this.pos_widget.order_widget.set_editable(false); if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){ this.pos_widget.onscreen_keyboard.hide(); } }, }); module.ClientListScreenWidget = module.ScreenWidget.extend({ template: 'ClientListScreenWidget', init: function(parent, options){ this._super(parent, options); }, show_leftpane: false, auto_back: true, show: function(){ var self = this; this._super(); this.renderElement(); this.details_visible = false; this.old_client = this.pos.get('selectedOrder').get('client'); this.new_client = this.old_client; this.$('.back').click(function(){ self.pos_widget.screen_selector.back(); }); this.$('.next').click(function(){ self.save_changes(); self.pos_widget.screen_selector.back(); }); this.$('.new-customer').click(function(){ self.display_client_details('edit',{ 'country_id': self.pos.company.country_id, }); }); var partners = this.pos.db.get_partners_sorted(1000); this.render_list(partners); this.reload_partners(); if( this.old_client ){ this.display_client_details('show',this.old_client,0); } this.$('.client-list-contents').delegate('.client-line','click',function(event){ self.line_select(event,$(this),parseInt($(this).data('id'))); }); var search_timeout = null; if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){ this.pos_widget.onscreen_keyboard.connect(this.$('.searchbox input')); } this.$('.searchbox input').on('keyup',function(event){ clearTimeout(search_timeout); var query = this.value; search_timeout = setTimeout(function(){ self.perform_search(query,event.which === 13); },70); }); this.$('.searchbox .search-clear').click(function(){ self.clear_search(); }); }, barcode_client_action: function(code){ if (this.editing_client) { this.$('.detail.barcode').val(code.code); } else if (this.pos.db.get_partner_by_ean13(code.code)) { this.display_client_details('show',this.pos.db.get_partner_by_ean13(code.code)); } }, perform_search: function(query, associate_result){ if(query){ var customers = this.pos.db.search_partner(query); this.display_client_details('hide'); if ( associate_result && customers.length === 1){ this.new_client = customers[0]; this.save_changes(); this.pos_widget.screen_selector.back(); } this.render_list(customers); }else{ var customers = this.pos.db.get_partners_sorted(); this.render_list(customers); } }, clear_search: function(){ var customers = this.pos.db.get_partners_sorted(1000); this.render_list(customers); this.$('.searchbox input')[0].value = ''; this.$('.searchbox input').focus(); }, render_list: function(partners){ var contents = this.$el[0].querySelector('.client-list-contents'); contents.innerHTML = ""; for(var i = 0, len = Math.min(partners.length,1000); i < len; i++){ var partner = partners[i]; var clientline_html = QWeb.render('ClientLine',{widget: this, partner:partners[i]}); var clientline = document.createElement('tbody'); clientline.innerHTML = clientline_html; clientline = clientline.childNodes[1]; if( partners === this.new_client ){ clientline.classList.add('highlight'); }else{ clientline.classList.remove('highlight'); } contents.appendChild(clientline); } }, save_changes: function(){ if( this.has_client_changed() ){ this.pos.get('selectedOrder').set_client(this.new_client); } }, has_client_changed: function(){ if( this.old_client && this.new_client ){ return this.old_client.id !== this.new_client.id; }else{ return !!this.old_client !== !!this.new_client; } }, toggle_save_button: function(){ var $button = this.$('.button.next'); if (this.editing_client) { $button.addClass('oe_hidden'); return; } else if( this.new_client ){ if( !this.old_client){ $button.text(_t('Set Customer')); }else{ $button.text(_t('Change Customer')); } }else{ $button.text(_t('Deselect Customer')); } $button.toggleClass('oe_hidden',!this.has_client_changed()); }, line_select: function(event,$line,id){ var partner = this.pos.db.get_partner_by_id(id); this.$('.client-list .lowlight').removeClass('lowlight'); if ( $line.hasClass('highlight') ){ $line.removeClass('highlight'); $line.addClass('lowlight'); this.display_client_details('hide',partner); this.new_client = null; this.toggle_save_button(); }else{ this.$('.client-list .highlight').removeClass('highlight'); $line.addClass('highlight'); var y = event.pageY - $line.parent().offset().top this.display_client_details('show',partner,y); this.new_client = partner; this.toggle_save_button(); } }, partner_icon_url: function(id){ return '/web/binary/image?model=res.partner&id='+id+'&field=image_small'; }, // ui handle for the 'edit selected customer' action edit_client_details: function(partner) { this.display_client_details('edit',partner); }, // ui handle for the 'cancel customer edit changes' action undo_client_details: function(partner) { if (!partner.id) { this.display_client_details('hide'); } else { this.display_client_details('show',partner); } }, // what happens when we save the changes on the client edit form -> we fetch the fields, sanitize them, // send them to the backend for update, and call saved_client_details() when the server tells us the // save was successfull. save_client_details: function(partner) { var self = this; var fields = {} this.$('.client-details-contents .detail').each(function(idx,el){ fields[el.name] = el.value; }); if (!fields.name) { this.pos_widget.screen_selector.show_popup('error',{ message: _t('A Customer Name Is Required'), }); return; } if (this.uploaded_picture) { fields.image = this.uploaded_picture; } fields.id = partner.id || false; fields.country_id = fields.country_id || false; fields.ean13 = fields.ean13 ? this.pos.barcode_reader.sanitize_ean(fields.ean13) : false; new instance.web.Model('res.partner').call('create_from_ui',[fields]).then(function(partner_id){ self.saved_client_details(partner_id); },function(err,event){ event.preventDefault(); self.pos_widget.screen_selector.show_popup('error',{ 'message':_t('Error: Could not Save Changes'), 'comment':_t('Your Internet connection is probably down.'), }); }); }, // what happens when we've just pushed modifications for a partner of id partner_id saved_client_details: function(partner_id){ var self = this; this.reload_partners().then(function(){ var partner = self.pos.db.get_partner_by_id(partner_id); if (partner) { self.new_client = partner; self.toggle_save_button(); self.display_client_details('show',partner); } else { // should never happen, because create_from_ui must return the id of the partner it // has created, and reload_partner() must have loaded the newly created partner. self.display_client_details('hide'); } }); }, // resizes an image, keeping the aspect ratio intact, // the resize is useful to avoid sending 12Mpixels jpegs // over a wireless connection. resize_image_to_dataurl: function(img, maxwidth, maxheight, callback){ img.onload = function(){ var png = new Image(); var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); var ratio = 1; if (img.width > maxwidth) { ratio = maxwidth / img.width; } if (img.height * ratio > maxheight) { ratio = maxheight / img.height; } var width = Math.floor(img.width * ratio); var height = Math.floor(img.height * ratio); canvas.width = width; canvas.height = height; ctx.drawImage(img,0,0,width,height); var dataurl = canvas.toDataURL(); callback(dataurl); } }, // Loads and resizes a File that contains an image. // callback gets a dataurl in case of success. load_image_file: function(file, callback){ var self = this; if (!file.type.match(/image.*/)) { this.pos_widget.screen_selector.show_popup('error',{ message:_t('Unsupported File Format'), comment:_t('Only web-compatible Image formats such as .png or .jpeg are supported'), }); return; } var reader = new FileReader(); reader.onload = function(event){ var dataurl = event.target.result; var img = new Image(); img.src = dataurl; self.resize_image_to_dataurl(img,800,600,callback); } reader.onerror = function(){ self.pos_widget.screen_selector.show_popup('error',{ message:_t('Could Not Read Image'), comment:_t('The provided file could not be read due to an unknown error'), }); }; reader.readAsDataURL(file); }, // This fetches partner changes on the server, and in case of changes, // rerenders the affected views reload_partners: function(){ var self = this; return this.pos.load_new_partners().then(function(){ self.render_list(self.pos.db.get_partners_sorted(1000)); // update the currently assigned client if it has been changed in db. var curr_client = self.pos.get_order().get_client(); if (curr_client) { self.pos.get_order().set_client(self.pos.db.get_partner_by_id(curr_client.id)); } }); }, // Shows,hides or edit the customer details box : // visibility: 'show', 'hide' or 'edit' // partner: the partner object to show or edit // clickpos: the height of the click on the list (in pixel), used // to maintain consistent scroll. display_client_details: function(visibility,partner,clickpos){ var self = this; var contents = this.$('.client-details-contents'); var parent = this.$('.client-list').parent(); var scroll = parent.scrollTop(); var height = contents.height(); contents.off('click','.button.edit'); contents.off('click','.button.save'); contents.off('click','.button.undo'); contents.on('click','.button.edit',function(){ self.edit_client_details(partner); }); contents.on('click','.button.save',function(){ self.save_client_details(partner); }); contents.on('click','.button.undo',function(){ self.undo_client_details(partner); }); this.editing_client = false; this.uploaded_picture = null; if(visibility === 'show'){ contents.empty(); contents.append($(QWeb.render('ClientDetails',{widget:this,partner:partner}))); var new_height = contents.height(); if(!this.details_visible){ if(clickpos < scroll + new_height + 20 ){ parent.scrollTop( clickpos - 20 ); }else{ parent.scrollTop(parent.scrollTop() + new_height); } }else{ parent.scrollTop(parent.scrollTop() - height + new_height); } this.details_visible = true; this.toggle_save_button(); } else if (visibility === 'edit') { this.editing_client = true; contents.empty(); contents.append($(QWeb.render('ClientDetailsEdit',{widget:this,partner:partner}))); this.toggle_save_button(); contents.find('.image-uploader').on('change',function(){ self.load_image_file(event.target.files[0],function(res){ if (res) { contents.find('.client-picture img, .client-picture .fa').remove(); contents.find('.client-picture').append(""); contents.find('.detail.picture').remove(); self.uploaded_picture = res; } }); }); } else if (visibility === 'hide') { contents.empty(); if( height > scroll ){ contents.css({height:height+'px'}); contents.animate({height:0},400,function(){ contents.css({height:''}); }); }else{ parent.scrollTop( parent.scrollTop() - height); } this.details_visible = false; this.toggle_save_button(); } }, close: function(){ this._super(); }, }); module.ReceiptScreenWidget = module.ScreenWidget.extend({ template: 'ReceiptScreenWidget', show_numpad: false, show_leftpane: false, show: function(){ this._super(); var self = this; var print_button = this.add_action_button({ label: _t('Print'), icon: '/point_of_sale/static/src/img/icons/png48/printer.png', click: function(){ self.print(); }, }); var finish_button = this.add_action_button({ label: _t('Next Order'), icon: '/point_of_sale/static/src/img/icons/png48/go-next.png', click: function() { self.finishOrder(); }, }); this.refresh(); if (!this.pos.get('selectedOrder')._printed) { this.print(); } // // The problem is that in chrome the print() is asynchronous and doesn't // execute until all rpc are finished. So it conflicts with the rpc used // to send the orders to the backend, and the user is able to go to the next // screen before the printing dialog is opened. The problem is that what's // printed is whatever is in the page when the dialog is opened and not when it's called, // and so you end up printing the product list instead of the receipt... // // Fixing this would need a re-architecturing // of the code to postpone sending of orders after printing. // // But since the print dialog also blocks the other asynchronous calls, the // button enabling in the setTimeout() is blocked until the printing dialog is // closed. But the timeout has to be big enough or else it doesn't work // 2 seconds is the same as the default timeout for sending orders and so the dialog // should have appeared before the timeout... so yeah that's not ultra reliable. finish_button.set_disabled(true); setTimeout(function(){ finish_button.set_disabled(false); }, 2000); }, print: function() { this.pos.get('selectedOrder')._printed = true; window.print(); }, finishOrder: function() { this.pos.get('selectedOrder').destroy(); }, refresh: function() { var order = this.pos.get('selectedOrder'); $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{ widget:this, order: order, orderlines: order.get('orderLines').models, paymentlines: order.get('paymentLines').models, })); }, close: function(){ this._super(); } }); module.PaymentScreenWidget = module.ScreenWidget.extend({ template: 'PaymentScreenWidget', back_screen: 'products', next_screen: 'receipt', init: function(parent, options) { var self = this; this._super(parent,options); this.pos.bind('change:selectedOrder',function(){ this.bind_events(); this.renderElement(); },this); this.bind_events(); this.decimal_point = instance.web._t.database.parameters.decimal_point; this.line_delete_handler = function(event){ var node = this; while(node && !node.classList.contains('paymentline')){ node = node.parentNode; } if(node){ self.pos.get('selectedOrder').removePaymentline(node.line) } event.stopPropagation(); }; this.line_change_handler = function(event){ var node = this; while(node && !node.classList.contains('paymentline')){ node = node.parentNode; } if(node){ var amount; try{ amount = instance.web.parse_value(this.value, {type: "float"}); } catch(e){ amount = 0; } node.line.set_amount(amount); } }; this.line_click_handler = function(event){ var node = this; while(node && !node.classList.contains('paymentline')){ node = node.parentNode; } if(node){ self.pos.get('selectedOrder').selectPaymentline(node.line); } }; this.hotkey_handler = function(event){ if(event.which === 13){ self.validate_order(); }else if(event.which === 27){ self.back(); } }; }, show: function(){ this._super(); var self = this; this.enable_numpad(); this.focus_selected_line(); document.body.addEventListener('keyup', this.hotkey_handler); this.add_action_button({ label: _t('Back'), icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png', click: function(){ self.back(); }, }); this.add_action_button({ label: _t('Validate'), name: 'validation', icon: '/point_of_sale/static/src/img/icons/png48/validate.png', click: function(){ self.validate_order(); }, }); if( this.pos.config.iface_invoicing ){ this.add_action_button({ label: _t('Invoice'), name: 'invoice', icon: '/point_of_sale/static/src/img/icons/png48/invoice.png', click: function(){ self.validate_order({invoice: true}); }, }); } if( this.pos.config.iface_cashdrawer ){ this.add_action_button({ label: _t('Cash'), name: 'cashbox', icon: '/point_of_sale/static/src/img/open-cashbox.png', click: function(){ self.pos.proxy.open_cashbox(); }, }); } this.update_payment_summary(); }, close: function(){ this._super(); this.disable_numpad(); document.body.removeEventListener('keyup',this.hotkey_handler); }, remove_empty_lines: function(){ var order = this.pos.get('selectedOrder'); var lines = order.get('paymentLines').models.slice(0); for(var i = 0; i < lines.length; i++){ var line = lines[i]; if(line.get_amount() === 0){ order.removePaymentline(line); } } }, back: function() { this.remove_empty_lines(); this.pos_widget.screen_selector.set_current_screen(this.back_screen); }, bind_events: function() { if(this.old_order){ this.old_order.unbind(null,null,this); } var order = this.pos.get('selectedOrder'); order.bind('change:selected_paymentline',this.focus_selected_line,this); this.old_order = order; if(this.old_paymentlines){ this.old_paymentlines.unbind(null,null,this); } var paymentlines = order.get('paymentLines'); paymentlines.bind('add', this.add_paymentline, this); paymentlines.bind('change:selected', this.rerender_paymentline, this); paymentlines.bind('change:amount', function(line){ if(!line.selected && line.node){ line.node.value = line.amount.toFixed(this.pos.currency.decimals); } this.update_payment_summary(); },this); paymentlines.bind('remove', this.remove_paymentline, this); paymentlines.bind('all', this.update_payment_summary, this); this.old_paymentlines = paymentlines; if(this.old_orderlines){ this.old_orderlines.unbind(null,null,this); } var orderlines = order.get('orderLines'); orderlines.bind('all', this.update_payment_summary, this); this.old_orderlines = orderlines; }, focus_selected_line: function(){ var line = this.pos.get('selectedOrder').selected_paymentline; if(line){ var input = line.node.querySelector('input'); if(!input){ return; } var value = input.value; input.focus(); if(this.numpad_state){ this.numpad_state.reset(); } if(Number(value) === 0){ input.value = ''; }else{ input.value = value; input.select(); } } }, add_paymentline: function(line) { var list_container = this.el.querySelector('.payment-lines'); list_container.appendChild(this.render_paymentline(line)); if(this.numpad_state){ this.numpad_state.reset(); } }, render_paymentline: function(line){ var el_html = openerp.qweb.render('Paymentline',{widget: this, line: line}); el_html = _.str.trim(el_html); var el_node = document.createElement('tbody'); el_node.innerHTML = el_html; el_node = el_node.childNodes[0]; el_node.line = line; el_node.querySelector('.paymentline-delete') .addEventListener('click', this.line_delete_handler); el_node.addEventListener('click', this.line_click_handler); el_node.querySelector('input') .addEventListener('keyup', this.line_change_handler); line.node = el_node; return el_node; }, rerender_paymentline: function(line){ var old_node = line.node; var new_node = this.render_paymentline(line); old_node.parentNode.replaceChild(new_node,old_node); }, remove_paymentline: function(line){ line.node.parentNode.removeChild(line.node); line.node = undefined; }, renderElement: function(){ this._super(); var paymentlines = this.pos.get('selectedOrder').get('paymentLines').models; var list_container = this.el.querySelector('.payment-lines'); for(var i = 0; i < paymentlines.length; i++){ list_container.appendChild(this.render_paymentline(paymentlines[i])); } this.update_payment_summary(); }, update_payment_summary: function() { var currentOrder = this.pos.get('selectedOrder'); var paidTotal = currentOrder.getPaidTotal(); var dueTotal = currentOrder.getTotalTaxIncluded(); var remaining = dueTotal > paidTotal ? dueTotal - paidTotal : 0; var change = paidTotal > dueTotal ? paidTotal - dueTotal : 0; this.$('.payment-due-total').html(this.format_currency(dueTotal)); this.$('.payment-paid-total').html(this.format_currency(paidTotal)); this.$('.payment-remaining').html(this.format_currency(remaining)); this.$('.payment-change').html(this.format_currency(change)); if(currentOrder.selected_orderline === undefined){ remaining = 1; // What is this ? } if(this.pos_widget.action_bar){ this.pos_widget.action_bar.set_button_disabled('validation', !this.is_paid()); this.pos_widget.action_bar.set_button_disabled('invoice', !this.is_paid()); } }, is_paid: function(){ var currentOrder = this.pos.get('selectedOrder'); return (currentOrder.getTotalTaxIncluded() < 0.000001 || currentOrder.getPaidTotal() + 0.000001 >= currentOrder.getTotalTaxIncluded()); }, validate_order: function(options) { var self = this; options = options || {}; var currentOrder = this.pos.get('selectedOrder'); if(currentOrder.get('orderLines').models.length === 0){ this.pos_widget.screen_selector.show_popup('error',{ 'message': _t('Empty Order'), 'comment': _t('There must be at least one product in your order before it can be validated'), }); return; } var plines = currentOrder.get('paymentLines').models; for (var i = 0; i < plines.length; i++) { if (plines[i].get_type() === 'bank' && plines[i].get_amount() < 0) { this.pos_widget.screen_selector.show_popup('error',{ 'message': _t('Negative Bank Payment'), 'comment': _t('You cannot have a negative amount in a Bank payment. Use a cash payment method to return money to the customer.'), }); return; } } if(!this.is_paid()){ return; } // The exact amount must be paid if there is no cash payment method defined. if (Math.abs(currentOrder.getTotalTaxIncluded() - currentOrder.getPaidTotal()) > 0.00001) { var cash = false; for (var i = 0; i < this.pos.cashregisters.length; i++) { cash = cash || (this.pos.cashregisters[i].journal.type === 'cash'); } if (!cash) { this.pos_widget.screen_selector.show_popup('error',{ message: _t('Cannot return change without a cash payment method'), comment: _t('There is no cash payment method available in this point of sale to handle the change.\n\n Please pay the exact amount or add a cash payment method in the point of sale configuration'), }); return; } } if (this.pos.config.iface_cashdrawer) { this.pos.proxy.open_cashbox(); } if(options.invoice){ // deactivate the validation button while we try to send the order this.pos_widget.action_bar.set_button_disabled('validation',true); this.pos_widget.action_bar.set_button_disabled('invoice',true); var invoiced = this.pos.push_and_invoice_order(currentOrder); invoiced.fail(function(error){ if(error === 'error-no-client'){ self.pos_widget.screen_selector.show_popup('error',{ message: _t('An anonymous order cannot be invoiced'), comment: _t('Please select a client for this order. This can be done by clicking the order tab'), }); }else{ self.pos_widget.screen_selector.show_popup('error',{ message: _t('The order could not be sent'), comment: _t('Check your internet connection and try again.'), }); } self.pos_widget.action_bar.set_button_disabled('validation',false); self.pos_widget.action_bar.set_button_disabled('invoice',false); }); invoiced.done(function(){ self.pos_widget.action_bar.set_button_disabled('validation',false); self.pos_widget.action_bar.set_button_disabled('invoice',false); self.pos.get('selectedOrder').destroy(); }); }else{ this.pos.push_order(currentOrder) if(this.pos.config.iface_print_via_proxy){ var receipt = currentOrder.export_for_printing(); this.pos.proxy.print_receipt(QWeb.render('XmlReceipt',{ receipt: receipt, widget: self, })); this.pos.get('selectedOrder').destroy(); //finish order and go back to scan screen }else{ this.pos_widget.screen_selector.set_current_screen(this.next_screen); } } // hide onscreen (iOS) keyboard setTimeout(function(){ document.activeElement.blur(); $("input").blur(); },250); }, enable_numpad: function(){ this.disable_numpad(); //ensure we don't register the callbacks twice this.numpad_state = this.pos_widget.numpad.state; if(this.numpad_state){ this.numpad_state.reset(); this.numpad_state.changeMode('payment'); this.numpad_state.bind('set_value', this.set_value, this); this.numpad_state.bind('change:mode', this.set_mode_back_to_payment, this); } }, disable_numpad: function(){ if(this.numpad_state){ this.numpad_state.unbind('set_value', this.set_value); this.numpad_state.unbind('change:mode',this.set_mode_back_to_payment); } }, set_mode_back_to_payment: function() { this.numpad_state.set({mode: 'payment'}); }, set_value: function(val) { var selected_line =this.pos.get('selectedOrder').selected_paymentline; if(selected_line){ selected_line.set_amount(val); selected_line.node.querySelector('input').value = selected_line.amount.toFixed(2); } }, }); }