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 openerp.point_of_sale.load_screens = function load_screens(instance, module){ //module is instance.point_of_sale
21 var QWeb = instance.web.qweb,
24 var round_pr = instance.web.round_precision
26 module.ScreenSelector = instance.web.Class.extend({
27 init: function(options){
28 this.pos = options.pos;
29 this.screen_set = options.screen_set || {};
30 this.popup_set = options.popup_set || {};
31 this.default_screen = options.default_screen;
32 this.startup_screen = options.startup_screen;
33 this.current_popup = null;
34 this.current_mode = options.default_mode || 'cashier';
35 this.current_screen = null;
37 for (var screen_name in this.screen_set) {
38 this.screen_set[screen_name].hide();
41 for (var popup_name in this.popup_set) {
42 this.popup_set[popup_name].hide();
45 if (this.pos.get_order()) {
46 this.pos.get_order().set_screen_data({
47 'screen': this.default_screen,
51 this.pos.bind('change:selectedOrder', this.load_saved_screen, this);
53 add_screen: function(screen_name, screen){
55 this.screen_set[screen_name] = screen;
58 show_popup: function(name,options){
59 if(this.current_popup){
62 this.current_popup = this.popup_set[name];
63 this.current_popup.show(options);
65 close_popup: function(){
66 if(this.current_popup){
67 this.current_popup.close();
68 this.current_popup.hide();
69 this.current_popup = null;
72 load_saved_screen: function(options){
73 options = options || {};
75 var selectedOrder = this.pos.get_order();
76 // FIXME : this changing screen behaviour is sometimes confusing ...
77 this.set_current_screen(selectedOrder.get_screen_data('screen') || options.default_screen || this.default_screen,null,'refresh');
78 //this.set_current_screen(this.default_screen,null,'refresh');
81 set_user_mode: function(user_mode){
82 if(user_mode !== this.current_mode){
84 this.current_mode = user_mode;
85 this.load_saved_screen();
88 get_user_mode: function(){
89 return this.current_mode;
91 set_current_screen: function(screen_name,params,refresh){
92 var screen = this.screen_set[screen_name];
94 console.error("ERROR: set_current_screen("+screen_name+") : screen not found");
99 var order = this.pos.get_order();
101 var old_screen_name = order.get_screen_data('screen');
103 order.set_screen_data('screen',screen_name);
106 order.set_screen_data('params',params);
109 if( screen_name !== old_screen_name ){
110 order.set_screen_data('previous-screen',old_screen_name);
114 if ( 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: function(){
124 return this.pos.get_order().get_screen_data('screen') || this.default_screen;
127 var previous = this.pos.get_order().get_screen_data('previous-screen');
129 this.set_current_screen(previous);
132 get_current_screen_param: function(param){
133 var params = this.pos.get_order().get_screen_data('params');
134 return params ? params[param] : undefined;
136 set_default_screen: function(){
137 this.set_current_screen(this.default_screen);
139 change_default_screen: function(screen){
140 this.default_screen = screen;
144 module.ScreenWidget = module.PosBaseWidget.extend({
149 init: function(parent,options){
150 this._super(parent,options);
154 help_button_action: function(){
155 this.pos_widget.screen_selector.show_popup('help');
158 barcode_product_screen: 'products', //if defined, this screen will be loaded when a product is scanned
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.
164 barcode_product_action: function(code){
166 if(self.pos.scan_product(code)){
167 if(self.barcode_product_screen){
168 self.pos_widget.screen_selector.set_current_screen(self.barcode_product_screen);
171 self.pos_widget.screen_selector.show_popup('error-barcode',code.code);
175 // what happens when a cashier id barcode is scanned.
176 // the default behavior is the following :
177 // - if there's a user with a matching barcode, put it as the active 'cashier', go to cashier mode, and return true
178 // - else : do nothing and return false. You probably want to extend this to show and appropriate error popup...
179 barcode_cashier_action: function(code){
180 var users = this.pos.users;
181 for(var i = 0, len = users.length; i < len; i++){
182 if(users[i].barcode === code.code){
183 this.pos.set_cashier(users[i]);
184 this.pos_widget.username.renderElement();
188 this.pos_widget.screen_selector.show_popup('error-barcode',code.code);
192 // what happens when a client id barcode is scanned.
193 // the default behavior is the following :
194 // - if there's a user with a matching barcode, put it as the active 'client' and return true
195 // - else : return false.
196 barcode_client_action: function(code){
197 var partner = this.pos.db.get_partner_by_barcode(code.code);
199 this.pos.get_order().set_client(partner);
202 this.pos_widget.screen_selector.show_popup('error-barcode',code.code);
206 // what happens when a discount barcode is scanned : the default behavior
207 // is to set the discount on the last order.
208 barcode_discount_action: function(code){
209 var last_orderline = this.pos.get_order().get_last_orderline();
211 last_orderline.set_discount(code.value)
214 // What happens when an invalid barcode is scanned : shows an error popup.
215 barcode_error_action: function(code){
216 this.pos_widget.screen_selector.show_popup('error-barcode',code.code);
219 // this method shows the screen and sets up all the widget related to this screen. Extend this method
220 // if you want to alter the behavior of the screen.
226 this.$el.removeClass('oe_hidden');
231 this.pos_widget.set_numpad_visible(this.show_numpad);
232 this.pos_widget.set_leftpane_visible(this.show_leftpane);
234 this.pos_widget.username.set_user_mode(this.pos_widget.screen_selector.get_user_mode());
235 this.pos.barcode_reader.set_action_callback({
236 'cashier': self.barcode_cashier_action ? function(code){ self.barcode_cashier_action(code); } : undefined ,
237 'product': self.barcode_product_action ? function(code){ self.barcode_product_action(code); } : undefined ,
238 'client' : self.barcode_client_action ? function(code){ self.barcode_client_action(code); } : undefined ,
239 'discount': self.barcode_discount_action ? function(code){ self.barcode_discount_action(code); } : undefined,
240 'error' : self.barcode_error_action ? function(code){ self.barcode_error_action(code); } : undefined,
244 // this method is called when the screen is closed to make place for a new screen. this is a good place
245 // to put your cleanup stuff as it is guaranteed that for each show() there is one and only one close()
247 if(this.pos.barcode_reader){
248 this.pos.barcode_reader.reset_action_callbacks();
252 // this methods hides the screen. It's not a good place to put your cleanup stuff as it is called on the
253 // POS initialization.
257 this.$el.addClass('oe_hidden');
261 // we need this because some screens re-render themselves when they are hidden
262 // (due to some events, or magic, or both...) we must make sure they remain hidden.
263 // the good solution would probably be to make them not re-render themselves when they
265 renderElement: function(){
269 this.$el.addClass('oe_hidden');
275 module.PopUpWidget = module.PosBaseWidget.extend({
278 this.$el.removeClass('oe_hidden');
281 /* called before hide, when a popup is closed */
284 /* hides the popup. keep in mind that this is called in the initialization pass of the
285 * pos instantiation, so you don't want to do anything fancy in here */
288 this.$el.addClass('oe_hidden');
293 module.FullscreenPopup = module.PopUpWidget.extend({
294 template:'FullscreenPopupWidget',
298 this.renderElement();
299 this.$('.button.fullscreen').off('click').click(function(){
300 window.document.body.webkitRequestFullscreen();
301 self.pos_widget.screen_selector.close_popup();
303 this.$('.button.cancel').off('click').click(function(){
304 self.pos_widget.screen_selector.close_popup();
307 ismobile: function(){
308 return typeof window.orientation !== 'undefined';
313 module.ErrorPopupWidget = module.PopUpWidget.extend({
314 template:'ErrorPopupWidget',
315 show: function(options){
316 options = options || {};
320 $('body').append('<audio src="/point_of_sale/static/src/sounds/error.wav" autoplay="true"></audio>');
322 this.message = options.message || _t('Error');
323 this.comment = options.comment || '';
325 this.renderElement();
327 this.pos.barcode_reader.save_callbacks();
328 this.pos.barcode_reader.reset_action_callbacks();
330 this.$('.footer .button').click(function(){
331 self.pos_widget.screen_selector.close_popup();
332 if ( options.confirm ) {
333 options.confirm.call(self);
339 this.pos.barcode_reader.restore_callbacks();
343 module.ErrorTracebackPopupWidget = module.ErrorPopupWidget.extend({
344 template:'ErrorTracebackPopupWidget',
347 module.ErrorBarcodePopupWidget = module.ErrorPopupWidget.extend({
348 template:'ErrorBarcodePopupWidget',
349 show: function(barcode){
350 this.barcode = barcode;
355 module.ConfirmPopupWidget = module.PopUpWidget.extend({
356 template: 'ConfirmPopupWidget',
357 show: function(options){
358 options = options || {};
362 this.message = options.message || '';
363 this.comment = options.comment || '';
364 this.renderElement();
366 this.$('.button.cancel').click(function(){
367 self.pos_widget.screen_selector.close_popup();
368 if( options.cancel ){
369 options.cancel.call(self);
373 this.$('.button.confirm').click(function(){
374 self.pos_widget.screen_selector.close_popup();
375 if( options.confirm ){
376 options.confirm.call(self);
383 * A popup that allows the user to select one item from a list.
385 * show_popup('selection',{
386 * message: 'Pick an Option',
387 * message: "Popup Title",
389 * { label: 'foobar', item: 45 },
390 * { label: 'bar foo', item: 'stuff' },
392 * confirm: function(item) {
393 * // get the item selected by the user.
395 * cancel: function(){
396 * // user chose nothing
401 module.SelectionPopupWidget = module.PopUpWidget.extend({
402 template: 'SelectionPopupWidget',
403 show: function(options){
404 options = options || {};
408 this.message = options.message || '';
409 this.list = options.list || [];
410 this.renderElement();
412 this.$('.button.cancel').click(function(){
413 self.pos_widget.screen_selector.close_popup();
415 options.cancel.call(self);
419 this.$('.selection-item').click(function(){
420 self.pos_widget.screen_selector.close_popup();
421 if (options.confirm) {
422 var item = self.list[parseInt($(this).data('item-index'))];
423 item = item ? item.item : item;
424 options.confirm.call(self,item);
430 module.TextInputPopupWidget = module.PopUpWidget.extend({
431 template: 'TextInputPopupWidget',
432 show: function(options){
433 options = options || {};
437 this.message = options.message || '';
438 this.comment = options.comment || '';
439 this.value = options.value || '';
440 this.renderElement();
441 this.$('input,textarea').focus();
443 this.$('.button.cancel').click(function(){
444 self.pos_widget.screen_selector.close_popup();
445 if( options.cancel ){
446 options.cancel.call(self);
450 this.$('.button.confirm').click(function(){
451 self.pos_widget.screen_selector.close_popup();
452 var value = self.$('input,textarea').val();
453 if( options.confirm ){
454 options.confirm.call(self,value);
460 module.TextAreaPopupWidget = module.TextInputPopupWidget.extend({
461 template: 'TextAreaPopupWidget',
464 module.NumberPopupWidget = module.PopUpWidget.extend({
465 template: 'NumberPopupWidget',
466 click_numpad_button: function($el,event){
467 this.numpad_input($el.data('action'));
469 numpad_input: function(input) { //FIXME -> Deduplicate code
470 var oldbuf = this.inputbuffer.slice(0);
473 if (this.firstinput) {
474 this.inputbuffer = "0.";
475 }else if (!this.inputbuffer.length || this.inputbuffer === '-') {
476 this.inputbuffer += "0.";
477 } else if (this.inputbuffer.indexOf('.') < 0){
478 this.inputbuffer = this.inputbuffer + '.';
480 } else if (input === 'CLEAR') {
481 this.inputbuffer = "";
482 } else if (input === 'BACKSPACE') {
483 this.inputbuffer = this.inputbuffer.substring(0,this.inputbuffer.length - 1);
484 } else if (input === '+') {
485 if ( this.inputbuffer[0] === '-' ) {
486 this.inputbuffer = this.inputbuffer.substring(1,this.inputbuffer.length);
488 } else if (input === '-') {
489 if ( this.inputbuffer[0] === '-' ) {
490 this.inputbuffer = this.inputbuffer.substring(1,this.inputbuffer.length);
492 this.inputbuffer = '-' + this.inputbuffer;
494 } else if (input[0] === '+' && !isNaN(parseFloat(input))) {
495 this.inputbuffer = '' + ((parseFloat(this.inputbuffer) || 0) + parseFloat(input));
496 } else if (!isNaN(parseInt(input))) {
497 if (this.firstinput) {
498 this.inputbuffer = '' + input;
500 this.inputbuffer += input;
504 this.firstinput = this.inputbuffer.length === 0;
506 if (this.inputbuffer !== oldbuf) {
507 this.$('.value').text(this.inputbuffer);
510 show: function(options){
511 options = options || {};
515 this.message = options.message || '';
516 this.comment = options.comment || '';
517 this.inputbuffer = options.value || '';
518 this.renderElement();
519 this.firstinput = true;
521 this.$('.input-button,.mode-button').click(function(event){
522 self.click_numpad_button($(this),event);
524 this.$('.button.cancel').click(function(){
525 self.pos_widget.screen_selector.close_popup();
526 if( options.cancel ){
527 options.cancel.call(self);
531 this.$('.button.confirm').click(function(){
532 self.pos_widget.screen_selector.close_popup();
533 if( options.confirm ){
534 options.confirm.call(self,self.inputbuffer);
540 module.PasswordPopupWidget = module.NumberPopupWidget.extend({
541 renderElement: function(){
543 this.$('.popup').addClass('popup-password'); // HELLO HACK !
547 module.ErrorNoClientPopupWidget = module.ErrorPopupWidget.extend({
548 template: 'ErrorNoClientPopupWidget',
551 module.ErrorInvoiceTransferPopupWidget = module.ErrorPopupWidget.extend({
552 template: 'ErrorInvoiceTransferPopupWidget',
555 module.UnsentOrdersPopupWidget = module.ConfirmPopupWidget.extend({
556 template: 'UnsentOrdersPopupWidget',
559 module.UnpaidOrdersPopupWidget = module.ConfirmPopupWidget.extend({
560 template: 'UnpaidOrdersPopupWidget',
563 module.ScaleScreenWidget = module.ScreenWidget.extend({
564 template:'ScaleScreenWidget',
566 next_screen: 'products',
567 previous_screen: 'products',
569 show_leftpane: false,
574 var queue = this.pos.proxy_queue;
577 this.renderElement();
579 this.hotkey_handler = function(event){
580 if(event.which === 13){
581 self.order_product();
582 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
583 }else if(event.which === 27){
584 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
588 $('body').on('keyup',this.hotkey_handler);
590 this.$('.back').click(function(){
591 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
594 this.$('.next,.buy-product').click(function(){
595 self.order_product();
596 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
599 queue.schedule(function(){
600 return self.pos.proxy.scale_read().then(function(weight){
601 self.set_weight(weight.weight);
603 },{duration:50, repeat: true});
606 get_product: function(){
607 var ss = this.pos_widget.screen_selector;
609 return ss.get_current_screen_param('product');
614 order_product: function(){
615 this.pos.get_order().add_product(this.get_product(),{ quantity: this.weight });
617 get_product_name: function(){
618 var product = this.get_product();
619 return (product ? product.display_name : undefined) || 'Unnamed Product';
621 get_product_price: function(){
622 var product = this.get_product();
623 return (product ? product.price : 0) || 0;
625 set_weight: function(weight){
626 this.weight = weight;
627 this.$('.weight').text(this.get_product_weight_string());
628 this.$('.computed-price').text(this.get_computed_price_string());
630 get_product_weight_string: function(){
631 var product = this.get_product();
632 var defaultstr = (this.weight || 0).toFixed(3) + ' Kg';
633 if(!product || !this.pos){
636 var unit_id = product.uom_id;
640 var unit = this.pos.units_by_id[unit_id[0]];
641 var weight = round_pr(this.weight || 0, unit.rounding);
642 var weightstr = weight.toFixed(Math.ceil(Math.log(1.0/unit.rounding) / Math.log(10) ));
646 get_computed_price_string: function(){
647 return this.format_currency(this.get_product_price() * this.weight);
652 $('body').off('keyup',this.hotkey_handler);
654 this.pos.proxy_queue.clear();
658 module.ProductScreenWidget = module.ScreenWidget.extend({
659 template:'ProductScreenWidget',
664 start: function(){ //FIXME this should work as renderElement... but then the categories aren't properly set. explore why
667 this.product_list_widget = new module.ProductListWidget(this,{
668 click_product_action: function(product){
669 if(product.to_weight && self.pos.config.iface_electronic_scale){
670 self.pos_widget.screen_selector.set_current_screen('scale',{product: product});
672 self.pos.get_order().add_product(product);
675 product_list: this.pos.db.get_product_by_category(0)
677 this.product_list_widget.replace(this.$('.placeholder-ProductListWidget'));
679 this.product_categories_widget = new module.ProductCategoriesWidget(this,{
680 product_list_widget: this.product_list_widget,
682 this.product_categories_widget.replace(this.$('.placeholder-ProductCategoriesWidget'));
689 this.product_categories_widget.reset_category();
691 this.pos_widget.order_widget.set_editable(true);
697 this.pos_widget.order_widget.set_editable(false);
699 if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
700 this.pos_widget.onscreen_keyboard.hide();
705 module.ClientListScreenWidget = module.ScreenWidget.extend({
706 template: 'ClientListScreenWidget',
708 init: function(parent, options){
709 this._super(parent, options);
710 this.partner_cache = new module.DomCache();
713 show_leftpane: false,
721 this.renderElement();
722 this.details_visible = false;
723 this.old_client = this.pos.get_order().get_client()
724 this.new_client = this.old_client;
726 this.$('.back').click(function(){
727 self.pos_widget.screen_selector.back();
730 this.$('.next').click(function(){
732 self.pos_widget.screen_selector.back();
735 this.$('.new-customer').click(function(){
736 self.display_client_details('edit',{
737 'country_id': self.pos.company.country_id,
741 var partners = this.pos.db.get_partners_sorted(1000);
742 this.render_list(partners);
744 this.reload_partners();
746 if( this.old_client ){
747 this.display_client_details('show',this.old_client,0);
750 this.$('.client-list-contents').delegate('.client-line','click',function(event){
751 self.line_select(event,$(this),parseInt($(this).data('id')));
754 var search_timeout = null;
756 if(this.pos.config.iface_vkeyboard && this.pos_widget.onscreen_keyboard){
757 this.pos_widget.onscreen_keyboard.connect(this.$('.searchbox input'));
760 this.$('.searchbox input').on('keyup',function(event){
761 clearTimeout(search_timeout);
763 var query = this.value;
765 search_timeout = setTimeout(function(){
766 self.perform_search(query,event.which === 13);
770 this.$('.searchbox .search-clear').click(function(){
774 barcode_client_action: function(code){
775 if (this.editing_client) {
776 this.$('.detail.barcode').val(code.code);
777 } else if (this.pos.db.get_partner_by_barcode(code.code)) {
778 this.display_client_details('show',this.pos.db.get_partner_by_barcode(code.code));
781 perform_search: function(query, associate_result){
783 var customers = this.pos.db.search_partner(query);
784 this.display_client_details('hide');
785 if ( associate_result && customers.length === 1){
786 this.new_client = customers[0];
788 this.pos_widget.screen_selector.back();
790 this.render_list(customers);
792 var customers = this.pos.db.get_partners_sorted();
793 this.render_list(customers);
796 clear_search: function(){
797 var customers = this.pos.db.get_partners_sorted(1000);
798 this.render_list(customers);
799 this.$('.searchbox input')[0].value = '';
800 this.$('.searchbox input').focus();
802 render_list: function(partners){
803 var contents = this.$el[0].querySelector('.client-list-contents');
804 contents.innerHTML = "";
805 for(var i = 0, len = Math.min(partners.length,1000); i < len; i++){
806 var partner = partners[i];
807 var clientline = this.partner_cache.get_node(partner.id);
809 var clientline_html = QWeb.render('ClientLine',{widget: this, partner:partners[i]});
810 var clientline = document.createElement('tbody');
811 clientline.innerHTML = clientline_html;
812 clientline = clientline.childNodes[1];
813 this.partner_cache.cache_node(partner.id,clientline);
815 if( partners === this.new_client ){
816 clientline.classList.add('highlight');
818 clientline.classList.remove('highlight');
820 contents.appendChild(clientline);
823 save_changes: function(){
824 if( this.has_client_changed() ){
825 this.pos.get_order().set_client(this.new_client);
828 has_client_changed: function(){
829 if( this.old_client && this.new_client ){
830 return this.old_client.id !== this.new_client.id;
832 return !!this.old_client !== !!this.new_client;
835 toggle_save_button: function(){
836 var $button = this.$('.button.next');
837 if (this.editing_client) {
838 $button.addClass('oe_hidden');
840 } else if( this.new_client ){
841 if( !this.old_client){
842 $button.text(_t('Set Customer'));
844 $button.text(_t('Change Customer'));
847 $button.text(_t('Deselect Customer'));
849 $button.toggleClass('oe_hidden',!this.has_client_changed());
851 line_select: function(event,$line,id){
852 var partner = this.pos.db.get_partner_by_id(id);
853 this.$('.client-list .lowlight').removeClass('lowlight');
854 if ( $line.hasClass('highlight') ){
855 $line.removeClass('highlight');
856 $line.addClass('lowlight');
857 this.display_client_details('hide',partner);
858 this.new_client = null;
859 this.toggle_save_button();
861 this.$('.client-list .highlight').removeClass('highlight');
862 $line.addClass('highlight');
863 var y = event.pageY - $line.parent().offset().top
864 this.display_client_details('show',partner,y);
865 this.new_client = partner;
866 this.toggle_save_button();
869 partner_icon_url: function(id){
870 return '/web/binary/image?model=res.partner&id='+id+'&field=image_small';
873 // ui handle for the 'edit selected customer' action
874 edit_client_details: function(partner) {
875 this.display_client_details('edit',partner);
878 // ui handle for the 'cancel customer edit changes' action
879 undo_client_details: function(partner) {
881 this.display_client_details('hide');
883 this.display_client_details('show',partner);
887 // what happens when we save the changes on the client edit form -> we fetch the fields, sanitize them,
888 // send them to the backend for update, and call saved_client_details() when the server tells us the
889 // save was successfull.
890 save_client_details: function(partner) {
894 this.$('.client-details-contents .detail').each(function(idx,el){
895 fields[el.name] = el.value;
899 this.pos_widget.screen_selector.show_popup('error',{
900 message: _t('A Customer Name Is Required'),
905 if (this.uploaded_picture) {
906 fields.image = this.uploaded_picture;
909 fields.id = partner.id || false;
910 fields.country_id = fields.country_id || false;
911 fields.barcode = fields.barcode ? this.pos.barcode_reader.sanitize_ean(fields.barcode) : false;
913 new instance.web.Model('res.partner').call('create_from_ui',[fields]).then(function(partner_id){
914 self.saved_client_details(partner_id);
915 },function(err,event){
916 event.preventDefault();
917 self.pos_widget.screen_selector.show_popup('error',{
918 'message':_t('Error: Could not Save Changes'),
919 'comment':_t('Your Internet connection is probably down.'),
924 // what happens when we've just pushed modifications for a partner of id partner_id
925 saved_client_details: function(partner_id){
927 this.reload_partners().then(function(){
928 var partner = self.pos.db.get_partner_by_id(partner_id);
930 self.new_client = partner;
931 self.toggle_save_button();
932 self.display_client_details('show',partner);
934 // should never happen, because create_from_ui must return the id of the partner it
935 // has created, and reload_partner() must have loaded the newly created partner.
936 self.display_client_details('hide');
941 // resizes an image, keeping the aspect ratio intact,
942 // the resize is useful to avoid sending 12Mpixels jpegs
943 // over a wireless connection.
944 resize_image_to_dataurl: function(img, maxwidth, maxheight, callback){
945 img.onload = function(){
946 var png = new Image();
947 var canvas = document.createElement('canvas');
948 var ctx = canvas.getContext('2d');
951 if (img.width > maxwidth) {
952 ratio = maxwidth / img.width;
954 if (img.height * ratio > maxheight) {
955 ratio = maxheight / img.height;
957 var width = Math.floor(img.width * ratio);
958 var height = Math.floor(img.height * ratio);
960 canvas.width = width;
961 canvas.height = height;
962 ctx.drawImage(img,0,0,width,height);
964 var dataurl = canvas.toDataURL();
969 // Loads and resizes a File that contains an image.
970 // callback gets a dataurl in case of success.
971 load_image_file: function(file, callback){
973 if (!file.type.match(/image.*/)) {
974 this.pos_widget.screen_selector.show_popup('error',{
975 message:_t('Unsupported File Format'),
976 comment:_t('Only web-compatible Image formats such as .png or .jpeg are supported'),
981 var reader = new FileReader();
982 reader.onload = function(event){
983 var dataurl = event.target.result;
984 var img = new Image();
986 self.resize_image_to_dataurl(img,800,600,callback);
988 reader.onerror = function(){
989 self.pos_widget.screen_selector.show_popup('error',{
990 message:_t('Could Not Read Image'),
991 comment:_t('The provided file could not be read due to an unknown error'),
994 reader.readAsDataURL(file);
997 // This fetches partner changes on the server, and in case of changes,
998 // rerenders the affected views
999 reload_partners: function(){
1001 return this.pos.load_new_partners().then(function(){
1002 self.render_list(self.pos.db.get_partners_sorted(1000));
1004 // update the currently assigned client if it has been changed in db.
1005 var curr_client = self.pos.get_order().get_client();
1007 self.pos.get_order().set_client(self.pos.db.get_partner_by_id(curr_client.id));
1012 // Shows,hides or edit the customer details box :
1013 // visibility: 'show', 'hide' or 'edit'
1014 // partner: the partner object to show or edit
1015 // clickpos: the height of the click on the list (in pixel), used
1016 // to maintain consistent scroll.
1017 display_client_details: function(visibility,partner,clickpos){
1019 var contents = this.$('.client-details-contents');
1020 var parent = this.$('.client-list').parent();
1021 var scroll = parent.scrollTop();
1022 var height = contents.height();
1024 contents.off('click','.button.edit');
1025 contents.off('click','.button.save');
1026 contents.off('click','.button.undo');
1027 contents.on('click','.button.edit',function(){ self.edit_client_details(partner); });
1028 contents.on('click','.button.save',function(){ self.save_client_details(partner); });
1029 contents.on('click','.button.undo',function(){ self.undo_client_details(partner); });
1030 this.editing_client = false;
1031 this.uploaded_picture = null;
1033 if(visibility === 'show'){
1035 contents.append($(QWeb.render('ClientDetails',{widget:this,partner:partner})));
1037 var new_height = contents.height();
1039 if(!this.details_visible){
1040 if(clickpos < scroll + new_height + 20 ){
1041 parent.scrollTop( clickpos - 20 );
1043 parent.scrollTop(parent.scrollTop() + new_height);
1046 parent.scrollTop(parent.scrollTop() - height + new_height);
1049 this.details_visible = true;
1050 this.toggle_save_button();
1051 } else if (visibility === 'edit') {
1052 this.editing_client = true;
1054 contents.append($(QWeb.render('ClientDetailsEdit',{widget:this,partner:partner})));
1055 this.toggle_save_button();
1057 contents.find('.image-uploader').on('change',function(){
1058 self.load_image_file(event.target.files[0],function(res){
1060 contents.find('.client-picture img, .client-picture .fa').remove();
1061 contents.find('.client-picture').append("<img src='"+res+"'>");
1062 contents.find('.detail.picture').remove();
1063 self.uploaded_picture = res;
1067 } else if (visibility === 'hide') {
1069 if( height > scroll ){
1070 contents.css({height:height+'px'});
1071 contents.animate({height:0},400,function(){
1072 contents.css({height:''});
1075 parent.scrollTop( parent.scrollTop() - height);
1077 this.details_visible = false;
1078 this.toggle_save_button();
1086 module.ReceiptScreenWidget = module.ScreenWidget.extend({
1087 template: 'ReceiptScreenWidget',
1089 show_leftpane: false,
1097 if (!this.pos.get_order()._printed && this.pos.config.iface_print_auto) {
1101 // The problem is that in chrome the print() is asynchronous and doesn't
1102 // execute until all rpc are finished. So it conflicts with the rpc used
1103 // to send the orders to the backend, and the user is able to go to the next
1104 // screen before the printing dialog is opened. The problem is that what's
1105 // printed is whatever is in the page when the dialog is opened and not when it's called,
1106 // and so you end up printing the product list instead of the receipt...
1108 // Fixing this would need a re-architecturing
1109 // of the code to postpone sending of orders after printing.
1111 // But since the print dialog also blocks the other asynchronous calls, the
1112 // button enabling in the setTimeout() is blocked until the printing dialog is
1113 // closed. But the timeout has to be big enough or else it doesn't work
1114 // 2 seconds is the same as the default timeout for sending orders and so the dialog
1115 // should have appeared before the timeout... so yeah that's not ultra reliable.
1117 this.lock_screen(true);
1118 setTimeout(function(){
1119 self.lock_screen(false);
1122 lock_screen: function(locked) {
1123 this._locked = locked;
1125 this.$('.next').removeClass('highlight');
1127 this.$('.next').addClass('highlight');
1131 this.pos.get_order()._printed = true;
1134 finish_order: function() {
1135 if (!this._locked) {
1136 this.pos.get_order().finalize();
1139 renderElement: function() {
1142 this.$('.next').click(function(){
1143 self.finish_order();
1145 this.$('.button.print').click(function(){
1149 refresh: function() {
1150 var order = this.pos.get_order();
1151 this.$('.pos-receipt-container').html(QWeb.render('PosTicket',{
1154 receipt: order.export_for_printing(),
1155 orderlines: order.get_orderlines(),
1156 paymentlines: order.get_paymentlines(),
1161 module.PaymentScreenWidget = module.ScreenWidget.extend({
1162 template: 'PaymentScreenWidget',
1163 back_screen: 'product',
1164 next_screen: 'receipt',
1165 show_leftpane: false,
1167 init: function(parent, options) {
1169 this._super(parent, options);
1171 this.pos.bind('change:selectedOrder',function(){
1172 this.renderElement();
1173 this.watch_order_changes();
1175 this.watch_order_changes();
1177 this.inputbuffer = "";
1178 this.firstinput = true;
1179 this.keyboard_handler = function(event){
1181 if ( event.keyCode === 13 ) { // Enter
1182 self.validate_order();
1183 } else if ( event.keyCode === 190 ) { // Dot
1185 } else if ( event.keyCode === 46 ) { // Delete
1187 } else if ( event.keyCode === 8 ) { // Backspace
1189 event.preventDefault(); // Prevents history back nav
1190 } else if ( event.keyCode >= 48 && event.keyCode <= 57 ){ // Numbers
1191 key = '' + (event.keyCode - 48);
1192 } else if ( event.keyCode >= 96 && event.keyCode <= 105 ){ // Numpad Numbers
1193 key = '' + (event.keyCode - 96);
1194 } else if ( event.keyCode === 189 || event.keyCode === 109 ) { // Minus
1196 } else if ( event.keyCode === 107 ) { // Plus
1200 self.payment_input(key);
1204 // resets the current input buffer
1205 reset_input: function(){
1206 var line = this.pos.get_order().selected_paymentline;
1207 this.firstinput = true;
1209 this.inputbuffer = this.format_currency_no_symbol(line.get_amount());
1211 this.inputbuffer = "";
1214 // handle both keyboard and numpad input. Accepts
1215 // a string that represents the key pressed.
1216 payment_input: function(input) {
1217 var oldbuf = this.inputbuffer.slice(0);
1219 if (input === '.') {
1220 if (this.firstinput) {
1221 this.inputbuffer = "0.";
1222 }else if (!this.inputbuffer.length || this.inputbuffer === '-') {
1223 this.inputbuffer += "0.";
1224 } else if (this.inputbuffer.indexOf('.') < 0){
1225 this.inputbuffer = this.inputbuffer + '.';
1227 } else if (input === 'CLEAR') {
1228 this.inputbuffer = "";
1229 } else if (input === 'BACKSPACE') {
1230 this.inputbuffer = this.inputbuffer.substring(0,this.inputbuffer.length - 1);
1231 } else if (input === '+') {
1232 if ( this.inputbuffer[0] === '-' ) {
1233 this.inputbuffer = this.inputbuffer.substring(1,this.inputbuffer.length);
1235 } else if (input === '-') {
1236 if ( this.inputbuffer[0] === '-' ) {
1237 this.inputbuffer = this.inputbuffer.substring(1,this.inputbuffer.length);
1239 this.inputbuffer = '-' + this.inputbuffer;
1241 } else if (input[0] === '+' && !isNaN(parseFloat(input))) {
1242 this.inputbuffer = '' + ((parseFloat(this.inputbuffer) || 0) + parseFloat(input));
1243 } else if (!isNaN(parseInt(input))) {
1244 if (this.firstinput) {
1245 this.inputbuffer = '' + input;
1247 this.inputbuffer += input;
1251 this.firstinput = this.inputbuffer.length === 0;
1253 if (this.inputbuffer !== oldbuf) {
1254 var order = this.pos.get_order();
1255 if (order.selected_paymentline) {
1256 order.selected_paymentline.set_amount(parseFloat(this.inputbuffer));
1257 this.order_changes();
1258 this.render_paymentlines();
1259 this.$('.paymentline.selected .edit').text(this.inputbuffer);
1263 click_numpad: function(button) {
1264 this.payment_input(button.data('action'));
1266 render_numpad: function() {
1268 var numpad = $(QWeb.render('PaymentScreen-Numpad', { widget:this }));
1269 numpad.on('click','button',function(){
1270 self.click_numpad($(this));
1274 click_delete_paymentline: function(cid){
1275 var lines = this.pos.get_order().get_paymentlines();
1276 for ( var i = 0; i < lines.length; i++ ) {
1277 if (lines[i].cid === cid) {
1278 this.pos.get_order().remove_paymentline(lines[i]);
1280 this.render_paymentlines();
1285 click_paymentline: function(cid){
1286 var lines = this.pos.get_order().get_paymentlines();
1287 for ( var i = 0; i < lines.length; i++ ) {
1288 if (lines[i].cid === cid) {
1289 this.pos.get_order().select_paymentline(lines[i]);
1291 this.render_paymentlines();
1296 render_paymentlines: function() {
1298 var order = this.pos.get_order();
1303 var lines = order.get_paymentlines();
1305 this.$('.paymentlines-container').empty();
1306 var lines = $(QWeb.render('PaymentScreen-Paymentlines', {
1309 paymentlines: lines,
1312 lines.on('click','.delete-button',function(){
1313 self.click_delete_paymentline($(this).data('cid'));
1316 lines.on('click','.paymentline',function(){
1317 self.click_paymentline($(this).data('cid'));
1320 lines.appendTo(this.$('.paymentlines-container'));
1322 click_paymentmethods: function(id) {
1323 var cashregister = null;
1324 for ( var i = 0; i < this.pos.cashregisters.length; i++ ) {
1325 if ( this.pos.cashregisters[i].journal_id[0] === id ){
1326 cashregister = this.pos.cashregisters[i];
1330 this.pos.get_order().add_paymentline( cashregister );
1332 this.render_paymentlines();
1334 render_paymentmethods: function() {
1336 var methods = $(QWeb.render('PaymentScreen-Paymentmethods', { widget:this }));
1337 methods.on('click','.paymentmethod',function(){
1338 self.click_paymentmethods($(this).data('id'));
1342 click_invoice: function(){
1343 var order = this.pos.get_order();
1344 order.set_to_invoice(!order.is_to_invoice());
1345 if (order.is_to_invoice()) {
1346 this.$('.js_invoice').addClass('highlight');
1348 this.$('.js_invoice').removeClass('highlight');
1351 click_set_customer: function(){
1352 this.pos_widget.screen_selector.set_current_screen('clientlist');
1354 click_back: function(){
1355 this.pos_widget.screen_selector.set_current_screen('products');
1357 renderElement: function() {
1361 var numpad = this.render_numpad();
1362 numpad.appendTo(this.$('.payment-numpad'));
1364 var methods = this.render_paymentmethods();
1365 methods.appendTo(this.$('.paymentmethods-container'));
1367 this.render_paymentlines();
1369 this.$('.back').click(function(){
1373 this.$('.next').click(function(){
1374 self.validate_order();
1377 this.$('.js_set_customer').click(function(){
1378 self.click_set_customer();
1380 this.$('.js_invoice').click(function(){
1381 self.click_invoice();
1386 this.pos.get_order().clean_empty_paymentlines();
1388 this.render_paymentlines();
1389 this.order_changes();
1390 window.document.body.addEventListener('keydown',this.keyboard_handler);
1394 window.document.body.removeEventListener('keydown',this.keyboard_handler);
1397 // sets up listeners to watch for order changes
1398 watch_order_changes: function() {
1400 var order = this.pos.get_order();
1405 this.old_order.unbind(null,null,this);
1407 order.bind('all',function(){
1408 self.order_changes();
1410 this.old_order = order;
1412 // called when the order is changed, used to show if
1413 // the order is paid or not
1414 order_changes: function(){
1416 var order = this.pos.get_order();
1419 } else if (order.is_paid()) {
1420 self.$('.next').addClass('highlight');
1422 self.$('.next').removeClass('highlight');
1425 print_escpos_receipt: function(){
1429 order: this.pos.get_order(),
1430 receipt: this.pos.get_order().export_for_printing(),
1433 this.pos.proxy.print_receipt(QWeb.render('XmlReceipt',env));
1436 // Check if the order is paid, then sends it to the backend,
1437 // and complete the sale process
1438 validate_order: function() {
1441 var order = this.pos.get_order();
1443 // FIXME: this check is there because the backend is unable to
1444 // process empty orders. This is not the right place to fix it.
1445 if (order.get_orderlines().length === 0) {
1446 this.pos_widget.screen_selector.show_popup('error',{
1447 'message': _t('Empty Order'),
1448 'comment': _t('There must be at least one product in your order before it can be validated'),
1453 if (!order.is_paid() || this.invoicing) {
1457 // The exact amount must be paid if there is no cash payment method defined.
1458 if (Math.abs(order.get_total_with_tax() - order.get_total_paid()) > 0.00001) {
1460 for (var i = 0; i < this.pos.cashregisters.length; i++) {
1461 cash = cash || (this.pos.cashregisters[i].journal.type === 'cash');
1464 this.pos_widget.screen_selector.show_popup('error',{
1465 message: _t('Cannot return change without a cash payment method'),
1466 comment: _t('There is no cash payment method available in this point of sale to handle the change.\n\n Please pay the exact amount or add a cash payment method in the point of sale configuration'),
1472 if (order.is_paid_with_cash() && this.pos.config.iface_cashdrawer) {
1474 this.pos.proxy.open_cashbox();
1477 if (order.is_to_invoice()) {
1478 var invoiced = this.pos.push_and_invoice_order(order);
1479 this.invoicing = true;
1481 invoiced.fail(function(error){
1482 self.invoicing = false;
1483 if (error === 'error-no-client') {
1484 self.pos_widget.screen_selector.show_popup('confirm',{
1485 message: _t('Please select the Customer'),
1486 comment: _t('You need to select the customer before you can invoice an order.'),
1487 confirm: function(){
1488 self.pos_widget.screen_selector.set_current_screen('clientlist');
1492 self.pos_widget.screen_selector.show_popup('error',{
1493 message: _t('The order could not be sent'),
1494 comment: _t('Check your internet connection and try again.'),
1499 invoiced.done(function(){
1500 self.invoicing = false;
1504 this.pos.push_order(order)
1505 if (this.pos.config.iface_print_via_proxy) {
1506 this.print_escpos_receipt();
1507 order.finalize(); //finish order and go back to scan screen
1509 this.pos_widget.screen_selector.set_current_screen(this.next_screen);