odoo.define('point_of_sale.chrome', function (require) { "use strict"; var PosBaseWidget = require('point_of_sale.BaseWidget'); var gui = require('point_of_sale.gui'); var keyboard = require('point_of_sale.keyboard'); var models = require('point_of_sale.models'); var AbstractAction = require('web.AbstractAction'); var core = require('web.core'); var ajax = require('web.ajax'); var CrashManager = require('web.CrashManager'); var BarcodeEvents = require('barcodes.BarcodeEvents').BarcodeEvents; var _t = core._t; var _lt = core._lt; var QWeb = core.qweb; /* -------- The Order Selector -------- */ // Allows the cashier to create / delete and // switch between orders. var OrderSelectorWidget = PosBaseWidget.extend({ template: 'OrderSelectorWidget', init: function(parent, options) { this._super(parent, options); this.pos.get('orders').bind('add remove change',this.renderElement,this); this.pos.bind('change:selectedOrder',this.renderElement,this); }, get_order_by_uid: function(uid) { var orders = this.pos.get_order_list(); for (var i = 0; i < orders.length; i++) { if (orders[i].uid === uid) { return orders[i]; } } return undefined; }, order_click_handler: function(event,$el) { var order = this.get_order_by_uid($el.data('uid')); if (order) { this.pos.set_order(order); } }, neworder_click_handler: function(event, $el) { this.pos.add_new_order(); }, deleteorder_click_handler: function(event, $el) { var self = this; var order = this.pos.get_order(); if (!order) { return; } else if ( !order.is_empty() ){ this.gui.show_popup('confirm',{ 'title': _t('Destroy Current Order ?'), 'body': _t('You will lose any data associated with the current order'), confirm: function(){ self.pos.delete_current_order(); }, }); } else { this.pos.delete_current_order(); } }, renderElement: function(){ var self = this; this._super(); this.$('.order-button.select-order').click(function(event){ self.order_click_handler(event,$(this)); }); this.$('.neworder-button').click(function(event){ self.neworder_click_handler(event,$(this)); }); this.$('.deleteorder-button').click(function(event){ self.deleteorder_click_handler(event,$(this)); }); }, }); /* ------- The User Name Widget ------- */ // Displays the current cashier's name var UsernameWidget = PosBaseWidget.extend({ template: 'UsernameWidget', init: function(parent, options){ options = options || {}; this._super(parent,options); }, get_name: function(){ var user = this.pos.get_cashier(); if(user){ return user.name; }else{ return ""; } }, }); /* -------- The Header Button --------- */ // Used to quickly add buttons with simple // labels and actions to the point of sale // header. var HeaderButtonWidget = PosBaseWidget.extend({ template: 'HeaderButtonWidget', init: function(parent, options){ options = options || {}; this._super(parent, options); this.action = options.action; this.label = options.label; this.button_class = options.button_class; }, renderElement: function(){ var self = this; this._super(); if(this.action){ this.$el.click(function(){ self.action(); }); } }, show: function() { this.$el.removeClass('oe_hidden'); }, hide: function() { this.$el.addClass('oe_hidden'); }, }); /* --------- The Debug Widget --------- */ // The debug widget lets the user control // and monitor the hardware and software status // without the use of the proxy, or to access // the raw locally stored db values, useful // for debugging var DebugWidget = PosBaseWidget.extend({ template: "DebugWidget", eans:{ admin_badge: '0410100000006', client_badge: '0420200000004', invalid_ean: '1232456', soda_33cl: '5449000000996', oranges_kg: '2100002031410', lemon_price: '2301000001560', unknown_product: '9900000000004', }, events:[ 'open_cashbox', 'print_receipt', 'scale_read', ], init: function(parent,options){ this._super(parent,options); var self = this; // for dragging the debug widget around this.dragging = false; this.dragpos = {x:0, y:0}; function eventpos(event){ if(event.touches && event.touches[0]){ return {x: event.touches[0].screenX, y: event.touches[0].screenY}; }else{ return {x: event.screenX, y: event.screenY}; } } this.dragend_handler = function(event){ self.dragging = false; }; this.dragstart_handler = function(event){ self.dragging = true; self.dragpos = eventpos(event); }; this.dragmove_handler = function(event){ if(self.dragging){ var top = this.offsetTop; var left = this.offsetLeft; var pos = eventpos(event); var dx = pos.x - self.dragpos.x; var dy = pos.y - self.dragpos.y; self.dragpos = pos; this.style.right = 'auto'; this.style.bottom = 'auto'; this.style.left = left + dx + 'px'; this.style.top = top + dy + 'px'; } event.preventDefault(); event.stopPropagation(); }; }, show: function() { this.$el.css({opacity:0}); this.$el.removeClass('oe_hidden'); this.$el.animate({opacity:1},250,'swing'); }, hide: function() { var self = this; this.$el.animate({opacity:0,},250,'swing',function(){ self.$el.addClass('oe_hidden'); }); }, start: function(){ var self = this; if (this.pos.debug) { this.show(); } this.el.addEventListener('mouseleave', this.dragend_handler); this.el.addEventListener('mouseup', this.dragend_handler); this.el.addEventListener('touchend', this.dragend_handler); this.el.addEventListener('touchcancel',this.dragend_handler); this.el.addEventListener('mousedown', this.dragstart_handler); this.el.addEventListener('touchstart', this.dragstart_handler); this.el.addEventListener('mousemove', this.dragmove_handler); this.el.addEventListener('touchmove', this.dragmove_handler); this.$('.toggle').click(function(){ self.hide(); }); this.$('.button.set_weight').click(function(){ var kg = Number(self.$('input.weight').val()); if(!isNaN(kg)){ self.pos.proxy.debug_set_weight(kg); } }); this.$('.button.reset_weight').click(function(){ self.$('input.weight').val(''); self.pos.proxy.debug_reset_weight(); }); this.$('.button.custom_ean').click(function(){ var ean = self.pos.barcode_reader.barcode_parser.sanitize_ean(self.$('input.ean').val() || '0'); self.$('input.ean').val(ean); self.pos.barcode_reader.scan(ean); }); this.$('.button.barcode').click(function(){ self.pos.barcode_reader.scan(self.$('input.ean').val()); }); this.$('.button.delete_orders').click(function(){ self.gui.show_popup('confirm',{ 'title': _t('Delete Paid Orders ?'), 'body': _t('This operation will permanently destroy all paid orders from the local storage. You will lose all the data. This operation cannot be undone.'), confirm: function(){ self.pos.db.remove_all_orders(); self.pos.set({synch: { state:'connected', pending: 0 }}); }, }); }); this.$('.button.delete_unpaid_orders').click(function(){ self.gui.show_popup('confirm',{ 'title': _t('Delete Unpaid Orders ?'), 'body': _t('This operation will destroy all unpaid orders in the browser. You will lose all the unsaved data and exit the point of sale. This operation cannot be undone.'), confirm: function(){ self.pos.db.remove_all_unpaid_orders(); window.location = '/'; }, }); }); this.$('.button.export_unpaid_orders').click(function(){ self.gui.prepare_download_link( self.pos.export_unpaid_orders(), _t("unpaid orders") + ' ' + moment().format('YYYY-MM-DD-HH-mm-ss') + '.json', ".export_unpaid_orders", ".download_unpaid_orders" ); }); this.$('.button.export_paid_orders').click(function() { self.gui.prepare_download_link( self.pos.export_paid_orders(), _t("paid orders") + ' ' + moment().format('YYYY-MM-DD-HH-mm-ss') + '.json', ".export_paid_orders", ".download_paid_orders" ); }); this.$('.button.display_refresh').click(function () { self.pos.proxy.message('display_refresh', {}); }); this.$('.button.import_orders input').on('change', function(event) { var file = event.target.files[0]; if (file) { var reader = new FileReader(); reader.onload = function(event) { var report = self.pos.import_orders(event.target.result); self.gui.show_popup('orderimport',{report:report}); }; reader.readAsText(file); } }); _.each(this.events, function(name){ self.pos.proxy.add_notification(name,function(){ self.$('.event.'+name).stop().clearQueue().css({'background-color':'#6CD11D'}); self.$('.event.'+name).animate({'background-color':'#1E1E1E'},2000); }); }); }, }); /* --------- The Status Widget -------- */ // Base class for widgets that want to display // status in the point of sale header. var StatusWidget = PosBaseWidget.extend({ status: ['connected','connecting','disconnected','warning','error'], set_status: function(status,msg){ for(var i = 0; i < this.status.length; i++){ this.$('.js_'+this.status[i]).addClass('oe_hidden'); } this.$('.js_'+status).removeClass('oe_hidden'); if(msg){ this.$('.js_msg').removeClass('oe_hidden').html(msg); }else{ this.$('.js_msg').addClass('oe_hidden').html(''); } }, }); /* ------- Synch. Notifications ------- */ // Displays if there are orders that could // not be submitted, and how many. var SynchNotificationWidget = StatusWidget.extend({ template: 'SynchNotificationWidget', start: function(){ var self = this; this.pos.bind('change:synch', function(pos,synch){ self.set_status(synch.state, synch.pending); }); this.$el.click(function(){ self.pos.push_order(null,{'show_error':true}); }); }, }); /* --------- The Proxy Status --------- */ // Displays the status of the hardware proxy // (connected, disconnected, errors ... ) var ProxyStatusWidget = StatusWidget.extend({ template: 'ProxyStatusWidget', set_smart_status: function(status){ if(status.status === 'connected'){ var warning = false; var msg = ''; if(this.pos.config.iface_scan_via_proxy){ var scanner = status.drivers.scanner ? status.drivers.scanner.status : false; if( scanner != 'connected' && scanner != 'connecting'){ warning = true; msg += _t('Scanner'); } } if( this.pos.config.iface_print_via_proxy || this.pos.config.iface_cashdrawer ){ var printer = status.drivers.escpos ? status.drivers.escpos.status : false; if( printer != 'connected' && printer != 'connecting'){ warning = true; msg = msg ? msg + ' & ' : msg; msg += _t('Printer'); } } if( this.pos.config.iface_electronic_scale ){ var scale = status.drivers.scale ? status.drivers.scale.status : false; if( scale != 'connected' && scale != 'connecting' ){ warning = true; msg = msg ? msg + ' & ' : msg; msg += _t('Scale'); } } msg = msg ? msg + ' ' + _t('Offline') : msg; this.set_status(warning ? 'warning' : 'connected', msg); }else{ this.set_status(status.status,''); } }, start: function(){ var self = this; this.set_smart_status(this.pos.proxy.get('status')); this.pos.proxy.on('change:status',this,function(eh,status){ //FIXME remove duplicate changes self.set_smart_status(status.newValue); }); this.$el.click(function(){ self.pos.connect_to_proxy(); }); }, }); /* --------- The Sale Details --------- */ // Generates a report to print the sales of the // day on a ticket var SaleDetailsButton = PosBaseWidget.extend({ template: 'SaleDetailsButton', start: function(){ var self = this; this.$el.click(function(){ self.pos.proxy.print_sale_details(); }); }, }); /* User interface for distant control over the Client display on the IoT Box */ // The boolean posbox_supports_display (in devices.js) will allow interaction to the IoT Box on true, prevents it otherwise // We don't want the incompatible IoT Box to be flooded with 404 errors on arrival of our many requests as it triggers losses of connections altogether var ClientScreenWidget = PosBaseWidget.extend({ template: 'ClientScreenWidget', change_status_display: function(status) { var msg = '' if (status === 'success') { this.$('.js_warning').addClass('oe_hidden'); this.$('.js_disconnected').addClass('oe_hidden'); this.$('.js_connected').removeClass('oe_hidden'); } else if (status === 'warning') { this.$('.js_disconnected').addClass('oe_hidden'); this.$('.js_connected').addClass('oe_hidden'); this.$('.js_warning').removeClass('oe_hidden'); msg = _t('Connected, Not Owned'); } else { this.$('.js_warning').addClass('oe_hidden'); this.$('.js_connected').addClass('oe_hidden'); this.$('.js_disconnected').removeClass('oe_hidden'); msg = _t('Disconnected') if (status === 'not_found') { msg = _t('Client Screen Unsupported. Please upgrade the IoT Box') } } this.$('.oe_customer_display_text').text(msg); }, status_loop: function() { var self = this; function loop() { if (self.pos.proxy.posbox_supports_display) { self.pos.proxy.test_ownership_of_client_screen().then( function (data) { if (typeof data === 'string') { data = JSON.parse(data); } if (data.status === 'OWNER') { self.change_status_display('success'); } else { self.change_status_display('warning'); } setTimeout(loop, 3000); }, function (err) { if (err.abort) { // Stop the loop return; } if (typeof err == "undefined") { self.change_status_display('failure'); } else { self.change_status_display('not_found'); self.pos.proxy.posbox_supports_display = false; } setTimeout(loop, 3000); } ); } } loop(); }, start: function(){ if (this.pos.config.iface_customer_facing_display) { this.show(); var self = this; this.$el.click(function(){ self.pos.render_html_for_customer_facing_display().then(function(rendered_html) { self.pos.proxy.take_ownership_over_client_screen(rendered_html).then( function(data) { if (typeof data === 'string') { data = JSON.parse(data); } if (data.status === 'success') { self.change_status_display('success'); } else { self.change_status_display('warning'); } if (!self.pos.proxy.posbox_supports_display) { self.pos.proxy.posbox_supports_display = true; self.status_loop(); } }, function(err) { if (typeof err == "undefined") { self.change_status_display('failure'); } else { self.change_status_display('not_found'); } }); }); }); this.status_loop(); } else { this.hide(); } }, }); /*--------------------------------------*\ | THE CHROME | \*======================================*/ // The Chrome is the main widget that contains // all other widgets in the PointOfSale. // // It is the first object instanciated and the // starting point of the point of sale code. // // It is mainly composed of : // - a header, containing the list of orders // - a leftpane, containing the list of bought // products (orderlines) // - a rightpane, containing the screens // (see pos_screens.js) // - popups // - an onscreen keyboard // - .gui which controls the switching between // screens and the showing/closing of popups var Chrome = PosBaseWidget.extend(AbstractAction.prototype, { template: 'Chrome', init: function() { var self = this; this._super(arguments[0],{}); this.started = new $.Deferred(); // resolves when DOM is online this.ready = new $.Deferred(); // resolves when the whole GUI has been loaded this.pos = new models.PosModel(this.getSession(), {chrome:this}); this.gui = new gui.Gui({pos: this.pos, chrome: this}); this.chrome = this; // So that chrome's childs have chrome set automatically this.pos.gui = this.gui; this.logo_click_time = 0; this.logo_click_count = 0; this.previous_touch_y_coordinate = -1; this.widget = {}; // contains references to subwidgets instances this.cleanup_dom(); this.pos.ready.then(function(){ self.build_chrome(); self.build_widgets(); self.disable_rubberbanding(); self.disable_backpace_back(); self.ready.resolve(); self.loading_hide(); self.replace_crashmanager(); self.pos.push_order(); }).guardedCatch(function (err) { // error when loading models data from the backend self.loading_error(err); }); }, cleanup_dom: function() { // remove default webclient handlers that induce click delay $(document).off(); $(window).off(); $('html').off(); $('body').off(); // The above lines removed the bindings, but we really need them for the barcode BarcodeEvents.start(); }, build_chrome: function() { var self = this; if ($.browser.chrome) { var chrome_version = $.browser.version.split('.')[0]; if (parseInt(chrome_version, 10) >= 50) { ajax.loadCSS('/point_of_sale/static/src/css/chrome50.css'); } } this.renderElement(); this.$('.pos-logo').click(function(){ self.click_logo(); }); if(this.pos.config.iface_big_scrollbars){ this.$el.addClass('big-scrollbars'); } }, // displays a system error with the error-traceback // popup. show_error: function(error) { this.gui.show_popup('error-traceback',{ 'title': error.message, 'body': error.message + '\n' + error.data.debug + '\n', }); }, // replaces the error handling of the existing crashmanager which // uses jquery dialog to display the error, to use the pos popup // instead replace_crashmanager: function() { var self = this; CrashManager.include({ show_error: function(error) { if (self.gui) { self.show_error(error); } else { this._super(error); } }, }); }, click_logo: function() { if (this.pos.debug) { this.widget.debug.show(); } else { var self = this; var time = (new Date()).getTime(); var delay = 500; if (this.logo_click_time + 500 < time) { this.logo_click_time = time; this.logo_click_count = 1; } else { this.logo_click_time = time; this.logo_click_count += 1; if (this.logo_click_count >= 6) { this.logo_click_count = 0; this.gui.sudo().then(function(){ self.widget.debug.show(); }); } } } }, _scrollable: function(element, scrolling_down){ var $element = $(element); var scrollable = true; if (! scrolling_down && $element.scrollTop() <= 0) { scrollable = false; } else if (scrolling_down && $element.scrollTop() + $element.height() >= element.scrollHeight) { scrollable = false; } return scrollable; }, disable_rubberbanding: function(){ var self = this; document.body.addEventListener('touchstart', function(event){ self.previous_touch_y_coordinate = event.touches[0].clientY; }); // prevent the pos body from being scrollable. document.body.addEventListener('touchmove',function(event){ var node = event.target; var current_touch_y_coordinate = event.touches[0].clientY; var scrolling_down; if (current_touch_y_coordinate < self.previous_touch_y_coordinate) { scrolling_down = true; } else { scrolling_down = false; } while(node){ if(node.classList && node.classList.contains('touch-scrollable') && self._scrollable(node, scrolling_down)){ return; } node = node.parentNode; } event.preventDefault(); }); }, // prevent backspace from performing a 'back' navigation disable_backpace_back: function() { $(document).on("keydown", function (e) { if (e.which === 8 && !$(e.target).is("input, textarea")) { e.preventDefault(); } }); }, loading_error: function(err){ var self = this; var title = err.message; var body = err.stack; if(err.message === 'XmlHttpRequestError '){ title = 'Network Failure (XmlHttpRequestError)'; body = 'The Point of Sale could not be loaded due to a network problem.\n Please check your internet connection.'; }else if(err.code === 200){ title = err.data.message; body = err.data.debug; } if( typeof body !== 'string' ){ body = 'Traceback not available.'; } var popup = $(QWeb.render('ErrorTracebackPopupWidget',{ widget: { options: {title: title , body: body }}, })); popup.find('.button').click(function(){ self.gui.close(); }); popup.css({ zindex: 9001 }); popup.appendTo(this.$el); }, loading_progress: function(fac){ this.$('.loader .loader-feedback').removeClass('oe_hidden'); this.$('.loader .progress').removeClass('oe_hidden').css({'width': ''+Math.floor(fac*100)+'%'}); }, loading_message: function(msg,progress){ this.$('.loader .loader-feedback').removeClass('oe_hidden'); this.$('.loader .message').text(msg); if (typeof progress !== 'undefined') { this.loading_progress(progress); } else { this.$('.loader .progress').addClass('oe_hidden'); } }, loading_skip: function(callback){ if(callback){ this.$('.loader .loader-feedback').removeClass('oe_hidden'); this.$('.loader .button.skip').removeClass('oe_hidden'); this.$('.loader .button.skip').off('click'); this.$('.loader .button.skip').click(callback); }else{ this.$('.loader .button.skip').addClass('oe_hidden'); } }, loading_hide: function(){ var self = this; this.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').addClass('oe_hidden');}); }, loading_show: function(){ this.$('.loader').removeClass('oe_hidden').animate({opacity:1},150,'swing'); }, widgets: [ { 'name': 'order_selector', 'widget': OrderSelectorWidget, 'replace': '.placeholder-OrderSelectorWidget', },{ 'name': 'sale_details', 'widget': SaleDetailsButton, 'append': '.pos-rightheader', 'condition': function(){ return this.pos.config.use_proxy; }, },{ 'name': 'proxy_status', 'widget': ProxyStatusWidget, 'append': '.pos-rightheader', 'condition': function(){ return this.pos.config.use_proxy; }, },{ 'name': 'screen_status', 'widget': ClientScreenWidget, 'append': '.pos-rightheader', 'condition': function(){ return this.pos.config.use_proxy; }, },{ 'name': 'notification', 'widget': SynchNotificationWidget, 'append': '.pos-rightheader', },{ 'name': 'close_button', 'widget': HeaderButtonWidget, 'append': '.pos-rightheader', 'args': { label: _t('Close'), action: function(){ this.$el.addClass('close_button'); var self = this; if (!this.confirmed) { this.$el.addClass('confirm'); this.$el.text(_t('Confirm')); this.confirmed = setTimeout(function(){ self.$el.removeClass('confirm'); self.$el.text(_t('Close')); self.confirmed = false; },2000); } else { clearTimeout(this.confirmed); this.gui.close(); } }, } },{ 'name': 'username', 'widget': UsernameWidget, 'replace': '.placeholder-UsernameWidget', },{ 'name': 'keyboard', 'widget': keyboard.OnscreenKeyboardWidget, 'replace': '.placeholder-OnscreenKeyboardWidget', },{ 'name': 'debug', 'widget': DebugWidget, 'append': '.pos-content', }, ], load_widgets: function(widgets) { for (var i = 0; i < widgets.length; i++) { var widget = widgets[i]; if ( !widget.condition || widget.condition.call(this) ) { var args = typeof widget.args === 'function' ? widget.args(this) : widget.args; var w = new widget.widget(this, args || {}); if (widget.replace) { w.replace(this.$(widget.replace)); } else if (widget.append) { w.appendTo(this.$(widget.append)); } else if (widget.prepend) { w.prependTo(this.$(widget.prepend)); } else { w.appendTo(this.$el); } this.widget[widget.name] = w; } } }, // This method instantiates all the screens, widgets, etc. build_widgets: function() { var self = this; this.load_widgets(this.widgets); this.screens = {}; var classe; for (var i = 0; i < this.gui.screen_classes.length; i++) { classe = this.gui.screen_classes[i]; if (!classe.condition || classe.condition.call(this)) { var screen = new classe.widget(this,{}); screen.appendTo(this.$('.screens')); this.screens[classe.name] = screen; this.gui.add_screen(classe.name, screen); } } this.popups = {}; _.forEach(this.gui.popup_classes, function (classe) { if (!classe.condition || classe.condition.call(self)) { var popup = new classe.widget(self,{}); popup.appendTo(self.$('.popups')).then(function () { self.popups[classe.name] = popup; self.gui.add_popup(classe.name, popup); }); } }); this.gui.set_startup_screen('products'); this.gui.set_default_screen('products'); }, destroy: function() { this.pos.destroy(); this._super(); } }); return { Chrome: Chrome, DebugWidget: DebugWidget, HeaderButtonWidget: HeaderButtonWidget, OrderSelectorWidget: OrderSelectorWidget, ProxyStatusWidget: ProxyStatusWidget, SaleDetailsButton: SaleDetailsButton, ClientScreenWidget: ClientScreenWidget, StatusWidget: StatusWidget, SynchNotificationWidget: SynchNotificationWidget, UsernameWidget: UsernameWidget, }; });
odoo.define('point_of_sale.popups', function (require) { "use strict"; // This file contains the Popups. // Popups must be loaded and named in chrome.js. // They are instanciated / destroyed with the .gui.show_popup() // and .gui.close_popup() methods. var PosBaseWidget = require('point_of_sale.BaseWidget'); var gui = require('point_of_sale.gui'); var _t = require('web.core')._t; var PopupWidget = PosBaseWidget.extend({ template: 'PopupWidget', init: function(parent, args) { this._super(parent, args); this.options = {}; }, events: { 'click .button.cancel': 'click_cancel', 'click .button.confirm': 'click_confirm', 'click .selection-item': 'click_item', 'click .input-button': 'click_numpad', 'click .mode-button': 'click_numpad', }, // show the popup ! show: function(options){ if(this.$el){ this.$el.removeClass('oe_hidden'); } if (typeof options === 'string') { this.options = {title: options}; } else { this.options = options || {}; } this.renderElement(); // popups block the barcode reader ... if (this.pos.barcode_reader) { this.pos.barcode_reader.save_callbacks(); this.pos.barcode_reader.reset_action_callbacks(); } }, // called before hide, when a popup is closed. // extend this if you want a custom action when the // popup is closed. close: function(){ if (this.pos.barcode_reader) { this.pos.barcode_reader.restore_callbacks(); } }, // hides the popup. keep in mind that this is called in // the initialization pass of the pos instantiation, // so you don't want to do anything fancy in here hide: function(){ if (this.$el) { this.$el.addClass('oe_hidden'); } }, // what happens when we click cancel // ( it should close the popup and do nothing ) click_cancel: function(){ this.gui.close_popup(); if (this.options.cancel) { this.options.cancel.call(this); } }, // what happens when we confirm the action click_confirm: function(){ this.gui.close_popup(); if (this.options.confirm) { this.options.confirm.call(this); } }, // Since Widget does not support extending the events declaration // we declared them all in the top class. click_item: function(){}, click_numad: function(){}, }); gui.define_popup({name:'alert', widget: PopupWidget}); var ErrorPopupWidget = PopupWidget.extend({ template:'ErrorPopupWidget', show: function(options){ this._super(options); this.gui.play_sound('error'); }, }); gui.define_popup({name:'error', widget: ErrorPopupWidget}); var ErrorTracebackPopupWidget = ErrorPopupWidget.extend({ template:'ErrorTracebackPopupWidget', show: function(opts) { var self = this; this._super(opts); this.$('.download').off('click').click(function(){ self.gui.download_file(self.options.body,'traceback.txt'); }); this.$('.email').off('click').click(function(){ self.gui.send_email( self.pos.company.email, _t('IMPORTANT: Bug Report From Odoo Point Of Sale'), self.options.body); }); } }); gui.define_popup({name:'error-traceback', widget: ErrorTracebackPopupWidget}); var ErrorBarcodePopupWidget = ErrorPopupWidget.extend({ template:'ErrorBarcodePopupWidget', show: function(barcode){ this._super({barcode: barcode}); }, }); gui.define_popup({name:'error-barcode', widget: ErrorBarcodePopupWidget}); var ConfirmPopupWidget = PopupWidget.extend({ template: 'ConfirmPopupWidget', }); gui.define_popup({name:'confirm', widget: ConfirmPopupWidget}); /** * A popup that allows the user to select one item from a list. * * show_popup('selection',{ * title: "Popup Title", * list: [ * { label: 'foobar', item: 45 }, * { label: 'bar foo', item: 'stuff' }, * ], * confirm: function(item) { * // get the item selected by the user. * }, * cancel: function(){ * // user chose nothing * } * }); */ var SelectionPopupWidget = PopupWidget.extend({ template: 'SelectionPopupWidget', show: function(options){ options = options || {}; var self = this; this._super(options); this.list = options.list || []; this.renderElement(); }, click_item : function(event) { this.gui.close_popup(); if (this.options.confirm) { var item = this.list[parseInt($(event.target).data('item-index'))]; item = item ? item.item : item; this.options.confirm.call(self,item); } } }); gui.define_popup({name:'selection', widget: SelectionPopupWidget}); var TextInputPopupWidget = PopupWidget.extend({ template: 'TextInputPopupWidget', show: function(options){ options = options || {}; this._super(options); this.renderElement(); this.$('input,textarea').focus(); }, click_confirm: function(){ var value = this.$('input,textarea').val(); this.gui.close_popup(); if( this.options.confirm ){ this.options.confirm.call(this,value); } }, }); gui.define_popup({name:'textinput', widget: TextInputPopupWidget}); var TextAreaPopupWidget = TextInputPopupWidget.extend({ template: 'TextAreaPopupWidget', }); gui.define_popup({name:'textarea', widget: TextAreaPopupWidget}); var PackLotLinePopupWidget = PopupWidget.extend({ template: 'PackLotLinePopupWidget', events: _.extend({}, PopupWidget.prototype.events, { 'click .remove-lot': 'remove_lot', 'keydown': 'add_lot', 'blur .packlot-line-input': 'lose_input_focus' }), show: function(options){ this._super(options); this.focus(); }, click_confirm: function(){ var pack_lot_lines = this.options.pack_lot_lines; this.$('.packlot-line-input').each(function(index, el){ var cid = $(el).attr('cid'), lot_name = $(el).val(); var pack_line = pack_lot_lines.get({cid: cid}); pack_line.set_lot_name(lot_name); }); pack_lot_lines.remove_empty_model(); pack_lot_lines.set_quantity_by_lot(); this.options.order.save_to_db(); this.gui.close_popup(); }, add_lot: function(ev) { if (ev.keyCode === $.ui.keyCode.ENTER){ var pack_lot_lines = this.options.pack_lot_lines, $input = $(ev.target), cid = $input.attr('cid'), lot_name = $input.val(); var lot_model = pack_lot_lines.get({cid: cid}); lot_model.set_lot_name(lot_name); // First set current model then add new one if(!pack_lot_lines.get_empty_model()){ var new_lot_model = lot_model.add(); this.focus_model = new_lot_model; } pack_lot_lines.set_quantity_by_lot(); this.renderElement(); this.focus(); } }, remove_lot: function(ev){ var pack_lot_lines = this.options.pack_lot_lines, $input = $(ev.target).prev(), cid = $input.attr('cid'); var lot_model = pack_lot_lines.get({cid: cid}); lot_model.remove(); pack_lot_lines.set_quantity_by_lot(); this.renderElement(); }, lose_input_focus: function(ev){ var $input = $(ev.target), cid = $input.attr('cid'); var lot_model = this.options.pack_lot_lines.get({cid: cid}); lot_model.set_lot_name($input.val()); }, focus: function(){ this.$("input[autofocus]").focus(); this.focus_model = false; // after focus clear focus_model on widget } }); gui.define_popup({name:'packlotline', widget:PackLotLinePopupWidget}); var NumberPopupWidget = PopupWidget.extend({ template: 'NumberPopupWidget', show: function(options){ options = options || {}; this._super(options); this.inputbuffer = '' + (options.value || ''); this.decimal_separator = _t.database.parameters.decimal_point; this.renderElement(); this.firstinput = true; }, click_numpad: function(event){ var newbuf = this.gui.numpad_input( this.inputbuffer, $(event.target).data('action'), {'firstinput': this.firstinput}); this.firstinput = (newbuf.length === 0); if (newbuf !== this.inputbuffer) { this.inputbuffer = newbuf; this.$('.value').text(this.inputbuffer); } }, click_confirm: function(){ this.gui.close_popup(); if( this.options.confirm ){ this.options.confirm.call(this,this.inputbuffer); } }, }); gui.define_popup({name:'number', widget: NumberPopupWidget}); var PasswordPopupWidget = NumberPopupWidget.extend({ renderElement: function(){ this._super(); this.$('.popup').addClass('popup-password'); }, }); gui.define_popup({name:'password', widget: PasswordPopupWidget}); var OrderImportPopupWidget = PopupWidget.extend({ template: 'OrderImportPopupWidget', }); gui.define_popup({name:'orderimport', widget: OrderImportPopupWidget}); return PopupWidget; });
odoo.define('pos_restaurant.floors', function (require) { "use strict"; var PosBaseWidget = require('point_of_sale.BaseWidget'); var chrome = require('point_of_sale.chrome'); var gui = require('point_of_sale.gui'); var models = require('point_of_sale.models'); var screens = require('point_of_sale.screens'); var core = require('web.core'); var rpc = require('web.rpc'); var QWeb = core.qweb; var _t = core._t; // At POS Startup, load the floors, and add them to the pos model models.load_models({ model: 'restaurant.floor', fields: ['name','background_color','table_ids','sequence'], domain: function(self){ return [['pos_config_id','=',self.config.id]]; }, loaded: function(self,floors){ self.floors = floors; self.floors_by_id = {}; for (var i = 0; i < floors.length; i++) { floors[i].tables = []; self.floors_by_id[floors[i].id] = floors[i]; } // Make sure they display in the correct order self.floors = self.floors.sort(function(a,b){ return a.sequence - b.sequence; }); // Ignore floorplan features if no floor specified. self.config.iface_floorplan = !!self.floors.length; }, }); // At POS Startup, after the floors are loaded, load the tables, and associate // them with their floor. models.load_models({ model: 'restaurant.table', fields: ['name','width','height','position_h','position_v','shape','floor_id','color','seats'], loaded: function(self,tables){ self.tables_by_id = {}; for (var i = 0; i < tables.length; i++) { self.tables_by_id[tables[i].id] = tables[i]; var floor = self.floors_by_id[tables[i].floor_id[0]]; if (floor) { floor.tables.push(tables[i]); tables[i].floor = floor; } } }, }); // The Table GUI element, should always be a child of the FloorScreenWidget var TableWidget = PosBaseWidget.extend({ template: 'TableWidget', init: function(parent, options){ this._super(parent, options); this.table = options.table; this.selected = false; this.moved = false; this.dragpos = {x:0, y:0}; this.handle_dragging = false; this.handle = null; }, // computes the absolute position of a DOM mouse event, used // when resizing tables event_position: function(event){ if(event.touches && event.touches[0]){ return {x: event.touches[0].screenX, y: event.touches[0].screenY}; }else{ return {x: event.screenX, y: event.screenY}; } }, // when a table is clicked, go to the table's orders // but if we're editing, we select/deselect it. click_handler: function(){ var self = this; var floorplan = this.getParent(); if (floorplan.editing) { setTimeout(function(){ // in a setTimeout to debounce with drag&drop start if (!self.dragging) { if (self.moved) { self.moved = false; } else if (!self.selected) { self.getParent().select_table(self); } else { self.getParent().deselect_tables(); } } },50); } else { floorplan.pos.set_table(this.table); } }, // drag and drop for moving the table, at drag start dragstart_handler: function(event,$el,drag){ if (this.selected && !this.handle_dragging) { this.dragging = true; this.dragpos = { x: drag.offsetX, y: drag.offsetY }; } }, // drag and drop for moving the table, at drag end dragend_handler: function(){ this.dragging = false; }, // drag and drop for moving the table, at every drop movement. dragmove_handler: function(event,$el,drag){ if (this.dragging) { var dx = drag.offsetX - this.dragpos.x; var dy = drag.offsetY - this.dragpos.y; this.dragpos = { x: drag.offsetX, y: drag.offsetY }; this.moved = true; this.table.position_v += dy; this.table.position_h += dx; $el.css(this.table_style()); } }, // drag and dropping the resizing handles handle_dragstart_handler: function(event, $el, drag) { if (this.selected && !this.dragging) { this.handle_dragging = true; this.handle_dragpos = this.event_position(event); this.handle = drag.target; } }, handle_dragend_handler: function() { this.handle_dragging = false; }, handle_dragmove_handler: function(event) { if (this.handle_dragging) { var pos = this.event_position(event); var dx = pos.x - this.handle_dragpos.x; var dy = pos.y - this.handle_dragpos.y; this.handle_dragpos = pos; this.moved = true; var cl = this.handle.classList; var MIN_SIZE = 40; // smaller than this, and it becomes impossible to edit. var tw = Math.max(MIN_SIZE, this.table.width); var th = Math.max(MIN_SIZE, this.table.height); var tx = this.table.position_h; var ty = this.table.position_v; if (cl.contains('left') && tw - dx >= MIN_SIZE) { tw -= dx; tx += dx; } else if (cl.contains('right') && tw + dx >= MIN_SIZE) { tw += dx; } if (cl.contains('top') && th - dy >= MIN_SIZE) { th -= dy; ty += dy; } else if (cl.contains('bottom') && th + dy >= MIN_SIZE) { th += dy; } this.table.width = tw; this.table.height = th; this.table.position_h = tx; this.table.position_v = ty; this.$el.css(this.table_style()); } }, set_table_color: function(color){ this.table.color = _.escape(color); this.$el.css({'background': this.table.color}); }, set_table_name: function(name){ if (name) { this.table.name = name; this.renderElement(); } }, set_table_seats: function(seats){ if (seats) { this.table.seats = Number(seats); this.renderElement(); } }, // The table's positioning is handled via css absolute positioning, // which is handled here. table_style: function(){ var table = this.table; function unit(val){ return '' + val + 'px'; } var style = { 'width': unit(table.width), 'height': unit(table.height), 'line-height': unit(table.height), 'margin-left': unit(-table.width/2), 'margin-top': unit(-table.height/2), 'top': unit(table.position_v + table.height/2), 'left': unit(table.position_h + table.width/2), 'border-radius': table.shape === 'round' ? unit(Math.max(table.width,table.height)/2) : '3px', }; if (table.color) { style.background = table.color; } if (table.height >= 150 && table.width >= 150) { style['font-size'] = '32px'; } return style; }, // convert the style dictionary to a ; separated string for inclusion in templates table_style_str: function(){ var style = this.table_style(); var str = ""; var s; for (s in style) { str += s + ":" + style[s] + "; "; } return str; }, // select the table (should be called via the floorplan) select: function() { this.selected = true; this.renderElement(); }, // deselect the table (should be called via the floorplan) deselect: function() { this.selected = false; this.renderElement(); this.save_changes(); }, // sends the table's modification to the server save_changes: function(){ var self = this; var fields = _.find(this.pos.models,function(model){ return model.model === 'restaurant.table'; }).fields; // we need a serializable copy of the table, containing only the fields defined on the server var serializable_table = {}; for (var i = 0; i < fields.length; i++) { if (typeof this.table[fields[i]] !== 'undefined') { serializable_table[fields[i]] = this.table[fields[i]]; } } // and the id ... serializable_table.id = this.table.id; rpc.query({ model: 'restaurant.table', method: 'create_from_ui', args: [serializable_table], }) .then(function (table_id){ rpc.query({ model: 'restaurant.table', method: 'search_read', args: [[['id', '=', table_id]], fields], limit: 1, }) .then(function (table){ for (var field in table) { self.table[field] = table[field]; } self.renderElement(); }); }, function(err,event) { self.gui.show_popup('error',{ 'title':_t('Changes could not be saved'), 'body': _t('You must be connected to the internet to save your changes.'), }); event.stopPropagation(); event.preventDefault(); }); }, // destroy the table. We do not really destroy it, we set it // to inactive so that it doesn't show up anymore, but it still // available on the database for the orders that depend on it. trash: function(){ var self = this; rpc.query({ model: 'restaurant.table', method: 'create_from_ui', args: [{'active':false,'id':this.table.id}], }) .then(function (table_id){ // Removing all references from the table and the table_widget in in the UI ... for (var i = 0; i < self.pos.floors.length; i++) { var floor = self.pos.floors[i]; for (var j = 0; j < floor.tables.length; j++) { if (floor.tables[j].id === table_id) { floor.tables.splice(j,1); break; } } } var floorplan = self.getParent(); for (var i = 0; i < floorplan.table_widgets.length; i++) { if (floorplan.table_widgets[i] === self) { floorplan.table_widgets.splice(i,1); } } if (floorplan.selected_table === self) { floorplan.selected_table = null; } floorplan.update_toolbar(); self.destroy(); }, function(err, event) { self.gui.show_popup('error', { 'title':_t('Changes could not be saved'), 'body': _t('You must be connected to the internet to save your changes.'), }); event.stopPropagation(); event.preventDefault(); }); }, get_notifications: function(){ //FIXME : Make this faster var orders = this.pos.get_table_orders(this.table); var notifications = {}; for (var i = 0; i < orders.length; i++) { if (orders[i].hasChangesToPrint()) { notifications.printing = true; break; } else if (orders[i].hasSkippedChanges()) { notifications.skipped = true; } } return notifications; }, update_click_handlers: function(editing){ var self = this; this.$el.off('mouseup touchend touchcancel click dragend'); if (editing) { this.$el.on('mouseup touchend touchcancel', function(event){ self.click_handler(event,$(this)); }); } else { this.$el.on('click dragend', function(event){ self.click_handler(event,$(this)); }); } }, renderElement: function(){ var self = this; this.order_count = this.pos.get_table_orders(this.table).length; this.customer_count = this.pos.get_customer_count(this.table); this.fill = Math.min(1,Math.max(0,this.customer_count / this.table.seats)); this.notifications = this.get_notifications(); this._super(); this.update_click_handlers(); this.$el.on('dragstart', function(event,drag){ self.dragstart_handler(event,$(this),drag); }); this.$el.on('drag', function(event,drag){ self.dragmove_handler(event,$(this),drag); }); this.$el.on('dragend', function(event,drag){ self.dragend_handler(event,$(this),drag); }); var handles = this.$el.find('.table-handle'); handles.on('dragstart', function(event,drag){ self.handle_dragstart_handler(event,$(this),drag); }); handles.on('drag', function(event,drag){ self.handle_dragmove_handler(event,$(this),drag); }); handles.on('dragend', function(event,drag){ self.handle_dragend_handler(event,$(this),drag); }); }, }); // The screen that allows you to select the floor, see and select the table, // as well as edit them. var FloorScreenWidget = screens.ScreenWidget.extend({ template: 'FloorScreenWidget', // Ignore products, discounts, and client barcodes barcode_product_action: function(code){}, barcode_discount_action: function(code){}, barcode_client_action: function(code){}, init: function(parent, options) { this._super(parent, options); this.floor = this.pos.floors[0]; this.table_widgets = []; this.selected_table = null; this.editing = false; }, hide: function(){ this._super(); if (this.editing) { this.toggle_editing(); } this.chrome.widget.order_selector.show(); }, show: function(){ this._super(); this.chrome.widget.order_selector.hide(); for (var i = 0; i < this.table_widgets.length; i++) { this.table_widgets[i].renderElement(); } this.check_empty_floor(); }, click_floor_button: function(event,$el){ var floor = this.pos.floors_by_id[$el.data('id')]; if (floor !== this.floor) { if (this.editing) { this.toggle_editing(); } this.floor = floor; this.selected_table = null; this.renderElement(); this.check_empty_floor(); } }, background_image_url: function(floor) { return '/web/image?model=restaurant.floor&id='+floor.id+'&field=background_image'; }, get_floor_style: function() { var style = ""; if (this.floor.background_image) { style += "background-image: url(" + this.background_image_url(this.floor) + "); "; } if (this.floor.background_color) { style += "background-color: " + _.escape(this.floor.background_color) + ";"; } return style; }, set_background_color: function(background) { var self = this; this.floor.background_color = background; rpc.query({ model: 'restaurant.floor', method: 'write', args: [[this.floor.id], {'background_color': background}], }) .fail(function (err, event){ self.gui.show_popup('error',{ 'title':_t('Changes could not be saved'), 'body': _t('You must be connected to the internet to save your changes.'), }); event.stopPropagation(); event.preventDefault(); }); this.$('.floor-map').css({"background-color": _.escape(background)}); }, deselect_tables: function(){ for (var i = 0; i < this.table_widgets.length; i++) { var table = this.table_widgets[i]; if (table.selected) { table.deselect(); } } this.selected_table = null; this.update_toolbar(); }, select_table: function(table_widget){ if (!table_widget.selected) { this.deselect_tables(); table_widget.select(); this.selected_table = table_widget; this.update_toolbar(); } }, tool_shape_action: function(){ if (this.selected_table) { var table = this.selected_table.table; if (table.shape === 'square') { table.shape = 'round'; } else { table.shape = 'square'; } this.selected_table.renderElement(); this.update_toolbar(); } }, tool_colorpicker_open: function(){ this.$('.color-picker').addClass('oe_hidden'); if (this.selected_table) { this.$('.color-picker.fg-picker').removeClass('oe_hidden'); } else { this.$('.color-picker.bg-picker').removeClass('oe_hidden'); } }, tool_colorpicker_pick: function(event,$el){ if (this.selected_table) { this.selected_table.set_table_color($el[0].style['background-color']); } else { this.set_background_color($el[0].style['background-color']); } }, tool_colorpicker_close: function(){ this.$('.color-picker').addClass('oe_hidden'); }, tool_rename_table: function(){ var self = this; if (this.selected_table) { this.gui.show_popup('textinput',{ 'title':_t('Table Name ?'), 'value': this.selected_table.table.name, 'confirm': function(value) { self.selected_table.set_table_name(value); }, }); } }, tool_change_seats: function(){ var self = this; if (this.selected_table) { this.gui.show_popup('number',{ 'title':_t('Number of Seats ?'), 'cheap': true, 'value': this.selected_table.table.seats, 'confirm': function(value) { self.selected_table.set_table_seats(value); }, }); } }, tool_duplicate_table: function(){ if (this.selected_table) { var tw = this.create_table(this.selected_table.table); tw.table.position_h += 10; tw.table.position_v += 10; tw.save_changes(); this.select_table(tw); } }, tool_new_table: function(){ var tw = this.create_table({ 'position_v': 100, 'position_h': 100, 'width': 75, 'height': 75, 'shape': 'square', 'seats': 1, }); tw.save_changes(); this.select_table(tw); this.check_empty_floor(); }, new_table_name: function(name){ if (name) { var num = Number((name.match(/\d+/g) || [])[0] || 0); var str = (name.replace(/\d+/g,'')); var n = {num: num, str:str}; n.num += 1; this.last_name = n; } else if (this.last_name) { this.last_name.num += 1; } else { this.last_name = {num: 1, str:'T'}; } return '' + this.last_name.str + this.last_name.num; }, create_table: function(params) { var table = {}; for (var p in params) { table[p] = params[p]; } table.name = this.new_table_name(params.name); delete table.id; table.floor_id = [this.floor.id,'']; table.floor = this.floor; this.floor.tables.push(table); var tw = new TableWidget(this,{table: table}); tw.appendTo('.floor-map .tables'); this.table_widgets.push(tw); return tw; }, tool_trash_table: function(){ var self = this; if (this.selected_table) { this.gui.show_popup('confirm',{ 'title': _t('Are you sure ?'), 'comment':_t('Removing a table cannot be undone'), 'confirm': function(){ self.selected_table.trash(); }, }); } }, toggle_editing: function(){ this.editing = !this.editing; this.update_toolbar(); this.update_table_click_handlers(); if (!this.editing) { this.deselect_tables(); } }, update_table_click_handlers: function(){ for (var i = 0; i < this.table_widgets.length; ++i) { if (this.editing) { this.table_widgets[i].update_click_handlers("editing"); } else { this.table_widgets[i].update_click_handlers(); } } }, check_empty_floor: function(){ if (!this.floor.tables.length) { if (!this.editing) { this.toggle_editing(); } this.$('.empty-floor').removeClass('oe_hidden'); } else { this.$('.empty-floor').addClass('oe_hidden'); } }, update_toolbar: function(){ if (this.editing) { this.$('.edit-bar').removeClass('oe_hidden'); this.$('.edit-button.editing').addClass('active'); } else { this.$('.edit-bar').addClass('oe_hidden'); this.$('.edit-button.editing').removeClass('active'); } if (this.selected_table) { this.$('.needs-selection').removeClass('disabled'); var table = this.selected_table.table; if (table.shape === 'square') { this.$('.button-option.square').addClass('oe_hidden'); this.$('.button-option.round').removeClass('oe_hidden'); } else { this.$('.button-option.square').removeClass('oe_hidden'); this.$('.button-option.round').addClass('oe_hidden'); } } else { this.$('.needs-selection').addClass('disabled'); } this.tool_colorpicker_close(); }, renderElement: function(){ var self = this; // cleanup table widgets from previous renders for (var i = 0; i < this.table_widgets.length; i++) { this.table_widgets[i].destroy(); } this.table_widgets = []; this._super(); for (var i = 0; i < this.floor.tables.length; i++) { var tw = new TableWidget(this,{ table: this.floor.tables[i], }); tw.appendTo(this.$('.floor-map .tables')); this.table_widgets.push(tw); } this.$('.floor-selector .button').click(function(event){ self.click_floor_button(event,$(this)); }); this.$('.edit-button.shape').click(function(){ self.tool_shape_action(); }); this.$('.edit-button.color').click(function(){ self.tool_colorpicker_open(); }); this.$('.edit-button.dup-table').click(function(){ self.tool_duplicate_table(); }); this.$('.edit-button.new-table').click(function(){ self.tool_new_table(); }); this.$('.edit-button.rename').click(function(){ self.tool_rename_table(); }); this.$('.edit-button.seats').click(function(){ self.tool_change_seats(); }); this.$('.edit-button.trash').click(function(){ self.tool_trash_table(); }); this.$('.color-picker .close-picker').click(function(event){ self.tool_colorpicker_close(); event.stopPropagation(); }); this.$('.color-picker .color').click(function(event){ self.tool_colorpicker_pick(event,$(this)); event.stopPropagation(); }); this.$('.edit-button.editing').click(function(){ self.toggle_editing(); }); this.$('.floor-map,.floor-map .tables').click(function(event){ if (event.target === self.$('.floor-map')[0] || event.target === self.$('.floor-map .tables')[0]) { self.deselect_tables(); } }); this.$('.color-picker .close-picker').click(function(event){ self.tool_colorpicker_close(); event.stopPropagation(); }); this.update_toolbar(); }, }); gui.define_screen({ 'name': 'floors', 'widget': FloorScreenWidget, 'condition': function(){ return this.pos.config.iface_floorplan; }, }); // Add the FloorScreen to the GUI, and set it as the default screen chrome.Chrome.include({ build_widgets: function(){ this._super(); if (this.pos.config.iface_floorplan) { this.gui.set_startup_screen('floors'); } }, }); // New orders are now associated with the current table, if any. var _super_order = models.Order.prototype; models.Order = models.Order.extend({ initialize: function() { _super_order.initialize.apply(this,arguments); if (!this.table) { this.table = this.pos.table; } this.customer_count = this.customer_count || 1; this.save_to_db(); }, export_as_JSON: function() { var json = _super_order.export_as_JSON.apply(this,arguments); json.table = this.table ? this.table.name : undefined; json.table_id = this.table ? this.table.id : false; json.floor = this.table ? this.table.floor.name : false; json.floor_id = this.table ? this.table.floor.id : false; json.customer_count = this.customer_count; return json; }, init_from_JSON: function(json) { _super_order.init_from_JSON.apply(this,arguments); this.table = this.pos.tables_by_id[json.table_id]; this.floor = this.table ? this.pos.floors_by_id[json.floor_id] : undefined; this.customer_count = json.customer_count || 1; }, export_for_printing: function() { var json = _super_order.export_for_printing.apply(this,arguments); json.table = this.table ? this.table.name : undefined; json.floor = this.table ? this.table.floor.name : undefined; json.customer_count = this.get_customer_count(); return json; }, get_customer_count: function(){ return this.customer_count; }, set_customer_count: function(count) { this.customer_count = Math.max(count,0); this.trigger('change'); }, }); // We need to modify the OrderSelector to hide itself when we're on // the floor plan chrome.OrderSelectorWidget.include({ floor_button_click_handler: function(){ this.pos.set_table(null); }, hide: function(){ this.$el.addClass('oe_invisible'); }, show: function(){ this.$el.removeClass('oe_invisible'); }, renderElement: function(){ var self = this; this._super(); if (this.pos.config.iface_floorplan) { if (this.pos.get_order()) { if (this.pos.table && this.pos.table.floor) { this.$('.orders').prepend(QWeb.render('BackToFloorButton',{table: this.pos.table, floor:this.pos.table.floor})); this.$('.floor-button').click(function(){ self.floor_button_click_handler(); }); } this.$el.removeClass('oe_invisible'); } else { this.$el.addClass('oe_invisible'); } } }, }); // We need to change the way the regular UI sees the orders, it // needs to only see the orders associated with the current table, // and when an order is validated, it needs to go back to the floor map. // // And when we change the table, we must create an order for that table // if there is none. var _super_posmodel = models.PosModel.prototype; models.PosModel = models.PosModel.extend({ initialize: function(session, attributes) { this.table = null; return _super_posmodel.initialize.call(this,session,attributes); }, transfer_order_to_different_table: function () { this.order_to_transfer_to_different_table = this.get_order(); // go to 'floors' screen, this will set the order to null and // eventually this will cause the gui to go to its // default_screen, which is 'floors' this.set_table(null); }, // changes the current table. set_table: function(table) { if (!table) { // no table ? go back to the floor plan, see ScreenSelector this.set_order(null); } else if (this.order_to_transfer_to_different_table) { this.order_to_transfer_to_different_table.table = table; this.order_to_transfer_to_different_table.save_to_db(); this.order_to_transfer_to_different_table = null; // set this table this.set_table(table); } else { this.table = table; var orders = this.get_order_list(); if (orders.length) { this.set_order(orders[0]); // and go to the first one ... } else { this.add_new_order(); // or create a new order with the current table } } }, // if we have tables, we do not load a default order, as the default order will be // set when the user selects a table. set_start_order: function() { if (!this.config.iface_floorplan) { _super_posmodel.set_start_order.apply(this,arguments); } }, // we need to prevent the creation of orders when there is no // table selected. add_new_order: function() { if (this.config.iface_floorplan) { if (this.table) { _super_posmodel.add_new_order.call(this); } else { console.warn("WARNING: orders cannot be created when there is no active table in restaurant mode"); } } else { _super_posmodel.add_new_order.apply(this,arguments); } }, // get the list of unpaid orders (associated to the current table) get_order_list: function() { var orders = _super_posmodel.get_order_list.call(this); if (!this.config.iface_floorplan) { return orders; } else if (!this.table) { return []; } else { var t_orders = []; for (var i = 0; i < orders.length; i++) { if ( orders[i].table === this.table) { t_orders.push(orders[i]); } } return t_orders; } }, // get the list of orders associated to a table. FIXME: should be O(1) get_table_orders: function(table) { var orders = _super_posmodel.get_order_list.call(this); var t_orders = []; for (var i = 0; i < orders.length; i++) { if (orders[i].table === table) { t_orders.push(orders[i]); } } return t_orders; }, // get customer count at table get_customer_count: function(table) { var orders = this.get_table_orders(table); var count = 0; for (var i = 0; i < orders.length; i++) { count += orders[i].get_customer_count(); } return count; }, // When we validate an order we go back to the floor plan. // When we cancel an order and there is multiple orders // on the table, stay on the table. on_removed_order: function(removed_order,index,reason){ if (this.config.iface_floorplan) { var order_list = this.get_order_list(); if( (reason === 'abandon' || removed_order.temporary) && order_list.length > 0){ this.set_order(order_list[index] || order_list[order_list.length -1]); }else{ // back to the floor plan this.set_table(null); } } else { _super_posmodel.on_removed_order.apply(this,arguments); } }, }); var TableGuestsButton = screens.ActionButtonWidget.extend({ template: 'TableGuestsButton', guests: function() { if (this.pos.get_order()) { return this.pos.get_order().customer_count; } else { return 0; } }, button_click: function() { var self = this; this.gui.show_popup('number', { 'title': _t('Guests ?'), 'cheap': true, 'value': this.pos.get_order().customer_count, 'confirm': function(value) { value = Math.max(1,Number(value)); self.pos.get_order().set_customer_count(value); self.renderElement(); }, }); }, }); screens.OrderWidget.include({ update_summary: function(){ this._super(); if (this.getParent().action_buttons && this.getParent().action_buttons.guests) { this.getParent().action_buttons.guests.renderElement(); } }, }); screens.define_action_button({ 'name': 'guests', 'widget': TableGuestsButton, 'condition': function(){ return this.pos.config.iface_floorplan; }, }); var TransferOrderButton = screens.ActionButtonWidget.extend({ template: 'TransferOrderButton', button_click: function() { this.pos.transfer_order_to_different_table(); }, }); screens.define_action_button({ 'name': 'transfer', 'widget': TransferOrderButton, 'condition': function(){ return this.pos.config.iface_floorplan; }, }); });
odoo.define('point_of_sale.popups', function (require) { "use strict"; // This file contains the Popups. // Popups must be loaded and named in chrome.js. // They are instanciated / destroyed with the .gui.show_popup() // and .gui.close_popup() methods. var PosBaseWidget = require('point_of_sale.BaseWidget'); var gui = require('point_of_sale.gui'); var PopupWidget = PosBaseWidget.extend({ init: function(parent, args) { this._super(parent, args); this.options = {}; }, events: { 'click .button.cancel': 'click_cancel', 'click .button.confirm': 'click_confirm', 'click .selection-item': 'click_item', 'click .input-button': 'click_numpad', 'click .mode-button': 'click_numpad', }, // show the popup ! show: function(options){ if(this.$el){ this.$el.removeClass('oe_hidden'); } if (typeof options === 'string') { this.options = {title: options}; } else { this.options = options || {}; } this.renderElement(); // popups block the barcode reader ... if (this.pos.barcode_reader) { this.pos.barcode_reader.save_callbacks(); this.pos.barcode_reader.reset_action_callbacks(); } }, // called before hide, when a popup is closed. // extend this if you want a custom action when the // popup is closed. close: function(){ if (this.pos.barcode_reader) { this.pos.barcode_reader.restore_callbacks(); } }, // hides the popup. keep in mind that this is called in // the initialization pass of the pos instantiation, // so you don't want to do anything fancy in here hide: function(){ if (this.$el) { this.$el.addClass('oe_hidden'); } }, // what happens when we click cancel // ( it should close the popup and do nothing ) click_cancel: function(){ this.gui.close_popup(); if (this.options.cancel) { this.options.cancel.call(this); } }, // what happens when we confirm the action click_confirm: function(){ this.gui.close_popup(); if (this.options.confirm) { this.options.confirm.call(this); } }, // Since Widget does not support extending the events declaration // we declared them all in the top class. click_item: function(){}, click_numad: function(){}, }); var ErrorPopupWidget = PopupWidget.extend({ template:'ErrorPopupWidget', show: function(options){ this._super(options); this.gui.play_sound('error'); }, }); gui.define_popup({name:'error', widget: ErrorPopupWidget}); var ErrorTracebackPopupWidget = ErrorPopupWidget.extend({ template:'ErrorTracebackPopupWidget', }); gui.define_popup({name:'error-traceback', widget: ErrorTracebackPopupWidget}); var ErrorBarcodePopupWidget = ErrorPopupWidget.extend({ template:'ErrorBarcodePopupWidget', show: function(barcode){ this._super({barcode: barcode}); }, }); gui.define_popup({name:'error-barcode', widget: ErrorBarcodePopupWidget}); var ConfirmPopupWidget = PopupWidget.extend({ template: 'ConfirmPopupWidget', }); gui.define_popup({name:'confirm', widget: ConfirmPopupWidget}); /** * A popup that allows the user to select one item from a list. * * show_popup('selection',{ * title: "Popup Title", * list: [ * { label: 'foobar', item: 45 }, * { label: 'bar foo', item: 'stuff' }, * ], * confirm: function(item) { * // get the item selected by the user. * }, * cancel: function(){ * // user chose nothing * } * }); */ var SelectionPopupWidget = PopupWidget.extend({ template: 'SelectionPopupWidget', show: function(options){ options = options || {}; var self = this; this._super(options); this.list = options.list || []; this.renderElement(); this.$('.selection-item').click(function(){ if (options.confirm) { var item = self.list[parseInt($(this).data('item-index'))]; item = item ? item.item : item; options.confirm.call(self,item); } }); }, click_item : function(event) { this.gui.close_popup(); if (this.options.confirm) { var item = this.list[parseInt($(event.target).data('item-index'))]; item = item ? item.item : item; } } }); gui.define_popup({name:'selection', widget: SelectionPopupWidget}); var TextInputPopupWidget = PopupWidget.extend({ template: 'TextInputPopupWidget', show: function(options){ options = options || {}; this._super(options); this.renderElement(); this.$('input,textarea').focus(); }, click_confirm: function(){ var value = this.$('input,textarea').val(); this.gui.close_popup(); if( this.options.confirm ){ this.options.confirm.call(this,value); } }, }); gui.define_popup({name:'textinput', widget: TextInputPopupWidget}); var TextAreaPopupWidget = TextInputPopupWidget.extend({ template: 'TextAreaPopupWidget', }); gui.define_popup({name:'textarea', widget: TextAreaPopupWidget}); var NumberPopupWidget = PopupWidget.extend({ template: 'NumberPopupWidget', show: function(options){ options = options || {}; this._super(options); this.inputbuffer = '' + options.value || ''; this.renderElement(); this.firstinput = true; }, click_numpad: function(event){ var newbuf = this.gui.numpad_input( this.inputbuffer, $(event.target).data('action'), {'firstinput': this.firstinput}); this.firstinput = (newbuf.length === 0); if (newbuf !== this.inputbuffer) { this.inputbuffer = newbuf; this.$('.value').text(this.inputbuffer); } }, click_confirm: function(){ this.gui.close_popup(); if( this.options.confirm ){ this.options.confirm.call(this,this.inputbuffer); } }, }); gui.define_popup({name:'number', widget: NumberPopupWidget}); var PasswordPopupWidget = NumberPopupWidget.extend({ renderElement: function(){ this._super(); this.$('.popup').addClass('popup-password'); }, }); gui.define_popup({name:'password', widget: PasswordPopupWidget}); var UnsentOrdersPopupWidget = ConfirmPopupWidget.extend({ template: 'UnsentOrdersPopupWidget', }); gui.define_popup({name:'unsent-orders', widget: UnsentOrdersPopupWidget}); var UnpaidOrdersPopupWidget = ConfirmPopupWidget.extend({ template: 'UnpaidOrdersPopupWidget', }); gui.define_popup({name:'unpaid-orders', widget: UnpaidOrdersPopupWidget}); });