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;
21 module.ScreenSelector = instance.web.Class.extend({
22 init: function(options){
23 this.pos = options.pos;
25 this.screen_set = options.screen_set || {};
27 this.popup_set = options.popup_set || {};
29 this.default_client_screen = options.default_client_screen;
30 this.default_cashier_screen = options.default_cashier_screen;
32 this.current_popup = null;
34 this.current_mode = options.default_mode || 'client';
36 this.current_screen = null;
38 for(screen_name in this.screen_set){
39 this.screen_set[screen_name].hide();
42 for(popup_name in this.popup_set){
43 this.popup_set[popup_name].hide();
46 this.selected_order = this.pos.get('selectedOrder');
47 this.selected_order.set_screen_data({
48 client_screen: this.default_client_screen,
49 cashier_screen: this.default_cashier_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){
60 if(this.current_popup){
63 this.current_popup = this.popup_set[name];
64 this.current_popup.show();
66 close_popup: function(){
67 if(this.current_popup){
68 this.current_popup.hide();
69 this.current_popup = null;
72 load_saved_screen: function(){
75 var selectedOrder = this.pos.get('selectedOrder');
77 if(this.current_mode === 'client'){
78 this.set_current_screen(selectedOrder.get_screen_data('client_screen') || this.default_client_screen,null,'refresh');
79 }else if(this.current_mode === 'cashier'){
80 this.set_current_screen(selectedOrder.get_screen_data('cashier_screen') || this.default_cashier_screen,null,'refresh');
82 this.selected_order = selectedOrder;
84 set_user_mode: function(user_mode){
85 if(user_mode !== this.current_mode){
87 this.current_mode = user_mode;
88 this.load_saved_screen();
91 get_user_mode: function(){
92 return this.current_mode;
94 set_current_screen: function(screen_name,params,refresh){
95 var screen = this.screen_set[screen_name];
97 console.error("ERROR: set_current_screen("+screen_name+") : screen not found");
101 var selectedOrder = this.pos.get('selectedOrder');
102 if(this.current_mode === 'client'){
103 selectedOrder.set_screen_data('client_screen',screen_name);
105 selectedOrder.set_screen_data('client_screen_params',params);
108 selectedOrder.set_screen_data('cashier_screen',screen_name);
110 selectedOrder.set_screen_data('cashier_screen_params',params);
114 if(screen && (refresh || screen !== this.current_screen)){
115 if(this.current_screen){
116 this.current_screen.close();
117 this.current_screen.hide();
119 this.current_screen = screen;
120 this.current_screen.show();
123 get_current_screen_param: function(param){
124 var selected_order = this.pos.get('selectedOrder');
125 if(this.current_mode === 'client'){
126 var params = selected_order.get_screen_data('client_screen_params');
128 var params = selected_order.get_screen_data('cashier_screen_params');
131 return params[param];
136 set_default_screen: function(){
137 this.set_current_screen(this.current_mode === 'client' ? this.default_client_screen : this.default_cashier_screen);
141 module.ScreenWidget = module.PosBaseWidget.extend({
146 init: function(parent,options){
147 this._super(parent,options);
151 help_button_action: function(){
152 this.pos_widget.screen_selector.show_popup('help');
155 barcode_product_screen: 'products', //if defined, this screen will be loaded when a product is scanned
156 barcode_product_error_popup: 'error', //if defined, this popup will be loaded when there's an error in the popup
158 // what happens when a product is scanned :
159 // it will add the product to the order and go to barcode_product_screen. Or show barcode_product_error_popup if
161 barcode_product_action: function(ean){
163 if(pos_widget.scan_product(ean)){
164 self.pos.proxy.scan_item_success(ean);
165 if(self.barcode_product_screen){
166 self.pos_widget.screen_selector.set_current_screen(self.barcode_product_screen);
169 self.pos.proxy.scan_item_error_unrecognized(ean);
170 if(self.barcode_product_error_popup && self.pos_widget.screen_selector.get_user_mode() !== 'cashier'){
171 self.pos_widget.screen_selector.show_popup(self.barcode_product_error_popup);
176 // what happens when a cashier id barcode is scanned.
177 // the default behavior is the following :
178 // - if there's a user with a matching ean, put it as the active 'cashier', go to cashier mode, and return true
179 // - else : do nothing and return false. You probably want to extend this to show and appropriate error popup...
180 barcode_cashier_action: function(ean){
181 var users = this.pos.get('user_list');
182 for(var i = 0, len = users.length; i < len; i++){
183 if(users[i].ean13 === ean.ean){
184 this.pos.set('cashier',users[i]);
185 this.pos_widget.username.refresh();
186 this.pos.proxy.cashier_mode_activated();
187 this.pos_widget.screen_selector.set_user_mode('cashier');
191 this.pos.proxy.scan_item_unrecognized(ean);
195 // what happens when a client id barcode is scanned.
196 // the default behavior is the following :
197 // - if there's a user with a matching ean, put it as the active 'client' and return true
198 // - else : return false.
199 barcode_client_action: function(ean){
200 var users = this.pos.get('user_list');
201 for(var i = 0, len = users.length; i < len; i++){
202 if(users[i].ean13 === ean.ean){
203 this.pos.get('selectedOrder').set_client(users[i]);
204 this.pos_widget.username.refresh();
205 this.pos.proxy.scan_item_success(ean);
209 this.pos.proxy.scan_item_unrecognized(ean);
211 //TODO start the transaction
214 // what happens when a discount barcode is scanned : the default behavior
215 // is to set the discount on the last order.
216 barcode_discount_action: function(ean){
217 this.pos.proxy.scan_item_success(ean);
218 var last_orderline = this.pos.get('selectedOrder').getLastOrderline();
220 last_orderline.set_discount(ean.value)
224 // shows an action bar on the screen. The actionbar is automatically shown when you add a button
225 // with add_action_button()
226 show_action_bar: function(){
227 this.pos_widget.action_bar.show();
228 this.$element.css({'bottom':'105px'});
231 // hides the action bar. The actionbar is automatically hidden when it is empty
232 hide_action_bar: function(){
233 this.pos_widget.action_bar.hide();
234 this.$element.css({'bottom':'0px'});
237 // adds a new button to the action bar. The button definition takes three parameters, all optional :
238 // - label: the text below the button
239 // - icon: a small icon that will be shown
240 // - click: a callback that will be executed when the button is clicked.
241 // the method returns a reference to the button widget, and automatically show the actionbar.
242 add_action_button: function(button_def){
243 this.show_action_bar();
244 return this.pos_widget.action_bar.add_new_button(button_def);
247 // this method shows the screen and sets up all the widget related to this screen. Extend this method
248 // if you want to alter the behavior of the screen.
254 this.$element.show();
257 if(this.pos_widget.action_bar.get_button_count() > 0){
258 this.show_action_bar();
260 this.hide_action_bar();
263 // we add the help button by default. we do this because the buttons are cleared on each refresh so that
264 // the button stay local to each screen
265 this.pos_widget.left_action_bar.add_new_button({
267 icon: '/point_of_sale/static/src/img/icons/png48/help.png',
268 click: function(){ self.help_button_action(); },
272 var cashier_mode = this.pos_widget.screen_selector.get_user_mode() === 'cashier';
274 this.pos_widget.set_numpad_visible(this.show_numpad && cashier_mode);
275 this.pos_widget.set_leftpane_visible(this.show_leftpane);
276 this.pos_widget.set_left_action_bar_visible(this.show_leftpane && !cashier_mode);
277 this.pos_widget.set_cashier_controls_visible(cashier_mode);
279 if(cashier_mode && this.pos.use_selfcheckout){
280 this.pos_widget.client_button.show();
282 this.pos_widget.client_button.hide();
285 this.pos_widget.close_button.show();
287 this.pos_widget.close_button.hide();
290 this.pos_widget.username.set_user_mode(this.pos_widget.screen_selector.get_user_mode());
292 this.pos.barcode_reader.set_action_callback({
293 'cashier': self.barcode_cashier_action ? function(ean){ self.barcode_cashier_action(ean); } : undefined ,
294 'product': self.barcode_product_action ? function(ean){ self.barcode_product_action(ean); } : undefined ,
295 'client' : self.barcode_client_action ? function(ean){ self.barcode_client_action(ean); } : undefined ,
296 'discount': self.barcode_discount_action ? function(ean){ self.barcode_discount_action(ean); } : undefined,
300 // this method is called when the screen is closed to make place for a new screen. this is a good place
301 // to put your cleanup stuff as it is guaranteed that for each show() there is one and only one close()
303 if(this.pos.barcode_reader){
304 this.pos.barcode_reader.reset_action_callbacks();
306 this.pos_widget.action_bar.destroy_buttons();
307 this.pos_widget.left_action_bar.destroy_buttons();
310 // this methods hides the screen. It's not a good place to put your cleanup stuff as it is called on the
311 // POS initialization.
315 this.$element.hide();
319 // we need this because some screens re-render themselves when they are hidden
320 // (due to some events, or magic, or both...) we must make sure they remain hidden.
321 // the good solution would probably be to make them not re-render themselves when they
323 renderElement: function(){
327 this.$element.hide();
333 module.PopUpWidget = module.PosBaseWidget.extend({
336 this.$element.show();
341 this.$element.hide();
346 module.HelpPopupWidget = module.PopUpWidget.extend({
347 template:'HelpPopupWidget',
350 this.pos.proxy.help_needed();
353 this.$element.find('.button').off('click').click(function(){
354 self.pos_widget.screen_selector.close_popup();
355 self.pos.proxy.help_canceled();
360 module.ErrorPopupWidget = module.PopUpWidget.extend({
361 template:'ErrorPopupWidget',
365 this.pos.proxy.help_needed();
366 this.pos.proxy.scan_item_error_unrecognized();
368 this.pos.barcode_reader.save_callbacks();
369 this.pos.barcode_reader.reset_action_callbacks();
370 this.pos.barcode_reader.set_action_callback({
371 'cashier': function(ean){
372 clearInterval(this.intervalID);
373 self.pos.proxy.cashier_mode_activated();
374 self.pos_widget.screen_selector.set_user_mode('cashier');
380 this.pos.proxy.help_canceled();
381 this.pos.barcode_reader.restore_callbacks();
385 module.ErrorProductNotRecognizedPopupWidget = module.ErrorPopupWidget.extend({
386 template:'ErrorProductNotRecognizedPopupWidget',
389 module.ErrorNoSessionPopupWidget = module.ErrorPopupWidget.extend({
390 template:'ErrorNoSessionPopupWidget',
393 module.ScaleInviteScreenWidget = module.ScreenWidget.extend({
394 template:'ScaleInviteScreenWidget',
397 previous_screen:'products',
403 self.pos.proxy.weighting_start();
405 this.intervalID = setInterval(function(){
406 var weight = self.pos.proxy.weighting_read_kg();
408 clearInterval(this.intervalID);
409 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
413 this.add_action_button({
415 icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
417 clearInterval(this.intervalID);
418 self.pos.proxy.weighting_end();
419 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
425 clearInterval(this.intervalID);
429 module.ScaleScreenWidget = module.ScreenWidget.extend({
430 template:'ScaleScreenWidget',
432 next_screen: 'products',
433 previous_screen: 'products',
437 this.renderElement();
441 this.add_action_button({
443 icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
445 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
449 this.validate_button = this.add_action_button({
451 icon: '/point_of_sale/static/src/img/icons/png48/validate.png',
453 self.order_product();
454 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
458 this.pos.proxy.weighting_start();
459 this.intervalID = setInterval(function(){
460 var weight = self.pos.proxy.weighting_read_kg();
461 if(weight != self.weight){
462 self.weight = weight;
463 self.renderElement();
467 renderElement: function(){
470 this.$('.product-picture').click(function(){
471 self.order_product();
472 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
475 get_product: function(){
476 var ss = this.pos_widget.screen_selector;
478 return ss.get_current_screen_param('product');
483 order_product: function(){
484 var weight = this.pos.proxy.weighting_read_kg();
485 this.pos.get('selectedOrder').addProduct(this.get_product(),{ quantity:weight });
487 get_product_name: function(){
488 var product = this.get_product();
489 return (product ? product.get('name') : undefined) || 'Unnamed Product';
491 get_product_price: function(){
492 var product = this.get_product();
493 return (product ? product.get('list_price') : 0) || 0;
495 get_product_image: function(){
496 var product = this.get_product();
497 return product ? product.get('image') : undefined;
499 get_product_weight: function(){
500 return this.weight || 0;
504 clearInterval(this.intervalID);
505 this.pos.proxy.weighting_end();
509 module.ClientPaymentScreenWidget = module.ScreenWidget.extend({
510 template:'ClientPaymentScreenWidget',
512 next_screen: 'welcome',
513 previous_screen: 'products',
519 this.pos.proxy.payment_request(this.pos.get('selectedOrder').getDueLeft(),'card','info'); //TODO TOTAL
521 this.intervalID = setInterval(function(){
522 var payment = self.pos.proxy.is_payment_accepted();
523 if(payment === 'payment_accepted'){
524 clearInterval(this.intervalID);
526 var currentOrder = self.pos.get('selectedOrder');
528 //we get the first cashregister marked as self-checkout
529 var selfCheckoutRegisters = [];
530 for(var i = 0; i < this.pos.get('cashRegisters').models.length; i++){
531 var cashregister = this.pos.get('cashRegisters').models[i];
532 if(cashregister.self_checkout_payment_method){
533 selfCheckoutRegisters.push(cashregister);
537 var cashregister = selfCheckoutRegisters[0] || this.pos.get('cashRegisters').models[0];
538 currentOrder.addPaymentLine(cashregister);
540 self.pos.push_order(currentOrder.exportAsJSON()).then(function() {
541 currentOrder.destroy();
542 self.pos.proxy.transaction_end();
543 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
545 }else if(payment === 'payment_rejected'){
546 clearInterval(this.intervalID);
547 //TODO show a tryagain thingie ?
551 this.add_action_button({
553 icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
555 clearInterval(this.intervalID);
556 self.pos.proxy.payment_canceled();
557 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
563 clearInterval(this.intervalID);
567 module.WelcomeScreenWidget = module.ScreenWidget.extend({
568 template:'WelcomeScreenWidget',
570 next_screen: 'products',
573 show_leftpane: false,
575 barcode_client_action: function(ean){
577 this.pos_widget.screen_selector.set_current_screen(this.next_screen);
583 $('.goodbye-message').css({opacity:1}).show();
584 setTimeout(function(){
585 $('.goodbye-message').animate({opacity:0},500,'swing',function(){$('.goodbye-message').hide();});
590 module.ProductScreenWidget = module.ScreenWidget.extend({
591 template:'ProductScreenWidget',
593 scale_screen: 'scale_invite',
594 client_next_screen: 'client_payment',
599 start: function(){ //FIXME this should work as renderElement... but then the categories aren't properly set. explore why
601 this.product_categories_widget = new module.ProductCategoriesWidget(this,{});
602 this.product_categories_widget.replace($('.placeholder-ProductCategoriesWidget'));
604 this.product_list_widget = new module.ProductListWidget(this,{
605 click_product_action: function(product){
606 if(product.get('to_weight') && self.pos.use_scale){
607 self.pos_widget.screen_selector.set_current_screen(self.scale_screen, {product: product});
609 self.pos.get('selectedOrder').addProduct(product);
613 this.product_list_widget.replace($('.placeholder-ProductListWidget'));
620 this.product_categories_widget.reset_category();
622 this.pos_widget.order_widget.set_numpad_state(this.pos_widget.numpad.state);
623 if(this.pos.use_virtual_keyboard){
624 this.pos_widget.onscreen_keyboard.connect();
627 if(this.pos_widget.screen_selector.current_mode === 'client'){
628 this.add_action_button({
630 icon: '/point_of_sale/static/src/img/icons/png48/go-next.png',
632 self.pos_widget.screen_selector.set_current_screen(self.client_next_screen);
640 this.pos_widget.order_widget.set_numpad_state(null);
641 this.pos_widget.payment_screen.set_numpad_state(null);
646 module.ReceiptScreenWidget = module.ScreenWidget.extend({
647 template: 'ReceiptScreenWidget',
652 init: function(parent, options) {
653 this._super(parent,options);
654 this.model = options.model;
655 this.user = this.pos.get('user');
656 this.company = this.pos.get('company');
657 this.shop_obj = this.pos.get('shop');
659 renderElement: function() {
661 this.pos.bind('change:selectedOrder', this.change_selected_order, this);
662 this.change_selected_order();
668 this.add_action_button({
670 icon: '/point_of_sale/static/src/img/icons/png48/printer.png',
671 click: function(){ self.print(); },
674 this.add_action_button({
676 icon: '/point_of_sale/static/src/img/icons/png48/go-next.png',
677 click: function() { self.finishOrder(); },
683 finishOrder: function() {
684 this.pos.get('selectedOrder').destroy();
686 change_selected_order: function() {
687 if (this.currentOrderLines)
688 this.currentOrderLines.unbind();
689 this.currentOrderLines = (this.pos.get('selectedOrder')).get('orderLines');
690 this.currentOrderLines.bind('add', this.refresh, this);
691 this.currentOrderLines.bind('change', this.refresh, this);
692 this.currentOrderLines.bind('remove', this.refresh, this);
693 if (this.currentPaymentLines)
694 this.currentPaymentLines.unbind();
695 this.currentPaymentLines = (this.pos.get('selectedOrder')).get('paymentLines');
696 this.currentPaymentLines.bind('all', this.refresh, this);
699 refresh: function() {
700 this.currentOrder = this.pos.get('selectedOrder');
701 $('.pos-receipt-container', this.$element).html(QWeb.render('PosTicket',{widget:this}));
705 module.PaymentScreenWidget = module.ScreenWidget.extend({
706 template: 'PaymentScreenWidget',
707 back_screen: 'products',
708 next_screen: 'receipt',
709 init: function(parent, options) {
710 this._super(parent,options);
711 this.model = options.model;
712 this.pos.bind('change:selectedOrder', this.change_selected_order, this);
713 this.bindPaymentLineEvents();
714 this.bind_orderline_events();
720 if(this.pos.use_cashbox){
721 this.pos.proxy.open_cashbox();
724 this.set_numpad_state(this.pos_widget.numpad.state);
726 this.back_button = this.add_action_button({
728 icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
730 self.pos_widget.screen_selector.set_current_screen(self.back_screen);
734 this.validate_button = this.add_action_button({
736 icon: '/point_of_sale/static/src/img/icons/png48/validate.png',
738 self.validateCurrentOrder();
744 this.pos_widget.order_widget.set_numpad_state(null);
745 this.pos_widget.payment_screen.set_numpad_state(null);
748 this.pos_widget.screen_selector.set_current_screen(self.back_screen);
750 validateCurrentOrder: function() {
752 var currentOrder = this.pos.get('selectedOrder');
758 this.validate_button.$element.addClass('disabled');
760 this.pos.push_order(currentOrder.exportAsJSON())
762 if(self.pos.use_proxy_printer){
763 self.pos.proxy.print_receipt(currentOrder.export_for_printing());
764 self.pos.get('selectedOrder').destroy(); //finish order and go back to scan screen
766 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
768 self.validate_button.$element.removeClass('disabled');
772 bindPaymentLineEvents: function() {
773 this.currentPaymentLines = (this.pos.get('selectedOrder')).get('paymentLines');
774 this.currentPaymentLines.bind('add', this.addPaymentLine, this);
775 this.currentPaymentLines.bind('remove', this.renderElement, this);
776 this.currentPaymentLines.bind('all', this.updatePaymentSummary, this);
778 bind_orderline_events: function() {
779 this.currentOrderLines = (this.pos.get('selectedOrder')).get('orderLines');
780 this.currentOrderLines.bind('all', this.updatePaymentSummary, this);
782 change_selected_order: function() {
783 this.currentPaymentLines.unbind();
784 this.bindPaymentLineEvents();
785 this.currentOrderLines.unbind();
786 this.bind_orderline_events();
787 this.renderElement();
789 addPaymentLine: function(newPaymentLine) {
790 var x = new module.PaymentlineWidget(null, {
791 payment_line: newPaymentLine
793 x.on_delete.add(_.bind(this.deleteLine, this, x));
794 x.appendTo(this.$('#paymentlines'));
796 renderElement: function() {
798 this.$('#paymentlines').empty();
799 this.currentPaymentLines.each(_.bind( function(paymentLine) {
800 this.addPaymentLine(paymentLine);
802 this.updatePaymentSummary();
804 deleteLine: function(lineWidget) {
805 this.currentPaymentLines.remove([lineWidget.payment_line]);
807 updatePaymentSummary: function() {
808 var currentOrder = this.pos.get('selectedOrder');
809 var paidTotal = currentOrder.getPaidTotal();
810 var dueTotal = currentOrder.getTotal();
811 var remaining = dueTotal > paidTotal ? dueTotal - paidTotal : 0;
812 var change = paidTotal > dueTotal ? paidTotal - dueTotal : 0;
814 this.$('#payment-due-total').html(dueTotal.toFixed(2));
815 this.$('#payment-paid-total').html(paidTotal.toFixed(2));
816 this.$('#payment-remaining').html(remaining.toFixed(2));
817 this.$('#payment-change').html(change.toFixed(2));
819 set_numpad_state: function(numpadState) {
820 if (this.numpadState) {
821 this.numpadState.unbind('set_value', this.set_value);
822 this.numpadState.unbind('change:mode', this.setNumpadMode);
824 this.numpadState = numpadState;
825 if (this.numpadState) {
826 this.numpadState.bind('set_value', this.set_value, this);
827 this.numpadState.bind('change:mode', this.setNumpadMode, this);
828 this.numpadState.reset();
829 this.setNumpadMode();
832 setNumpadMode: function() {
833 this.numpadState.set({mode: 'payment'});
835 set_value: function(val) {
836 this.currentPaymentLines.last().set({amount: val});