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_client_screen = options.default_client_screen;
33 this.default_cashier_screen = options.default_cashier_screen;
35 this.current_popup = null;
37 this.current_mode = options.default_mode || 'client';
39 this.current_screen = null;
41 for(screen_name in this.screen_set){
42 this.screen_set[screen_name].hide();
45 for(popup_name in this.popup_set){
46 this.popup_set[popup_name].hide();
49 this.selected_order = this.pos.get('selectedOrder');
50 this.selected_order.set_screen_data({
51 client_screen: this.default_client_screen,
52 cashier_screen: this.default_cashier_screen,
55 this.pos.bind('change:selectedOrder', this.load_saved_screen, this);
57 add_screen: function(screen_name, screen){
59 this.screen_set[screen_name] = screen;
62 show_popup: function(name){
63 if(this.current_popup){
66 this.current_popup = this.popup_set[name];
67 this.current_popup.show();
69 close_popup: function(){
70 if(this.current_popup){
71 this.current_popup.close();
72 this.current_popup.hide();
73 this.current_popup = null;
76 load_saved_screen: function(){
79 var selectedOrder = this.pos.get('selectedOrder');
81 if(this.current_mode === 'client'){
82 this.set_current_screen(selectedOrder.get_screen_data('client_screen') || this.default_client_screen,null,'refresh');
83 }else if(this.current_mode === 'cashier'){
84 this.set_current_screen(selectedOrder.get_screen_data('cashier_screen') || this.default_cashier_screen,null,'refresh');
86 this.selected_order = selectedOrder;
88 set_user_mode: function(user_mode){
89 if(user_mode !== this.current_mode){
91 this.current_mode = user_mode;
92 this.load_saved_screen();
95 get_user_mode: function(){
96 return this.current_mode;
98 set_current_screen: function(screen_name,params,refresh){
99 var screen = this.screen_set[screen_name];
101 console.error("ERROR: set_current_screen("+screen_name+") : screen not found");
105 var selectedOrder = this.pos.get('selectedOrder');
106 if(this.current_mode === 'client'){
107 selectedOrder.set_screen_data('client_screen',screen_name);
109 selectedOrder.set_screen_data('client_screen_params',params);
112 selectedOrder.set_screen_data('cashier_screen',screen_name);
114 selectedOrder.set_screen_data('cashier_screen_params',params);
118 if(screen && (refresh || screen !== this.current_screen)){
119 if(this.current_screen){
120 this.current_screen.close();
121 this.current_screen.hide();
123 this.current_screen = screen;
124 this.current_screen.show();
127 get_current_screen_param: function(param){
128 var selected_order = this.pos.get('selectedOrder');
129 if(this.current_mode === 'client'){
130 var params = selected_order.get_screen_data('client_screen_params');
132 var params = selected_order.get_screen_data('cashier_screen_params');
135 return params[param];
140 set_default_screen: function(){
141 this.set_current_screen(this.current_mode === 'client' ? this.default_client_screen : this.default_cashier_screen);
145 module.ScreenWidget = module.PosBaseWidget.extend({
150 init: function(parent,options){
151 this._super(parent,options);
155 help_button_action: function(){
156 this.pos_widget.screen_selector.show_popup('help');
159 barcode_product_screen: 'products', //if defined, this screen will be loaded when a product is scanned
160 barcode_product_error_popup: 'error-product', //if defined, this popup will be loaded when there's an error in the popup
162 hotkeys_handlers: {},
164 // what happens when a product is scanned :
165 // it will add the product to the order and go to barcode_product_screen. Or show barcode_product_error_popup if
167 barcode_product_action: function(code){
169 if(self.pos.scan_product(code)){
170 self.pos.proxy.scan_item_success(code);
171 if(self.barcode_product_screen){
172 self.pos_widget.screen_selector.set_current_screen(self.barcode_product_screen);
175 self.pos.proxy.scan_item_error_unrecognized(code);
176 if(self.barcode_product_error_popup && self.pos_widget.screen_selector.get_user_mode() !== 'cashier'){
177 self.pos_widget.screen_selector.show_popup(self.barcode_product_error_popup);
182 // what happens when a cashier id barcode is scanned.
183 // the default behavior is the following :
184 // - if there's a user with a matching ean, put it as the active 'cashier', go to cashier mode, and return true
185 // - else : do nothing and return false. You probably want to extend this to show and appropriate error popup...
186 barcode_cashier_action: function(code){
187 var users = this.pos.users;
188 for(var i = 0, len = users.length; i < len; i++){
189 if(users[i].ean13 === code.code){
190 this.pos.cashier = users[i];
191 this.pos_widget.username.refresh();
192 this.pos.proxy.cashier_mode_activated();
193 this.pos_widget.screen_selector.set_user_mode('cashier');
197 this.pos.proxy.scan_item_error_unrecognized(code);
201 // what happens when a client id barcode is scanned.
202 // the default behavior is the following :
203 // - if there's a user with a matching ean, put it as the active 'client' and return true
204 // - else : return false.
205 barcode_client_action: function(code){
206 var partners = this.pos.partners;
207 for(var i = 0, len = partners.length; i < len; i++){
208 if(partners[i].ean13 === code.code){
209 this.pos.get('selectedOrder').set_client(partners[i]);
210 this.pos_widget.username.refresh();
211 this.pos.proxy.scan_item_success(code);
215 this.pos.proxy.scan_item_error_unrecognized(code);
217 //TODO start the transaction
220 // what happens when a discount barcode is scanned : the default behavior
221 // is to set the discount on the last order.
222 barcode_discount_action: function(code){
223 this.pos.proxy.scan_item_success(code);
224 var last_orderline = this.pos.get('selectedOrder').getLastOrderline();
226 last_orderline.set_discount(code.value)
230 // shows an action bar on the screen. The actionbar is automatically shown when you add a button
231 // with add_action_button()
232 show_action_bar: function(){
233 this.pos_widget.action_bar.show();
236 // hides the action bar. The actionbar is automatically hidden when it is empty
237 hide_action_bar: function(){
238 this.pos_widget.action_bar.hide();
241 // adds a new button to the action bar. The button definition takes three parameters, all optional :
242 // - label: the text below the button
243 // - icon: a small icon that will be shown
244 // - click: a callback that will be executed when the button is clicked.
245 // the method returns a reference to the button widget, and automatically show the actionbar.
246 add_action_button: function(button_def){
247 this.show_action_bar();
248 return this.pos_widget.action_bar.add_new_button(button_def);
251 // this method shows the screen and sets up all the widget related to this screen. Extend this method
252 // if you want to alter the behavior of the screen.
258 this.$el.removeClass('oe_hidden');
261 if(this.pos_widget.action_bar.get_button_count() > 0){
262 this.show_action_bar();
264 this.hide_action_bar();
267 // we add the help button by default. we do this because the buttons are cleared on each refresh so that
268 // the button stay local to each screen
269 this.pos_widget.left_action_bar.add_new_button({
271 icon: '/point_of_sale/static/src/img/icons/png48/help.png',
272 click: function(){ self.help_button_action(); },
276 this.cashier_mode = this.pos_widget.screen_selector.get_user_mode() === 'cashier';
278 this.pos_widget.set_numpad_visible(this.show_numpad && this.cashier_mode);
279 this.pos_widget.set_leftpane_visible(this.show_leftpane);
280 this.pos_widget.set_left_action_bar_visible(this.show_leftpane && !this.cashier_mode);
281 this.pos_widget.set_cashier_controls_visible(this.cashier_mode);
283 if(this.cashier_mode && this.pos.config.iface_self_checkout){
284 this.pos_widget.client_button.show();
286 this.pos_widget.client_button.hide();
288 if(this.cashier_mode){
289 this.pos_widget.close_button.show();
291 this.pos_widget.close_button.hide();
294 this.pos_widget.username.set_user_mode(this.pos_widget.screen_selector.get_user_mode());
296 this.pos.barcode_reader.set_action_callback({
297 'cashier': self.barcode_cashier_action ? function(code){ self.barcode_cashier_action(code); } : undefined ,
298 'product': self.barcode_product_action ? function(code){ self.barcode_product_action(code); } : undefined ,
299 'client' : self.barcode_client_action ? function(code){ self.barcode_client_action(code); } : undefined ,
300 'discount': self.barcode_discount_action ? function(code){ self.barcode_discount_action(code); } : undefined,
304 // this method is called when the screen is closed to make place for a new screen. this is a good place
305 // to put your cleanup stuff as it is guaranteed that for each show() there is one and only one close()
307 if(this.pos.barcode_reader){
308 this.pos.barcode_reader.reset_action_callbacks();
310 this.pos_widget.action_bar.destroy_buttons();
311 this.pos_widget.left_action_bar.destroy_buttons();
314 // this methods hides the screen. It's not a good place to put your cleanup stuff as it is called on the
315 // POS initialization.
319 this.$el.addClass('oe_hidden');
323 // we need this because some screens re-render themselves when they are hidden
324 // (due to some events, or magic, or both...) we must make sure they remain hidden.
325 // the good solution would probably be to make them not re-render themselves when they
327 renderElement: function(){
331 this.$el.addClass('oe_hidden');
337 module.PopUpWidget = module.PosBaseWidget.extend({
340 this.$el.removeClass('oe_hidden');
343 /* called before hide, when a popup is closed */
346 /* hides the popup. keep in mind that this is called in the initialization pass of the
347 * pos instantiation, so you don't want to do anything fancy in here */
350 this.$el.addClass('oe_hidden');
355 module.HelpPopupWidget = module.PopUpWidget.extend({
356 template:'HelpPopupWidget',
359 this.pos.proxy.help_needed();
362 this.$el.find('.button').off('click').click(function(){
363 self.pos_widget.screen_selector.close_popup();
367 this.pos.proxy.help_canceled();
371 module.ChooseReceiptPopupWidget = module.PopUpWidget.extend({
372 template:'ChooseReceiptPopupWidget',
375 this.renderElement();
377 var currentOrder = self.pos.get('selectedOrder');
379 this.$('.button.receipt').off('click').click(function(){
380 currentOrder.set_receipt_type('receipt');
381 self.pos_widget.screen_selector.set_current_screen('products');
384 this.$('.button.invoice').off('click').click(function(){
385 currentOrder.set_receipt_type('invoice');
386 self.pos_widget.screen_selector.set_current_screen('products');
389 get_client_name: function(){
390 var client = this.pos.get('selectedOrder').get_client();
399 module.ErrorPopupWidget = module.PopUpWidget.extend({
400 template:'ErrorPopupWidget',
404 this.pos.proxy.help_needed();
405 this.pos.proxy.scan_item_error_unrecognized();
407 this.pos.barcode_reader.save_callbacks();
408 this.pos.barcode_reader.reset_action_callbacks();
409 this.pos.barcode_reader.set_action_callback({
410 'cashier': function(code){
411 clearInterval(this.intervalID);
412 self.pos.proxy.cashier_mode_activated();
413 self.pos_widget.screen_selector.set_user_mode('cashier');
416 this.$('.footer .button').off('click').click(function(){
417 self.pos_widget.screen_selector.close_popup();
422 this.pos.proxy.help_canceled();
423 this.pos.barcode_reader.restore_callbacks();
427 module.ProductErrorPopupWidget = module.ErrorPopupWidget.extend({
428 template:'ProductErrorPopupWidget',
431 module.ErrorSessionPopupWidget = module.ErrorPopupWidget.extend({
432 template:'ErrorSessionPopupWidget',
435 module.ErrorNegativePricePopupWidget = module.ErrorPopupWidget.extend({
436 template:'ErrorNegativePricePopupWidget',
439 module.ErrorNoClientPopupWidget = module.ErrorPopupWidget.extend({
440 template: 'ErrorNoClientPopupWidget',
443 module.ErrorInvoiceTransferPopupWidget = module.ErrorPopupWidget.extend({
444 template: 'ErrorInvoiceTransferPopupWidget',
447 module.ScaleInviteScreenWidget = module.ScreenWidget.extend({
448 template:'ScaleInviteScreenWidget',
451 previous_screen:'products',
456 var queue = this.pos.proxy_queue;
458 queue.schedule(function(){
459 return self.pos.proxy.weighting_start();
460 },{ important: true });
462 queue.schedule(function(){
463 return self.pos.proxy.weighting_read_kg().then(function(weight){
465 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
468 },{duration: 100, repeat: true});
470 this.add_action_button({
472 icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
474 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
481 this.pos.proxy_queue.clear();
482 this.pos.proxy_queue.schedule(function(){
483 return self.pos.proxy.weighting_end();
484 },{ important: true });
488 module.ScaleScreenWidget = module.ScreenWidget.extend({
489 template:'ScaleScreenWidget',
491 next_screen: 'products',
492 previous_screen: 'products',
497 var queue = this.pos.proxy_queue;
500 this.renderElement();
502 this.hotkey_handler = function(event){
503 if(event.which === 13){
504 self.order_product();
505 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
506 }else if(event.which === 27){
507 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
511 $('body').on('keyup',this.hotkey_handler);
513 this.add_action_button({
515 icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
517 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
521 this.validate_button = this.add_action_button({
522 label: _t('Validate'),
523 icon: '/point_of_sale/static/src/img/icons/png48/validate.png',
525 self.order_product();
526 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
530 queue.schedule(function(){
531 return self.pos.proxy.scale_read().then(function(weight){
532 self.set_weight(weight.weight);
534 },{duration:50, repeat: true});
537 renderElement: function(){
540 this.$('.product-picture').click(function(){
541 self.order_product();
542 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
545 get_product: function(){
546 var ss = this.pos_widget.screen_selector;
548 return ss.get_current_screen_param('product');
553 order_product: function(){
554 this.pos.get('selectedOrder').addProduct(this.get_product(),{ quantity: this.weight });
556 get_product_name: function(){
557 var product = this.get_product();
558 return (product ? product.name : undefined) || 'Unnamed Product';
560 get_product_price: function(){
561 var product = this.get_product();
562 return (product ? product.price : 0) || 0;
564 set_weight: function(weight){
565 this.weight = weight;
566 this.$('.js-weight').text(this.get_product_weight_string());
568 get_product_weight_string: function(){
569 var product = this.get_product();
570 var defaultstr = (this.weight || 0).toFixed(3) + ' Kg';
571 if(!product || !this.pos){
574 var unit_id = product.uos_id || product.uom_id;
578 var unit = this.pos.units_by_id[unit_id[0]];
579 var weight = round_pr(this.weight || 0, unit.rounding);
580 var weightstr = weight.toFixed(Math.ceil(Math.log(1.0/unit.rounding) / Math.log(10) ));
584 get_product_image_url: function(){
585 var product = this.get_product();
587 return window.location.origin + '/web/binary/image?model=product.product&field=image_medium&id='+product.id;
595 $('body').off('keyup',this.hotkey_handler);
597 this.pos.proxy_queue.clear();
602 module.ClientPaymentScreenWidget = module.ScreenWidget.extend({
603 template:'ClientPaymentScreenWidget',
605 next_screen: 'welcome',
606 previous_screen: 'products',
612 this.queue = new module.JobQueue();
613 this.canceled = false;
616 // initiates the connection to the payment terminal and starts the update requests
617 this.start = function(){
618 var def = new $.Deferred();
619 self.pos.proxy.payment_request(self.pos.get('selectedOrder').getDueLeft())
622 self.queue.schedule(self.update);
623 }else if(ack.indexOf('error') === 0){
624 console.error('cannot make payment. TODO');
626 console.error('unknown payment request return value:',ack);
633 // gets updated status from the payment terminal and performs the appropriate consequences
634 this.update = function(){
635 var def = new $.Deferred();
637 return def.resolve();
639 self.pos.proxy.payment_status()
640 .done(function(status){
641 if(status.status === 'paid'){
643 var currentOrder = self.pos.get('selectedOrder');
645 //we get the first cashregister marked as self-checkout
646 var selfCheckoutRegisters = [];
647 for(var i = 0; i < self.pos.cashregisters.length; i++){
648 var cashregister = self.pos.cashregisters[i];
649 if(cashregister.self_checkout_payment_method){
650 selfCheckoutRegisters.push(cashregister);
654 var cashregister = selfCheckoutRegisters[0] || self.pos.cashregisters[0];
655 currentOrder.addPaymentline(cashregister);
656 self.pos.push_order(currentOrder)
657 currentOrder.destroy();
658 self.pos.proxy.transaction_end();
659 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
661 }else if(status.status.indexOf('error') === 0){
662 console.error('error in payment request. TODO');
663 }else if(status.status === 'waiting'){
664 self.queue.schedule(self.update,200);
666 console.error('unknown status value:',status.status);
673 // cancels a payment.
674 this.cancel = function(){
675 if(!self.paid && !self.canceled){
676 self.canceled = true;
677 self.pos.proxy.payment_cancel();
678 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
681 return (new $.Deferred()).resolve();
684 if(this.pos.get('selectedOrder').getDueLeft() <= 0){
685 this.pos_widget.screen_selector.show_popup('error-negative-price');
687 this.queue.schedule(this.start);
690 this.add_action_button({
692 icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
694 self.queue.schedule(self.cancel);
695 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
701 this.queue.schedule(this.cancel);
708 module.WelcomeScreenWidget = module.ScreenWidget.extend({
709 template:'WelcomeScreenWidget',
711 next_screen: 'products',
714 show_leftpane: false,
717 $('.goodbye-message').click(function(){
718 $(this).addClass('oe_hidden');
722 barcode_product_action: function(code){
723 this.pos.proxy.transaction_start();
727 barcode_client_action: function(code){
728 this.pos.proxy.transaction_start();
730 $('.goodbye-message').addClass('oe_hidden');
731 this.pos_widget.screen_selector.show_popup('choose-receipt');
738 this.add_action_button({
740 icon: '/point_of_sale/static/src/img/icons/png48/help.png',
742 $('.goodbye-message').css({opacity:1}).addClass('oe_hidden');
743 self.help_button_action();
747 $('.goodbye-message').css({opacity:1}).removeClass('oe_hidden');
748 setTimeout(function(){
749 $('.goodbye-message').animate({opacity:0},500,'swing',function(){$('.goodbye-message').addClass('oe_hidden');});
754 module.ProductScreenWidget = module.ScreenWidget.extend({
755 template:'ProductScreenWidget',
757 scale_screen: 'scale',
758 client_scale_screen : 'scale_invite',
759 client_next_screen: 'client_payment',
764 start: function(){ //FIXME this should work as renderElement... but then the categories aren't properly set. explore why
767 this.product_list_widget = new module.ProductListWidget(this,{
768 click_product_action: function(product){
769 if(product.to_weight && self.pos.config.iface_electronic_scale){
770 self.pos_widget.screen_selector.set_current_screen( self.cashier_mode ? self.scale_screen : self.client_scale_screen, {product: product});
772 self.pos.get('selectedOrder').addProduct(product);
775 product_list: this.pos.db.get_product_by_category(0)
777 this.product_list_widget.replace($('.placeholder-ProductListWidget'));
779 this.product_categories_widget = new module.ProductCategoriesWidget(this,{
780 product_list_widget: this.product_list_widget,
782 this.product_categories_widget.replace($('.placeholder-ProductCategoriesWidget'));
789 this.product_categories_widget.reset_category();
791 this.pos_widget.order_widget.set_editable(true);
793 if(this.pos_widget.screen_selector.current_mode === 'client'){
794 this.add_action_button({
796 icon: '/point_of_sale/static/src/img/icons/png48/go-next.png',
798 self.pos_widget.screen_selector.set_current_screen(self.client_next_screen);
807 this.pos_widget.order_widget.set_editable(false);
809 if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
810 this.pos_widget.onscreen_keyboard.hide();
815 module.ReceiptScreenWidget = module.ScreenWidget.extend({
816 template: 'ReceiptScreenWidget',
825 var print_button = this.add_action_button({
827 icon: '/point_of_sale/static/src/img/icons/png48/printer.png',
828 click: function(){ self.print(); },
831 var finish_button = this.add_action_button({
832 label: _t('Next Order'),
833 icon: '/point_of_sale/static/src/img/icons/png48/go-next.png',
834 click: function() { self.finishOrder(); },
841 // The problem is that in chrome the print() is asynchronous and doesn't
842 // execute until all rpc are finished. So it conflicts with the rpc used
843 // to send the orders to the backend, and the user is able to go to the next
844 // screen before the printing dialog is opened. The problem is that what's
845 // printed is whatever is in the page when the dialog is opened and not when it's called,
846 // and so you end up printing the product list instead of the receipt...
848 // Fixing this would need a re-architecturing
849 // of the code to postpone sending of orders after printing.
851 // But since the print dialog also blocks the other asynchronous calls, the
852 // button enabling in the setTimeout() is blocked until the printing dialog is
853 // closed. But the timeout has to be big enough or else it doesn't work
854 // 2 seconds is the same as the default timeout for sending orders and so the dialog
855 // should have appeared before the timeout... so yeah that's not ultra reliable.
857 finish_button.set_disabled(true);
858 setTimeout(function(){
859 finish_button.set_disabled(false);
865 finishOrder: function() {
866 this.pos.get('selectedOrder').destroy();
868 refresh: function() {
869 var order = this.pos.get('selectedOrder');
870 $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{
873 orderlines: order.get('orderLines').models,
874 paymentlines: order.get('paymentLines').models,
882 module.PaymentScreenWidget = module.ScreenWidget.extend({
883 template: 'PaymentScreenWidget',
884 back_screen: 'products',
885 next_screen: 'receipt',
886 init: function(parent, options) {
888 this._super(parent,options);
890 this.pos.bind('change:selectedOrder',function(){
892 this.renderElement();
897 this.line_delete_handler = function(event){
899 while(node && !node.classList.contains('paymentline')){
900 node = node.parentNode;
903 self.pos.get('selectedOrder').removePaymentline(node.line)
905 event.stopPropagation();
908 this.line_change_handler = function(event){
910 while(node && !node.classList.contains('paymentline')){
911 node = node.parentNode;
914 node.line.set_amount(this.value);
919 this.line_click_handler = function(event){
921 while(node && !node.classList.contains('paymentline')){
922 node = node.parentNode;
925 self.pos.get('selectedOrder').selectPaymentline(node.line);
929 this.hotkey_handler = function(event){
930 if(event.which === 13){
931 self.validate_order();
932 }else if(event.which === 27){
942 this.enable_numpad();
943 this.focus_selected_line();
945 document.body.addEventListener('keyup', this.hotkey_handler);
949 this.add_action_button({
951 icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
957 this.add_action_button({
958 label: _t('Validate'),
960 icon: '/point_of_sale/static/src/img/icons/png48/validate.png',
962 self.validate_order();
966 if( this.pos.config.iface_invoicing ){
967 this.add_action_button({
970 icon: '/point_of_sale/static/src/img/icons/png48/invoice.png',
972 self.validate_order({invoice: true});
977 if( this.pos.config.iface_cashdrawer ){
978 this.add_action_button({
981 icon: '/point_of_sale/static/src/img/open-cashbox.png',
983 self.pos.proxy.open_cashbox();
988 this.update_payment_summary();
993 this.disable_numpad();
994 document.body.removeEventListener('keyup',this.hotkey_handler);
996 remove_empty_lines: function(){
997 var order = this.pos.get('selectedOrder');
998 var lines = order.get('paymentLines').models.slice(0);
999 for(var i = 0; i < lines.length; i++){
1000 var line = lines[i];
1001 if(line.get_amount() === 0){
1002 order.removePaymentline(line);
1007 this.remove_empty_lines();
1008 this.pos_widget.screen_selector.set_current_screen(this.back_screen);
1010 bind_events: function() {
1012 this.old_order.unbind(null,null,this);
1014 var order = this.pos.get('selectedOrder');
1015 order.bind('change:selected_paymentline',this.focus_selected_line,this);
1017 this.old_order = order;
1019 if(this.old_paymentlines){
1020 this.old_paymentlines.unbind(null,null,this);
1022 var paymentlines = order.get('paymentLines');
1023 paymentlines.bind('add', this.add_paymentline, this);
1024 paymentlines.bind('change:selected', this.rerender_paymentline, this);
1025 paymentlines.bind('change:amount', function(line){
1026 if(!line.selected && line.node){
1027 line.node.value = line.amount.toFixed(2);
1029 this.update_payment_summary();
1031 paymentlines.bind('remove', this.remove_paymentline, this);
1032 paymentlines.bind('all', this.update_payment_summary, this);
1034 this.old_paymentlines = paymentlines;
1036 if(this.old_orderlines){
1037 this.old_orderlines.unbind(null,null,this);
1039 var orderlines = order.get('orderLines');
1040 orderlines.bind('all', this.update_payment_summary, this);
1042 this.old_orderlines = orderlines;
1044 focus_selected_line: function(){
1045 var line = this.pos.get('selectedOrder').selected_paymentline;
1047 var input = line.node.querySelector('input');
1051 var value = input.value;
1054 if(this.numpad_state){
1055 this.numpad_state.reset();
1058 if(Number(value) === 0){
1061 input.value = value;
1066 add_paymentline: function(line) {
1067 var list_container = this.el.querySelector('.payment-lines');
1068 list_container.appendChild(this.render_paymentline(line));
1070 if(this.numpad_state){
1071 this.numpad_state.reset();
1074 render_paymentline: function(line){
1075 var el_html = openerp.qweb.render('Paymentline',{widget: this, line: line});
1076 el_html = _.str.trim(el_html);
1078 var el_node = document.createElement('tbody');
1079 el_node.innerHTML = el_html;
1080 el_node = el_node.childNodes[0];
1081 el_node.line = line;
1082 el_node.querySelector('.paymentline-delete')
1083 .addEventListener('click', this.line_delete_handler);
1084 el_node.addEventListener('click', this.line_click_handler);
1085 el_node.querySelector('input')
1086 .addEventListener('keyup', this.line_change_handler);
1088 line.node = el_node;
1092 rerender_paymentline: function(line){
1093 var old_node = line.node;
1094 var new_node = this.render_paymentline(line);
1096 old_node.parentNode.replaceChild(new_node,old_node);
1098 remove_paymentline: function(line){
1099 line.node.parentNode.removeChild(line.node);
1100 line.node = undefined;
1102 renderElement: function(){
1105 var paymentlines = this.pos.get('selectedOrder').get('paymentLines').models;
1106 var list_container = this.el.querySelector('.payment-lines');
1108 for(var i = 0; i < paymentlines.length; i++){
1109 list_container.appendChild(this.render_paymentline(paymentlines[i]));
1112 this.update_payment_summary();
1114 update_payment_summary: function() {
1115 var currentOrder = this.pos.get('selectedOrder');
1116 var paidTotal = currentOrder.getPaidTotal();
1117 var dueTotal = currentOrder.getTotalTaxIncluded();
1118 var remaining = dueTotal > paidTotal ? dueTotal - paidTotal : 0;
1119 var change = paidTotal > dueTotal ? paidTotal - dueTotal : 0;
1121 this.$('.payment-due-total').html(this.format_currency(dueTotal));
1122 this.$('.payment-paid-total').html(this.format_currency(paidTotal));
1123 this.$('.payment-remaining').html(this.format_currency(remaining));
1124 this.$('.payment-change').html(this.format_currency(change));
1125 if(currentOrder.selected_orderline === undefined){
1126 remaining = 1; // What is this ?
1129 if(this.pos_widget.action_bar){
1130 this.pos_widget.action_bar.set_button_disabled('validation', !this.is_paid());
1131 this.pos_widget.action_bar.set_button_disabled('invoice', !this.is_paid());
1134 is_paid: function(){
1135 var currentOrder = this.pos.get('selectedOrder');
1136 return (currentOrder.getTotalTaxIncluded() < 0.000001
1137 || currentOrder.getPaidTotal() + 0.000001 >= currentOrder.getTotalTaxIncluded());
1140 validate_order: function(options) {
1142 options = options || {};
1144 var currentOrder = this.pos.get('selectedOrder');
1146 if(!this.is_paid()){
1150 if( this.pos.config.iface_cashdrawer
1151 && this.pos.get('selectedOrder').get('paymentLines').find( function(pl){
1152 return pl.cashregister.journal.type === 'cash';
1154 this.pos.proxy.open_cashbox();
1157 if(options.invoice){
1158 // deactivate the validation button while we try to send the order
1159 this.pos_widget.action_bar.set_button_disabled('validation',true);
1160 this.pos_widget.action_bar.set_button_disabled('invoice',true);
1162 var invoiced = this.pos.push_and_invoice_order(currentOrder);
1164 invoiced.fail(function(error){
1165 if(error === 'error-no-client'){
1166 self.pos_widget.screen_selector.show_popup('error-no-client');
1168 self.pos_widget.screen_selector.show_popup('error-invoice-transfer');
1170 self.pos_widget.action_bar.set_button_disabled('validation',false);
1171 self.pos_widget.action_bar.set_button_disabled('invoice',false);
1174 invoiced.done(function(){
1175 self.pos_widget.action_bar.set_button_disabled('validation',false);
1176 self.pos_widget.action_bar.set_button_disabled('invoice',false);
1177 self.pos.get('selectedOrder').destroy();
1181 this.pos.push_order(currentOrder)
1182 if(this.pos.config.iface_print_via_proxy){
1183 var receipt = currentOrder.export_for_printing();
1184 this.pos.proxy.print_receipt(QWeb.render('XmlReceipt',{
1187 this.pos.get('selectedOrder').destroy(); //finish order and go back to scan screen
1189 this.pos_widget.screen_selector.set_current_screen(this.next_screen);
1193 // hide onscreen (iOS) keyboard
1194 setTimeout(function(){
1195 document.activeElement.blur();
1199 enable_numpad: function(){
1200 this.disable_numpad(); //ensure we don't register the callbacks twice
1201 this.numpad_state = this.pos_widget.numpad.state;
1202 if(this.numpad_state){
1203 this.numpad_state.reset();
1204 this.numpad_state.changeMode('payment');
1205 this.numpad_state.bind('set_value', this.set_value, this);
1206 this.numpad_state.bind('change:mode', this.set_mode_back_to_payment, this);
1210 disable_numpad: function(){
1211 if(this.numpad_state){
1212 this.numpad_state.unbind('set_value', this.set_value);
1213 this.numpad_state.unbind('change:mode',this.set_mode_back_to_payment);
1216 set_mode_back_to_payment: function() {
1217 this.numpad_state.set({mode: 'payment'});
1219 set_value: function(val) {
1220 var selected_line =this.pos.get('selectedOrder').selected_paymentline;
1222 selected_line.set_amount(val);
1223 selected_line.node.querySelector('input').value = selected_line.amount.toFixed(2);