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);
214 // shows an action bar on the screen. The actionbar is automatically shown when you add a button
215 // with add_action_button()
216 show_action_bar: function(){
217 this.pos_widget.action_bar.show();
220 // hides the action bar. The actionbar is automatically hidden when it is empty
221 hide_action_bar: function(){
222 this.pos_widget.action_bar.hide();
225 // adds a new button to the action bar. The button definition takes three parameters, all optional :
226 // - label: the text below the button
227 // - icon: a small icon that will be shown
228 // - click: a callback that will be executed when the button is clicked.
229 // the method returns a reference to the button widget, and automatically show the actionbar.
230 add_action_button: function(button_def){
231 this.show_action_bar();
232 return this.pos_widget.action_bar.add_new_button(button_def);
235 // this method shows the screen and sets up all the widget related to this screen. Extend this method
236 // if you want to alter the behavior of the screen.
242 this.$el.removeClass('oe_hidden');
245 if(this.pos_widget.action_bar.get_button_count() > 0){
246 this.show_action_bar();
248 this.hide_action_bar();
253 this.pos_widget.set_numpad_visible(this.show_numpad);
254 this.pos_widget.set_leftpane_visible(this.show_leftpane);
256 this.pos_widget.username.set_user_mode(this.pos_widget.screen_selector.get_user_mode());
258 this.pos.barcode_reader.set_action_callback({
259 'cashier': self.barcode_cashier_action ? function(code){ self.barcode_cashier_action(code); } : undefined ,
260 'product': self.barcode_product_action ? function(code){ self.barcode_product_action(code); } : undefined ,
261 'client' : self.barcode_client_action ? function(code){ self.barcode_client_action(code); } : undefined ,
262 'discount': self.barcode_discount_action ? function(code){ self.barcode_discount_action(code); } : undefined,
263 'error' : self.barcode_error_action ? function(code){ self.barcode_error_action(code); } : undefined,
267 // this method is called when the screen is closed to make place for a new screen. this is a good place
268 // to put your cleanup stuff as it is guaranteed that for each show() there is one and only one close()
270 if(this.pos.barcode_reader){
271 this.pos.barcode_reader.reset_action_callbacks();
273 this.pos_widget.action_bar.destroy_buttons();
276 // this methods hides the screen. It's not a good place to put your cleanup stuff as it is called on the
277 // POS initialization.
281 this.$el.addClass('oe_hidden');
285 // we need this because some screens re-render themselves when they are hidden
286 // (due to some events, or magic, or both...) we must make sure they remain hidden.
287 // the good solution would probably be to make them not re-render themselves when they
289 renderElement: function(){
293 this.$el.addClass('oe_hidden');
299 module.PopUpWidget = module.PosBaseWidget.extend({
302 this.$el.removeClass('oe_hidden');
305 /* called before hide, when a popup is closed */
308 /* hides the popup. keep in mind that this is called in the initialization pass of the
309 * pos instantiation, so you don't want to do anything fancy in here */
312 this.$el.addClass('oe_hidden');
317 module.ErrorPopupWidget = module.PopUpWidget.extend({
318 template:'ErrorPopupWidget',
319 show: function(options){
320 options = options || {};
324 $('body').append('<audio src="/point_of_sale/static/src/sounds/error.wav" autoplay="true"></audio>');
326 this.message = options.message || _t('Error');
327 this.comment = options.comment || '';
329 this.renderElement();
331 this.pos.barcode_reader.save_callbacks();
332 this.pos.barcode_reader.reset_action_callbacks();
334 this.$('.footer .button').click(function(){
335 self.pos_widget.screen_selector.close_popup();
336 if ( options.confirm ) {
337 options.confirm.call(self);
343 this.pos.barcode_reader.restore_callbacks();
347 module.ErrorTracebackPopupWidget = module.ErrorPopupWidget.extend({
348 template:'ErrorTracebackPopupWidget',
351 module.ErrorBarcodePopupWidget = module.ErrorPopupWidget.extend({
352 template:'ErrorBarcodePopupWidget',
353 show: function(barcode){
354 this.barcode = barcode;
359 module.ConfirmPopupWidget = module.PopUpWidget.extend({
360 template: 'ConfirmPopupWidget',
361 show: function(options){
365 this.message = options.message || '';
366 this.comment = options.comment || '';
367 this.renderElement();
369 this.$('.button.cancel').click(function(){
370 self.pos_widget.screen_selector.close_popup();
371 if( options.cancel ){
372 options.cancel.call(self);
376 this.$('.button.confirm').click(function(){
377 self.pos_widget.screen_selector.close_popup();
378 if( options.confirm ){
379 options.confirm.call(self);
385 module.ErrorNoClientPopupWidget = module.ErrorPopupWidget.extend({
386 template: 'ErrorNoClientPopupWidget',
389 module.ErrorInvoiceTransferPopupWidget = module.ErrorPopupWidget.extend({
390 template: 'ErrorInvoiceTransferPopupWidget',
393 module.UnsentOrdersPopupWidget = module.PopUpWidget.extend({
394 template: 'UnsentOrdersPopupWidget',
395 show: function(options){
397 this._super(options);
398 this.renderElement();
399 this.$('.button.confirm').click(function(){
400 self.pos_widget.screen_selector.close_popup();
405 module.ScaleScreenWidget = module.ScreenWidget.extend({
406 template:'ScaleScreenWidget',
408 next_screen: 'products',
409 previous_screen: 'products',
411 show_leftpane: false,
416 var queue = this.pos.proxy_queue;
419 this.renderElement();
421 this.hotkey_handler = function(event){
422 if(event.which === 13){
423 self.order_product();
424 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
425 }else if(event.which === 27){
426 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
430 $('body').on('keyup',this.hotkey_handler);
432 this.$('.back').click(function(){
433 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
436 this.$('.next,.buy-product').click(function(){
437 self.order_product();
438 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
441 queue.schedule(function(){
442 return self.pos.proxy.scale_read().then(function(weight){
443 self.set_weight(weight.weight);
445 },{duration:50, repeat: true});
448 get_product: function(){
449 var ss = this.pos_widget.screen_selector;
451 return ss.get_current_screen_param('product');
456 order_product: function(){
457 this.pos.get('selectedOrder').addProduct(this.get_product(),{ quantity: this.weight });
459 get_product_name: function(){
460 var product = this.get_product();
461 return (product ? product.display_name : undefined) || 'Unnamed Product';
463 get_product_price: function(){
464 var product = this.get_product();
465 return (product ? product.price : 0) || 0;
467 set_weight: function(weight){
468 this.weight = weight;
469 this.$('.weight').text(this.get_product_weight_string());
470 this.$('.computed-price').text(this.get_computed_price_string());
472 get_product_weight_string: function(){
473 var product = this.get_product();
474 var defaultstr = (this.weight || 0).toFixed(3) + ' Kg';
475 if(!product || !this.pos){
478 var unit_id = product.uos_id || product.uom_id;
482 var unit = this.pos.units_by_id[unit_id[0]];
483 var weight = round_pr(this.weight || 0, unit.rounding);
484 var weightstr = weight.toFixed(Math.ceil(Math.log(1.0/unit.rounding) / Math.log(10) ));
488 get_computed_price_string: function(){
489 return this.format_currency(this.get_product_price() * this.weight);
494 $('body').off('keyup',this.hotkey_handler);
496 this.pos.proxy_queue.clear();
500 module.ProductScreenWidget = module.ScreenWidget.extend({
501 template:'ProductScreenWidget',
506 start: function(){ //FIXME this should work as renderElement... but then the categories aren't properly set. explore why
509 this.product_list_widget = new module.ProductListWidget(this,{
510 click_product_action: function(product){
511 if(product.to_weight && self.pos.config.iface_electronic_scale){
512 self.pos_widget.screen_selector.set_current_screen('scale',{product: product});
514 self.pos.get('selectedOrder').addProduct(product);
517 product_list: this.pos.db.get_product_by_category(0)
519 this.product_list_widget.replace(this.$('.placeholder-ProductListWidget'));
521 this.product_categories_widget = new module.ProductCategoriesWidget(this,{
522 product_list_widget: this.product_list_widget,
524 this.product_categories_widget.replace(this.$('.placeholder-ProductCategoriesWidget'));
531 this.product_categories_widget.reset_category();
533 this.pos_widget.order_widget.set_editable(true);
539 this.pos_widget.order_widget.set_editable(false);
541 if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
542 this.pos_widget.onscreen_keyboard.hide();
547 module.ClientListScreenWidget = module.ScreenWidget.extend({
548 template: 'ClientListScreenWidget',
550 init: function(parent, options){
551 this._super(parent, options);
554 show_leftpane: false,
562 this.renderElement();
563 this.details_visible = false;
564 this.old_client = this.pos.get('selectedOrder').get('client');
565 this.new_client = this.old_client;
567 this.$('.back').click(function(){
568 self.pos_widget.screen_selector.back();
571 this.$('.next').click(function(){
573 self.pos_widget.screen_selector.back();
576 this.$('.new-customer').click(function(){
577 self.display_client_details('edit',{
578 'country_id': self.pos.company.country_id,
582 var partners = this.pos.db.get_partners_sorted(1000);
583 this.render_list(partners);
585 this.reload_partners();
587 if( this.old_client ){
588 this.display_client_details('show',this.old_client,0);
591 this.$('.client-list-contents').delegate('.client-line','click',function(event){
592 self.line_select(event,$(this),parseInt($(this).data('id')));
595 var search_timeout = null;
597 if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
598 this.pos_widget.onscreen_keyboard.connect(this.$('.searchbox input'));
601 this.$('.searchbox input').on('keyup',function(event){
602 clearTimeout(search_timeout);
604 var query = this.value;
606 search_timeout = setTimeout(function(){
607 self.perform_search(query,event.which === 13);
611 this.$('.searchbox .search-clear').click(function(){
615 barcode_client_action: function(code){
616 if (this.editing_client) {
617 this.$('.detail.barcode').val(code.code);
618 } else if (this.pos.db.get_partner_by_ean13(code.code)) {
619 this.display_client_details('show',this.pos.db.get_partner_by_ean13(code.code));
622 perform_search: function(query, associate_result){
624 var customers = this.pos.db.search_partner(query);
625 this.display_client_details('hide');
626 if ( associate_result && customers.length === 1){
627 this.new_client = customers[0];
629 this.pos_widget.screen_selector.back();
631 this.render_list(customers);
633 var customers = this.pos.db.get_partners_sorted();
634 this.render_list(customers);
637 clear_search: function(){
638 var customers = this.pos.db.get_partners_sorted(1000);
639 this.render_list(customers);
640 this.$('.searchbox input')[0].value = '';
641 this.$('.searchbox input').focus();
643 render_list: function(partners){
644 var contents = this.$el[0].querySelector('.client-list-contents');
645 contents.innerHTML = "";
646 for(var i = 0, len = Math.min(partners.length,1000); i < len; i++){
647 var partner = partners[i];
648 var clientline_html = QWeb.render('ClientLine',{widget: this, partner:partners[i]});
649 var clientline = document.createElement('tbody');
650 clientline.innerHTML = clientline_html;
651 clientline = clientline.childNodes[1];
653 if( partners === this.new_client ){
654 clientline.classList.add('highlight');
656 clientline.classList.remove('highlight');
659 contents.appendChild(clientline);
662 save_changes: function(){
663 if( this.has_client_changed() ){
664 this.pos.get('selectedOrder').set_client(this.new_client);
667 has_client_changed: function(){
668 if( this.old_client && this.new_client ){
669 return this.old_client.id !== this.new_client.id;
671 return !!this.old_client !== !!this.new_client;
674 toggle_save_button: function(){
675 var $button = this.$('.button.next');
676 if (this.editing_client) {
677 $button.addClass('oe_hidden');
679 } else if( this.new_client ){
680 if( !this.old_client){
681 $button.text(_t('Set Customer'));
683 $button.text(_t('Change Customer'));
686 $button.text(_t('Deselect Customer'));
688 $button.toggleClass('oe_hidden',!this.has_client_changed());
690 line_select: function(event,$line,id){
691 var partner = this.pos.db.get_partner_by_id(id);
692 this.$('.client-list .lowlight').removeClass('lowlight');
693 if ( $line.hasClass('highlight') ){
694 $line.removeClass('highlight');
695 $line.addClass('lowlight');
696 this.display_client_details('hide',partner);
697 this.new_client = null;
698 this.toggle_save_button();
700 this.$('.client-list .highlight').removeClass('highlight');
701 $line.addClass('highlight');
702 var y = event.pageY - $line.parent().offset().top
703 this.display_client_details('show',partner,y);
704 this.new_client = partner;
705 this.toggle_save_button();
708 partner_icon_url: function(id){
709 return '/web/binary/image?model=res.partner&id='+id+'&field=image_small';
712 // ui handle for the 'edit selected customer' action
713 edit_client_details: function(partner) {
714 this.display_client_details('edit',partner);
717 // ui handle for the 'cancel customer edit changes' action
718 undo_client_details: function(partner) {
720 this.display_client_details('hide');
722 this.display_client_details('show',partner);
726 // what happens when we save the changes on the client edit form -> we fetch the fields, sanitize them,
727 // send them to the backend for update, and call saved_client_details() when the server tells us the
728 // save was successfull.
729 save_client_details: function(partner) {
733 this.$('.client-details-contents .detail').each(function(idx,el){
734 fields[el.name] = el.value;
738 this.pos_widget.screen_selector.show_popup('error',{
739 message: _t('A Customer Name Is Required'),
744 if (this.uploaded_picture) {
745 fields.image = this.uploaded_picture;
748 fields.id = partner.id || false;
749 fields.country_id = fields.country_id || false;
750 fields.ean13 = fields.ean13 ? this.pos.barcode_reader.sanitize_ean(fields.ean13) : false;
752 new instance.web.Model('res.partner').call('create_from_ui',[fields]).then(function(partner_id){
753 self.saved_client_details(partner_id);
757 // what happens when we've just pushed modifications for a partner of id partner_id
758 saved_client_details: function(partner_id){
760 this.reload_partners().then(function(){
761 var partner = self.pos.db.get_partner_by_id(partner_id);
763 self.new_client = partner;
764 self.toggle_save_button();
765 self.display_client_details('show',partner);
767 // should never happen, because create_from_ui must return the id of the partner it
768 // has created, and reload_partner() must have loaded the newly created partner.
769 self.display_client_details('hide');
774 // resizes an image, keeping the aspect ratio intact,
775 // the resize is useful to avoid sending 12Mpixels jpegs
776 // over a wireless connection.
777 resize_image_to_dataurl: function(img, maxwidth, maxheight, callback){
778 img.onload = function(){
779 var png = new Image();
780 var canvas = document.createElement('canvas');
781 var ctx = canvas.getContext('2d');
784 if (img.width > maxwidth) {
785 ratio = maxwidth / img.width;
787 if (img.height * ratio > maxheight) {
788 ratio = maxheight / img.height;
790 var width = Math.floor(img.width * ratio);
791 var height = Math.floor(img.height * ratio);
793 canvas.width = width;
794 canvas.height = height;
795 ctx.drawImage(img,0,0,width,height);
797 var dataurl = canvas.toDataURL();
802 // Loads and resizes a File that contains an image.
803 // callback gets a dataurl in case of success.
804 load_image_file: function(file, callback){
806 if (!file.type.match(/image.*/)) {
807 this.pos_widget.screen_selector.show_popup('error',{
808 message:_t('Unsupported File Format'),
809 comment:_t('Only web-compatible Image formats such as .png or .jpeg are supported'),
814 var reader = new FileReader();
815 reader.onload = function(event){
816 var dataurl = event.target.result;
817 var img = new Image();
819 self.resize_image_to_dataurl(img,800,600,callback);
821 reader.onerror = function(){
822 self.pos_widget.screen_selector.show_popup('error',{
823 message:_t('Could Not Read Image'),
824 comment:_t('The provided file could not be read due to an unknown error'),
827 reader.readAsDataURL(file);
830 // This fetches partner changes on the server, and in case of changes,
831 // rerenders the affected views
832 reload_partners: function(){
834 return this.pos.load_new_partners().then(function(){
835 self.render_list(self.pos.db.get_partners_sorted(1000));
837 // update the currently assigned client if it has been changed in db.
838 var curr_client = self.pos.get_order().get_client();
840 self.pos.get_order().set_client(self.pos.db.get_partner_by_id(curr_client.id));
845 // Shows,hides or edit the customer details box :
846 // visibility: 'show', 'hide' or 'edit'
847 // partner: the partner object to show or edit
848 // clickpos: the height of the click on the list (in pixel), used
849 // to maintain consistent scroll.
850 display_client_details: function(visibility,partner,clickpos){
852 var contents = this.$('.client-details-contents');
853 var parent = this.$('.client-list').parent();
854 var scroll = parent.scrollTop();
855 var height = contents.height();
857 contents.off('click','.button.edit');
858 contents.off('click','.button.save');
859 contents.off('click','.button.undo');
860 contents.on('click','.button.edit',function(){ self.edit_client_details(partner); });
861 contents.on('click','.button.save',function(){ self.save_client_details(partner); });
862 contents.on('click','.button.undo',function(){ self.undo_client_details(partner); });
863 this.editing_client = false;
864 this.uploaded_picture = null;
866 if(visibility === 'show'){
868 contents.append($(QWeb.render('ClientDetails',{widget:this,partner:partner})));
870 var new_height = contents.height();
872 if(!this.details_visible){
873 if(clickpos < scroll + new_height + 20 ){
874 parent.scrollTop( clickpos - 20 );
876 parent.scrollTop(parent.scrollTop() + new_height);
879 parent.scrollTop(parent.scrollTop() - height + new_height);
882 this.details_visible = true;
883 this.toggle_save_button();
884 } else if (visibility === 'edit') {
885 this.editing_client = true;
887 contents.append($(QWeb.render('ClientDetailsEdit',{widget:this,partner:partner})));
888 this.toggle_save_button();
890 contents.find('.image-uploader').on('change',function(){
891 self.load_image_file(event.target.files[0],function(res){
893 contents.find('.client-picture img, .client-picture .fa').remove();
894 contents.find('.client-picture').append("<img src='"+res+"'>");
895 contents.find('.detail.picture').remove();
896 self.uploaded_picture = res;
900 } else if (visibility === 'hide') {
902 if( height > scroll ){
903 contents.css({height:height+'px'});
904 contents.animate({height:0},400,function(){
905 contents.css({height:''});
908 parent.scrollTop( parent.scrollTop() - height);
910 this.details_visible = false;
911 this.toggle_save_button();
919 module.ReceiptScreenWidget = module.ScreenWidget.extend({
920 template: 'ReceiptScreenWidget',
929 var print_button = this.add_action_button({
931 icon: '/point_of_sale/static/src/img/icons/png48/printer.png',
932 click: function(){ self.print(); },
935 var finish_button = this.add_action_button({
936 label: _t('Next Order'),
937 icon: '/point_of_sale/static/src/img/icons/png48/go-next.png',
938 click: function() { self.finishOrder(); },
945 // The problem is that in chrome the print() is asynchronous and doesn't
946 // execute until all rpc are finished. So it conflicts with the rpc used
947 // to send the orders to the backend, and the user is able to go to the next
948 // screen before the printing dialog is opened. The problem is that what's
949 // printed is whatever is in the page when the dialog is opened and not when it's called,
950 // and so you end up printing the product list instead of the receipt...
952 // Fixing this would need a re-architecturing
953 // of the code to postpone sending of orders after printing.
955 // But since the print dialog also blocks the other asynchronous calls, the
956 // button enabling in the setTimeout() is blocked until the printing dialog is
957 // closed. But the timeout has to be big enough or else it doesn't work
958 // 2 seconds is the same as the default timeout for sending orders and so the dialog
959 // should have appeared before the timeout... so yeah that's not ultra reliable.
961 finish_button.set_disabled(true);
962 setTimeout(function(){
963 finish_button.set_disabled(false);
969 finishOrder: function() {
970 this.pos.get('selectedOrder').destroy();
972 refresh: function() {
973 var order = this.pos.get('selectedOrder');
974 $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{
977 orderlines: order.get('orderLines').models,
978 paymentlines: order.get('paymentLines').models,
987 module.PaymentScreenWidget = module.ScreenWidget.extend({
988 template: 'PaymentScreenWidget',
989 back_screen: 'products',
990 next_screen: 'receipt',
991 init: function(parent, options) {
993 this._super(parent,options);
995 this.pos.bind('change:selectedOrder',function(){
997 this.renderElement();
1002 this.line_delete_handler = function(event){
1004 while(node && !node.classList.contains('paymentline')){
1005 node = node.parentNode;
1008 self.pos.get('selectedOrder').removePaymentline(node.line)
1010 event.stopPropagation();
1013 this.line_change_handler = function(event){
1015 while(node && !node.classList.contains('paymentline')){
1016 node = node.parentNode;
1019 node.line.set_amount(this.value);
1023 this.line_click_handler = function(event){
1025 while(node && !node.classList.contains('paymentline')){
1026 node = node.parentNode;
1029 self.pos.get('selectedOrder').selectPaymentline(node.line);
1033 this.hotkey_handler = function(event){
1034 if(event.which === 13){
1035 self.validate_order();
1036 }else if(event.which === 27){
1046 this.enable_numpad();
1047 this.focus_selected_line();
1049 document.body.addEventListener('keyup', this.hotkey_handler);
1051 this.add_action_button({
1053 icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
1059 this.add_action_button({
1060 label: _t('Validate'),
1062 icon: '/point_of_sale/static/src/img/icons/png48/validate.png',
1064 self.validate_order();
1068 if( this.pos.config.iface_invoicing ){
1069 this.add_action_button({
1072 icon: '/point_of_sale/static/src/img/icons/png48/invoice.png',
1074 self.validate_order({invoice: true});
1079 if( this.pos.config.iface_cashdrawer ){
1080 this.add_action_button({
1083 icon: '/point_of_sale/static/src/img/open-cashbox.png',
1085 self.pos.proxy.open_cashbox();
1090 this.update_payment_summary();
1095 this.disable_numpad();
1096 document.body.removeEventListener('keyup',this.hotkey_handler);
1098 remove_empty_lines: function(){
1099 var order = this.pos.get('selectedOrder');
1100 var lines = order.get('paymentLines').models.slice(0);
1101 for(var i = 0; i < lines.length; i++){
1102 var line = lines[i];
1103 if(line.get_amount() === 0){
1104 order.removePaymentline(line);
1109 this.remove_empty_lines();
1110 this.pos_widget.screen_selector.set_current_screen(this.back_screen);
1112 bind_events: function() {
1114 this.old_order.unbind(null,null,this);
1116 var order = this.pos.get('selectedOrder');
1117 order.bind('change:selected_paymentline',this.focus_selected_line,this);
1119 this.old_order = order;
1121 if(this.old_paymentlines){
1122 this.old_paymentlines.unbind(null,null,this);
1124 var paymentlines = order.get('paymentLines');
1125 paymentlines.bind('add', this.add_paymentline, this);
1126 paymentlines.bind('change:selected', this.rerender_paymentline, this);
1127 paymentlines.bind('change:amount', function(line){
1128 if(!line.selected && line.node){
1129 line.node.value = line.amount.toFixed(2);
1131 this.update_payment_summary();
1133 paymentlines.bind('remove', this.remove_paymentline, this);
1134 paymentlines.bind('all', this.update_payment_summary, this);
1136 this.old_paymentlines = paymentlines;
1138 if(this.old_orderlines){
1139 this.old_orderlines.unbind(null,null,this);
1141 var orderlines = order.get('orderLines');
1142 orderlines.bind('all', this.update_payment_summary, this);
1144 this.old_orderlines = orderlines;
1146 focus_selected_line: function(){
1147 var line = this.pos.get('selectedOrder').selected_paymentline;
1149 var input = line.node.querySelector('input');
1153 var value = input.value;
1156 if(this.numpad_state){
1157 this.numpad_state.reset();
1160 if(Number(value) === 0){
1163 input.value = value;
1168 add_paymentline: function(line) {
1169 var list_container = this.el.querySelector('.payment-lines');
1170 list_container.appendChild(this.render_paymentline(line));
1172 if(this.numpad_state){
1173 this.numpad_state.reset();
1176 render_paymentline: function(line){
1177 var el_html = openerp.qweb.render('Paymentline',{widget: this, line: line});
1178 el_html = _.str.trim(el_html);
1180 var el_node = document.createElement('tbody');
1181 el_node.innerHTML = el_html;
1182 el_node = el_node.childNodes[0];
1183 el_node.line = line;
1184 el_node.querySelector('.paymentline-delete')
1185 .addEventListener('click', this.line_delete_handler);
1186 el_node.addEventListener('click', this.line_click_handler);
1187 el_node.querySelector('input')
1188 .addEventListener('keyup', this.line_change_handler);
1190 line.node = el_node;
1194 rerender_paymentline: function(line){
1195 var old_node = line.node;
1196 var new_node = this.render_paymentline(line);
1198 old_node.parentNode.replaceChild(new_node,old_node);
1200 remove_paymentline: function(line){
1201 line.node.parentNode.removeChild(line.node);
1202 line.node = undefined;
1204 renderElement: function(){
1207 var paymentlines = this.pos.get('selectedOrder').get('paymentLines').models;
1208 var list_container = this.el.querySelector('.payment-lines');
1210 for(var i = 0; i < paymentlines.length; i++){
1211 list_container.appendChild(this.render_paymentline(paymentlines[i]));
1214 this.update_payment_summary();
1216 update_payment_summary: function() {
1217 var currentOrder = this.pos.get('selectedOrder');
1218 var paidTotal = currentOrder.getPaidTotal();
1219 var dueTotal = currentOrder.getTotalTaxIncluded();
1220 var remaining = dueTotal > paidTotal ? dueTotal - paidTotal : 0;
1221 var change = paidTotal > dueTotal ? paidTotal - dueTotal : 0;
1223 this.$('.payment-due-total').html(this.format_currency(dueTotal));
1224 this.$('.payment-paid-total').html(this.format_currency(paidTotal));
1225 this.$('.payment-remaining').html(this.format_currency(remaining));
1226 this.$('.payment-change').html(this.format_currency(change));
1227 if(currentOrder.selected_orderline === undefined){
1228 remaining = 1; // What is this ?
1231 if(this.pos_widget.action_bar){
1232 this.pos_widget.action_bar.set_button_disabled('validation', !this.is_paid());
1233 this.pos_widget.action_bar.set_button_disabled('invoice', !this.is_paid());
1236 is_paid: function(){
1237 var currentOrder = this.pos.get('selectedOrder');
1238 return (currentOrder.getTotalTaxIncluded() < 0.000001
1239 || currentOrder.getPaidTotal() + 0.000001 >= currentOrder.getTotalTaxIncluded());
1242 validate_order: function(options) {
1244 options = options || {};
1246 var currentOrder = this.pos.get('selectedOrder');
1248 if(!this.is_paid()){
1252 // The exact amount must be paid if there is no cash payment method defined.
1253 if (Math.abs(currentOrder.getTotalTaxIncluded() - currentOrder.getPaidTotal()) > 0.00001) {
1255 for (var i = 0; i < this.pos.cashregisters.length; i++) {
1256 cash = cash || (this.pos.cashregisters[i].journal.type === 'cash');
1259 this.pos_widget.screen_selector.show_popup('error',{
1260 message: _t('Cannot return change without a cash payment method'),
1261 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'),
1267 if (this.pos.config.iface_cashdrawer) {
1268 this.pos.proxy.open_cashbox();
1271 if(options.invoice){
1272 // deactivate the validation button while we try to send the order
1273 this.pos_widget.action_bar.set_button_disabled('validation',true);
1274 this.pos_widget.action_bar.set_button_disabled('invoice',true);
1276 var invoiced = this.pos.push_and_invoice_order(currentOrder);
1278 invoiced.fail(function(error){
1279 if(error === 'error-no-client'){
1280 self.pos_widget.screen_selector.show_popup('error',{
1281 message: _t('An anonymous order cannot be invoiced'),
1282 comment: _t('Please select a client for this order. This can be done by clicking the order tab'),
1285 self.pos_widget.screen_selector.show_popup('error',{
1286 message: _t('The order could not be sent'),
1287 comment: _t('Check your internet connection and try again.'),
1290 self.pos_widget.action_bar.set_button_disabled('validation',false);
1291 self.pos_widget.action_bar.set_button_disabled('invoice',false);
1294 invoiced.done(function(){
1295 self.pos_widget.action_bar.set_button_disabled('validation',false);
1296 self.pos_widget.action_bar.set_button_disabled('invoice',false);
1297 self.pos.get('selectedOrder').destroy();
1301 this.pos.push_order(currentOrder)
1302 if(this.pos.config.iface_print_via_proxy){
1303 var receipt = currentOrder.export_for_printing();
1304 this.pos.proxy.print_receipt(QWeb.render('XmlReceipt',{
1305 receipt: receipt, widget: self,
1307 this.pos.get('selectedOrder').destroy(); //finish order and go back to scan screen
1309 this.pos_widget.screen_selector.set_current_screen(this.next_screen);
1313 // hide onscreen (iOS) keyboard
1314 setTimeout(function(){
1315 document.activeElement.blur();
1319 enable_numpad: function(){
1320 this.disable_numpad(); //ensure we don't register the callbacks twice
1321 this.numpad_state = this.pos_widget.numpad.state;
1322 if(this.numpad_state){
1323 this.numpad_state.reset();
1324 this.numpad_state.changeMode('payment');
1325 this.numpad_state.bind('set_value', this.set_value, this);
1326 this.numpad_state.bind('change:mode', this.set_mode_back_to_payment, this);
1330 disable_numpad: function(){
1331 if(this.numpad_state){
1332 this.numpad_state.unbind('set_value', this.set_value);
1333 this.numpad_state.unbind('change:mode',this.set_mode_back_to_payment);
1336 set_mode_back_to_payment: function() {
1337 this.numpad_state.set({mode: 'payment'});
1339 set_value: function(val) {
1340 var selected_line =this.pos.get('selectedOrder').selected_paymentline;
1342 selected_line.set_amount(val);
1343 selected_line.node.querySelector('input').value = selected_line.amount.toFixed(2);