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 module.ScreenSelector = instance.web.Class.extend({
23 init: function(options){
24 this.pos = options.pos;
26 this.screen_set = options.screen_set || {};
28 this.popup_set = options.popup_set || {};
30 this.default_client_screen = options.default_client_screen;
31 this.default_cashier_screen = options.default_cashier_screen;
33 this.current_popup = null;
35 this.current_mode = options.default_mode || 'client';
37 this.current_screen = null;
39 for(screen_name in this.screen_set){
40 this.screen_set[screen_name].hide();
43 for(popup_name in this.popup_set){
44 this.popup_set[popup_name].hide();
47 this.selected_order = this.pos.get('selectedOrder');
48 this.selected_order.set_screen_data({
49 client_screen: this.default_client_screen,
50 cashier_screen: this.default_cashier_screen,
53 this.pos.bind('change:selectedOrder', this.load_saved_screen, this);
55 add_screen: function(screen_name, screen){
57 this.screen_set[screen_name] = screen;
60 show_popup: function(name){
61 if(this.current_popup){
64 this.current_popup = this.popup_set[name];
65 this.current_popup.show();
67 close_popup: function(){
68 if(this.current_popup){
69 this.current_popup.close();
70 this.current_popup.hide();
71 this.current_popup = null;
74 load_saved_screen: function(){
77 var selectedOrder = this.pos.get('selectedOrder');
79 if(this.current_mode === 'client'){
80 this.set_current_screen(selectedOrder.get_screen_data('client_screen') || this.default_client_screen,null,'refresh');
81 }else if(this.current_mode === 'cashier'){
82 this.set_current_screen(selectedOrder.get_screen_data('cashier_screen') || this.default_cashier_screen,null,'refresh');
84 this.selected_order = selectedOrder;
86 set_user_mode: function(user_mode){
87 if(user_mode !== this.current_mode){
89 this.current_mode = user_mode;
90 this.load_saved_screen();
93 get_user_mode: function(){
94 return this.current_mode;
96 set_current_screen: function(screen_name,params,refresh){
97 var screen = this.screen_set[screen_name];
99 console.error("ERROR: set_current_screen("+screen_name+") : screen not found");
103 var selectedOrder = this.pos.get('selectedOrder');
104 if(this.current_mode === 'client'){
105 selectedOrder.set_screen_data('client_screen',screen_name);
107 selectedOrder.set_screen_data('client_screen_params',params);
110 selectedOrder.set_screen_data('cashier_screen',screen_name);
112 selectedOrder.set_screen_data('cashier_screen_params',params);
116 if(screen && (refresh || screen !== this.current_screen)){
117 if(this.current_screen){
118 this.current_screen.close();
119 this.current_screen.hide();
121 this.current_screen = screen;
122 this.current_screen.show();
125 get_current_screen_param: function(param){
126 var selected_order = this.pos.get('selectedOrder');
127 if(this.current_mode === 'client'){
128 var params = selected_order.get_screen_data('client_screen_params');
130 var params = selected_order.get_screen_data('cashier_screen_params');
133 return params[param];
138 set_default_screen: function(){
139 this.set_current_screen(this.current_mode === 'client' ? this.default_client_screen : this.default_cashier_screen);
143 module.ScreenWidget = module.PosBaseWidget.extend({
148 init: function(parent,options){
149 this._super(parent,options);
153 help_button_action: function(){
154 this.pos_widget.screen_selector.show_popup('help');
157 barcode_product_screen: 'products', //if defined, this screen will be loaded when a product is scanned
158 barcode_product_error_popup: 'error-product', //if defined, this popup will be loaded when there's an error in the popup
160 hotkeys_handlers: {},
162 // what happens when a product is scanned :
163 // it will add the product to the order and go to barcode_product_screen. Or show barcode_product_error_popup if
165 barcode_product_action: function(code){
167 if(self.pos.scan_product(code)){
168 self.pos.proxy.scan_item_success(code);
169 if(self.barcode_product_screen){
170 self.pos_widget.screen_selector.set_current_screen(self.barcode_product_screen);
173 self.pos.proxy.scan_item_error_unrecognized(code);
174 if(self.barcode_product_error_popup && self.pos_widget.screen_selector.get_user_mode() !== 'cashier'){
175 self.pos_widget.screen_selector.show_popup(self.barcode_product_error_popup);
180 // what happens when a cashier id barcode is scanned.
181 // the default behavior is the following :
182 // - if there's a user with a matching ean, put it as the active 'cashier', go to cashier mode, and return true
183 // - else : do nothing and return false. You probably want to extend this to show and appropriate error popup...
184 barcode_cashier_action: function(code){
185 var users = this.pos.users;
186 for(var i = 0, len = users.length; i < len; i++){
187 if(users[i].ean13 === code.code){
188 this.pos.cashier = users[i];
189 this.pos_widget.username.refresh();
190 this.pos.proxy.cashier_mode_activated();
191 this.pos_widget.screen_selector.set_user_mode('cashier');
195 this.pos.proxy.scan_item_error_unrecognized(code);
199 // what happens when a client id barcode is scanned.
200 // the default behavior is the following :
201 // - if there's a user with a matching ean, put it as the active 'client' and return true
202 // - else : return false.
203 barcode_client_action: function(code){
204 var partners = this.pos.partners;
205 for(var i = 0, len = partners.length; i < len; i++){
206 if(partners[i].ean13 === code.code){
207 this.pos.get('selectedOrder').set_client(partners[i]);
208 this.pos_widget.username.refresh();
209 this.pos.proxy.scan_item_success(code);
213 this.pos.proxy.scan_item_error_unrecognized(code);
215 //TODO start the transaction
218 // what happens when a discount barcode is scanned : the default behavior
219 // is to set the discount on the last order.
220 barcode_discount_action: function(code){
221 this.pos.proxy.scan_item_success(code);
222 var last_orderline = this.pos.get('selectedOrder').getLastOrderline();
224 last_orderline.set_discount(code.value)
228 // shows an action bar on the screen. The actionbar is automatically shown when you add a button
229 // with add_action_button()
230 show_action_bar: function(){
231 this.pos_widget.action_bar.show();
234 // hides the action bar. The actionbar is automatically hidden when it is empty
235 hide_action_bar: function(){
236 this.pos_widget.action_bar.hide();
239 // adds a new button to the action bar. The button definition takes three parameters, all optional :
240 // - label: the text below the button
241 // - icon: a small icon that will be shown
242 // - click: a callback that will be executed when the button is clicked.
243 // the method returns a reference to the button widget, and automatically show the actionbar.
244 add_action_button: function(button_def){
245 this.show_action_bar();
246 return this.pos_widget.action_bar.add_new_button(button_def);
249 // this method shows the screen and sets up all the widget related to this screen. Extend this method
250 // if you want to alter the behavior of the screen.
256 this.$el.removeClass('oe_hidden');
259 if(this.pos_widget.action_bar.get_button_count() > 0){
260 this.show_action_bar();
262 this.hide_action_bar();
265 // we add the help button by default. we do this because the buttons are cleared on each refresh so that
266 // the button stay local to each screen
267 this.pos_widget.left_action_bar.add_new_button({
269 icon: '/point_of_sale/static/src/img/icons/png48/help.png',
270 click: function(){ self.help_button_action(); },
274 this.cashier_mode = this.pos_widget.screen_selector.get_user_mode() === 'cashier';
276 this.pos_widget.set_numpad_visible(this.show_numpad && this.cashier_mode);
277 this.pos_widget.set_leftpane_visible(this.show_leftpane);
278 this.pos_widget.set_left_action_bar_visible(this.show_leftpane && !this.cashier_mode);
279 this.pos_widget.set_cashier_controls_visible(this.cashier_mode);
281 if(this.cashier_mode && this.pos.config.iface_self_checkout){
282 this.pos_widget.client_button.show();
284 this.pos_widget.client_button.hide();
286 if(this.cashier_mode){
287 this.pos_widget.close_button.show();
289 this.pos_widget.close_button.hide();
292 this.pos_widget.username.set_user_mode(this.pos_widget.screen_selector.get_user_mode());
294 this.pos.barcode_reader.set_action_callback({
295 'cashier': self.barcode_cashier_action ? function(code){ self.barcode_cashier_action(code); } : undefined ,
296 'product': self.barcode_product_action ? function(code){ self.barcode_product_action(code); } : undefined ,
297 'client' : self.barcode_client_action ? function(code){ self.barcode_client_action(code); } : undefined ,
298 'discount': self.barcode_discount_action ? function(code){ self.barcode_discount_action(code); } : undefined,
302 // this method is called when the screen is closed to make place for a new screen. this is a good place
303 // to put your cleanup stuff as it is guaranteed that for each show() there is one and only one close()
305 if(this.pos.barcode_reader){
306 this.pos.barcode_reader.reset_action_callbacks();
308 this.pos_widget.action_bar.destroy_buttons();
309 this.pos_widget.left_action_bar.destroy_buttons();
312 // this methods hides the screen. It's not a good place to put your cleanup stuff as it is called on the
313 // POS initialization.
317 this.$el.addClass('oe_hidden');
321 // we need this because some screens re-render themselves when they are hidden
322 // (due to some events, or magic, or both...) we must make sure they remain hidden.
323 // the good solution would probably be to make them not re-render themselves when they
325 renderElement: function(){
329 this.$el.addClass('oe_hidden');
335 module.PopUpWidget = module.PosBaseWidget.extend({
338 this.$el.removeClass('oe_hidden');
341 /* called before hide, when a popup is closed */
344 /* hides the popup. keep in mind that this is called in the initialization pass of the
345 * pos instantiation, so you don't want to do anything fancy in here */
348 this.$el.addClass('oe_hidden');
353 module.HelpPopupWidget = module.PopUpWidget.extend({
354 template:'HelpPopupWidget',
357 this.pos.proxy.help_needed();
360 this.$el.find('.button').off('click').click(function(){
361 self.pos_widget.screen_selector.close_popup();
365 this.pos.proxy.help_canceled();
369 module.ChooseReceiptPopupWidget = module.PopUpWidget.extend({
370 template:'ChooseReceiptPopupWidget',
373 this.renderElement();
375 var currentOrder = self.pos.get('selectedOrder');
377 this.$('.button.receipt').off('click').click(function(){
378 currentOrder.set_receipt_type('receipt');
379 self.pos_widget.screen_selector.set_current_screen('products');
382 this.$('.button.invoice').off('click').click(function(){
383 currentOrder.set_receipt_type('invoice');
384 self.pos_widget.screen_selector.set_current_screen('products');
387 get_client_name: function(){
388 var client = this.pos.get('selectedOrder').get_client();
397 module.ErrorPopupWidget = module.PopUpWidget.extend({
398 template:'ErrorPopupWidget',
402 this.pos.proxy.help_needed();
403 this.pos.proxy.scan_item_error_unrecognized();
405 this.pos.barcode_reader.save_callbacks();
406 this.pos.barcode_reader.reset_action_callbacks();
407 this.pos.barcode_reader.set_action_callback({
408 'cashier': function(code){
409 clearInterval(this.intervalID);
410 self.pos.proxy.cashier_mode_activated();
411 self.pos_widget.screen_selector.set_user_mode('cashier');
414 this.$('.footer .button').off('click').click(function(){
415 self.pos_widget.screen_selector.close_popup();
420 this.pos.proxy.help_canceled();
421 this.pos.barcode_reader.restore_callbacks();
425 module.ProductErrorPopupWidget = module.ErrorPopupWidget.extend({
426 template:'ProductErrorPopupWidget',
429 module.ErrorSessionPopupWidget = module.ErrorPopupWidget.extend({
430 template:'ErrorSessionPopupWidget',
433 module.ErrorNegativePricePopupWidget = module.ErrorPopupWidget.extend({
434 template:'ErrorNegativePricePopupWidget',
437 module.ErrorNoClientPopupWidget = module.ErrorPopupWidget.extend({
438 template: 'ErrorNoClientPopupWidget',
441 module.ErrorInvoiceTransferPopupWidget = module.ErrorPopupWidget.extend({
442 template: 'ErrorInvoiceTransferPopupWidget',
445 module.ScaleInviteScreenWidget = module.ScreenWidget.extend({
446 template:'ScaleInviteScreenWidget',
449 previous_screen:'products',
454 var queue = this.pos.proxy_queue;
456 queue.schedule(function(){
457 return self.pos.proxy.weighting_start();
458 },{ important: true });
460 queue.schedule(function(){
461 return self.pos.proxy.weighting_read_kg().then(function(weight){
463 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
466 },{duration: 100, repeat: true});
468 this.add_action_button({
470 icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
472 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
479 this.pos.proxy_queue.clear();
480 this.pos.proxy_queue.schedule(function(){
481 return self.pos.proxy.weighting_end();
482 },{ important: true });
486 module.ScaleScreenWidget = module.ScreenWidget.extend({
487 template:'ScaleScreenWidget',
489 next_screen: 'products',
490 previous_screen: 'products',
495 var queue = this.pos.proxy_queue;
498 this.renderElement();
500 this.hotkey_handler = function(event){
501 if(event.which === 13){
502 self.order_product();
503 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
504 }else if(event.which === 27){
505 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
509 $('body').on('keyup',this.hotkey_handler);
511 this.add_action_button({
513 icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
515 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
519 this.validate_button = this.add_action_button({
520 label: _t('Validate'),
521 icon: '/point_of_sale/static/src/img/icons/png48/validate.png',
523 self.order_product();
524 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
528 queue.schedule(function(){
529 return self.pos.proxy.scale_read().then(function(weight){
530 self.set_weight(weight.weight);
532 },{duration:50, repeat: true});
535 renderElement: function(){
538 this.$('.product-picture').click(function(){
539 self.order_product();
540 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
543 get_product: function(){
544 var ss = this.pos_widget.screen_selector;
546 return ss.get_current_screen_param('product');
551 order_product: function(){
552 this.pos.get('selectedOrder').addProduct(this.get_product(),{ quantity: this.weight });
554 get_product_name: function(){
555 var product = this.get_product();
556 return (product ? product.name : undefined) || 'Unnamed Product';
558 get_product_price: function(){
559 var product = this.get_product();
560 return (product ? product.price : 0) || 0;
562 set_weight: function(weight){
563 this.weight = weight;
564 this.$('.js-weight').text(this.get_product_weight_string());
566 get_product_weight_string: function(){
567 return (this.weight || 0).toFixed(3) + ' Kg';
569 get_product_image_url: function(){
570 var product = this.get_product();
572 return window.location.origin + '/web/binary/image?model=product.product&field=image_medium&id='+product.id;
580 $('body').off('keyup',this.hotkey_handler);
582 this.pos.proxy_queue.clear();
587 module.ClientPaymentScreenWidget = module.ScreenWidget.extend({
588 template:'ClientPaymentScreenWidget',
590 next_screen: 'welcome',
591 previous_screen: 'products',
597 this.queue = new module.JobQueue();
598 this.canceled = false;
601 // initiates the connection to the payment terminal and starts the update requests
602 this.start = function(){
603 var def = new $.Deferred();
604 self.pos.proxy.payment_request(self.pos.get('selectedOrder').getDueLeft())
607 self.queue.schedule(self.update);
608 }else if(ack.indexOf('error') === 0){
609 console.error('cannot make payment. TODO');
611 console.error('unknown payment request return value:',ack);
618 // gets updated status from the payment terminal and performs the appropriate consequences
619 this.update = function(){
620 var def = new $.Deferred();
622 return def.resolve();
624 self.pos.proxy.payment_status()
625 .done(function(status){
626 if(status.status === 'paid'){
628 var currentOrder = self.pos.get('selectedOrder');
630 //we get the first cashregister marked as self-checkout
631 var selfCheckoutRegisters = [];
632 for(var i = 0; i < self.pos.cashregisters.length; i++){
633 var cashregister = self.pos.cashregisters[i];
634 if(cashregister.self_checkout_payment_method){
635 selfCheckoutRegisters.push(cashregister);
639 var cashregister = selfCheckoutRegisters[0] || self.pos.cashregisters[0];
640 currentOrder.addPaymentline(cashregister);
641 self.pos.push_order(currentOrder)
642 currentOrder.destroy();
643 self.pos.proxy.transaction_end();
644 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
646 }else if(status.status.indexOf('error') === 0){
647 console.error('error in payment request. TODO');
648 }else if(status.status === 'waiting'){
649 self.queue.schedule(self.update,200);
651 console.error('unknown status value:',status.status);
658 // cancels a payment.
659 this.cancel = function(){
660 if(!self.paid && !self.canceled){
661 self.canceled = true;
662 self.pos.proxy.payment_cancel();
663 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
666 return (new $.Deferred()).resolve();
669 if(this.pos.get('selectedOrder').getDueLeft() <= 0){
670 this.pos_widget.screen_selector.show_popup('error-negative-price');
672 this.queue.schedule(this.start);
675 this.add_action_button({
677 icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
679 self.queue.schedule(self.cancel);
680 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
686 this.queue.schedule(this.cancel);
693 module.WelcomeScreenWidget = module.ScreenWidget.extend({
694 template:'WelcomeScreenWidget',
696 next_screen: 'products',
699 show_leftpane: false,
702 $('.goodbye-message').click(function(){
703 $(this).addClass('oe_hidden');
707 barcode_product_action: function(code){
708 this.pos.proxy.transaction_start();
712 barcode_client_action: function(code){
713 this.pos.proxy.transaction_start();
715 $('.goodbye-message').addClass('oe_hidden');
716 this.pos_widget.screen_selector.show_popup('choose-receipt');
723 this.add_action_button({
725 icon: '/point_of_sale/static/src/img/icons/png48/help.png',
727 $('.goodbye-message').css({opacity:1}).addClass('oe_hidden');
728 self.help_button_action();
732 $('.goodbye-message').css({opacity:1}).removeClass('oe_hidden');
733 setTimeout(function(){
734 $('.goodbye-message').animate({opacity:0},500,'swing',function(){$('.goodbye-message').addClass('oe_hidden');});
739 module.ProductScreenWidget = module.ScreenWidget.extend({
740 template:'ProductScreenWidget',
742 scale_screen: 'scale',
743 client_scale_screen : 'scale_invite',
744 client_next_screen: 'client_payment',
749 start: function(){ //FIXME this should work as renderElement... but then the categories aren't properly set. explore why
752 this.product_list_widget = new module.ProductListWidget(this,{
753 click_product_action: function(product){
754 if(product.to_weight && self.pos.config.iface_electronic_scale){
755 self.pos_widget.screen_selector.set_current_screen( self.cashier_mode ? self.scale_screen : self.client_scale_screen, {product: product});
757 self.pos.get('selectedOrder').addProduct(product);
760 product_list: this.pos.db.get_product_by_category(0)
762 this.product_list_widget.replace($('.placeholder-ProductListWidget'));
764 this.product_categories_widget = new module.ProductCategoriesWidget(this,{
765 product_list_widget: this.product_list_widget,
767 this.product_categories_widget.replace($('.placeholder-ProductCategoriesWidget'));
774 this.product_categories_widget.reset_category();
776 this.pos_widget.order_widget.set_editable(true);
778 if(this.pos_widget.screen_selector.current_mode === 'client'){
779 this.add_action_button({
781 icon: '/point_of_sale/static/src/img/icons/png48/go-next.png',
783 self.pos_widget.screen_selector.set_current_screen(self.client_next_screen);
792 this.pos_widget.order_widget.set_editable(false);
794 if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
795 this.pos_widget.onscreen_keyboard.hide();
800 module.ReceiptScreenWidget = module.ScreenWidget.extend({
801 template: 'ReceiptScreenWidget',
810 var print_button = this.add_action_button({
812 icon: '/point_of_sale/static/src/img/icons/png48/printer.png',
813 click: function(){ self.print(); },
816 var finish_button = this.add_action_button({
817 label: _t('Next Order'),
818 icon: '/point_of_sale/static/src/img/icons/png48/go-next.png',
819 click: function() { self.finishOrder(); },
826 // The problem is that in chrome the print() is asynchronous and doesn't
827 // execute until all rpc are finished. So it conflicts with the rpc used
828 // to send the orders to the backend, and the user is able to go to the next
829 // screen before the printing dialog is opened. The problem is that what's
830 // printed is whatever is in the page when the dialog is opened and not when it's called,
831 // and so you end up printing the product list instead of the receipt...
833 // Fixing this would need a re-architecturing
834 // of the code to postpone sending of orders after printing.
836 // But since the print dialog also blocks the other asynchronous calls, the
837 // button enabling in the setTimeout() is blocked until the printing dialog is
838 // closed. But the timeout has to be big enough or else it doesn't work
839 // 2 seconds is the same as the default timeout for sending orders and so the dialog
840 // should have appeared before the timeout... so yeah that's not ultra reliable.
842 finish_button.set_disabled(true);
843 setTimeout(function(){
844 finish_button.set_disabled(false);
850 finishOrder: function() {
851 this.pos.get('selectedOrder').destroy();
853 refresh: function() {
854 var order = this.pos.get('selectedOrder');
855 $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{
858 orderlines: order.get('orderLines').models,
859 paymentlines: order.get('paymentLines').models,
867 module.PaymentScreenWidget = module.ScreenWidget.extend({
868 template: 'PaymentScreenWidget',
869 back_screen: 'products',
870 next_screen: 'receipt',
871 init: function(parent, options) {
873 this._super(parent,options);
875 this.pos.bind('change:selectedOrder',function(){
877 this.renderElement();
882 this.line_delete_handler = function(event){
884 while(node && !node.classList.contains('paymentline')){
885 node = node.parentNode;
888 self.pos.get('selectedOrder').removePaymentline(node.line)
890 event.stopPropagation();
893 this.line_change_handler = function(event){
895 while(node && !node.classList.contains('paymentline')){
896 node = node.parentNode;
899 node.line.set_amount(this.value);
904 this.line_click_handler = function(event){
906 while(node && !node.classList.contains('paymentline')){
907 node = node.parentNode;
910 self.pos.get('selectedOrder').selectPaymentline(node.line);
914 this.hotkey_handler = function(event){
915 if(event.which === 13){
916 self.validate_order();
917 }else if(event.which === 27){
927 this.enable_numpad();
928 this.focus_selected_line();
930 document.body.addEventListener('keyup', this.hotkey_handler);
934 this.add_action_button({
936 icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
942 this.add_action_button({
943 label: _t('Validate'),
945 icon: '/point_of_sale/static/src/img/icons/png48/validate.png',
947 self.validate_order();
951 if( this.pos.config.iface_invoicing ){
952 this.add_action_button({
955 icon: '/point_of_sale/static/src/img/icons/png48/invoice.png',
957 self.validate_order({invoice: true});
962 if( this.pos.config.iface_cashdrawer ){
963 this.add_action_button({
966 icon: '/point_of_sale/static/src/img/open-cashbox.png',
968 self.pos.proxy.open_cashbox();
973 this.update_payment_summary();
978 this.disable_numpad();
979 document.body.removeEventListener('keyup',this.hotkey_handler);
981 remove_empty_lines: function(){
982 var order = this.pos.get('selectedOrder');
983 var lines = order.get('paymentLines').models.slice(0);
984 for(var i = 0; i < lines.length; i++){
986 if(line.get_amount() === 0){
987 order.removePaymentline(line);
992 this.remove_empty_lines();
993 this.pos_widget.screen_selector.set_current_screen(this.back_screen);
995 bind_events: function() {
997 this.old_order.unbind(null,null,this);
999 var order = this.pos.get('selectedOrder');
1000 order.bind('change:selected_paymentline',this.focus_selected_line,this);
1002 this.old_order = order;
1004 if(this.old_paymentlines){
1005 this.old_paymentlines.unbind(null,null,this);
1007 var paymentlines = order.get('paymentLines');
1008 paymentlines.bind('add', this.add_paymentline, this);
1009 paymentlines.bind('change:selected', this.rerender_paymentline, this);
1010 paymentlines.bind('change:amount', function(line){
1011 if(!line.selected && line.node){
1012 line.node.value = line.amount.toFixed(2);
1014 this.update_payment_summary();
1016 paymentlines.bind('remove', this.remove_paymentline, this);
1017 paymentlines.bind('all', this.update_payment_summary, this);
1019 this.old_paymentlines = paymentlines;
1021 if(this.old_orderlines){
1022 this.old_orderlines.unbind(null,null,this);
1024 var orderlines = order.get('orderLines');
1025 orderlines.bind('all', this.update_payment_summary, this);
1027 this.old_orderlines = orderlines;
1029 focus_selected_line: function(){
1030 var line = this.pos.get('selectedOrder').selected_paymentline;
1032 var input = line.node.querySelector('input');
1036 var value = input.value;
1039 if(this.numpad_state){
1040 this.numpad_state.reset();
1043 if(Number(value) === 0){
1046 input.value = value;
1051 add_paymentline: function(line) {
1052 var list_container = this.el.querySelector('.payment-lines');
1053 list_container.appendChild(this.render_paymentline(line));
1055 if(this.numpad_state){
1056 this.numpad_state.reset();
1059 render_paymentline: function(line){
1060 var el_html = openerp.qweb.render('Paymentline',{widget: this, line: line});
1061 el_html = _.str.trim(el_html);
1063 var el_node = document.createElement('tbody');
1064 el_node.innerHTML = el_html;
1065 el_node = el_node.childNodes[0];
1066 el_node.line = line;
1067 el_node.querySelector('.paymentline-delete')
1068 .addEventListener('click', this.line_delete_handler);
1069 el_node.addEventListener('click', this.line_click_handler);
1070 el_node.querySelector('input')
1071 .addEventListener('keyup', this.line_change_handler);
1073 line.node = el_node;
1077 rerender_paymentline: function(line){
1078 var old_node = line.node;
1079 var new_node = this.render_paymentline(line);
1081 old_node.parentNode.replaceChild(new_node,old_node);
1083 remove_paymentline: function(line){
1084 line.node.parentNode.removeChild(line.node);
1085 line.node = undefined;
1087 renderElement: function(){
1090 var paymentlines = this.pos.get('selectedOrder').get('paymentLines').models;
1091 var list_container = this.el.querySelector('.payment-lines');
1093 for(var i = 0; i < paymentlines.length; i++){
1094 list_container.appendChild(this.render_paymentline(paymentlines[i]));
1097 this.update_payment_summary();
1099 update_payment_summary: function() {
1100 var currentOrder = this.pos.get('selectedOrder');
1101 var paidTotal = currentOrder.getPaidTotal();
1102 var dueTotal = currentOrder.getTotalTaxIncluded();
1103 var remaining = dueTotal > paidTotal ? dueTotal - paidTotal : 0;
1104 var change = paidTotal > dueTotal ? paidTotal - dueTotal : 0;
1106 this.$('.payment-due-total').html(this.format_currency(dueTotal));
1107 this.$('.payment-paid-total').html(this.format_currency(paidTotal));
1108 this.$('.payment-remaining').html(this.format_currency(remaining));
1109 this.$('.payment-change').html(this.format_currency(change));
1110 if(currentOrder.selected_orderline === undefined){
1111 remaining = 1; // What is this ?
1114 if(this.pos_widget.action_bar){
1115 this.pos_widget.action_bar.set_button_disabled('validation', !this.is_paid());
1116 this.pos_widget.action_bar.set_button_disabled('invoice', !this.is_paid());
1119 is_paid: function(){
1120 var currentOrder = this.pos.get('selectedOrder');
1121 return (currentOrder.getTotalTaxIncluded() < 0.000001
1122 || currentOrder.getPaidTotal() + 0.000001 >= currentOrder.getTotalTaxIncluded());
1125 validate_order: function(options) {
1127 options = options || {};
1129 var currentOrder = this.pos.get('selectedOrder');
1131 if(!this.is_paid()){
1135 if( this.pos.config.iface_cashdrawer
1136 && this.pos.get('selectedOrder').get('paymentLines').find( function(pl){
1137 return pl.cashregister.journal.type === 'cash';
1139 this.pos.proxy.open_cashbox();
1142 if(options.invoice){
1143 // deactivate the validation button while we try to send the order
1144 this.pos_widget.action_bar.set_button_disabled('validation',true);
1145 this.pos_widget.action_bar.set_button_disabled('invoice',true);
1147 var invoiced = this.pos.push_and_invoice_order(currentOrder);
1149 invoiced.fail(function(error){
1150 if(error === 'error-no-client'){
1151 self.pos_widget.screen_selector.show_popup('error-no-client');
1153 self.pos_widget.screen_selector.show_popup('error-invoice-transfer');
1155 self.pos_widget.action_bar.set_button_disabled('validation',false);
1156 self.pos_widget.action_bar.set_button_disabled('invoice',false);
1159 invoiced.done(function(){
1160 self.pos_widget.action_bar.set_button_disabled('validation',false);
1161 self.pos_widget.action_bar.set_button_disabled('invoice',false);
1162 self.pos.get('selectedOrder').destroy();
1166 this.pos.push_order(currentOrder)
1167 if(this.pos.config.iface_print_via_proxy){
1168 var receipt = currentOrder.export_for_printing();
1169 this.pos.proxy.print_receipt(QWeb.render('XmlReceipt',{
1172 this.pos.get('selectedOrder').destroy(); //finish order and go back to scan screen
1174 this.pos_widget.screen_selector.set_current_screen(this.next_screen);
1178 // hide onscreen (iOS) keyboard
1179 setTimeout(function(){
1180 document.activeElement.blur();
1184 enable_numpad: function(){
1185 this.disable_numpad(); //ensure we don't register the callbacks twice
1186 this.numpad_state = this.pos_widget.numpad.state;
1187 if(this.numpad_state){
1188 this.numpad_state.reset();
1189 this.numpad_state.changeMode('payment');
1190 this.numpad_state.bind('set_value', this.set_value, this);
1191 this.numpad_state.bind('change:mode', this.set_mode_back_to_payment, this);
1195 disable_numpad: function(){
1196 if(this.numpad_state){
1197 this.numpad_state.unbind('set_value', this.set_value);
1198 this.numpad_state.unbind('change:mode',this.set_mode_back_to_payment);
1201 set_mode_back_to_payment: function() {
1202 this.numpad_state.set({mode: 'payment'});
1204 set_value: function(val) {
1205 var selected_line =this.pos.get('selectedOrder').selected_paymentline;
1207 selected_line.set_amount(val);
1208 selected_line.node.querySelector('input').value = selected_line.amount.toFixed(2);