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({
48 user_mode : this.current_mode,
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.hide();
70 this.current_popup = null;
73 load_saved_screen: function(){
76 var selectedOrder = this.pos.get('selectedOrder');
78 if(this.current_mode === 'client'){
79 this.set_current_screen(selectedOrder.get('client_screen') || this.default_client_screen);
80 }else if(this.current_mode === 'cashier'){
81 this.set_current_screen(selectedOrder.get('cashier_screen') || this.default_cashier_screen);
83 this.selected_order = selectedOrder;
85 set_user_mode: function(user_mode){
86 if(user_mode !== this.current_mode){
88 this.current_mode = user_mode;
89 this.load_saved_screen();
92 get_user_mode: function(){
93 return this.current_mode;
95 set_current_screen: function(screen_name){
96 var screen = this.screen_set[screen_name];
99 var selectedOrder = this.pos.get('selectedOrder');
100 if(this.current_mode === 'client'){
101 selectedOrder.set({'client_screen': screen_name});
103 selectedOrder.set({'cashier_screen': screen_name});
106 if(screen && screen !== this.current_screen){
107 if(this.current_screen){
108 this.current_screen.close();
109 this.current_screen.hide();
111 this.current_screen = screen;
112 this.current_screen.show();
115 set_default_screen: function(){
116 this.set_current_screen(this.current_mode === 'client' ? this.default_client_screen : this.default_cashier_screen);
120 module.ScreenWidget = module.PosBaseWidget.extend({
125 init: function(parent,options){
126 this._super(parent,options);
130 help_button_action: function(){
131 this.pos_widget.screen_selector.show_popup('help');
134 logout_button_action: function(){
135 this.pos_widget.screen_selector.set_user_mode('client');
138 barcode_product_screen: 'scan', //if defined, this screen will be loaded when a product is scanned
139 barcode_product_error_popup: 'error', //if defined, this popup will be loaded when there's an error in the popup
141 // what happens when a product is scanned :
142 // it will add the product to the order and go to barcode_product_screen. Or show barcode_product_error_popup if
144 barcode_product_action: function(ean){
145 if(this.pos_widget.scan_product(ean)){
146 this.pos.proxy.scan_item_success();
147 if(this.barcode_product_screen){
148 this.pos_widget.screen_selector.set_current_screen(this.barcode_product_screen);
151 if(this.barcode_product_error_popup){
152 this.pos_widget.screen_selector.show_popup(this.barcode_product_error_popup);
157 // what happens when a cashier id barcode is scanned.
158 // the default behavior is the following :
159 // - if there's a user with a matching ean, put it as the active 'cashier', go to cashier mode, and return true
160 // - else : do nothing and return false. You probably want to extend this to show and appropriate error popup...
161 barcode_cashier_action: function(ean){
162 var users = this.pos.get('user_list');
163 for(var i = 0, len = users.length; i < len; i++){
164 if(users[i].ean === ean.ean){
165 this.pos.set('cashier',users[i]);
166 this.pos_widget.username.refresh();
167 this.pos.proxy.cashier_mode_activated();
168 this.pos_widget.screen_selector.set_user_mode('cashier');
175 // what happens when a client id barcode is scanned.
176 // the default behavior is the following :
177 // - if there's a user with a matching ean, put it as the active 'client' and return true
178 // - else : return false.
179 barcode_client_action: function(ean){
180 var users = this.pos.get('user_list');
181 for(var i = 0, len = users.length; i < len; i++){
182 if(users[i].ean === ean.ean){
183 this.pos.set('client',users[i]);
184 this.pos_widget.username.refresh();
189 //TODO start the transaction
192 // what happens when a discount barcode is scanned : the default behavior
193 // is to set the discount on the last order.
194 barcode_discount_action: function(ean){
195 var last_orderline = this.pos.get('selectedOrder').getLastOrderline();
197 last_orderline.set_discount(ean.value)
201 // this method shows the screen and sets up all the widget related to this screen. Extend this method
202 // if you want to alter the behavior of the screen.
206 this.$element.show();
210 var cashier_mode = this.pos_widget.screen_selector.get_user_mode() === 'cashier';
212 this.pos_widget.set_numpad_visible(this.show_numpad && cashier_mode);
213 this.pos_widget.set_leftpane_visible(this.show_leftpane);
214 this.pos_widget.set_cashier_controls_visible(cashier_mode);
215 this.pos_widget.action_bar.set_element_visible('help-button', !cashier_mode, function(){ self.help_button_action(); });
216 this.pos_widget.action_bar.set_element_visible('logout-button', cashier_mode, function(){ self.logout_button_action(); });
217 this.pos_widget.action_bar.set_element_visible('close-button', cashier_mode);
219 this.pos_widget.username.set_user_mode(this.pos_widget.screen_selector.get_user_mode());
221 this.pos.barcode_reader.set_action_callback({
222 'cashier': self.barcode_cashier_action ? function(ean){ self.barcode_cashier_action(ean); } : undefined ,
223 'product': self.barcode_product_action ? function(ean){ self.barcode_product_action(ean); } : undefined ,
224 'client' : self.barcode_client_action ? function(ean){ self.barcode_client_action(ean); } : undefined ,
225 'discount': self.barcode_discount_action ? function(ean){ self.barcode_discount_action(ean); } : undefined,
230 // this method is called when the screen is closed to make place for a new screen. this is a good place
231 // to put your cleanup stuff as it is guaranteed that for each show() there is one and only one close()
233 if(this.pos.barcode_reader){
234 this.pos.barcode_reader.reset_action_callbacks();
236 if(this.pos_widget.action_bar){
237 this.pos_widget.action_bar.destroy_buttons();
241 // this methods hides the screen. It's not a good place to put your cleanup stuff as it is called on the
242 // POS initialization.
246 this.$element.hide();
250 // we need this because some screens re-render themselves when they are hidden
251 // (due to some events, or magic, or both...) we must make sure they remain hidden.
252 // the good solution would probably be to make them not re-render themselves when they
254 renderElement: function(){
258 this.$element.hide();
264 module.PopUpWidget = module.PosBaseWidget.extend({
267 this.$element.show();
272 this.$element.hide();
277 module.HelpPopupWidget = module.PopUpWidget.extend({
278 template:'HelpPopupWidget',
281 this.pos.proxy.help_needed();
284 this.$element.find('.button').off('click').click(function(){
285 self.pos_widget.screen_selector.close_popup();
286 self.pos.proxy.help_canceled();
291 module.ReceiptPopupWidget = module.PopUpWidget.extend({
292 template:'ReceiptPopupWidget',
296 this.$element.find('.receipt').off('click').click(function(){
297 console.log('TODO receipt'); //TODO
298 self.pos_widget.screen_selector.set_current_screen('scan');
300 this.$element.find('.invoice').off('click').click(function(){
301 console.log('TODO invoice'); //TODO
302 self.pos_widget.screen_selector.set_current_screen('scan');
307 module.ErrorPopupWidget = module.PopUpWidget.extend({
308 template:'ErrorPopupWidget',
312 this.pos.proxy.help_needed();
313 this.pos.proxy.scan_item_error_unrecognized();
315 this.pos.barcode_reader.save_callbacks();
316 this.pos.barcode_reader.reset_action_callbacks();
317 this.pos.barcode_reader.set_action_callback({
318 'cashier': function(ean){
319 clearInterval(this.intervalID);
320 self.pos.proxy.cashier_mode_activated();
321 self.pos_widget.screen_selector.set_user_mode('cashier');
327 this.pos.proxy.help_canceled();
328 this.pos.barcode_reader.restore_callbacks();
332 module.ErrorProductNotRecognizedPopupWidget = module.ErrorPopupWidget.extend({
333 template:'ErrorProductNotRecognizedPopupWidget',
336 module.ErrorNoSessionPopupWidget = module.ErrorPopupWidget.extend({
337 template:'ErrorNoSessionPopupWidget',
340 module.ScaleInviteScreenWidget = module.ScreenWidget.extend({
341 template:'ScaleInviteScreenWidget',
347 self.pos.proxy.weighting_start();
349 this.intervalID = setInterval(function(){
350 var weight = self.pos.proxy.weighting_read_kg();
352 clearInterval(this.intervalID);
353 self.pos_widget.screen_selector.set_current_screen('scale_product');
357 this.pos_widget.action_bar.add_new_button(
360 icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
362 clearInterval(this.intervalID);
363 self.pos.proxy.weighting_end();
364 if( self.pos_widget.screen_selector.get_user_mode() === 'client'){
365 self.pos_widget.screen_selector.set_current_screen('scan');
367 self.pos_widget.screen_selector.set_current_screen('products');
375 clearInterval(this.intervalID);
379 module.ScaleProductScreenWidget = module.ScreenWidget.extend({
380 template:'ScaleProductSelectionScreenWidget',
382 this.product_categories_widget = new module.ProductCategoriesWidget(this,{
384 product_type: 'weightable',
386 this.product_categories_widget.replace($('.placeholder-ProductCategoriesWidget'));
388 this.product_list_widget = new module.ProductListWidget(this,{
391 this.product_list_widget.replace($('.placeholder-ProductListWidget'));
397 this.product_categories_widget.reset_category();
398 this.pos_widget.order_widget.set_numpad_state(this.pos_widget.numpad.state);
400 if(this.pos_widget.screen_selector.get_user_mode() === 'client'){
401 this.pos_widget.action_bar.add_new_button({
403 icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
405 self.pos_widget.screen_selector.set_current_screen('scan');
408 this.product_list_widget.set_next_screen('scan');
410 this.pos_widget.action_bar.add_new_button({
412 icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
414 self.pos_widget.screen_selector.set_current_screen('products');
417 this.product_list_widget.set_next_screen('products');
420 this.pos.proxy.weighting_start();
421 this.last_weight = this.product_list_widget.weight;
422 this.intervalID = setInterval(function(){
423 var weight = self.pos.proxy.weighting_read_kg();
424 if(weight != self.last_weight){
425 self.product_list_widget.set_weight(weight);
426 self.last_weight = weight;
432 this.pos_widget.order_widget.set_numpad_state(null);
433 this.pos_widget.payment_screen.set_numpad_state(null);
434 clearInterval(this.intervalID);
435 this.pos.proxy.weighting_end();
439 module.ScaleScreenWidget = module.ScreenWidget.extend({
440 template:'ScaleScreenWidget',
445 this.pos.proxy.weighting_start();
446 this.intervalID = setInterval(function(){
447 var weight = self.pos.proxy.weighting_read_kg();
448 if(weight != self.weight){
449 self.weight = weight;
450 self.renderElement();
456 clearInterval(this.intervalID);
457 this.pos.proxy.weighting_end();
461 module.ClientPaymentScreenWidget = module.ScreenWidget.extend({
462 template:'ClientPaymentScreenWidget',
467 this.pos.proxy.payment_request(this.pos.get('selectedOrder').getDueLeft(),'card','info'); //TODO TOTAL
469 this.intervalID = setInterval(function(){
470 var payment = self.pos.proxy.is_payment_accepted();
471 if(payment === 'payment_accepted'){
472 clearInterval(this.intervalID);
474 var currentOrder = self.pos.get('selectedOrder');
476 //we get the first cashregister marked as self-checkout
477 var selfCheckoutRegisters = [];
478 for(var i = 0; i < this.pos.get('cashRegisters').models.length; i++){
479 var cashregister = this.pos.get('cashRegisters').models[i];
480 if(cashregister.self_checkout_payment_method){
481 selfCheckoutRegisters.push(cashregister);
485 var cashregister = selfCheckoutRegisters[0] || this.pos.get('cashRegisters').models[0];
486 currentOrder.addPaymentLine(cashregister);
488 self.pos.push_order(currentOrder.exportAsJSON()).then(function() {
489 currentOrder.destroy();
490 self.pos.proxy.transaction_end();
491 self.pos_widget.screen_selector.set_current_screen('welcome');
493 }else if(payment === 'payment_rejected'){
494 clearInterval(this.intervalID);
495 //TODO show a tryagain thingie ?
499 this.pos_widget.action_bar.add_new_button(
502 icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
503 click: function(){ //TODO Go to ask for weighting screen
504 clearInterval(this.intervalID);
505 self.pos.proxy.payment_canceled();
506 self.pos_widget.screen_selector.set_current_screen('scan');
513 clearInterval(this.intervalID);
517 module.WelcomeScreenWidget = module.ScreenWidget.extend({
518 template:'WelcomeScreenWidget',
521 show_leftpane: false,
527 if(this.pos.use_scale){
528 this.pos_widget.action_bar.add_new_button({
530 icon: '/point_of_sale/static/src/img/icons/png48/scale.png',
532 self.pos_widget.screen_selector.set_current_screen('scale_invite');
539 module.ScanProductScreenWidget = module.ScreenWidget.extend({
540 template:'ScanProductScreenWidget',
549 if(self.pos.use_scale){
550 this.pos_widget.action_bar.add_new_button({
552 icon: '/point_of_sale/static/src/img/icons/png48/scale.png',
554 self.pos_widget.screen_selector.set_current_screen('scale_invite');
559 this.pos_widget.action_bar.add_new_button({
561 icon: '/point_of_sale/static/src/img/icons/png48/go-next.png',
563 self.pos_widget.screen_selector.set_current_screen('client_payment');
569 module.SearchProductScreenWidget = module.ScreenWidget.extend({
570 template:'SearchProductScreenWidget',
575 start: function(){ //FIXME this should work as renderElement... but then the categories aren't properly set. explore why
576 this.product_categories_widget = new module.ProductCategoriesWidget(this,{});
577 this.product_categories_widget.replace($('.placeholder-ProductCategoriesWidget'));
579 this.product_list_widget = new module.ProductListWidget(this,{});
580 this.product_list_widget.replace($('.placeholder-ProductListWidget'));
587 this.product_categories_widget.reset_category();
589 this.pos_widget.order_widget.set_numpad_state(this.pos_widget.numpad.state);
591 if(this.pos.use_scale){
592 this.pos_widget.action_bar.add_new_button({
594 icon: '/point_of_sale/static/src/img/icons/png48/scale.png',
596 self.pos_widget.screen_selector.set_current_screen('scale_invite');
604 this.pos_widget.order_widget.set_numpad_state(null);
605 this.pos_widget.payment_screen.set_numpad_state(null);
610 module.ReceiptScreenWidget = module.ScreenWidget.extend({
611 template: 'ReceiptScreenWidget',
616 init: function(parent, options) {
617 this._super(parent,options);
618 this.model = options.model;
619 this.user = this.pos.get('user');
620 this.company = this.pos.get('company');
621 this.shop_obj = this.pos.get('shop');
623 renderElement: function() {
625 this.pos.bind('change:selectedOrder', this.change_selected_order, this);
626 this.change_selected_order();
632 this.pos_widget.action_bar.add_new_button({
634 icon: '/point_of_sale/static/src/img/icons/png48/printer.png',
635 click: function(){ self.print(); },
638 this.pos_widget.action_bar.add_new_button({
640 icon: '/point_of_sale/static/src/img/icons/png48/go-next.png',
641 click: function() { self.finishOrder(); },
647 finishOrder: function() {
648 this.pos.get('selectedOrder').destroy();
650 change_selected_order: function() {
651 if (this.currentOrderLines)
652 this.currentOrderLines.unbind();
653 this.currentOrderLines = (this.pos.get('selectedOrder')).get('orderLines');
654 this.currentOrderLines.bind('add', this.refresh, this);
655 this.currentOrderLines.bind('change', this.refresh, this);
656 this.currentOrderLines.bind('remove', this.refresh, this);
657 if (this.currentPaymentLines)
658 this.currentPaymentLines.unbind();
659 this.currentPaymentLines = (this.pos.get('selectedOrder')).get('paymentLines');
660 this.currentPaymentLines.bind('all', this.refresh, this);
663 refresh: function() {
664 this.currentOrder = this.pos.get('selectedOrder');
665 $('.pos-receipt-container', this.$element).html(QWeb.render('PosTicket',{widget:this}));
669 module.PaymentScreenWidget = module.ScreenWidget.extend({
670 template: 'PaymentScreenWidget',
671 init: function(parent, options) {
672 this._super(parent,options);
673 this.model = options.model;
674 this.pos.bind('change:selectedOrder', this.change_selected_order, this);
675 this.bindPaymentLineEvents();
676 this.bind_orderline_events();
682 this.set_numpad_state(this.pos_widget.numpad.state);
684 this.back_button = this.pos_widget.action_bar.add_new_button({
686 icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
688 self.pos_widget.screen_selector.set_current_screen('products');
692 this.validate_button = this.pos_widget.action_bar.add_new_button({
694 icon: '/point_of_sale/static/src/img/icons/png48/validate.png',
696 self.validateCurrentOrder();
702 this.pos_widget.order_widget.set_numpad_state(null);
703 this.pos_widget.payment_screen.set_numpad_state(null);
706 this.pos_widget.screen_selector.set_current_screen('products');
708 validateCurrentOrder: function() {
710 var currentOrder = this.pos.get('selectedOrder');
712 this.validate_button.$element.attr('disabled','disabled'); //FIXME is the css actually using this attr ?
714 this.pos.push_order(currentOrder.exportAsJSON())
716 self.validate_button.$element.removeAttr('disabled');
717 if(self.pos.use_proxy_printer){
718 self.pos.get('selectedOrder').destroy(); //finish order and go back to scan screen
720 self.pos_widget.screen_selector.set_current_screen('receipt');
724 bindPaymentLineEvents: function() {
725 this.currentPaymentLines = (this.pos.get('selectedOrder')).get('paymentLines');
726 this.currentPaymentLines.bind('add', this.addPaymentLine, this);
727 this.currentPaymentLines.bind('remove', this.renderElement, this);
728 this.currentPaymentLines.bind('all', this.updatePaymentSummary, this);
730 bind_orderline_events: function() {
731 this.currentOrderLines = (this.pos.get('selectedOrder')).get('orderLines');
732 this.currentOrderLines.bind('all', this.updatePaymentSummary, this);
734 change_selected_order: function() {
735 this.currentPaymentLines.unbind();
736 this.bindPaymentLineEvents();
737 this.currentOrderLines.unbind();
738 this.bind_orderline_events();
739 this.renderElement();
741 addPaymentLine: function(newPaymentLine) {
742 console.log('NEW PAYMENT LINE WIDGET',newPaymentLine);
743 var x = new module.PaymentlineWidget(null, {
744 payment_line: newPaymentLine
746 x.on_delete.add(_.bind(this.deleteLine, this, x));
747 x.appendTo(this.$('#paymentlines'));
749 renderElement: function() {
751 this.$('#paymentlines').empty();
752 this.currentPaymentLines.each(_.bind( function(paymentLine) {
753 this.addPaymentLine(paymentLine);
755 this.updatePaymentSummary();
757 deleteLine: function(lineWidget) {
758 this.currentPaymentLines.remove([lineWidget.model]);
760 updatePaymentSummary: function() {
761 var currentOrder, dueTotal, paidTotal, remaining, remainingAmount;
762 currentOrder = this.pos.get('selectedOrder');
763 paidTotal = currentOrder.getPaidTotal();
764 dueTotal = currentOrder.getTotal();
765 this.$element.find('#payment-due-total').html(dueTotal.toFixed(2));
766 this.$element.find('#payment-paid-total').html(paidTotal.toFixed(2));
767 remainingAmount = dueTotal - paidTotal;
768 remaining = remainingAmount > 0 ? 0 : (-remainingAmount).toFixed(2);
769 $('#payment-remaining').html(remaining);
771 set_numpad_state: function(numpadState) {
772 if (this.numpadState) {
773 this.numpadState.unbind('set_value', this.set_value);
774 this.numpadState.unbind('change:mode', this.setNumpadMode);
776 this.numpadState = numpadState;
777 if (this.numpadState) {
778 this.numpadState.bind('set_value', this.set_value, this);
779 this.numpadState.bind('change:mode', this.setNumpadMode, this);
780 this.numpadState.reset();
781 this.setNumpadMode();
784 setNumpadMode: function() {
785 this.numpadState.set({mode: 'payment'});
787 set_value: function(val) {
788 this.currentPaymentLines.last().set({amount: val});