2 // this file contains the screens definitions. Screens are the
3 // content of the right pane of the pos, containing the main functionalities.
4 // screens are contained in the PosWidget, in pos_widget.js
5 // all screens are present in the dom at all time, but only one is shown at the
8 // transition between screens is made possible by the use of the screen_selector,
9 // which is responsible of hiding and showing the screens, as well as maintaining
10 // the state of the screens between different orders.
12 // all screens inherit from ScreenWidget. the only addition from the base widgets
13 // are show() and hide() which shows and hides the screen but are also used to
14 // bind and unbind actions on widgets and devices. The screen_selector guarantees
15 // that only one screen is shown at the same time and that show() is called after all
18 function openerp_pos_screens(instance, module){ //module is instance.point_of_sale
19 var QWeb = instance.web.qweb,
22 var round_pr = instance.web.round_precision
24 module.ScreenSelector = instance.web.Class.extend({
25 init: function(options){
26 this.pos = options.pos;
28 this.screen_set = options.screen_set || {};
30 this.popup_set = options.popup_set || {};
32 this.default_screen = options.default_screen;
34 this.current_popup = null;
36 this.current_mode = options.default_mode || 'cashier';
38 this.current_screen = null;
40 for(screen_name in this.screen_set){
41 this.screen_set[screen_name].hide();
44 for(popup_name in this.popup_set){
45 this.popup_set[popup_name].hide();
48 this.pos.get('selectedOrder').set_screen_data({
49 'screen': this.default_screen,
52 this.pos.bind('change:selectedOrder', this.load_saved_screen, this);
54 add_screen: function(screen_name, screen){
56 this.screen_set[screen_name] = screen;
59 show_popup: function(name,options){
60 if(this.current_popup){
63 this.current_popup = this.popup_set[name];
64 this.current_popup.show(options);
66 close_popup: function(){
67 if(this.current_popup){
68 this.current_popup.close();
69 this.current_popup.hide();
70 this.current_popup = null;
73 load_saved_screen: function(){
75 var selectedOrder = this.pos.get('selectedOrder');
76 // FIXME : this changing screen behaviour is sometimes confusing ...
77 this.set_current_screen(selectedOrder.get_screen_data('screen') || this.default_screen,null,'refresh');
78 //this.set_current_screen(this.default_screen,null,'refresh');
81 set_user_mode: function(user_mode){
82 if(user_mode !== this.current_mode){
84 this.current_mode = user_mode;
85 this.load_saved_screen();
88 get_user_mode: function(){
89 return this.current_mode;
91 set_current_screen: function(screen_name,params,refresh){
92 var screen = this.screen_set[screen_name];
94 console.error("ERROR: set_current_screen("+screen_name+") : screen not found");
99 var order = this.pos.get('selectedOrder');
100 var old_screen_name = order.get_screen_data('screen');
102 order.set_screen_data('screen',screen_name);
105 order.set_screen_data('params',params);
108 if( screen_name !== old_screen_name ){
109 order.set_screen_data('previous-screen',old_screen_name);
112 if ( refresh || screen !== this.current_screen){
113 if(this.current_screen){
114 this.current_screen.close();
115 this.current_screen.hide();
117 this.current_screen = screen;
118 this.current_screen.show();
121 get_current_screen: function(){
122 return this.pos.get('selectedOrder').get_screen_data('screen') || this.default_screen;
125 var previous = this.pos.get('selectedOrder').get_screen_data('previous-screen');
127 this.set_current_screen(previous);
130 get_current_screen_param: function(param){
131 var params = this.pos.get('selectedOrder').get_screen_data('params');
132 return params ? params[param] : undefined;
134 set_default_screen: function(){
135 this.set_current_screen(this.default_screen);
139 module.ScreenWidget = module.PosBaseWidget.extend({
144 init: function(parent,options){
145 this._super(parent,options);
149 help_button_action: function(){
150 this.pos_widget.screen_selector.show_popup('help');
153 barcode_product_screen: 'products', //if defined, this screen will be loaded when a product is scanned
155 hotkeys_handlers: {},
157 // what happens when a product is scanned :
158 // it will add the product to the order and go to barcode_product_screen.
159 barcode_product_action: function(code){
161 if(self.pos.scan_product(code)){
162 if(self.barcode_product_screen){
163 self.pos_widget.screen_selector.set_current_screen(self.barcode_product_screen);
166 self.pos_widget.screen_selector.show_popup('error-barcode',code.code);
170 // what happens when a cashier id barcode is scanned.
171 // the default behavior is the following :
172 // - if there's a user with a matching ean, put it as the active 'cashier', go to cashier mode, and return true
173 // - else : do nothing and return false. You probably want to extend this to show and appropriate error popup...
174 barcode_cashier_action: function(code){
175 var users = this.pos.users;
176 for(var i = 0, len = users.length; i < len; i++){
177 if(users[i].ean13 === code.code){
178 this.pos.cashier = users[i];
179 this.pos_widget.username.refresh();
183 this.pos_widget.screen_selector.show_popup('error-barcode',code.code);
187 // what happens when a client id barcode is scanned.
188 // the default behavior is the following :
189 // - if there's a user with a matching ean, put it as the active 'client' and return true
190 // - else : return false.
191 barcode_client_action: function(code){
192 var partner = this.pos.db.get_partner_by_ean13(code.code);
194 this.pos.get('selectedOrder').set_client(partner);
195 this.pos_widget.username.refresh();
198 this.pos_widget.screen_selector.show_popup('error-barcode',code.code);
202 // what happens when a discount barcode is scanned : the default behavior
203 // is to set the discount on the last order.
204 barcode_discount_action: function(code){
205 var last_orderline = this.pos.get('selectedOrder').getLastOrderline();
207 last_orderline.set_discount(code.value)
210 // What happens when an invalid barcode is scanned : shows an error popup.
211 barcode_error_action: function(code){
212 this.pos_widget.screen_selector.show_popup('error-barcode',code.code);
215 // this method shows the screen and sets up all the widget related to this screen. Extend this method
216 // if you want to alter the behavior of the screen.
222 this.$el.removeClass('oe_hidden');
227 this.pos_widget.set_numpad_visible(this.show_numpad);
228 this.pos_widget.set_leftpane_visible(this.show_leftpane);
230 this.pos_widget.username.set_user_mode(this.pos_widget.screen_selector.get_user_mode());
232 this.pos.barcode_reader.set_action_callback({
233 'cashier': self.barcode_cashier_action ? function(code){ self.barcode_cashier_action(code); } : undefined ,
234 'product': self.barcode_product_action ? function(code){ self.barcode_product_action(code); } : undefined ,
235 'client' : self.barcode_client_action ? function(code){ self.barcode_client_action(code); } : undefined ,
236 'discount': self.barcode_discount_action ? function(code){ self.barcode_discount_action(code); } : undefined,
237 'error' : self.barcode_error_action ? function(code){ self.barcode_error_action(code); } : undefined,
241 // this method is called when the screen is closed to make place for a new screen. this is a good place
242 // to put your cleanup stuff as it is guaranteed that for each show() there is one and only one close()
244 if(this.pos.barcode_reader){
245 this.pos.barcode_reader.reset_action_callbacks();
249 // this methods hides the screen. It's not a good place to put your cleanup stuff as it is called on the
250 // POS initialization.
254 this.$el.addClass('oe_hidden');
258 // we need this because some screens re-render themselves when they are hidden
259 // (due to some events, or magic, or both...) we must make sure they remain hidden.
260 // the good solution would probably be to make them not re-render themselves when they
262 renderElement: function(){
266 this.$el.addClass('oe_hidden');
272 module.PopUpWidget = module.PosBaseWidget.extend({
275 this.$el.removeClass('oe_hidden');
278 /* called before hide, when a popup is closed */
281 /* hides the popup. keep in mind that this is called in the initialization pass of the
282 * pos instantiation, so you don't want to do anything fancy in here */
285 this.$el.addClass('oe_hidden');
290 module.FullscreenPopup = module.PopUpWidget.extend({
291 template:'FullscreenPopupWidget',
295 this.renderElement();
296 this.$('.button.fullscreen').off('click').click(function(){
297 window.document.body.webkitRequestFullscreen();
298 self.pos_widget.screen_selector.close_popup();
300 this.$('.button.cancel').off('click').click(function(){
301 self.pos_widget.screen_selector.close_popup();
304 ismobile: function(){
305 return typeof window.orientation !== 'undefined';
310 module.ErrorPopupWidget = module.PopUpWidget.extend({
311 template:'ErrorPopupWidget',
312 show: function(options){
313 options = options || {};
317 $('body').append('<audio src="/point_of_sale/static/src/sounds/error.wav" autoplay="true"></audio>');
319 this.message = options.message || _t('Error');
320 this.comment = options.comment || '';
322 this.renderElement();
324 this.pos.barcode_reader.save_callbacks();
325 this.pos.barcode_reader.reset_action_callbacks();
327 this.$('.footer .button').click(function(){
328 self.pos_widget.screen_selector.close_popup();
329 if ( options.confirm ) {
330 options.confirm.call(self);
336 this.pos.barcode_reader.restore_callbacks();
340 module.ErrorTracebackPopupWidget = module.ErrorPopupWidget.extend({
341 template:'ErrorTracebackPopupWidget',
344 module.ErrorBarcodePopupWidget = module.ErrorPopupWidget.extend({
345 template:'ErrorBarcodePopupWidget',
346 show: function(barcode){
347 this.barcode = barcode;
352 module.ConfirmPopupWidget = module.PopUpWidget.extend({
353 template: 'ConfirmPopupWidget',
354 show: function(options){
358 this.message = options.message || '';
359 this.comment = options.comment || '';
360 this.renderElement();
362 this.$('.button.cancel').click(function(){
363 self.pos_widget.screen_selector.close_popup();
364 if( options.cancel ){
365 options.cancel.call(self);
369 this.$('.button.confirm').click(function(){
370 self.pos_widget.screen_selector.close_popup();
371 if( options.confirm ){
372 options.confirm.call(self);
378 module.ErrorInvoiceTransferPopupWidget = module.ErrorPopupWidget.extend({
379 template: 'ErrorInvoiceTransferPopupWidget',
382 module.UnsentOrdersPopupWidget = module.PopUpWidget.extend({
383 template: 'UnsentOrdersPopupWidget',
384 show: function(options){
386 this._super(options);
387 this.renderElement();
388 this.$('.button.confirm').click(function(){
389 self.pos_widget.screen_selector.close_popup();
394 module.ScaleScreenWidget = module.ScreenWidget.extend({
395 template:'ScaleScreenWidget',
397 next_screen: 'products',
398 previous_screen: 'products',
400 show_leftpane: false,
405 var queue = this.pos.proxy_queue;
408 this.renderElement();
410 this.hotkey_handler = function(event){
411 if(event.which === 13){
412 self.order_product();
413 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
414 }else if(event.which === 27){
415 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
419 $('body').on('keyup',this.hotkey_handler);
421 this.$('.back').click(function(){
422 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
425 this.$('.next,.buy-product').click(function(){
426 self.order_product();
427 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
430 queue.schedule(function(){
431 return self.pos.proxy.scale_read().then(function(weight){
432 self.set_weight(weight.weight);
434 },{duration:50, repeat: true});
437 get_product: function(){
438 var ss = this.pos_widget.screen_selector;
440 return ss.get_current_screen_param('product');
445 order_product: function(){
446 this.pos.get('selectedOrder').addProduct(this.get_product(),{ quantity: this.weight });
448 get_product_name: function(){
449 var product = this.get_product();
450 return (product ? product.display_name : undefined) || 'Unnamed Product';
452 get_product_price: function(){
453 var product = this.get_product();
454 return (product ? product.price : 0) || 0;
456 set_weight: function(weight){
457 this.weight = weight;
458 this.$('.weight').text(this.get_product_weight_string());
459 this.$('.computed-price').text(this.get_computed_price_string());
461 get_product_weight_string: function(){
462 var product = this.get_product();
463 var defaultstr = (this.weight || 0).toFixed(3) + ' Kg';
464 if(!product || !this.pos){
467 var unit_id = product.uom_id;
471 var unit = this.pos.units_by_id[unit_id[0]];
472 var weight = round_pr(this.weight || 0, unit.rounding);
473 var weightstr = weight.toFixed(Math.ceil(Math.log(1.0/unit.rounding) / Math.log(10) ));
477 get_computed_price_string: function(){
478 return this.format_currency(this.get_product_price() * this.weight);
483 $('body').off('keyup',this.hotkey_handler);
485 this.pos.proxy_queue.clear();
489 module.ProductScreenWidget = module.ScreenWidget.extend({
490 template:'ProductScreenWidget',
495 start: function(){ //FIXME this should work as renderElement... but then the categories aren't properly set. explore why
498 this.product_list_widget = new module.ProductListWidget(this,{
499 click_product_action: function(product){
500 if(product.to_weight && self.pos.config.iface_electronic_scale){
501 self.pos_widget.screen_selector.set_current_screen('scale',{product: product});
503 self.pos.get('selectedOrder').addProduct(product);
506 product_list: this.pos.db.get_product_by_category(0)
508 this.product_list_widget.replace(this.$('.placeholder-ProductListWidget'));
510 this.product_categories_widget = new module.ProductCategoriesWidget(this,{
511 product_list_widget: this.product_list_widget,
513 this.product_categories_widget.replace(this.$('.placeholder-ProductCategoriesWidget'));
520 this.product_categories_widget.reset_category();
522 this.pos_widget.order_widget.set_editable(true);
528 this.pos_widget.order_widget.set_editable(false);
530 if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
531 this.pos_widget.onscreen_keyboard.hide();
536 module.ClientListScreenWidget = module.ScreenWidget.extend({
537 template: 'ClientListScreenWidget',
539 init: function(parent, options){
540 this._super(parent, options);
541 this.partner_cache = new module.DomCache();
544 show_leftpane: false,
552 this.renderElement();
553 this.details_visible = false;
554 this.old_client = this.pos.get('selectedOrder').get('client');
555 this.new_client = this.old_client;
557 this.$('.back').click(function(){
558 self.pos_widget.screen_selector.back();
561 this.$('.next').click(function(){
563 self.pos_widget.screen_selector.back();
566 this.$('.new-customer').click(function(){
567 self.display_client_details('edit',{
568 'country_id': self.pos.company.country_id,
572 var partners = this.pos.db.get_partners_sorted(1000);
573 this.render_list(partners);
575 this.reload_partners();
577 if( this.old_client ){
578 this.display_client_details('show',this.old_client,0);
581 this.$('.client-list-contents').delegate('.client-line','click',function(event){
582 self.line_select(event,$(this),parseInt($(this).data('id')));
585 var search_timeout = null;
587 if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
588 this.pos_widget.onscreen_keyboard.connect(this.$('.searchbox input'));
591 this.$('.searchbox input').on('keyup',function(event){
592 clearTimeout(search_timeout);
594 var query = this.value;
596 search_timeout = setTimeout(function(){
597 self.perform_search(query,event.which === 13);
601 this.$('.searchbox .search-clear').click(function(){
605 barcode_client_action: function(code){
606 if (this.editing_client) {
607 this.$('.detail.barcode').val(code.code);
608 } else if (this.pos.db.get_partner_by_ean13(code.code)) {
609 this.display_client_details('show',this.pos.db.get_partner_by_ean13(code.code));
612 perform_search: function(query, associate_result){
614 var customers = this.pos.db.search_partner(query);
615 this.display_client_details('hide');
616 if ( associate_result && customers.length === 1){
617 this.new_client = customers[0];
619 this.pos_widget.screen_selector.back();
621 this.render_list(customers);
623 var customers = this.pos.db.get_partners_sorted();
624 this.render_list(customers);
627 clear_search: function(){
628 var customers = this.pos.db.get_partners_sorted(1000);
629 this.render_list(customers);
630 this.$('.searchbox input')[0].value = '';
631 this.$('.searchbox input').focus();
633 render_list: function(partners){
634 var contents = this.$el[0].querySelector('.client-list-contents');
635 contents.innerHTML = "";
636 for(var i = 0, len = Math.min(partners.length,1000); i < len; i++){
637 var partner = partners[i];
638 var clientline = this.partner_cache.get_node(partner.id);
640 var clientline_html = QWeb.render('ClientLine',{widget: this, partner:partners[i]});
641 var clientline = document.createElement('tbody');
642 clientline.innerHTML = clientline_html;
643 clientline = clientline.childNodes[1];
644 this.partner_cache.cache_node(partner.id,clientline);
646 if( partners === this.new_client ){
647 clientline.classList.add('highlight');
649 clientline.classList.remove('highlight');
651 contents.appendChild(clientline);
654 save_changes: function(){
655 if( this.has_client_changed() ){
656 this.pos.get('selectedOrder').set_client(this.new_client);
659 has_client_changed: function(){
660 if( this.old_client && this.new_client ){
661 return this.old_client.id !== this.new_client.id;
663 return !!this.old_client !== !!this.new_client;
666 toggle_save_button: function(){
667 var $button = this.$('.button.next');
668 if (this.editing_client) {
669 $button.addClass('oe_hidden');
671 } else if( this.new_client ){
672 if( !this.old_client){
673 $button.text(_t('Set Customer'));
675 $button.text(_t('Change Customer'));
678 $button.text(_t('Deselect Customer'));
680 $button.toggleClass('oe_hidden',!this.has_client_changed());
682 line_select: function(event,$line,id){
683 var partner = this.pos.db.get_partner_by_id(id);
684 this.$('.client-list .lowlight').removeClass('lowlight');
685 if ( $line.hasClass('highlight') ){
686 $line.removeClass('highlight');
687 $line.addClass('lowlight');
688 this.display_client_details('hide',partner);
689 this.new_client = null;
690 this.toggle_save_button();
692 this.$('.client-list .highlight').removeClass('highlight');
693 $line.addClass('highlight');
694 var y = event.pageY - $line.parent().offset().top
695 this.display_client_details('show',partner,y);
696 this.new_client = partner;
697 this.toggle_save_button();
700 partner_icon_url: function(id){
701 return '/web/binary/image?model=res.partner&id='+id+'&field=image_small';
704 // ui handle for the 'edit selected customer' action
705 edit_client_details: function(partner) {
706 this.display_client_details('edit',partner);
709 // ui handle for the 'cancel customer edit changes' action
710 undo_client_details: function(partner) {
712 this.display_client_details('hide');
714 this.display_client_details('show',partner);
718 // what happens when we save the changes on the client edit form -> we fetch the fields, sanitize them,
719 // send them to the backend for update, and call saved_client_details() when the server tells us the
720 // save was successfull.
721 save_client_details: function(partner) {
725 this.$('.client-details-contents .detail').each(function(idx,el){
726 fields[el.name] = el.value;
730 this.pos_widget.screen_selector.show_popup('error',{
731 message: _t('A Customer Name Is Required'),
736 if (this.uploaded_picture) {
737 fields.image = this.uploaded_picture;
740 fields.id = partner.id || false;
741 fields.country_id = fields.country_id || false;
742 fields.ean13 = fields.ean13 ? this.pos.barcode_reader.sanitize_ean(fields.ean13) : false;
744 new instance.web.Model('res.partner').call('create_from_ui',[fields]).then(function(partner_id){
745 self.saved_client_details(partner_id);
746 },function(err,event){
747 event.preventDefault();
748 self.pos_widget.screen_selector.show_popup('error',{
749 'message':_t('Error: Could not Save Changes'),
750 'comment':_t('Your Internet connection is probably down.'),
755 // what happens when we've just pushed modifications for a partner of id partner_id
756 saved_client_details: function(partner_id){
758 this.reload_partners().then(function(){
759 var partner = self.pos.db.get_partner_by_id(partner_id);
761 self.new_client = partner;
762 self.toggle_save_button();
763 self.display_client_details('show',partner);
765 // should never happen, because create_from_ui must return the id of the partner it
766 // has created, and reload_partner() must have loaded the newly created partner.
767 self.display_client_details('hide');
772 // resizes an image, keeping the aspect ratio intact,
773 // the resize is useful to avoid sending 12Mpixels jpegs
774 // over a wireless connection.
775 resize_image_to_dataurl: function(img, maxwidth, maxheight, callback){
776 img.onload = function(){
777 var png = new Image();
778 var canvas = document.createElement('canvas');
779 var ctx = canvas.getContext('2d');
782 if (img.width > maxwidth) {
783 ratio = maxwidth / img.width;
785 if (img.height * ratio > maxheight) {
786 ratio = maxheight / img.height;
788 var width = Math.floor(img.width * ratio);
789 var height = Math.floor(img.height * ratio);
791 canvas.width = width;
792 canvas.height = height;
793 ctx.drawImage(img,0,0,width,height);
795 var dataurl = canvas.toDataURL();
800 // Loads and resizes a File that contains an image.
801 // callback gets a dataurl in case of success.
802 load_image_file: function(file, callback){
804 if (!file.type.match(/image.*/)) {
805 this.pos_widget.screen_selector.show_popup('error',{
806 message:_t('Unsupported File Format'),
807 comment:_t('Only web-compatible Image formats such as .png or .jpeg are supported'),
812 var reader = new FileReader();
813 reader.onload = function(event){
814 var dataurl = event.target.result;
815 var img = new Image();
817 self.resize_image_to_dataurl(img,800,600,callback);
819 reader.onerror = function(){
820 self.pos_widget.screen_selector.show_popup('error',{
821 message:_t('Could Not Read Image'),
822 comment:_t('The provided file could not be read due to an unknown error'),
825 reader.readAsDataURL(file);
828 // This fetches partner changes on the server, and in case of changes,
829 // rerenders the affected views
830 reload_partners: function(){
832 return this.pos.load_new_partners().then(function(){
833 self.render_list(self.pos.db.get_partners_sorted(1000));
835 // update the currently assigned client if it has been changed in db.
836 var curr_client = self.pos.get_order().get_client();
838 self.pos.get_order().set_client(self.pos.db.get_partner_by_id(curr_client.id));
843 // Shows,hides or edit the customer details box :
844 // visibility: 'show', 'hide' or 'edit'
845 // partner: the partner object to show or edit
846 // clickpos: the height of the click on the list (in pixel), used
847 // to maintain consistent scroll.
848 display_client_details: function(visibility,partner,clickpos){
850 var contents = this.$('.client-details-contents');
851 var parent = this.$('.client-list').parent();
852 var scroll = parent.scrollTop();
853 var height = contents.height();
855 contents.off('click','.button.edit');
856 contents.off('click','.button.save');
857 contents.off('click','.button.undo');
858 contents.on('click','.button.edit',function(){ self.edit_client_details(partner); });
859 contents.on('click','.button.save',function(){ self.save_client_details(partner); });
860 contents.on('click','.button.undo',function(){ self.undo_client_details(partner); });
861 this.editing_client = false;
862 this.uploaded_picture = null;
864 if(visibility === 'show'){
866 contents.append($(QWeb.render('ClientDetails',{widget:this,partner:partner})));
868 var new_height = contents.height();
870 if(!this.details_visible){
871 if(clickpos < scroll + new_height + 20 ){
872 parent.scrollTop( clickpos - 20 );
874 parent.scrollTop(parent.scrollTop() + new_height);
877 parent.scrollTop(parent.scrollTop() - height + new_height);
880 this.details_visible = true;
881 this.toggle_save_button();
882 } else if (visibility === 'edit') {
883 this.editing_client = true;
885 contents.append($(QWeb.render('ClientDetailsEdit',{widget:this,partner:partner})));
886 this.toggle_save_button();
888 contents.find('.image-uploader').on('change',function(){
889 self.load_image_file(event.target.files[0],function(res){
891 contents.find('.client-picture img, .client-picture .fa').remove();
892 contents.find('.client-picture').append("<img src='"+res+"'>");
893 contents.find('.detail.picture').remove();
894 self.uploaded_picture = res;
898 } else if (visibility === 'hide') {
900 if( height > scroll ){
901 contents.css({height:height+'px'});
902 contents.animate({height:0},400,function(){
903 contents.css({height:''});
906 parent.scrollTop( parent.scrollTop() - height);
908 this.details_visible = false;
909 this.toggle_save_button();
917 module.ReceiptScreenWidget = module.ScreenWidget.extend({
918 template: 'ReceiptScreenWidget',
920 show_leftpane: false,
928 if (!this.pos.get('selectedOrder')._printed) {
932 // The problem is that in chrome the print() is asynchronous and doesn't
933 // execute until all rpc are finished. So it conflicts with the rpc used
934 // to send the orders to the backend, and the user is able to go to the next
935 // screen before the printing dialog is opened. The problem is that what's
936 // printed is whatever is in the page when the dialog is opened and not when it's called,
937 // and so you end up printing the product list instead of the receipt...
939 // Fixing this would need a re-architecturing
940 // of the code to postpone sending of orders after printing.
942 // But since the print dialog also blocks the other asynchronous calls, the
943 // button enabling in the setTimeout() is blocked until the printing dialog is
944 // closed. But the timeout has to be big enough or else it doesn't work
945 // 2 seconds is the same as the default timeout for sending orders and so the dialog
946 // should have appeared before the timeout... so yeah that's not ultra reliable.
948 this.lock_screen(true);
949 setTimeout(function(){
950 self.lock_screen(false);
953 lock_screen: function(locked) {
954 this._locked = locked;
956 this.$('.next').removeClass('highlight');
958 this.$('.next').addClass('highlight');
962 this.pos.get('selectedOrder')._printed = true;
965 finish_order: function() {
967 this.pos.get_order().finalize();
970 renderElement: function() {
973 this.$('.next').click(function(){
976 this.$('.button.print').click(function(){
980 refresh: function() {
981 var order = this.pos.get_order();
982 this.$('.pos-receipt-container').html(QWeb.render('PosTicket',{
985 orderlines: order.get('orderLines').models,
986 paymentlines: order.get('paymentLines').models,
991 module.PaymentScreenWidget = module.ScreenWidget.extend({
992 template: 'PaymentScreenWidget',
993 back_screen: 'product',
994 next_screen: 'receipt',
995 show_leftpane: false,
997 init: function(parent, options) {
999 this._super(parent, options);
1001 this.pos.bind('change:selectedOrder',function(){
1002 this.renderElement();
1003 this.watch_order_changes();
1005 this.watch_order_changes();
1007 this.inputbuffer = "";
1008 this.firstinput = true;
1009 this.keyboard_handler = function(event){
1011 if ( event.keyCode === 13 ) { // Enter
1012 self.validate_order();
1013 } else if ( event.keyCode === 190 ) { // Dot
1015 } else if ( event.keyCode === 46 ) { // Delete
1017 } else if ( event.keyCode === 8 ) { // Backspace
1019 event.preventDefault(); // Prevents history back nav
1020 } else if ( event.keyCode >= 48 && event.keyCode <= 57 ){ // Numbers
1021 key = '' + (event.keyCode - 48);
1022 } else if ( event.keyCode >= 96 && event.keyCode <= 105 ){ // Numpad Numbers
1023 key = '' + (event.keyCode - 96);
1024 } else if ( event.keyCode === 189 || event.keyCode === 109 ) { // Minus
1026 } else if ( event.keyCode === 107 ) { // Plus
1030 self.payment_input(key);
1034 // resets the current input buffer
1035 reset_input: function(){
1036 var line = this.pos.get_order().selected_paymentline;
1037 this.firstinput = true;
1039 this.inputbuffer = this.format_currency_no_symbol(line.get_amount());
1041 this.inputbuffer = "";
1044 // handle both keyboard and numpad input. Accepts
1045 // a string that represents the key pressed.
1046 payment_input: function(input) {
1047 var oldbuf = this.inputbuffer.slice(0);
1049 if (input === '.') {
1050 if (this.firstinput) {
1051 this.inputbuffer = "0.";
1052 }else if (!this.inputbuffer.length || this.inputbuffer === '-') {
1053 this.inputbuffer += "0.";
1054 } else if (this.inputbuffer.indexOf('.') < 0){
1055 this.inputbuffer = this.inputbuffer + '.';
1057 } else if (input === 'CLEAR') {
1058 this.inputbuffer = "";
1059 } else if (input === 'BACKSPACE') {
1060 this.inputbuffer = this.inputbuffer.substring(0,this.inputbuffer.length - 1);
1061 } else if (input === '+') {
1062 if ( this.inputbuffer[0] === '-' ) {
1063 this.inputbuffer = this.inputbuffer.substring(1,this.inputbuffer.length);
1065 } else if (input === '-') {
1066 if ( this.inputbuffer[0] === '-' ) {
1067 this.inputbuffer = this.inputbuffer.substring(1,this.inputbuffer.length);
1069 this.inputbuffer = '-' + this.inputbuffer;
1071 } else if (input[0] === '+' && !isNaN(parseFloat(input))) {
1072 this.inputbuffer = '' + ((parseFloat(this.inputbuffer) || 0) + parseFloat(input));
1073 } else if (!isNaN(parseInt(input))) {
1074 if (this.firstinput) {
1075 this.inputbuffer = '' + input;
1077 this.inputbuffer += input;
1081 this.firstinput = false;
1083 if (this.inputbuffer !== oldbuf) {
1084 var order = this.pos.get_order();
1085 if (order.selected_paymentline) {
1086 order.selected_paymentline.set_amount(parseFloat(this.inputbuffer));
1087 this.order_changes();
1088 this.render_paymentlines();
1089 this.$('.paymentline.selected .edit').text(this.inputbuffer);
1093 click_numpad: function(button) {
1094 this.payment_input(button.data('action'));
1096 render_numpad: function() {
1098 var numpad = $(QWeb.render('PaymentScreen-Numpad', { widget:this }));
1099 numpad.on('click','button',function(){
1100 self.click_numpad($(this));
1104 click_delete_paymentline: function(cid){
1105 var lines = this.pos.get_order().get('paymentLines').models;
1106 for ( var i = 0; i < lines.length; i++ ) {
1107 if (lines[i].cid === cid) {
1108 this.pos.get_order().removePaymentline(lines[i]);
1110 this.render_paymentlines();
1115 click_paymentline: function(cid){
1116 var lines = this.pos.get_order().get('paymentLines').models;
1117 for ( var i = 0; i < lines.length; i++ ) {
1118 if (lines[i].cid === cid) {
1119 this.pos.get_order().selectPaymentline(lines[i]);
1121 this.render_paymentlines();
1126 render_paymentlines: function() {
1128 var order = this.pos.get_order();
1129 var lines = order.get('paymentLines').models;
1131 this.$('.paymentlines-container').empty();
1132 var lines = $(QWeb.render('PaymentScreen-Paymentlines', {
1135 paymentlines: lines,
1138 lines.on('click','.delete-button',function(){
1139 self.click_delete_paymentline($(this).data('cid'));
1142 lines.on('click','.paymentline',function(){
1143 self.click_paymentline($(this).data('cid'));
1146 lines.appendTo(this.$('.paymentlines-container'));
1148 click_paymentmethods: function(id) {
1149 var cashregister = null;
1150 for ( var i = 0; i < this.pos.cashregisters.length; i++ ) {
1151 if ( this.pos.cashregisters[i].journal_id[0] === id ){
1152 cashregister = this.pos.cashregisters[i];
1156 this.pos.get_order().addPaymentline( cashregister );
1158 this.render_paymentlines();
1160 render_paymentmethods: function() {
1162 var methods = $(QWeb.render('PaymentScreen-Paymentmethods', { widget:this }));
1163 methods.on('click','.paymentmethod',function(){
1164 self.click_paymentmethods($(this).data('id'));
1168 click_invoice: function(){
1169 var order = this.pos.get_order();
1170 order.set_to_invoice(!order.is_to_invoice());
1171 if (order.is_to_invoice()) {
1172 this.$('.js_invoice').addClass('highlight');
1174 this.$('.js_invoice').removeClass('highlight');
1177 renderElement: function() {
1181 var numpad = this.render_numpad();
1182 numpad.appendTo(this.$('.payment-numpad'));
1184 var methods = this.render_paymentmethods();
1185 methods.appendTo(this.$('.paymentmethods-container'));
1187 this.render_paymentlines();
1189 this.$('.back').click(function(){
1190 self.pos_widget.screen_selector.back();
1193 this.$('.next').click(function(){
1194 self.validate_order();
1197 this.$('.js_invoice').click(function(){
1198 self.click_invoice();
1203 this.pos.get_order().clean_empty_paymentlines();
1205 this.render_paymentlines();
1206 this.order_changes();
1207 window.document.body.addEventListener('keydown',this.keyboard_handler);
1211 window.document.body.removeEventListener('keydown',this.keyboard_handler);
1214 // sets up listeners to watch for order changes
1215 watch_order_changes: function() {
1217 var order = this.pos.get_order();
1219 this.old_order.unbind(null,null,this);
1221 order.bind('all',function(){
1222 self.order_changes();
1224 this.old_order = order;
1226 // called when the order is changed, used to show if
1227 // the order is paid or not
1228 order_changes: function(){
1230 var order = this.pos.get_order();
1231 if (order.isPaid()) {
1232 self.$('.next').addClass('highlight');
1234 self.$('.next').removeClass('highlight');
1237 // Check if the order is paid, then sends it to the backend,
1238 // and complete the sale process
1239 validate_order: function() {
1242 var order = this.pos.get_order();
1244 if(order.get('orderLines').models.length === 0){
1245 this.pos_widget.screen_selector.show_popup('error',{
1246 'message': _t('Empty Order'),
1247 'comment': _t('There must be at least one product in your order before it can be validated'),
1252 if (!order.isPaid() || this.invoicing) {
1256 // The exact amount must be paid if there is no cash payment method defined.
1257 if (Math.abs(order.getTotalTaxIncluded() - order.getPaidTotal()) > 0.00001) {
1259 for (var i = 0; i < this.pos.cashregisters.length; i++) {
1260 cash = cash || (this.pos.cashregisters[i].journal.type === 'cash');
1263 this.pos_widget.screen_selector.show_popup('error',{
1264 message: _t('Cannot return change without a cash payment method'),
1265 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'),
1271 if (order.isPaidWithCash() && this.pos.config.iface_cashdrawer) {
1273 this.pos.proxy.open_cashbox();
1276 if (order.is_to_invoice()) {
1277 var invoiced = this.pos.push_and_invoice_order(order);
1278 this.invoicing = true;
1280 invoiced.fail(function(error){
1281 self.invoicing = false;
1282 if (error === 'error-no-client') {
1283 self.pos_widget.screen_selector.show_popup('confirm',{
1284 message: _t('Please select the Customer'),
1285 comment: _t('You need to select the customer before you can invoice an order.'),
1286 confirm: function(){
1287 self.pos_widget.screen_selector.set_current_screen('clientlist');
1291 self.pos_widget.screen_selector.show_popup('error',{
1292 message: _t('The order could not be sent'),
1293 comment: _t('Check your internet connection and try again.'),
1298 invoiced.done(function(){
1299 self.invoicing = false;
1303 this.pos.push_order(order)
1304 if (this.pos.config.iface_print_via_proxy) {
1305 var receipt = currentOrder.export_for_printing();
1306 this.pos.proxy.print_receipt(QWeb.render('XmlReceipt',{
1307 receipt: receipt, widget: self,
1309 order.finalize(); //finish order and go back to scan screen
1311 this.pos_widget.screen_selector.set_current_screen(this.next_screen);