From 1d23c24d79a0759d2832ffc2f10b3469470ce0d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Tue, 10 Mar 2015 08:46:18 +0100 Subject: [PATCH] [REF] point_of_sale: update to the new module system --- addons/point_of_sale/static/src/js/chrome.js | 1336 +++--- addons/point_of_sale/static/src/js/db.js | 930 ++--- addons/point_of_sale/static/src/js/devices.js | 1124 ++--- addons/point_of_sale/static/src/js/gui.js | 679 +-- .../point_of_sale/static/src/js/keyboard.js | 374 +- addons/point_of_sale/static/src/js/main.js | 29 +- addons/point_of_sale/static/src/js/models.js | 3644 +++++++++-------- addons/point_of_sale/static/src/js/popups.js | 434 +- addons/point_of_sale/static/src/js/screens.js | 3293 +++++++-------- addons/point_of_sale/static/src/js/tests.js | 173 +- .../static/src/js/widget_base.js | 146 +- addons/point_of_sale/views/templates.xml | 19 +- 12 files changed, 6123 insertions(+), 6058 deletions(-) diff --git a/addons/point_of_sale/static/src/js/chrome.js b/addons/point_of_sale/static/src/js/chrome.js index a0dc5a6076f..8baee366c55 100644 --- a/addons/point_of_sale/static/src/js/chrome.js +++ b/addons/point_of_sale/static/src/js/chrome.js @@ -1,667 +1,679 @@ -openerp.point_of_sale.load_chrome = function load_chrome(instance, module){ //module is instance.point_of_sale - "use strict"; +odoo.define('point_of_sale.chrome', ['point_of_sale.BaseWidget', 'point_of_sale.gui', 'point_of_sale.keyboard', 'point_of_sale.models', 'web.core'], function (require) { +"use strict"; - var QWeb = instance.web.qweb; - var _t = instance.web._t; +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 core = require('web.core'); - /* -------- The Order Selector -------- */ - - // Allows the cashier to create / delete and - // switch between orders. - module.OrderSelectorWidget = module.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]; - } +var _t = core._t; +var QWeb = core.qweb; +var PosModel = models.PosModel; + +/* -------- 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 and allows - // to switch between cashiers. - - module.UsernameWidget = module.PosBaseWidget.extend({ - template: 'UsernameWidget', - init: function(parent, options){ - var options = options || {}; - this._super(parent,options); - }, - renderElement: function(){ - var self = this; - this._super(); - - this.$el.click(function(){ - self.click_username(); - }); - }, - click_username: function(){ - var self = this; - this.gui.select_user({ - 'security': true, - 'current_user': this.pos.get_cashier(), - 'title': _t('Change Cashier'), - }).then(function(user){ - self.pos.set_cashier(user); - self.renderElement(); - }); - }, - get_name: function(){ - var user = this.pos.cashier || this.pos.user; - 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. - - module.HeaderButtonWidget = module.PosBaseWidget.extend({ - template: 'HeaderButtonWidget', - init: function(parent, options){ - options = options || {}; - this._super(parent, options); - this.action = options.action; - this.label = options.label; - }, - 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 - - module.DebugWidget = module.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() { - var self = this; - 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.show_orders').click(function(){ - self.gui.show_popup('unsent-orders'); - }); - this.$('.button.delete_orders').click(function(){ - self.gui.show_popup('confirm',{ - 'title': _t('Delete Unsent Orders ?'), - 'body': _t('This operation will permanently destroy all unsent 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.show_unpaid_orders').click(function(){ - self.gui.show_popup('unpaid-orders'); - }); - this.$('.button.delete_unpaid_orders').click(function(){ - self.gui.show_popup('confirm',{ - 'title': _t('Delete Unpaid Orders ?'), - 'body': _t('This operation will permanently destroy all unpaid orders from all sessions that have been put in the local storage. You will lose all the data and exit the point of sale. This operation cannot be undone.'), - confirm: function(){ - self.pos.db.remove_all_unpaid_orders(); - window.location = '/'; - }, - }); - }); - _.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. - - module.StatusWidget = module.PosBaseWidget.extend({ - status: ['connected','connecting','disconnected','warning'], - set_status: function(status,msg){ - var self = this; - 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. - - module.SynchNotificationWidget = module.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(); - }); - }, - }); - - /* --------- The Proxy Status --------- */ - - // Displays the status of the hardware proxy - // (connected, disconnected, errors ... ) - - module.ProxyStatusWidget = module.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 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 - - module.Chrome = module.PosBaseWidget.extend({ - 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 module.PosModel(this.session,{chrome:this}); - this.gui = new module.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.widget = {}; // contains references to subwidgets instances - - this.cleanup_dom(); - - this.pos.ready.done(function(){ - self.build_chrome(); - self.build_widgets(); - self.disable_rubberbanding(); - self.ready.resolve(); - self.loading_hide(); - }).fail(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(); - $(self.$el).parent().off(); - $('document').off(); - $('.oe_web_client').off(); - $('.openerp_webclient_container').off(); - }, - - build_chrome: function() { - var self = this; - FastClick.attach(document.body); - - instance.webclient.set_content_full_screen(true); - - this.renderElement(); - - this.$('.pos-logo').click(function(){ - self.click_logo(); - }); - - if(this.pos.config.iface_big_scrollbars){ - this.$el.addClass('big-scrollbars'); - } - }, - - 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(); - }); - } - } - } - }, - - disable_rubberbanding: function(){ - // prevent the pos body from being scrollable. - document.body.addEventListener('touchmove',function(event){ - var node = event.target; - while(node){ - if(node.classList && node.classList.contains('touch-scrollable')){ - return; - } - node = node.parentNode; - } - event.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.message === 'OpenERP Server Error'){ - 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(){ - 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': module.OrderSelectorWidget, - 'replace': '.placeholder-OrderSelectorWidget', - },{ - 'name': 'proxy_status', - 'widget': module.ProxyStatusWidget, - 'append': '.pos-rightheader', - 'condition': function(){ return this.pos.config.use_proxy }, - },{ - 'name': 'notification', - 'widget': module.SynchNotificationWidget, - 'append': '.pos-rightheader', - },{ - 'name': 'close_button', - 'widget': module.HeaderButtonWidget, - 'append': '.pos-rightheader', - 'args': { - label: _t('Close'), - action: function(){ - 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': module.UsernameWidget, - 'replace': '.placeholder-UsernameWidget', - },{ - 'name': 'keyboard', - 'widget': module.OnscreenKeyboardWidget, - 'replace': '.placeholder-OnscreenKeyboardWidget', - },{ - 'name': 'debug', - 'widget': module.DebugWidget, - 'append': '.pos-content', - }, - ], - - // This method instantiates all the screens, widgets, etc. - build_widgets: function() { - var self = this; - - for (var i = 0; i < this.widgets.length; i++) { - var def = this.widgets[i]; - if ( !def.condition || def.condition.call(this) ) { - var args = typeof def.args === 'function' ? def.args(this) : def.args; - var w = new def.widget(this, args || {}); - if (def.replace) { - w.replace(this.$(def.replace)); - } else if (def.append) { - w.appendTo(this.$(def.append)); - } else if (def.prepend) { - w.prependTo(this.$(def.prepend)); - } else { - w.appendTo(this.$el); - } - this.widget[def.name] = w; - } - } - - this.screens = {}; - for (var i = 0; i < this.gui.screen_classes.length; i++) { - var 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 = {}; - for (var i = 0; i < this.gui.popup_classes.length; i++) { - var classe = this.gui.popup_classes[i]; - if (!classe.condition || classe.condition.call(this)) { - var popup = new classe.widget(this,{}); - popup.appendTo(this.$('.popups')); - this.popups[classe.name] = popup; - this.gui.add_popup(classe.name, popup); - } - } - - this.gui.set_startup_screen('products'); - this.gui.set_default_screen('products'); - - }, - - destroy: function() { - this.pos.destroy(); - instance.webclient.set_content_full_screen(false); - this._super(); } - }); -} + 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 and allows +// to switch between cashiers. + +var UsernameWidget = PosBaseWidget.extend({ + template: 'UsernameWidget', + init: function(parent, options){ + options = options || {}; + this._super(parent,options); + }, + renderElement: function(){ + var self = this; + this._super(); + + this.$el.click(function(){ + self.click_username(); + }); + }, + click_username: function(){ + var self = this; + this.gui.select_user({ + 'security': true, + 'current_user': this.pos.get_cashier(), + 'title': _t('Change Cashier'), + }).then(function(user){ + self.pos.set_cashier(user); + self.renderElement(); + }); + }, + get_name: function(){ + var user = this.pos.cashier || this.pos.user; + 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; + }, + 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.show_orders').click(function(){ + self.gui.show_popup('unsent-orders'); + }); + this.$('.button.delete_orders').click(function(){ + self.gui.show_popup('confirm',{ + 'title': _t('Delete Unsent Orders ?'), + 'body': _t('This operation will permanently destroy all unsent 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.show_unpaid_orders').click(function(){ + self.gui.show_popup('unpaid-orders'); + }); + this.$('.button.delete_unpaid_orders').click(function(){ + self.gui.show_popup('confirm',{ + 'title': _t('Delete Unpaid Orders ?'), + 'body': _t('This operation will permanently destroy all unpaid orders from all sessions that have been put in the local storage. You will lose all the data and exit the point of sale. This operation cannot be undone.'), + confirm: function(){ + self.pos.db.remove_all_unpaid_orders(); + window.location = '/'; + }, + }); + }); + _.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'], + 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(); + }); + }, +}); + +/* --------- 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 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({ + 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 PosModel(this.session,{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.widget = {}; // contains references to subwidgets instances + + this.cleanup_dom(); + + this.pos.ready.done(function(){ + self.build_chrome(); + self.build_widgets(); + self.disable_rubberbanding(); + self.ready.resolve(); + self.loading_hide(); + }).fail(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(); + $(this.$el).parent().off(); + $('document').off(); + $('.oe_web_client').off(); + $('.openerp_webclient_container').off(); + }, + + build_chrome: function() { + var self = this; + FastClick.attach(document.body); + core.bus.trigger('set_full_screen', true); + + this.renderElement(); + + this.$('.pos-logo').click(function(){ + self.click_logo(); + }); + + if(this.pos.config.iface_big_scrollbars){ + this.$el.addClass('big-scrollbars'); + } + }, + + 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(); + }); + } + } + } + }, + + disable_rubberbanding: function(){ + // prevent the pos body from being scrollable. + document.body.addEventListener('touchmove',function(event){ + var node = event.target; + while(node){ + if(node.classList && node.classList.contains('touch-scrollable')){ + return; + } + node = node.parentNode; + } + event.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.message === 'OpenERP Server Error'){ + 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': 'proxy_status', + 'widget': ProxyStatusWidget, + '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(){ + 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', + }, + ], + + // This method instantiates all the screens, widgets, etc. + build_widgets: function() { + var classe; + + for (var i = 0; i < this.widgets.length; i++) { + var def = this.widgets[i]; + if ( !def.condition || def.condition.call(this) ) { + var args = typeof def.args === 'function' ? def.args(this) : def.args; + var w = new def.widget(this, args || {}); + if (def.replace) { + w.replace(this.$(def.replace)); + } else if (def.append) { + w.appendTo(this.$(def.append)); + } else if (def.prepend) { + w.prependTo(this.$(def.prepend)); + } else { + w.appendTo(this.$el); + } + this.widget[def.name] = w; + } + } + + this.screens = {}; + for (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 = {}; + for (i = 0; i < this.gui.popup_classes.length; i++) { + classe = this.gui.popup_classes[i]; + if (!classe.condition || classe.condition.call(this)) { + var popup = new classe.widget(this,{}); + popup.appendTo(this.$('.popups')); + this.popups[classe.name] = popup; + this.gui.add_popup(classe.name, popup); + } + } + + this.gui.set_startup_screen('products'); + this.gui.set_default_screen('products'); + + }, + + destroy: function() { + this.pos.destroy(); + core.bus.trigger('set_full_screen', false); + this._super(); + } +}); + +return { + Chrome: Chrome, + OrderSelectorWidget: OrderSelectorWidget, +}; +}); + diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index 2729d7118e9..c928e1f0eef 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -1,479 +1,485 @@ -openerp.point_of_sale.load_db = function load_db(instance, module){ - "use strict"; +odoo.define('point_of_sale.DB', ['web.core'], function (require) { +"use strict"; - /* The PosDB holds reference to data that is either - * - static: does not change between pos reloads - * - persistent : must stay between reloads ( orders ) +var core = require('web.core'); +/* The PosDB holds reference to data that is either + * - static: does not change between pos reloads + * - persistent : must stay between reloads ( orders ) + */ + +var PosDB = core.Class.extend({ + name: 'openerp_pos_db', //the prefix of the localstorage data + limit: 100, // the maximum number of results returned by a search + init: function(options){ + options = options || {}; + this.name = options.name || this.name; + this.limit = options.limit || this.limit; + + if (options.uuid) { + this.name = this.name + '_' + options.uuid; + } + + //cache the data in memory to avoid roundtrips to the localstorage + this.cache = {}; + + this.product_by_id = {}; + this.product_by_barcode = {}; + this.product_by_category_id = {}; + + this.partner_sorted = []; + this.partner_by_id = {}; + this.partner_by_barcode = {}; + this.partner_search_string = ""; + this.partner_write_date = null; + + this.category_by_id = {}; + this.root_category_id = 0; + this.category_products = {}; + this.category_ancestors = {}; + this.category_childs = {}; + this.category_parent = {}; + this.category_search_string = {}; + this.packagings_by_id = {}; + this.packagings_by_product_tmpl_id = {}; + this.packagings_by_barcode = {}; + }, + + /* + * sets an uuid to prevent conflict in locally stored data between multiple databases running + * in the same browser at the same origin (Doing this is not advised !) */ + set_uuid: function(uuid){ + this.name = this.name + '_' + uuid; + }, - module.PosDB = instance.web.Class.extend({ - name: 'openerp_pos_db', //the prefix of the localstorage data - limit: 100, // the maximum number of results returned by a search - init: function(options){ - options = options || {}; - this.name = options.name || this.name; - this.limit = options.limit || this.limit; - - if (options.uuid) { - this.name = this.name + '_' + options.uuid; - } - - //cache the data in memory to avoid roundtrips to the localstorage - this.cache = {}; - - this.product_by_id = {}; - this.product_by_barcode = {}; - this.product_by_category_id = {}; - - this.partner_sorted = []; - this.partner_by_id = {}; - this.partner_by_barcode = {}; - this.partner_search_string = ""; - this.partner_write_date = null; - - this.category_by_id = {}; - this.root_category_id = 0; - this.category_products = {}; - this.category_ancestors = {}; - this.category_childs = {}; - this.category_parent = {}; - this.category_search_string = {}; - this.packagings_by_id = {}; - this.packagings_by_product_tmpl_id = {}; - this.packagings_by_barcode = {}; - }, - - /* - * sets an uuid to prevent conflict in locally stored data between multiple databases running - * in the same browser at the same origin (Doing this is not advised !) - */ - set_uuid: function(uuid){ - this.name = this.name + '_' + uuid; - }, - - /* returns the category object from its id. If you pass a list of id as parameters, you get - * a list of category objects. - */ - get_category_by_id: function(categ_id){ - if(categ_id instanceof Array){ - var list = []; - for(var i = 0, len = categ_id.length; i < len; i++){ - var cat = this.category_by_id[categ_id[i]]; - if(cat){ - list.push(cat); - }else{ - console.error("get_category_by_id: no category has id:",categ_id[i]); - } - } - return list; - }else{ - return this.category_by_id[categ_id]; - } - }, - /* returns a list of the category's child categories ids, or an empty list - * if a category has no childs */ - get_category_childs_ids: function(categ_id){ - return this.category_childs[categ_id] || []; - }, - /* returns a list of all ancestors (parent, grand-parent, etc) categories ids - * starting from the root category to the direct parent */ - get_category_ancestors_ids: function(categ_id){ - return this.category_ancestors[categ_id] || []; - }, - /* returns the parent category's id of a category, or the root_category_id if no parent. - * the root category is parent of itself. */ - get_category_parent_id: function(categ_id){ - return this.category_parent[categ_id] || this.root_category_id; - }, - /* adds categories definitions to the database. categories is a list of categories objects as - * returned by the openerp server. Categories must be inserted before the products or the - * product/ categories association may (will) not work properly */ - add_categories: function(categories){ - var self = this; - if(!this.category_by_id[this.root_category_id]){ - this.category_by_id[this.root_category_id] = { - id : this.root_category_id, - name : 'Root', - }; - } - for(var i=0, len = categories.length; i < len; i++){ - this.category_by_id[categories[i].id] = categories[i]; - } - for(var i=0, len = categories.length; i < len; i++){ - var cat = categories[i]; - var parent_id = cat.parent_id[0] || this.root_category_id; - this.category_parent[cat.id] = cat.parent_id[0]; - if(!this.category_childs[parent_id]){ - this.category_childs[parent_id] = []; - } - this.category_childs[parent_id].push(cat.id); - } - function make_ancestors(cat_id, ancestors){ - self.category_ancestors[cat_id] = ancestors; - - ancestors = ancestors.slice(0); - ancestors.push(cat_id); - - var childs = self.category_childs[cat_id] || []; - for(var i=0, len = childs.length; i < len; i++){ - make_ancestors(childs[i], ancestors); - } - } - make_ancestors(this.root_category_id, []); - }, - category_contains: function(categ_id, product_id) { - var product = this.product_by_id[product_id]; - if (product) { - var cid = product.pos_categ_id[0]; - while (cid && cid !== categ_id){ - cid = this.category_parent[cid]; - } - return !!cid; - } - return false; - }, - /* loads a record store from the database. returns default if nothing is found */ - load: function(store,deft){ - if(this.cache[store] !== undefined){ - return this.cache[store]; - } - var data = localStorage[this.name + '_' + store]; - if(data !== undefined && data !== ""){ - data = JSON.parse(data); - this.cache[store] = data; - return data; - }else{ - return deft; - } - }, - /* saves a record store to the database */ - save: function(store,data){ - var str_data = JSON.stringify(data); - localStorage[this.name + '_' + store] = JSON.stringify(data); - this.cache[store] = data; - }, - _product_search_string: function(product){ - var str = product.display_name; - if (product.barcode) { - str += '|' + product.barcode; - } - if (product.default_code) { - str += '|' + product.default_code; - } - if (product.description) { - str += '|' + product.description; - } - if (product.description_sale) { - str += '|' + product.description_sale; - } - var packagings = this.packagings_by_product_tmpl_id[product.product_tmpl_id] || []; - for (var i = 0; i < packagings.length; i++) { - str += '|' + packagings[i].barcode; - } - str = product.id + ':' + str.replace(':','') + '\n'; - return str; - }, - add_products: function(products){ - var stored_categories = this.product_by_category_id; - - if(!products instanceof Array){ - products = [products]; - } - for(var i = 0, len = products.length; i < len; i++){ - var product = products[i]; - var search_string = this._product_search_string(product); - var categ_id = product.pos_categ_id ? product.pos_categ_id[0] : this.root_category_id; - product.product_tmpl_id = product.product_tmpl_id[0]; - if(!stored_categories[categ_id]){ - stored_categories[categ_id] = []; - } - stored_categories[categ_id].push(product.id); - - if(this.category_search_string[categ_id] === undefined){ - this.category_search_string[categ_id] = ''; - } - this.category_search_string[categ_id] += search_string; - - var ancestors = this.get_category_ancestors_ids(categ_id) || []; - - for(var j = 0, jlen = ancestors.length; j < jlen; j++){ - var ancestor = ancestors[j]; - if(! stored_categories[ancestor]){ - stored_categories[ancestor] = []; - } - stored_categories[ancestor].push(product.id); - - if( this.category_search_string[ancestor] === undefined){ - this.category_search_string[ancestor] = ''; - } - this.category_search_string[ancestor] += search_string; - } - this.product_by_id[product.id] = product; - if(product.barcode){ - this.product_by_barcode[product.barcode] = product; - } - } - }, - add_packagings: function(packagings){ - for(var i = 0, len = packagings.length; i < len; i++){ - var pack = packagings[i]; - this.packagings_by_id[pack.id] = pack; - if(!this.packagings_by_product_tmpl_id[pack.product_tmpl_id[0]]){ - this.packagings_by_product_tmpl_id[pack.product_tmpl_id[0]] = []; - } - this.packagings_by_product_tmpl_id[pack.product_tmpl_id[0]].push(pack); - if(pack.barcode){ - this.packagings_by_barcode[pack.barcode] = pack; - } - } - }, - _partner_search_string: function(partner){ - var str = partner.name; - if(partner.barcode){ - str += '|' + partner.barcode; - } - if(partner.address){ - str += '|' + partner.address; - } - if(partner.phone){ - str += '|' + partner.phone.split(' ').join(''); - } - if(partner.mobile){ - str += '|' + partner.mobile.split(' ').join(''); - } - if(partner.email){ - str += '|' + partner.email; - } - str = '' + partner.id + ':' + str.replace(':','') + '\n'; - return str; - }, - add_partners: function(partners){ - var updated_count = 0; - var new_write_date = ''; - for(var i = 0, len = partners.length; i < len; i++){ - var partner = partners[i]; - - if ( this.partner_write_date && - this.partner_by_id[partner.id] && - new Date(this.partner_write_date).getTime() + 1000 >= - new Date(partner.write_date).getTime() ) { - // FIXME: The write_date is stored with milisec precision in the database - // but the dates we get back are only precise to the second. This means when - // you read partners modified strictly after time X, you get back partners that were - // modified X - 1 sec ago. - continue; - } else if ( new_write_date < partner.write_date ) { - new_write_date = partner.write_date; - } - if (!this.partner_by_id[partner.id]) { - this.partner_sorted.push(partner.id); - } - this.partner_by_id[partner.id] = partner; - - updated_count += 1; - } - - this.partner_write_date = new_write_date || this.partner_write_date; - - if (updated_count) { - // If there were updates, we need to completely - // rebuild the search string and the barcode indexing - - this.partner_search_string = ""; - this.partner_by_barcode = {}; - - for (var id in this.partner_by_id) { - var partner = this.partner_by_id[id]; - - if(partner.barcode){ - this.partner_by_barcode[partner.barcode] = partner; - } - partner.address = (partner.street || '') +', '+ - (partner.zip || '') +' '+ - (partner.city || '') +', '+ - (partner.country_id[1] || ''); - this.partner_search_string += this._partner_search_string(partner); - } - } - return updated_count; - }, - get_partner_write_date: function(){ - return this.partner_write_date || "1970-01-01 00:00:00"; - }, - get_partner_by_id: function(id){ - return this.partner_by_id[id]; - }, - get_partner_by_barcode: function(barcode){ - return this.partner_by_barcode[barcode]; - }, - get_partners_sorted: function(max_count){ - max_count = max_count ? Math.min(this.partner_sorted.length, max_count) : this.partner_sorted.length; - var partners = []; - for (var i = 0; i < max_count; i++) { - partners.push(this.partner_by_id[this.partner_sorted[i]]); - } - return partners; - }, - search_partner: function(query){ - try { - query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g,'.'); - query = query.replace(' ','.+'); - var re = RegExp("([0-9]+):.*?"+query,"gi"); - }catch(e){ - return []; - } - var results = []; - for(var i = 0; i < this.limit; i++){ - var r = re.exec(this.partner_search_string); - if(r){ - var id = Number(r[1]); - results.push(this.get_partner_by_id(id)); - }else{ - break; - } - } - return results; - }, - /* removes all the data from the database. TODO : being able to selectively remove data */ - clear: function(stores){ - for(var i = 0, len = arguments.length; i < len; i++){ - localStorage.removeItem(this.name + '_' + arguments[i]); - } - }, - /* this internal methods returns the count of properties in an object. */ - _count_props : function(obj){ - var count = 0; - for(var prop in obj){ - if(obj.hasOwnProperty(prop)){ - count++; - } - } - return count; - }, - get_product_by_id: function(id){ - return this.product_by_id[id]; - }, - get_product_by_barcode: function(barcode){ - if(this.product_by_barcode[barcode]){ - return this.product_by_barcode[barcode]; - } - var pack = this.packagings_by_barcode[barcode]; - if(pack){ - return this.product_by_id[pack.product_tmpl_id[0]]; - } - return undefined; - }, - get_product_by_category: function(category_id){ - var product_ids = this.product_by_category_id[category_id]; + /* returns the category object from its id. If you pass a list of id as parameters, you get + * a list of category objects. + */ + get_category_by_id: function(categ_id){ + if(categ_id instanceof Array){ var list = []; - if (product_ids) { - for (var i = 0, len = Math.min(product_ids.length, this.limit); i < len; i++) { - list.push(this.product_by_id[product_ids[i]]); + for(var i = 0, len = categ_id.length; i < len; i++){ + var cat = this.category_by_id[categ_id[i]]; + if(cat){ + list.push(cat); + }else{ + console.error("get_category_by_id: no category has id:",categ_id[i]); } } return list; - }, - /* returns a list of products with : - * - a category that is or is a child of category_id, - * - a name, package or barcode containing the query (case insensitive) - */ - search_product_in_category: function(category_id, query){ - try { - query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g,'.'); - query = query.replace(' ','.+'); - var re = RegExp("([0-9]+):.*?"+query,"gi"); - }catch(e){ - return []; + }else{ + return this.category_by_id[categ_id]; + } + }, + /* returns a list of the category's child categories ids, or an empty list + * if a category has no childs */ + get_category_childs_ids: function(categ_id){ + return this.category_childs[categ_id] || []; + }, + /* returns a list of all ancestors (parent, grand-parent, etc) categories ids + * starting from the root category to the direct parent */ + get_category_ancestors_ids: function(categ_id){ + return this.category_ancestors[categ_id] || []; + }, + /* returns the parent category's id of a category, or the root_category_id if no parent. + * the root category is parent of itself. */ + get_category_parent_id: function(categ_id){ + return this.category_parent[categ_id] || this.root_category_id; + }, + /* adds categories definitions to the database. categories is a list of categories objects as + * returned by the openerp server. Categories must be inserted before the products or the + * product/ categories association may (will) not work properly */ + add_categories: function(categories){ + var self = this; + if(!this.category_by_id[this.root_category_id]){ + this.category_by_id[this.root_category_id] = { + id : this.root_category_id, + name : 'Root', + }; + } + for(var i=0, len = categories.length; i < len; i++){ + this.category_by_id[categories[i].id] = categories[i]; + } + len = categories.length; + for(i=0; i < len; i++){ + var cat = categories[i]; + var parent_id = cat.parent_id[0] || this.root_category_id; + this.category_parent[cat.id] = cat.parent_id[0]; + if(!this.category_childs[parent_id]){ + this.category_childs[parent_id] = []; } - var results = []; - for(var i = 0; i < this.limit; i++){ - var r = re.exec(this.category_search_string[category_id]); - if(r){ - var id = Number(r[1]); - results.push(this.get_product_by_id(id)); - }else{ - break; + this.category_childs[parent_id].push(cat.id); + } + function make_ancestors(cat_id, ancestors){ + self.category_ancestors[cat_id] = ancestors; + + ancestors = ancestors.slice(0); + ancestors.push(cat_id); + + var childs = self.category_childs[cat_id] || []; + for(var i=0, len = childs.length; i < len; i++){ + make_ancestors(childs[i], ancestors); + } + } + make_ancestors(this.root_category_id, []); + }, + category_contains: function(categ_id, product_id) { + var product = this.product_by_id[product_id]; + if (product) { + var cid = product.pos_categ_id[0]; + while (cid && cid !== categ_id){ + cid = this.category_parent[cid]; + } + return !!cid; + } + return false; + }, + /* loads a record store from the database. returns default if nothing is found */ + load: function(store,deft){ + if(this.cache[store] !== undefined){ + return this.cache[store]; + } + var data = localStorage[this.name + '_' + store]; + if(data !== undefined && data !== ""){ + data = JSON.parse(data); + this.cache[store] = data; + return data; + }else{ + return deft; + } + }, + /* saves a record store to the database */ + save: function(store,data){ + localStorage[this.name + '_' + store] = JSON.stringify(data); + this.cache[store] = data; + }, + _product_search_string: function(product){ + var str = product.display_name; + if (product.barcode) { + str += '|' + product.barcode; + } + if (product.default_code) { + str += '|' + product.default_code; + } + if (product.description) { + str += '|' + product.description; + } + if (product.description_sale) { + str += '|' + product.description_sale; + } + var packagings = this.packagings_by_product_tmpl_id[product.product_tmpl_id] || []; + for (var i = 0; i < packagings.length; i++) { + str += '|' + packagings[i].barcode; + } + str = product.id + ':' + str.replace(':','') + '\n'; + return str; + }, + add_products: function(products){ + var stored_categories = this.product_by_category_id; + + if(!products instanceof Array){ + products = [products]; + } + for(var i = 0, len = products.length; i < len; i++){ + var product = products[i]; + var search_string = this._product_search_string(product); + var categ_id = product.pos_categ_id ? product.pos_categ_id[0] : this.root_category_id; + product.product_tmpl_id = product.product_tmpl_id[0]; + if(!stored_categories[categ_id]){ + stored_categories[categ_id] = []; + } + stored_categories[categ_id].push(product.id); + + if(this.category_search_string[categ_id] === undefined){ + this.category_search_string[categ_id] = ''; + } + this.category_search_string[categ_id] += search_string; + + var ancestors = this.get_category_ancestors_ids(categ_id) || []; + + for(var j = 0, jlen = ancestors.length; j < jlen; j++){ + var ancestor = ancestors[j]; + if(! stored_categories[ancestor]){ + stored_categories[ancestor] = []; } - } - return results; - }, + stored_categories[ancestor].push(product.id); - /* paid orders */ - add_order: function(order){ - var order_id = order.uid; - var orders = this.load('orders',[]); - - // if the order was already stored, we overwrite its data - for(var i = 0, len = orders.length; i < len; i++){ - if(orders[i].id === order_id){ - orders[i].data = order; - this.save('orders',orders); - return order_id; + if( this.category_search_string[ancestor] === undefined){ + this.category_search_string[ancestor] = ''; } + this.category_search_string[ancestor] += search_string; } + this.product_by_id[product.id] = product; + if(product.barcode){ + this.product_by_barcode[product.barcode] = product; + } + } + }, + add_packagings: function(packagings){ + for(var i = 0, len = packagings.length; i < len; i++){ + var pack = packagings[i]; + this.packagings_by_id[pack.id] = pack; + if(!this.packagings_by_product_tmpl_id[pack.product_tmpl_id[0]]){ + this.packagings_by_product_tmpl_id[pack.product_tmpl_id[0]] = []; + } + this.packagings_by_product_tmpl_id[pack.product_tmpl_id[0]].push(pack); + if(pack.barcode){ + this.packagings_by_barcode[pack.barcode] = pack; + } + } + }, + _partner_search_string: function(partner){ + var str = partner.name; + if(partner.barcode){ + str += '|' + partner.barcode; + } + if(partner.address){ + str += '|' + partner.address; + } + if(partner.phone){ + str += '|' + partner.phone.split(' ').join(''); + } + if(partner.mobile){ + str += '|' + partner.mobile.split(' ').join(''); + } + if(partner.email){ + str += '|' + partner.email; + } + str = '' + partner.id + ':' + str.replace(':','') + '\n'; + return str; + }, + add_partners: function(partners){ + var updated_count = 0; + var new_write_date = ''; + var partner; + for(var i = 0, len = partners.length; i < len; i++){ + partner = partners[i]; - orders.push({id: order_id, data: order}); - this.save('orders',orders); - return order_id; - }, - remove_order: function(order_id){ - var orders = this.load('orders',[]); - orders = _.filter(orders, function(order){ - return order.id !== order_id; - }); - this.save('orders',orders); - }, - remove_all_orders: function(){ - this.save('orders',[]); - }, - get_orders: function(){ - return this.load('orders',[]); - }, - get_order: function(order_id){ - var orders = this.get_orders(); - for(var i = 0, len = orders.length; i < len; i++){ - if(orders[i].id === order_id){ - return orders[i]; + if ( this.partner_write_date && + this.partner_by_id[partner.id] && + new Date(this.partner_write_date).getTime() + 1000 >= + new Date(partner.write_date).getTime() ) { + // FIXME: The write_date is stored with milisec precision in the database + // but the dates we get back are only precise to the second. This means when + // you read partners modified strictly after time X, you get back partners that were + // modified X - 1 sec ago. + continue; + } else if ( new_write_date < partner.write_date ) { + new_write_date = partner.write_date; + } + if (!this.partner_by_id[partner.id]) { + this.partner_sorted.push(partner.id); + } + this.partner_by_id[partner.id] = partner; + + updated_count += 1; + } + + this.partner_write_date = new_write_date || this.partner_write_date; + + if (updated_count) { + // If there were updates, we need to completely + // rebuild the search string and the barcode indexing + + this.partner_search_string = ""; + this.partner_by_barcode = {}; + + for (var id in this.partner_by_id) { + partner = this.partner_by_id[id]; + + if(partner.barcode){ + this.partner_by_barcode[partner.barcode] = partner; } + partner.address = (partner.street || '') +', '+ + (partner.zip || '') +' '+ + (partner.city || '') +', '+ + (partner.country_id[1] || ''); + this.partner_search_string += this._partner_search_string(partner); } - return undefined; - }, - - /* working orders */ - save_unpaid_order: function(order){ - var order_id = order.uid; - var orders = this.load('unpaid_orders',[]); - var serialized = order.export_as_JSON(); - - for (var i = 0; i < orders.length; i++) { - if (orders[i].id === order_id){ - orders[i].data = serialized; - this.save('unpaid_orders',orders); - return order_id; - } + } + return updated_count; + }, + get_partner_write_date: function(){ + return this.partner_write_date || "1970-01-01 00:00:00"; + }, + get_partner_by_id: function(id){ + return this.partner_by_id[id]; + }, + get_partner_by_barcode: function(barcode){ + return this.partner_by_barcode[barcode]; + }, + get_partners_sorted: function(max_count){ + max_count = max_count ? Math.min(this.partner_sorted.length, max_count) : this.partner_sorted.length; + var partners = []; + for (var i = 0; i < max_count; i++) { + partners.push(this.partner_by_id[this.partner_sorted[i]]); + } + return partners; + }, + search_partner: function(query){ + try { + query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g,'.'); + query = query.replace(' ','.+'); + var re = RegExp("([0-9]+):.*?"+query,"gi"); + }catch(e){ + return []; + } + var results = []; + for(var i = 0; i < this.limit; i++){ + var r = re.exec(this.partner_search_string); + if(r){ + var id = Number(r[1]); + results.push(this.get_partner_by_id(id)); + }else{ + break; } - - orders.push({id: order_id, data: serialized}); - this.save('unpaid_orders',orders); - return order_id; - }, - remove_unpaid_order: function(order){ - var orders = this.load('unpaid_orders',[]); - orders = _.filter(orders, function(o){ - return o.id !== order.uid; - }); - this.save('unpaid_orders',orders); - }, - remove_all_unpaid_orders: function(){ - this.save('unpaid_orders',[]); - }, - get_unpaid_orders: function(){ - var saved = this.load('unpaid_orders',[]); - var orders = []; - for (var i = 0; i < saved.length; i++) { - orders.push(saved[i].data); + } + return results; + }, + /* removes all the data from the database. TODO : being able to selectively remove data */ + clear: function(){ + for(var i = 0, len = arguments.length; i < len; i++){ + localStorage.removeItem(this.name + '_' + arguments[i]); + } + }, + /* this internal methods returns the count of properties in an object. */ + _count_props : function(obj){ + var count = 0; + for(var prop in obj){ + if(obj.hasOwnProperty(prop)){ + count++; } - return orders; - }, - }); -} + } + return count; + }, + get_product_by_id: function(id){ + return this.product_by_id[id]; + }, + get_product_by_barcode: function(barcode){ + if(this.product_by_barcode[barcode]){ + return this.product_by_barcode[barcode]; + } + var pack = this.packagings_by_barcode[barcode]; + if(pack){ + return this.product_by_id[pack.product_tmpl_id[0]]; + } + return undefined; + }, + get_product_by_category: function(category_id){ + var product_ids = this.product_by_category_id[category_id]; + var list = []; + if (product_ids) { + for (var i = 0, len = Math.min(product_ids.length, this.limit); i < len; i++) { + list.push(this.product_by_id[product_ids[i]]); + } + } + return list; + }, + /* returns a list of products with : + * - a category that is or is a child of category_id, + * - a name, package or barcode containing the query (case insensitive) + */ + search_product_in_category: function(category_id, query){ + try { + query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g,'.'); + query = query.replace(' ','.+'); + var re = RegExp("([0-9]+):.*?"+query,"gi"); + }catch(e){ + return []; + } + var results = []; + for(var i = 0; i < this.limit; i++){ + var r = re.exec(this.category_search_string[category_id]); + if(r){ + var id = Number(r[1]); + results.push(this.get_product_by_id(id)); + }else{ + break; + } + } + return results; + }, + + /* paid orders */ + add_order: function(order){ + var order_id = order.uid; + var orders = this.load('orders',[]); + + // if the order was already stored, we overwrite its data + for(var i = 0, len = orders.length; i < len; i++){ + if(orders[i].id === order_id){ + orders[i].data = order; + this.save('orders',orders); + return order_id; + } + } + + orders.push({id: order_id, data: order}); + this.save('orders',orders); + return order_id; + }, + remove_order: function(order_id){ + var orders = this.load('orders',[]); + orders = _.filter(orders, function(order){ + return order.id !== order_id; + }); + this.save('orders',orders); + }, + remove_all_orders: function(){ + this.save('orders',[]); + }, + get_orders: function(){ + return this.load('orders',[]); + }, + get_order: function(order_id){ + var orders = this.get_orders(); + for(var i = 0, len = orders.length; i < len; i++){ + if(orders[i].id === order_id){ + return orders[i]; + } + } + return undefined; + }, + + /* working orders */ + save_unpaid_order: function(order){ + var order_id = order.uid; + var orders = this.load('unpaid_orders',[]); + var serialized = order.export_as_JSON(); + + for (var i = 0; i < orders.length; i++) { + if (orders[i].id === order_id){ + orders[i].data = serialized; + this.save('unpaid_orders',orders); + return order_id; + } + } + + orders.push({id: order_id, data: serialized}); + this.save('unpaid_orders',orders); + return order_id; + }, + remove_unpaid_order: function(order){ + var orders = this.load('unpaid_orders',[]); + orders = _.filter(orders, function(o){ + return o.id !== order.uid; + }); + this.save('unpaid_orders',orders); + }, + remove_all_unpaid_orders: function(){ + this.save('unpaid_orders',[]); + }, + get_unpaid_orders: function(){ + var saved = this.load('unpaid_orders',[]); + var orders = []; + for (var i = 0; i < saved.length; i++) { + orders.push(saved[i].data); + } + return orders; + }, +}); + +return PosDB; + +}); + diff --git a/addons/point_of_sale/static/src/js/devices.js b/addons/point_of_sale/static/src/js/devices.js index d788de75fc8..0a965eda8a1 100644 --- a/addons/point_of_sale/static/src/js/devices.js +++ b/addons/point_of_sale/static/src/js/devices.js @@ -1,615 +1,623 @@ -openerp.point_of_sale.load_devices = function load_devices(instance,module){ //module is instance.point_of_sale - "use strict"; +odoo.define('point_of_sale.devices', ['web.core', 'web.Session'], function (require) { +"use strict"; - var _t = instance.web._t; +var core = require('web.core'); +var Session = require('web.Session'); - // the JobQueue schedules a sequence of 'jobs'. each job is - // a function returning a deferred. the queue waits for each job to finish - // before launching the next. Each job can also be scheduled with a delay. - // the is used to prevent parallel requests to the proxy. +var _t = core._t; - module.JobQueue = function(){ - var queue = []; - var running = false; - var scheduled_end_time = 0; - var end_of_queue = (new $.Deferred()).resolve(); - var stoprepeat = false; +// the JobQueue schedules a sequence of 'jobs'. each job is +// a function returning a deferred. the queue waits for each job to finish +// before launching the next. Each job can also be scheduled with a delay. +// the is used to prevent parallel requests to the proxy. - var run = function(){ - if(end_of_queue.state() === 'resolved'){ - end_of_queue = new $.Deferred(); - } - if(queue.length > 0){ - running = true; - var job = queue[0]; - if(!job.opts.repeat || stoprepeat){ - queue.shift(); - stoprepeat = false; - } +var JobQueue = function(){ + var queue = []; + var running = false; + var scheduled_end_time = 0; + var end_of_queue = (new $.Deferred()).resolve(); + var stoprepeat = false; - // the time scheduled for this job - scheduled_end_time = (new Date()).getTime() + (job.opts.duration || 0); - - // we run the job and put in def when it finishes - var def = job.fun() || (new $.Deferred()).resolve(); - - // we don't care if a job fails ... - def.always(function(){ - // we run the next job after the scheduled_end_time, even if it finishes before - setTimeout(function(){ - run(); - }, Math.max(0, scheduled_end_time - (new Date()).getTime()) ); - }); - }else{ - running = false; - scheduled_end_time = 0; - end_of_queue.resolve(); - } - }; - - // adds a job to the schedule. - // opts : { - // duration : the job is guaranteed to finish no quicker than this (milisec) - // repeat : if true, the job will be endlessly repeated - // important : if true, the scheduled job cannot be canceled by a queue.clear() - // } - this.schedule = function(fun, opts){ - queue.push({fun:fun, opts:opts || {}}); - if(!running){ - run(); - } + var run = function(){ + if(end_of_queue.state() === 'resolved'){ + end_of_queue = new $.Deferred(); } + if(queue.length > 0){ + running = true; + var job = queue[0]; + if(!job.opts.repeat || stoprepeat){ + queue.shift(); + stoprepeat = false; + } - // remove all jobs from the schedule (except the ones marked as important) - this.clear = function(){ - queue = _.filter(queue,function(job){job.opts.important === true}); - }; + // the time scheduled for this job + scheduled_end_time = (new Date()).getTime() + (job.opts.duration || 0); - // end the repetition of the current job - this.stoprepeat = function(){ - stoprepeat = true; - }; - - // returns a deferred that resolves when all scheduled - // jobs have been run. - // ( jobs added after the call to this method are considered as well ) - this.finished = function(){ - return end_of_queue; + // we run the job and put in def when it finishes + var def = job.fun() || (new $.Deferred()).resolve(); + + // we don't care if a job fails ... + def.always(function(){ + // we run the next job after the scheduled_end_time, even if it finishes before + setTimeout(function(){ + run(); + }, Math.max(0, scheduled_end_time - (new Date()).getTime()) ); + }); + }else{ + running = false; + scheduled_end_time = 0; + end_of_queue.resolve(); + } + }; + + // adds a job to the schedule. + // opts : { + // duration : the job is guaranteed to finish no quicker than this (milisec) + // repeat : if true, the job will be endlessly repeated + // important : if true, the scheduled job cannot be canceled by a queue.clear() + // } + this.schedule = function(fun, opts){ + queue.push({fun:fun, opts:opts || {}}); + if(!running){ + run(); } - }; + // remove all jobs from the schedule (except the ones marked as important) + this.clear = function(){ + queue = _.filter(queue,function(job){return job.opts.important === true;}); + }; - // this object interfaces with the local proxy to communicate to the various hardware devices - // connected to the Point of Sale. As the communication only goes from the POS to the proxy, - // methods are used both to signal an event, and to fetch information. + // end the repetition of the current job + this.stoprepeat = function(){ + stoprepeat = true; + }; + + // returns a deferred that resolves when all scheduled + // jobs have been run. + // ( jobs added after the call to this method are considered as well ) + this.finished = function(){ + return end_of_queue; + }; - module.ProxyDevice = instance.web.Class.extend(openerp.PropertiesMixin,{ - init: function(parent,options){ - openerp.PropertiesMixin.init.call(this,parent); - var self = this; - options = options || {}; - var url = options.url || 'http://localhost:8069'; +}; - this.pos = parent; - - this.weighting = false; - this.debug_weight = 0; - this.use_debug_weight = false; - this.paying = false; - this.default_payment_status = { - status: 'waiting', - message: '', - payment_method: undefined, - receipt_client: undefined, - receipt_shop: undefined, - }; - this.custom_payment_status = this.default_payment_status; +// this object interfaces with the local proxy to communicate to the various hardware devices +// connected to the Point of Sale. As the communication only goes from the POS to the proxy, +// methods are used both to signal an event, and to fetch information. - this.receipt_queue = []; +var ProxyDevice = core.Class.extend(core.mixins.PropertiesMixin,{ + init: function(parent,options){ + core.mixins.PropertiesMixin.init.call(this,parent); + var self = this; + options = options || {}; - this.notifications = {}; - this.bypass_proxy = false; + this.pos = parent; + + this.weighting = false; + this.debug_weight = 0; + this.use_debug_weight = false; - this.connection = null; - this.host = ''; - this.keptalive = false; + this.paying = false; + this.default_payment_status = { + status: 'waiting', + message: '', + payment_method: undefined, + receipt_client: undefined, + receipt_shop: undefined, + }; + this.custom_payment_status = this.default_payment_status; - this.set('status',{}); + this.receipt_queue = []; + this.notifications = {}; + this.bypass_proxy = false; + + this.connection = null; + this.host = ''; + this.keptalive = false; + + this.set('status',{}); + + this.set_connection_status('disconnected'); + + this.on('change:status',this,function(eh,status){ + status = status.newValue; + if(status.status === 'connected'){ + self.print_receipt(); + } + }); + + window.hw_proxy = this; + }, + set_connection_status: function(status,drivers){ + var oldstatus = this.get('status'); + var newstatus = {}; + newstatus.status = status; + newstatus.drivers = status === 'disconnected' ? {} : oldstatus.drivers; + newstatus.drivers = drivers ? drivers : newstatus.drivers; + this.set('status',newstatus); + }, + disconnect: function(){ + if(this.get('status').status !== 'disconnected'){ + this.connection.destroy(); this.set_connection_status('disconnected'); + } + }, - this.on('change:status',this,function(eh,status){ - status = status.newValue; - if(status.status === 'connected'){ - self.print_receipt(); + // connects to the specified url + connect: function(url){ + var self = this; + this.connection = new Session(undefined,url, { use_cors: true}); + this.host = url; + this.set_connection_status('connecting',{}); + + return this.message('handshake').then(function(response){ + if(response){ + self.set_connection_status('connected'); + localStorage.hw_proxy_url = url; + self.keepalive(); + }else{ + self.set_connection_status('disconnected'); + console.error('Connection refused by the Proxy'); + } + },function(){ + self.set_connection_status('disconnected'); + console.error('Could not connect to the Proxy'); + }); + }, + + // find a proxy and connects to it. for options see find_proxy + // - force_ip : only try to connect to the specified ip. + // - port: what port to listen to (default 8069) + // - progress(fac) : callback for search progress ( fac in [0,1] ) + autoconnect: function(options){ + var self = this; + this.set_connection_status('connecting',{}); + var found_url = new $.Deferred(); + var success = new $.Deferred(); + + if ( options.force_ip ){ + // if the ip is forced by server config, bailout on fail + found_url = this.try_hard_to_connect(options.force_ip, options); + }else if( localStorage.hw_proxy_url ){ + // try harder when we remember a good proxy url + found_url = this.try_hard_to_connect(localStorage.hw_proxy_url, options) + .then(null,function(){ + return self.find_proxy(options); + }); + }else{ + // just find something quick + found_url = this.find_proxy(options); + } + + success = found_url.then(function(url){ + return self.connect(url); + }); + + success.fail(function(){ + self.set_connection_status('disconnected'); + }); + + return success; + }, + + // starts a loop that updates the connection status + keepalive: function(){ + var self = this; + + function status(){ + self.connection.rpc('/hw_proxy/status_json',{},{timeout:2500}) + .then(function(driver_status){ + self.set_connection_status('connected',driver_status); + },function(){ + if(self.get('status').status !== 'connecting'){ + self.set_connection_status('disconnected'); + } + }).always(function(){ + setTimeout(status,5000); + }); + } + + if(!this.keptalive){ + this.keptalive = true; + status(); + } + }, + + message : function(name,params){ + var callbacks = this.notifications[name] || []; + for(var i = 0; i < callbacks.length; i++){ + callbacks[i](params); + } + if(this.get('status').status !== 'disconnected'){ + return this.connection.rpc('/hw_proxy/' + name, params || {}); + }else{ + return (new $.Deferred()).reject(); + } + }, + + // try several time to connect to a known proxy url + try_hard_to_connect: function(url,options){ + options = options || {}; + var port = ':' + (options.port || '8069'); + + this.set_connection_status('connecting'); + + if(url.indexOf('//') < 0){ + url = 'http://'+url; + } + + if(url.indexOf(':',5) < 0){ + url = url+port; + } + + // try real hard to connect to url, with a 1sec timeout and up to 'retries' retries + function try_real_hard_to_connect(url, retries, done){ + + done = done || new $.Deferred(); + + $.ajax({ + url: url + '/hw_proxy/hello', + method: 'GET', + timeout: 1000, + }) + .done(function(){ + done.resolve(url); + }) + .fail(function(){ + if(retries > 0){ + try_real_hard_to_connect(url,retries-1,done); + }else{ + done.reject(); } }); + return done; + } - window.hw_proxy = this; - }, - set_connection_status: function(status,drivers){ - var oldstatus = this.get('status'); - var newstatus = {}; - newstatus.status = status; - newstatus.drivers = status === 'disconnected' ? {} : oldstatus.drivers; - newstatus.drivers = drivers ? drivers : newstatus.drivers; - this.set('status',newstatus); - }, - disconnect: function(){ - if(this.get('status').status !== 'disconnected'){ - this.connection.destroy(); - this.set_connection_status('disconnected'); + return try_real_hard_to_connect(url,3); + }, + + // returns as a deferred a valid host url that can be used as proxy. + // options: + // - port: what port to listen to (default 8069) + // - progress(fac) : callback for search progress ( fac in [0,1] ) + find_proxy: function(options){ + options = options || {}; + var self = this; + var port = ':' + (options.port || '8069'); + var urls = []; + var found = false; + var parallel = 8; + var done = new $.Deferred(); // will be resolved with the proxies valid urls + var threads = []; + var progress = 0; + + + urls.push('http://localhost'+port); + for(var i = 0; i < 256; i++){ + urls.push('http://192.168.0.'+i+port); + urls.push('http://192.168.1.'+i+port); + urls.push('http://10.0.0.'+i+port); + } + + var prog_inc = 1/urls.length; + + function update_progress(){ + progress = found ? 1 : progress + prog_inc; + if(options.progress){ + options.progress(progress); } - }, + } - // connects to the specified url - connect: function(url){ - var self = this; - this.connection = new instance.web.Session(undefined,url, { use_cors: true}); - this.host = url; - this.set_connection_status('connecting',{}); + function thread(done){ + var url = urls.shift(); - return this.message('handshake').then(function(response){ - if(response){ - self.set_connection_status('connected'); - localStorage['hw_proxy_url'] = url; - self.keepalive(); - }else{ - self.set_connection_status('disconnected'); - console.error('Connection refused by the Proxy'); - } - },function(){ - self.set_connection_status('disconnected'); - console.error('Could not connect to the Proxy'); - }); - }, + done = done || new $.Deferred(); - // find a proxy and connects to it. for options see find_proxy - // - force_ip : only try to connect to the specified ip. - // - port: what port to listen to (default 8069) - // - progress(fac) : callback for search progress ( fac in [0,1] ) - autoconnect: function(options){ - var self = this; - this.set_connection_status('connecting',{}); - var found_url = new $.Deferred(); - var success = new $.Deferred(); - - if ( options.force_ip ){ - // if the ip is forced by server config, bailout on fail - found_url = this.try_hard_to_connect(options.force_ip, options) - }else if( localStorage['hw_proxy_url'] ){ - // try harder when we remember a good proxy url - found_url = this.try_hard_to_connect(localStorage['hw_proxy_url'], options) - .then(null,function(){ - return self.find_proxy(options); - }); - }else{ - // just find something quick - found_url = this.find_proxy(options); + if( !url || found || !self.searching_for_proxy ){ + done.resolve(); + return done; } - success = found_url.then(function(url){ - return self.connect(url); - }); - - success.fail(function(){ - self.set_connection_status('disconnected'); - }); - - return success; - }, - - // starts a loop that updates the connection status - keepalive: function(){ - var self = this; - - function status(){ - self.connection.rpc('/hw_proxy/status_json',{},{timeout:2500}) - .then(function(driver_status){ - self.set_connection_status('connected',driver_status); - },function(){ - if(self.get('status').status !== 'connecting'){ - self.set_connection_status('disconnected'); - } - }).always(function(){ - setTimeout(status,5000); - }); - } - - if(!this.keptalive){ - this.keptalive = true; - status(); - }; - }, - - message : function(name,params){ - var callbacks = this.notifications[name] || []; - for(var i = 0; i < callbacks.length; i++){ - callbacks[i](params); - } - if(this.get('status').status !== 'disconnected'){ - return this.connection.rpc('/hw_proxy/' + name, params || {}); - }else{ - return (new $.Deferred()).reject(); - } - }, - - // try several time to connect to a known proxy url - try_hard_to_connect: function(url,options){ - options = options || {}; - var port = ':' + (options.port || '8069'); - - this.set_connection_status('connecting'); - - if(url.indexOf('//') < 0){ - url = 'http://'+url; - } - - if(url.indexOf(':',5) < 0){ - url = url+port; - } - - // try real hard to connect to url, with a 1sec timeout and up to 'retries' retries - function try_real_hard_to_connect(url, retries, done){ - - done = done || new $.Deferred(); - - var c = $.ajax({ + $.ajax({ url: url + '/hw_proxy/hello', method: 'GET', - timeout: 1000, - }) - .done(function(){ + timeout: 400, + }).done(function(){ + found = true; + update_progress(); done.resolve(url); }) .fail(function(){ - if(retries > 0){ - try_real_hard_to_connect(url,retries-1,done); - }else{ - done.reject(); - } + update_progress(); + thread(done); }); - return done; - } - - return try_real_hard_to_connect(url,3); - }, - - // returns as a deferred a valid host url that can be used as proxy. - // options: - // - port: what port to listen to (default 8069) - // - progress(fac) : callback for search progress ( fac in [0,1] ) - find_proxy: function(options){ - options = options || {}; - var self = this; - var port = ':' + (options.port || '8069'); - var urls = []; - var found = false; - var parallel = 8; - var done = new $.Deferred(); // will be resolved with the proxies valid urls - var threads = []; - var progress = 0; - - - urls.push('http://localhost'+port); - for(var i = 0; i < 256; i++){ - urls.push('http://192.168.0.'+i+port); - urls.push('http://192.168.1.'+i+port); - urls.push('http://10.0.0.'+i+port); - } - - var prog_inc = 1/urls.length; - - function update_progress(){ - progress = found ? 1 : progress + prog_inc; - if(options.progress){ - options.progress(progress); - } - } - - function thread(done){ - var url = urls.shift(); - - done = done || new $.Deferred(); - - if( !url || found || !self.searching_for_proxy ){ - done.resolve(); - return done; - } - - var c = $.ajax({ - url: url + '/hw_proxy/hello', - method: 'GET', - timeout: 400, - }).done(function(){ - found = true; - update_progress(); - done.resolve(url); - }) - .fail(function(){ - update_progress(); - thread(done); - }); - - return done; - } - - this.searching_for_proxy = true; - - for(var i = 0, len = Math.min(parallel,urls.length); i < len; i++){ - threads.push(thread()); - } - - $.when.apply($,threads).then(function(){ - var urls = []; - for(var i = 0; i < arguments.length; i++){ - if(arguments[i]){ - urls.push(arguments[i]); - } - } - done.resolve(urls[0]); - }); return done; - }, + } - stop_searching: function(){ - this.searching_for_proxy = false; - this.set_connection_status('disconnected'); - }, + this.searching_for_proxy = true; - // this allows the client to be notified when a proxy call is made. The notification - // callback will be executed with the same arguments as the proxy call - add_notification: function(name, callback){ - if(!this.notifications[name]){ - this.notifications[name] = []; - } - this.notifications[name].push(callback); - }, + var len = Math.min(parallel,urls.length); + for(i = 0; i < len; i++){ + threads.push(thread()); + } - // returns the weight on the scale. - scale_read: function(){ - var self = this; - var ret = new $.Deferred(); - if (self.use_debug_weight) { - return (new $.Deferred()).resolve({weight:this.debug_weight, unit:'Kg', info:'ok'}); - } - this.message('scale_read',{}) - .then(function(weight){ - ret.resolve(weight); - }, function(){ //failed to read weight - ret.resolve({weight:0.0, unit:'Kg', info:'ok'}); - }); - return ret; - }, - - // sets a custom weight, ignoring the proxy returned value. - debug_set_weight: function(kg){ - this.use_debug_weight = true; - this.debug_weight = kg; - }, - - // resets the custom weight and re-enable listening to the proxy for weight values - debug_reset_weight: function(){ - this.use_debug_weight = false; - this.debug_weight = 0; - }, - - // ask for the cashbox (the physical box where you store the cash) to be opened - open_cashbox: function(){ - return this.message('open_cashbox'); - }, - - /* - * ask the printer to print a receipt - */ - print_receipt: function(receipt){ - var self = this; - if(receipt){ - this.receipt_queue.push(receipt); - } - var aborted = false; - function send_printing_job(){ - if (self.receipt_queue.length > 0){ - var r = self.receipt_queue.shift(); - self.message('print_xml_receipt',{ receipt: r },{ timeout: 5000 }) - .then(function(){ - send_printing_job(); - },function(error){ - if (error) { - self.pos.chrome.screen_selector.show_popup('error-traceback',{ - 'title': _t('Printing Error: ') + error.data.message, - 'body': error.data.debug, - }); - return; - } - self.receipt_queue.unshift(r) - }); + $.when.apply($,threads).then(function(){ + var urls = []; + for(var i = 0; i < arguments.length; i++){ + if(arguments[i]){ + urls.push(arguments[i]); } } - send_printing_job(); - }, + done.resolve(urls[0]); + }); - // asks the proxy to log some information, as with the debug.log you can provide several arguments. - log: function(){ - return this.message('log',{'arguments': _.toArray(arguments)}); - }, + return done; + }, - }); + stop_searching: function(){ + this.searching_for_proxy = false; + this.set_connection_status('disconnected'); + }, - // this module interfaces with the barcode reader. It assumes the barcode reader - // is set-up to act like a keyboard. Use connect() and disconnect() to activate - // and deactivate the barcode reader. Use set_action_callbacks to tell it - // what to do when it reads a barcode. - module.BarcodeReader = instance.web.Class.extend({ - actions:[ - 'product', - 'cashier', - 'client', - ], + // this allows the client to be notified when a proxy call is made. The notification + // callback will be executed with the same arguments as the proxy call + add_notification: function(name, callback){ + if(!this.notifications[name]){ + this.notifications[name] = []; + } + this.notifications[name].push(callback); + }, + + // returns the weight on the scale. + scale_read: function(){ + var self = this; + var ret = new $.Deferred(); + if (self.use_debug_weight) { + return (new $.Deferred()).resolve({weight:this.debug_weight, unit:'Kg', info:'ok'}); + } + this.message('scale_read',{}) + .then(function(weight){ + ret.resolve(weight); + }, function(){ //failed to read weight + ret.resolve({weight:0.0, unit:'Kg', info:'ok'}); + }); + return ret; + }, - init: function(attributes){ - this.pos = attributes.pos; - this.action_callback = {}; - this.proxy = attributes.proxy; - this.remote_scanning = false; - this.remote_active = 0; + // sets a custom weight, ignoring the proxy returned value. + debug_set_weight: function(kg){ + this.use_debug_weight = true; + this.debug_weight = kg; + }, - this.barcode_parser = attributes.barcode_parser; + // resets the custom weight and re-enable listening to the proxy for weight values + debug_reset_weight: function(){ + this.use_debug_weight = false; + this.debug_weight = 0; + }, - this.action_callback_stack = []; - }, + // ask for the cashbox (the physical box where you store the cash) to be opened + open_cashbox: function(){ + return this.message('open_cashbox'); + }, - set_barcode_parser: function(barcode_parser) { - this.barcode_parser = barcode_parser; - }, - - save_callbacks: function(){ - var callbacks = {}; - for(var name in this.action_callback){ - callbacks[name] = this.action_callback[name]; - } - this.action_callback_stack.push(callbacks); - }, - - restore_callbacks: function(){ - if(this.action_callback_stack.length){ - var callbacks = this.action_callback_stack.pop(); - this.action_callback = callbacks; - } - }, - - // when a barcode is scanned and parsed, the callback corresponding - // to its type is called with the parsed_barcode as a parameter. - // (parsed_barcode is the result of parse_barcode(barcode)) - // - // callbacks is a Map of 'actions' : callback(parsed_barcode) - // that sets the callback for each action. if a callback for the - // specified action already exists, it is replaced. - // - // possible actions include : - // 'product' | 'cashier' | 'client' | 'discount' - set_action_callback: function(action, callback){ - if(arguments.length == 2){ - this.action_callback[action] = callback; - }else{ - var actions = arguments[0]; - for(var action in actions){ - this.set_action_callback(action,actions[action]); - } - } - }, - - //remove all action callbacks - reset_action_callbacks: function(){ - for(var action in this.action_callback){ - this.action_callback[action] = undefined; - } - }, - - scan: function(code){ - if (!code) { - return; - } - var parsed_result = this.barcode_parser.parse_barcode(code); - - if(parsed_result.type in {'product':'', 'weight':'', 'price':''}){ //barcode is associated to a product - if(this.action_callback['product']){ - this.action_callback['product'](parsed_result); - } - } - else if (parsed_result.type in {'cashier':'', 'client':'', 'discount':''}){ - if(this.action_callback[parsed_result.type]){ - this.action_callback[parsed_result.type](parsed_result); - } - } - else{ - this.action_callback['error'](parsed_result); - } - }, - - // starts catching keyboard events and tries to interpret codebar - // calling the callbacks when needed. - connect: function(){ - - var self = this; - var code = ""; - var timeStamp = 0; - var timeout = null; - - this.handler = function(e){ - - if(e.which === 13){ //ignore returns - e.preventDefault(); - return; - } - - if(timeStamp + 50 < new Date().getTime()){ - code = ""; - } - - timeStamp = new Date().getTime(); - clearTimeout(timeout); - - code += String.fromCharCode(e.which); - - // we wait for a while after the last input to be sure that we are not mistakingly - // returning a code which is a prefix of a bigger one : - // Internal Ref 5449 vs EAN13 5449000... - - timeout = setTimeout(function(){ - if(code.length >= 3){ - self.scan(code); - } - code = ""; - },100); - }; - - $('body').on('keypress', this.handler); - }, - - // stops catching keyboard events - disconnect: function(){ - $('body').off('keypress', this.handler) - }, - - // the barcode scanner will listen on the hw_proxy/scanner interface for - // scan events until disconnect_from_proxy is called - connect_to_proxy: function(){ - var self = this; - this.remote_scanning = true; - if(this.remote_active >= 1){ - return; - } - this.remote_active = 1; - - function waitforbarcode(){ - return self.proxy.connection.rpc('/hw_proxy/scanner',{},{timeout:7500}) - .then(function(barcode){ - if(!self.remote_scanning){ - self.remote_active = 0; - return; - } - self.scan(barcode); - waitforbarcode(); - }, - function(){ - if(!self.remote_scanning){ - self.remote_active = 0; + /* + * ask the printer to print a receipt + */ + print_receipt: function(receipt){ + var self = this; + if(receipt){ + this.receipt_queue.push(receipt); + } + function send_printing_job(){ + if (self.receipt_queue.length > 0){ + var r = self.receipt_queue.shift(); + self.message('print_xml_receipt',{ receipt: r },{ timeout: 5000 }) + .then(function(){ + send_printing_job(); + },function(error){ + if (error) { + self.pos.chrome.screen_selector.show_popup('error-traceback',{ + 'title': _t('Printing Error: ') + error.data.message, + 'body': error.data.debug, + }); return; } - setTimeout(waitforbarcode,5000); + self.receipt_queue.unshift(r); }); } - waitforbarcode(); - }, + } + send_printing_job(); + }, - // the barcode scanner will stop listening on the hw_proxy/scanner remote interface - disconnect_from_proxy: function(){ - this.remote_scanning = false; - }, - }); + // asks the proxy to log some information, as with the debug.log you can provide several arguments. + log: function(){ + return this.message('log',{'arguments': _.toArray(arguments)}); + }, -} +}); + +// this module interfaces with the barcode reader. It assumes the barcode reader +// is set-up to act like a keyboard. Use connect() and disconnect() to activate +// and deactivate the barcode reader. Use set_action_callbacks to tell it +// what to do when it reads a barcode. +var BarcodeReader = core.Class.extend({ + actions:[ + 'product', + 'cashier', + 'client', + ], + + init: function(attributes){ + this.pos = attributes.pos; + this.action_callback = {}; + this.proxy = attributes.proxy; + this.remote_scanning = false; + this.remote_active = 0; + + this.barcode_parser = attributes.barcode_parser; + + this.action_callback_stack = []; + }, + + set_barcode_parser: function(barcode_parser) { + this.barcode_parser = barcode_parser; + }, + + save_callbacks: function(){ + var callbacks = {}; + for(var name in this.action_callback){ + callbacks[name] = this.action_callback[name]; + } + this.action_callback_stack.push(callbacks); + }, + + restore_callbacks: function(){ + if(this.action_callback_stack.length){ + var callbacks = this.action_callback_stack.pop(); + this.action_callback = callbacks; + } + }, + + // when a barcode is scanned and parsed, the callback corresponding + // to its type is called with the parsed_barcode as a parameter. + // (parsed_barcode is the result of parse_barcode(barcode)) + // + // callbacks is a Map of 'actions' : callback(parsed_barcode) + // that sets the callback for each action. if a callback for the + // specified action already exists, it is replaced. + // + // possible actions include : + // 'product' | 'cashier' | 'client' | 'discount' + set_action_callback: function(action, callback){ + if(arguments.length == 2){ + this.action_callback[action] = callback; + }else{ + var actions = arguments[0]; + for(var action in actions){ + this.set_action_callback(action,actions[action]); + } + } + }, + + //remove all action callbacks + reset_action_callbacks: function(){ + for(var action in this.action_callback){ + this.action_callback[action] = undefined; + } + }, + + scan: function(code){ + if (!code) { + return; + } + var parsed_result = this.barcode_parser.parse_barcode(code); + + if(parsed_result.type in {'product':'', 'weight':'', 'price':''}){ //barcode is associated to a product + if(this.action_callback.product){ + this.action_callback.product(parsed_result); + } + } + else if (parsed_result.type in {'cashier':'', 'client':'', 'discount':''}){ + if(this.action_callback[parsed_result.type]){ + this.action_callback[parsed_result.type](parsed_result); + } + } + else{ + this.action_callback.error(parsed_result); + } + }, + + // starts catching keyboard events and tries to interpret codebar + // calling the callbacks when needed. + connect: function(){ + + var self = this; + var code = ""; + var timeStamp = 0; + var timeout = null; + + this.handler = function(e){ + + if(e.which === 13){ //ignore returns + e.preventDefault(); + return; + } + + if(timeStamp + 50 < new Date().getTime()){ + code = ""; + } + + timeStamp = new Date().getTime(); + clearTimeout(timeout); + + code += String.fromCharCode(e.which); + + // we wait for a while after the last input to be sure that we are not mistakingly + // returning a code which is a prefix of a bigger one : + // Internal Ref 5449 vs EAN13 5449000... + + timeout = setTimeout(function(){ + if(code.length >= 3){ + self.scan(code); + } + code = ""; + },100); + }; + + $('body').on('keypress', this.handler); + }, + + // stops catching keyboard events + disconnect: function(){ + $('body').off('keypress', this.handler); + }, + + // the barcode scanner will listen on the hw_proxy/scanner interface for + // scan events until disconnect_from_proxy is called + connect_to_proxy: function(){ + var self = this; + this.remote_scanning = true; + if(this.remote_active >= 1){ + return; + } + this.remote_active = 1; + + function waitforbarcode(){ + return self.proxy.connection.rpc('/hw_proxy/scanner',{},{timeout:7500}) + .then(function(barcode){ + if(!self.remote_scanning){ + self.remote_active = 0; + return; + } + self.scan(barcode); + waitforbarcode(); + }, + function(){ + if(!self.remote_scanning){ + self.remote_active = 0; + return; + } + setTimeout(waitforbarcode,5000); + }); + } + waitforbarcode(); + }, + + // the barcode scanner will stop listening on the hw_proxy/scanner remote interface + disconnect_from_proxy: function(){ + this.remote_scanning = false; + }, +}); + +return { + JobQueue: JobQueue, + ProxyDevice: ProxyDevice, + BarcodeReader: BarcodeReader, +}; + +}); diff --git a/addons/point_of_sale/static/src/js/gui.js b/addons/point_of_sale/static/src/js/gui.js index 20e754b07e5..bd0f6236947 100644 --- a/addons/point_of_sale/static/src/js/gui.js +++ b/addons/point_of_sale/static/src/js/gui.js @@ -1,366 +1,371 @@ - +odoo.define('point_of_sale.gui', ['web.core', 'web.Model'], function (require) { +"use strict"; // this file contains the Gui, which is the pos 'controller'. // It contains high level methods to manipulate the interface // such as changing between screens, creating popups, etc. // // it is available to all pos objects trough the '.gui' field. -openerp.point_of_sale.load_gui = function load_gui(instance, module) { - "use strict"; +var core = require('web.core'); +var Model = require('web.Model'); - var QWeb = instance.web.qweb; - var _t = instance.web._t; +var _t = core._t; - module.Gui = instance.web.Class.extend({ - screen_classes: [], - popup_classes: [], - init: function(options){ - var self = this; - this.pos = options.pos; - this.chrome = options.chrome; - this.screen_instances = {}; - this.popup_instances = {}; - this.default_screen = null; - this.startup_screen = null; - this.current_popup = null; - this.current_screen = null; +var Gui = core.Class.extend({ + screen_classes: [], + popup_classes: [], + init: function(options){ + var self = this; + this.pos = options.pos; + this.chrome = options.chrome; + this.screen_instances = {}; + this.popup_instances = {}; + this.default_screen = null; + this.startup_screen = null; + this.current_popup = null; + this.current_screen = null; - this.chrome.ready.then(function(){ - var order = self.pos.get_order(); - if (order) { - self.show_saved_screen(order); - } else { - self.show_screen(self.startup_screen); - } - self.pos.bind('change:selectedOrder', function(){ - self.show_saved_screen(self.pos.get_order()); + this.chrome.ready.then(function(){ + var order = self.pos.get_order(); + if (order) { + self.show_saved_screen(order); + } else { + self.show_screen(self.startup_screen); + } + self.pos.bind('change:selectedOrder', function(){ + self.show_saved_screen(self.pos.get_order()); + }); + }); + }, + + /* ---- Gui: SCREEN MANIPULATION ---- */ + + // register a screen widget to the gui, + // it must have been inserted into the dom. + add_screen: function(name, screen){ + screen.hide(); + this.screen_instances[name] = screen; + }, + + // sets the screen that will be displayed + // for new orders + set_default_screen: function(name){ + this.default_screen = name; + }, + + // sets the screen that will be displayed + // when no orders are present + set_startup_screen: function(name) { + this.startup_screen = name; + }, + + // display the screen saved in an order, + // called when the user changes the current order + // no screen saved ? -> display default_screen + // no order ? -> display startup_screen + show_saved_screen: function(order,options) { + options = options || {}; + this.close_popup(); + if (order) { + this.show_screen(order.get_screen_data('screen') || + options.default_screen || + this.default_screen, + null,'refresh'); + } else { + this.show_screen(this.startup_screen); + } + }, + + // display a screen. + // If there is an order, the screen will be saved in the order + // - params: used to load a screen with parameters, for + // example loading a 'product_details' screen for a specific product. + // - refresh: if you want the screen to cycle trough show / hide even + // if you are already on the same screen. + show_screen: function(screen_name,params,refresh) { + var screen = this.screen_instances[screen_name]; + if (!screen) { + console.error("ERROR: show_screen("+screen_name+") : screen not found"); + } + + this.close_popup(); + + var order = this.pos.get_order(); + if (order) { + var old_screen_name = order.get_screen_data('screen'); + + order.set_screen_data('screen',screen_name); + + if(params){ + order.set_screen_data('params',params); + } + + if( screen_name !== old_screen_name ){ + order.set_screen_data('previous-screen',old_screen_name); + } + } + + if (refresh || screen !== this.current_screen) { + if (this.current_screen) { + this.current_screen.close(); + this.current_screen.hide(); + } + this.current_screen = screen; + this.current_screen.show(); + } + }, + + // returns the current screen. + get_current_screen: function() { + return this.pos.get_order() ? ( this.pos.get_order().get_screen_data('screen') || this.default_screen ) : this.startup_screen; + }, + + // goes to the previous screen (as specified in the order). The history only + // goes 1 deep ... + back: function() { + var previous = this.pos.get_order().get_screen_data('previous-screen'); + if (previous) { + this.show_screen(previous); + } + }, + + // returns the parameter specified when this screen was displayed + get_current_screen_param: function(param) { + if (this.pos.get_order()) { + var params = this.pos.get_order().get_screen_data('params'); + return params ? params[param] : undefined; + } else { + return undefined; + } + }, + + /* ---- Gui: POPUP MANIPULATION ---- */ + + // registers a new popup in the GUI. + // the popup must have been previously inserted + // into the dom. + add_popup: function(name, popup) { + popup.hide(); + this.popup_instances[name] = popup; + }, + + // displays a popup. Popup do not stack, + // are not remembered by the order, and are + // closed by screen changes or new popups. + show_popup: function(name,options) { + if (this.current_popup) { + this.close_popup(); + } + this.current_popup = this.popup_instances[name]; + this.current_popup.show(options); + }, + + // close the current popup. + close_popup: function() { + if (this.current_popup) { + this.current_popup.close(); + this.current_popup.hide(); + this.current_popup = null; + } + }, + + // is there an active popup ? + has_popup: function() { + return !!this.current_popup; + }, + + /* ---- Gui: ACCESS CONTROL ---- */ + + // A Generic UI that allow to select a user from a list. + // It returns a deferred that resolves with the selected user + // upon success. Several options are available : + // - security: passwords will be asked + // - only_managers: restricts the list to managers + // - current_user: password will not be asked if this + // user is selected. + // - title: The title of the user selection list. + select_user: function(options){ + options = options || {}; + var self = this; + var def = new $.Deferred(); + + var list = []; + for (var i = 0; i < this.pos.users.length; i++) { + var user = this.pos.users[i]; + if (!options.only_managers || user.role === 'manager') { + list.push({ + 'label': user.name, + 'item': user, }); - }); - }, - - /* ---- Gui: SCREEN MANIPULATION ---- */ - - // register a screen widget to the gui, - // it must have been inserted into the dom. - add_screen: function(name, screen){ - screen.hide(); - this.screen_instances[name] = screen; - }, - - // sets the screen that will be displayed - // for new orders - set_default_screen: function(name){ - this.default_screen = name; - }, - - // sets the screen that will be displayed - // when no orders are present - set_startup_screen: function(name) { - this.startup_screen = name; - }, - - // display the screen saved in an order, - // called when the user changes the current order - // no screen saved ? -> display default_screen - // no order ? -> display startup_screen - show_saved_screen: function(order,options) { - options = options || {}; - this.close_popup(); - if (order) { - this.show_screen(order.get_screen_data('screen') || - options.default_screen || - this.default_screen, - null,'refresh'); - } else { - this.show_screen(this.startup_screen); } - }, + } - // display a screen. - // If there is an order, the screen will be saved in the order - // - params: used to load a screen with parameters, for - // example loading a 'product_details' screen for a specific product. - // - refresh: if you want the screen to cycle trough show / hide even - // if you are already on the same screen. - show_screen: function(screen_name,params,refresh) { - var screen = this.screen_instances[screen_name]; - if (!screen) { - console.error("ERROR: show_screen("+screen_name+") : screen not found"); - } + this.show_popup('selection',{ + 'title': options.title || _t('Select User'), + list: list, + confirm: function(user){ def.resolve(user); }, + cancel: function(){ def.reject(); }, + }); - this.close_popup(); - - var order = this.pos.get_order(); - if (order) { - var old_screen_name = order.get_screen_data('screen'); - - order.set_screen_data('screen',screen_name); - - if(params){ - order.set_screen_data('params',params); - } - - if( screen_name !== old_screen_name ){ - order.set_screen_data('previous-screen',old_screen_name); - } - } - - if (refresh || screen !== this.current_screen) { - if (this.current_screen) { - this.current_screen.close(); - this.current_screen.hide(); - } - this.current_screen = screen; - this.current_screen.show(); - } - }, - - // returns the current screen. - get_current_screen: function() { - return this.pos.get_order() ? ( this.pos.get_order().get_screen_data('screen') || this.default_screen ) : this.startup_screen; - }, - - // goes to the previous screen (as specified in the order). The history only - // goes 1 deep ... - back: function() { - var previous = this.pos.get_order().get_screen_data('previous-screen'); - if (previous) { - this.show_screen(previous); - } - }, - - // returns the parameter specified when this screen was displayed - get_current_screen_param: function(param) { - if (this.pos.get_order()) { - var params = this.pos.get_order().get_screen_data('params'); - return params ? params[param] : undefined; - } else { - return undefined; - } - }, - - /* ---- Gui: POPUP MANIPULATION ---- */ - - // registers a new popup in the GUI. - // the popup must have been previously inserted - // into the dom. - add_popup: function(name, popup) { - popup.hide(); - this.popup_instances[name] = popup; - }, - - // displays a popup. Popup do not stack, - // are not remembered by the order, and are - // closed by screen changes or new popups. - show_popup: function(name,options) { - if (this.current_popup) { - this.close_popup(); - } - this.current_popup = this.popup_instances[name]; - this.current_popup.show(options); - }, - - // close the current popup. - close_popup: function() { - if (this.current_popup) { - this.current_popup.close(); - this.current_popup.hide(); - this.current_popup = null; - } - }, - - // is there an active popup ? - has_popup: function() { - return !!this.current_popup; - }, - - /* ---- Gui: ACCESS CONTROL ---- */ - - // A Generic UI that allow to select a user from a list. - // It returns a deferred that resolves with the selected user - // upon success. Several options are available : - // - security: passwords will be asked - // - only_managers: restricts the list to managers - // - current_user: password will not be asked if this - // user is selected. - // - title: The title of the user selection list. - select_user: function(options){ - options = options || {}; - var self = this; - var def = new $.Deferred(); - - var list = []; - for (var i = 0; i < this.pos.users.length; i++) { - var user = this.pos.users[i]; - if (!options.only_managers || user.role === 'manager') { - list.push({ - 'label': user.name, - 'item': user, - }); - } - } - - this.show_popup('selection',{ - 'title': options.title || _t('Select User'), - list: list, - confirm: function(user){ def.resolve(user); }, - cancel: function(){ def.reject(); }, - }); - - return def.then(function(user){ - if (options.security && user !== options.current_user && user.pos_security_pin) { - return self.ask_password(user.pos_security_pin).then(function(){ - return user; - }); - } else { + return def.then(function(user){ + if (options.security && user !== options.current_user && user.pos_security_pin) { + return self.ask_password(user.pos_security_pin).then(function(){ return user; - } - }); - }, - - // Ask for a password, and checks if it this - // the same as specified by the function call. - // returns a deferred that resolves on success, - // fails on failure. - ask_password: function(password) { - var self = this; - var ret = new $.Deferred(); - if (password) { - this.gui.show_popup('password',{ - 'title': _t('Password ?'), - confirm: function(pw) { - if (pw !== password) { - self.show_popup('error',_t('Incorrect Password')); - ret.reject(); - } else { - ret.resolve(); - } - }, }); } else { - ret.resolve(); + return user; } - return ret; - }, + }); + }, - // checks if the current user (or the user provided) has manager - // access rights. If not, a popup is shown allowing the user to - // temporarily login as an administrator. - // This method returns a deferred, that succeeds with the - // manager user when the login is successfull. - sudo: function(user){ - var def = new $.Deferred(); - user = user || this.pos.get_cashier(); - - if (user.role === 'manager') { - return new $.Deferred().resolve(user); - } else { - return this.select_user({ - security: true, - only_managers: true, - title: _t('Login as a Manager'), - }) - } - }, - - /* ---- Gui: CLOSING THE POINT OF SALE ---- */ - - close: function() { - var self = this; - this.chrome.loading_show(); - this.chrome.loading_message(_t('Closing ...')); - - this.pos.push_order().then(function(){ - return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_client_pos_menu']], ['res_id']) - .pipe(function(res) { - window.location = '/web#action=' + res[0]['res_id']; - },function(err,event) { - event.preventDefault(); - self.show_popup('error',{ - 'title': _t('Could not close the point of sale.'), - 'body': _t('Your internet connection is probably down.'), - }); - self.chrome.widget.close_button.renderElement(); - }); + // Ask for a password, and checks if it this + // the same as specified by the function call. + // returns a deferred that resolves on success, + // fails on failure. + ask_password: function(password) { + var self = this; + var ret = new $.Deferred(); + if (password) { + this.gui.show_popup('password',{ + 'title': _t('Password ?'), + confirm: function(pw) { + if (pw !== password) { + self.show_popup('error',_t('Incorrect Password')); + ret.reject(); + } else { + ret.resolve(); + } + }, }); - }, + } else { + ret.resolve(); + } + return ret; + }, - /* ---- Gui: SOUND ---- */ + // checks if the current user (or the user provided) has manager + // access rights. If not, a popup is shown allowing the user to + // temporarily login as an administrator. + // This method returns a deferred, that succeeds with the + // manager user when the login is successfull. + sudo: function(user){ + user = user || this.pos.get_cashier(); - play_sound: function(sound) { - var src = ''; - if (sound === 'error') { - src = "/point_of_sale/static/src/sounds/error.wav"; - } else if (sound === 'bell') { - src = "/point_of_sale/static/src/sounds/bell.wav"; + if (user.role === 'manager') { + return new $.Deferred().resolve(user); + } else { + return this.select_user({ + security: true, + only_managers: true, + title: _t('Login as a Manager'), + }); + } + }, + + /* ---- Gui: CLOSING THE POINT OF SALE ---- */ + + close: function() { + var self = this; + this.chrome.loading_show(); + this.chrome.loading_message(_t('Closing ...')); + + this.pos.push_order().then(function(){ + return new Model("ir.model.data").get_func("search_read")([['name', '=', 'action_client_pos_menu']], ['res_id']) + .pipe(function(res) { + window.location = '/web#action=' + res[0]['res_id']; + },function(err,event) { + event.preventDefault(); + self.show_popup('error',{ + 'title': _t('Could not close the point of sale.'), + 'body': _t('Your internet connection is probably down.'), + }); + self.chrome.widget.close_button.renderElement(); + }); + }); + }, + + /* ---- Gui: SOUND ---- */ + + play_sound: function(sound) { + var src = ''; + if (sound === 'error') { + src = "/point_of_sale/static/src/sounds/error.wav"; + } else if (sound === 'bell') { + src = "/point_of_sale/static/src/sounds/bell.wav"; + } else { + console.error('Unknown sound: ',sound); + return; + } + $('body').append(''); + }, + + /* ---- Gui: KEYBOARD INPUT ---- */ + + // This is a helper to handle numpad keyboard input. + // - buffer: an empty or number string + // - input: '[0-9],'+','-','.','CLEAR','BACKSPACE' + // - options: 'firstinput' -> will clear buffer if + // input is '[0-9]' or '.' + // returns the new buffer containing the modifications + // (the original is not touched) + numpad_input: function(buffer, input, options) { + var newbuf = buffer.slice(0); + options = options || {}; + + if (input === '.') { + if (options.firstinput) { + newbuf = "0."; + }else if (!newbuf.length || newbuf === '-') { + newbuf += "0."; + } else if (newbuf.indexOf('.') < 0){ + newbuf = newbuf + '.'; + } + } else if (input === 'CLEAR') { + newbuf = ""; + } else if (input === 'BACKSPACE') { + newbuf = newbuf.substring(0,newbuf.length - 1); + } else if (input === '+') { + if ( newbuf[0] === '-' ) { + newbuf = newbuf.substring(1,newbuf.length); + } + } else if (input === '-') { + if ( newbuf[0] === '-' ) { + newbuf = newbuf.substring(1,newbuf.length); } else { - console.error('Unknown sound: ',sound); - return; + newbuf = '-' + newbuf; } - $('body').append(''); - }, - - /* ---- Gui: KEYBOARD INPUT ---- */ - - // This is a helper to handle numpad keyboard input. - // - buffer: an empty or number string - // - input: '[0-9],'+','-','.','CLEAR','BACKSPACE' - // - options: 'firstinput' -> will clear buffer if - // input is '[0-9]' or '.' - // returns the new buffer containing the modifications - // (the original is not touched) - numpad_input: function(buffer, input, options) { - var options = options || {}; - var newbuf = buffer.slice(0); - - if (input === '.') { - if (options.firstinput) { - newbuf = "0."; - }else if (!newbuf.length || newbuf === '-') { - newbuf += "0."; - } else if (newbuf.indexOf('.') < 0){ - newbuf = newbuf + '.'; - } - } else if (input === 'CLEAR') { - newbuf = ""; - } else if (input === 'BACKSPACE') { - newbuf = newbuf.substring(0,newbuf.length - 1); - } else if (input === '+') { - if ( newbuf[0] === '-' ) { - newbuf = newbuf.substring(1,newbuf.length); - } - } else if (input === '-') { - if ( newbuf[0] === '-' ) { - newbuf = newbuf.substring(1,newbuf.length); - } else { - newbuf = '-' + newbuf; - } - } else if (input[0] === '+' && !isNaN(parseFloat(input))) { - newbuf = '' + ((parseFloat(newbuf) || 0) + parseFloat(input)); - } else if (!isNaN(parseInt(input))) { - if (options.firstinput) { - newbuf = '' + input; - } else { - newbuf += input; - } + } else if (input[0] === '+' && !isNaN(parseFloat(input))) { + newbuf = '' + ((parseFloat(newbuf) || 0) + parseFloat(input)); + } else if (!isNaN(parseInt(input))) { + if (options.firstinput) { + newbuf = '' + input; + } else { + newbuf += input; } + } - // End of input buffer at 12 characters. - if (newbuf.length > buffer.length && newbuf.length > 12) { - this.play_sound('bell'); - return buffer.slice(0); - } + // End of input buffer at 12 characters. + if (newbuf.length > buffer.length && newbuf.length > 12) { + this.play_sound('bell'); + return buffer.slice(0); + } - return newbuf; - }, - }); + return newbuf; + }, +}); - module.Gui.define_screen = function (classe) { - module.Gui.prototype.screen_classes.push(classe); - }; +var define_screen = function (classe) { + Gui.prototype.screen_classes.push(classe); +}; - module.Gui.define_popup = function (classe) { - module.Gui.prototype.popup_classes.push(classe); - }; -}; +var define_popup = function (classe) { + Gui.prototype.popup_classes.push(classe); +}; +return { + Gui: Gui, + define_screen: define_screen, + define_popup: define_popup, +}; + +}); diff --git a/addons/point_of_sale/static/src/js/keyboard.js b/addons/point_of_sale/static/src/js/keyboard.js index 1249810c4ba..3bb894731c0 100644 --- a/addons/point_of_sale/static/src/js/keyboard.js +++ b/addons/point_of_sale/static/src/js/keyboard.js @@ -1,201 +1,207 @@ -openerp.point_of_sale.load_keyboard = function load_keyboard(instance, module){ //module is instance.point_of_sale - "use strict"; +odoo.define('point_of_sale.keyboard', ['web.Widget'], function (require) { +"use strict"; + +var Widget = require('web.Widget'); + +// ---------- OnScreen Keyboard Widget ---------- +// A Widget that displays an onscreen keyboard. +// There are two options when creating the widget : +// +// * 'keyboard_model' : 'simple' (default) | 'full' +// The 'full' emulates a PC keyboard, while 'simple' emulates an 'android' one. +// +// * 'input_selector : (default: '.searchbox input') +// defines the dom element that the keyboard will write to. +// +// The widget is initially hidden. It can be shown with this.show(), and is +// automatically shown when the input_selector gets focused. + +var OnscreenKeyboardWidget = Widget.extend({ + template: 'OnscreenKeyboardSimple', + init: function(parent, options){ + this._super(parent,options); + options = options || {}; + + this.keyboard_model = options.keyboard_model || 'simple'; + if(this.keyboard_model === 'full'){ + this.template = 'OnscreenKeyboardFull'; + } + + this.input_selector = options.input_selector || '.searchbox input'; + this.$target = null; + + //Keyboard state + this.capslock = false; + this.shift = false; + this.numlock = false; + }, - // ---------- OnScreen Keyboard Widget ---------- - // A Widget that displays an onscreen keyboard. - // There are two options when creating the widget : - // - // * 'keyboard_model' : 'simple' (default) | 'full' - // The 'full' emulates a PC keyboard, while 'simple' emulates an 'android' one. - // - // * 'input_selector : (default: '.searchbox input') - // defines the dom element that the keyboard will write to. - // - // The widget is initially hidden. It can be shown with this.show(), and is - // automatically shown when the input_selector gets focused. + connect : function(target){ + var self = this; + this.$target = $(target); + this.$target.focus(function(){self.show();}); + }, + generateEvent: function(type,key){ + var event = document.createEvent("KeyboardEvent"); + var initMethod = event.initKeyboardEvent ? 'initKeyboardEvent' : 'initKeyEvent'; + event[initMethod]( type, + true, //bubbles + true, //cancelable + window, //viewArg + false, //ctrl + false, //alt + false, //shift + false, //meta + ((typeof key.code === 'undefined') ? key.char.charCodeAt(0) : key.code), + ((typeof key.char === 'undefined') ? String.fromCharCode(key.code) : key.char) + ); + return event; - module.OnscreenKeyboardWidget = instance.web.Widget.extend({ - template: 'OnscreenKeyboardSimple', - init: function(parent, options){ - var self = this; - this._super(parent,options); - options = options || {}; + }, - this.keyboard_model = options.keyboard_model || 'simple'; - if(this.keyboard_model === 'full'){ - this.template = 'OnscreenKeyboardFull'; - } - - this.input_selector = options.input_selector || '.searchbox input'; - this.$target = null; - - //Keyboard state - this.capslock = false; - this.shift = false; - this.numlock = false; - }, - - connect : function(target){ - var self = this; - this.$target = $(target); - this.$target.focus(function(){self.show();}); - }, - generateEvent: function(type,key){ - var event = document.createEvent("KeyboardEvent"); - var initMethod = event.initKeyboardEvent ? 'initKeyboardEvent' : 'initKeyEvent'; - event[initMethod]( type, - true, //bubbles - true, //cancelable - window, //viewArg - false, //ctrl - false, //alt - false, //shift - false, //meta - ((typeof key.code === 'undefined') ? key.char.charCodeAt(0) : key.code), - ((typeof key.char === 'undefined') ? String.fromCharCode(key.code) : key.char) - ); - return event; - - }, - - // Write a character to the input zone - writeCharacter: function(character){ - var input = this.$target[0]; - input.dispatchEvent(this.generateEvent('keydown',{char: character})); - if(character !== '\n'){ - input.value += character; - } - input.dispatchEvent(this.generateEvent('keyup',{char: character})); - }, - - // Removes the last character from the input zone. - deleteCharacter: function(){ - var input = this.$target[0]; + // Write a character to the input zone + writeCharacter: function(character){ + var input = this.$target[0]; + input.dispatchEvent(this.generateEvent('keydown',{char: character})); + if(character !== '\n'){ + input.value += character; + } + input.dispatchEvent(this.generateEvent('keyup',{char: character})); + }, + + // Removes the last character from the input zone. + deleteCharacter: function(){ + var input = this.$target[0]; + input.dispatchEvent(this.generateEvent('keydown',{code: 8})); + input.value = input.value.substr(0, input.value.length -1); + input.dispatchEvent(this.generateEvent('keyup',{code: 8})); + }, + + // Clears the content of the input zone. + deleteAllCharacters: function(){ + var input = this.$target[0]; + if(input.value){ input.dispatchEvent(this.generateEvent('keydown',{code: 8})); - input.value = input.value.substr(0, input.value.length -1); + input.value = ""; input.dispatchEvent(this.generateEvent('keyup',{code: 8})); - }, - - // Clears the content of the input zone. - deleteAllCharacters: function(){ - var input = this.$target[0]; - if(input.value){ - input.dispatchEvent(this.generateEvent('keydown',{code: 8})); - input.value = ""; - input.dispatchEvent(this.generateEvent('keyup',{code: 8})); - } - }, + } + }, - // Makes the keyboard show and slide from the bottom of the screen. - show: function(){ - $('.keyboard_frame').show().css({'height':'235px'}); - }, + // Makes the keyboard show and slide from the bottom of the screen. + show: function(){ + $('.keyboard_frame').show().css({'height':'235px'}); + }, + + // Makes the keyboard hide by sliding to the bottom of the screen. + hide: function(){ + $('.keyboard_frame') + .css({'height':'0'}) + .hide(); + this.reset(); + }, + + //What happens when the shift key is pressed : toggle case, remove capslock + toggleShift: function(){ + $('.letter').toggleClass('uppercase'); + $('.symbol span').toggle(); - // Makes the keyboard hide by sliding to the bottom of the screen. - hide: function(){ - $('.keyboard_frame') - .css({'height':'0'}) - .hide(); - this.reset(); - }, - - //What happens when the shift key is pressed : toggle case, remove capslock - toggleShift: function(){ - $('.letter').toggleClass('uppercase'); + this.shift = (this.shift === true) ? false : true; + this.capslock = false; + }, + + //what happens when capslock is pressed : toggle case, set capslock + toggleCapsLock: function(){ + $('.letter').toggleClass('uppercase'); + this.capslock = true; + }, + + //What happens when numlock is pressed : toggle symbols and numlock label + toggleNumLock: function(){ + $('.symbol span').toggle(); + $('.numlock span').toggle(); + this.numlock = (this.numlock === true ) ? false : true; + }, + + //After a key is pressed, shift is disabled. + removeShift: function(){ + if (this.shift === true) { $('.symbol span').toggle(); + if (this.capslock === false) $('.letter').toggleClass('uppercase'); - self.shift = (self.shift === true) ? false : true; - self.capslock = false; - }, - - //what happens when capslock is pressed : toggle case, set capslock - toggleCapsLock: function(){ - $('.letter').toggleClass('uppercase'); - self.capslock = true; - }, - - //What happens when numlock is pressed : toggle symbols and numlock label - toggleNumLock: function(){ - $('.symbol span').toggle(); - $('.numlock span').toggle(); - self.numlock = (self.numlock === true ) ? false : true; - }, + this.shift = false; + } + }, - //After a key is pressed, shift is disabled. - removeShift: function(){ - if (self.shift === true) { - $('.symbol span').toggle(); - if (this.capslock === false) $('.letter').toggleClass('uppercase'); - - self.shift = false; + // Resets the keyboard to its original state; capslock: false, shift: false, numlock: false + reset: function(){ + if(this.shift){ + this.toggleShift(); + } + if(this.capslock){ + this.toggleCapsLock(); + } + if(this.numlock){ + this.toggleNumLock(); + } + }, + + //called after the keyboard is in the DOM, sets up the key bindings. + start: function(){ + var self = this; + + //this.show(); + + + $('.close_button').click(function(){ + self.deleteAllCharacters(); + self.hide(); + }); + + // Keyboard key click handling + $('.keyboard li').click(function(){ + + var $this = $(this), + character = $this.html(); // If it's a lowercase letter, nothing happens to this variable + + if ($this.hasClass('left-shift') || $this.hasClass('right-shift')) { + self.toggleShift(); + return false; } - }, - - // Resets the keyboard to its original state; capslock: false, shift: false, numlock: false - reset: function(){ - if(this.shift){ - this.toggleShift(); + + if ($this.hasClass('capslock')) { + self.toggleCapsLock(); + return false; } - if(this.capslock){ - this.toggleCapsLock(); + + if ($this.hasClass('delete')) { + self.deleteCharacter(); + return false; } - if(this.numlock){ - this.toggleNumLock(); + + if ($this.hasClass('numlock')){ + self.toggleNumLock(); + return false; } - }, + + // Special characters + if ($this.hasClass('symbol')) character = $('span:visible', $this).html(); + if ($this.hasClass('space')) character = ' '; + if ($this.hasClass('tab')) character = "\t"; + if ($this.hasClass('return')) character = "\n"; + + // Uppercase letter + if ($this.hasClass('uppercase')) character = character.toUpperCase(); + + // Remove shift once a key is clicked. + self.removeShift(); - //called after the keyboard is in the DOM, sets up the key bindings. - start: function(){ - var self = this; + self.writeCharacter(character); + }); + }, +}); - //this.show(); +return { + OnscreenKeyboardWidget: OnscreenKeyboardWidget, +}; - - $('.close_button').click(function(){ - self.deleteAllCharacters(); - self.hide(); - }); - - // Keyboard key click handling - $('.keyboard li').click(function(){ - - var $this = $(this), - character = $this.html(); // If it's a lowercase letter, nothing happens to this variable - - if ($this.hasClass('left-shift') || $this.hasClass('right-shift')) { - self.toggleShift(); - return false; - } - - if ($this.hasClass('capslock')) { - self.toggleCapsLock(); - return false; - } - - if ($this.hasClass('delete')) { - self.deleteCharacter(); - return false; - } - - if ($this.hasClass('numlock')){ - self.toggleNumLock(); - return false; - } - - // Special characters - if ($this.hasClass('symbol')) character = $('span:visible', $this).html(); - if ($this.hasClass('space')) character = ' '; - if ($this.hasClass('tab')) character = "\t"; - if ($this.hasClass('return')) character = "\n"; - - // Uppercase letter - if ($this.hasClass('uppercase')) character = character.toUpperCase(); - - // Remove shift once a key is clicked. - self.removeShift(); - - self.writeCharacter(character); - }); - }, - }); -} +}); diff --git a/addons/point_of_sale/static/src/js/main.js b/addons/point_of_sale/static/src/js/main.js index 10e087c446d..1d2f4f3238f 100644 --- a/addons/point_of_sale/static/src/js/main.js +++ b/addons/point_of_sale/static/src/js/main.js @@ -1,27 +1,10 @@ -openerp.point_of_sale = function(openerp) { - "use strict"; +odoo.define('point_of_sale.main', ['point_of_sale.chrome', 'web.core'], function (require) { +"use strict"; - var module = openerp.point_of_sale; +var chrome = require('point_of_sale.chrome'); +var core = require('web.core'); - openerp.point_of_sale.load_db(openerp,module); - - openerp.point_of_sale.load_models(openerp,module); - - openerp.point_of_sale.load_basewidget(openerp,module); - - openerp.point_of_sale.load_keyboard(openerp,module); - - openerp.point_of_sale.load_gui(openerp,module); - - openerp.point_of_sale.load_popups(openerp,module); - - openerp.point_of_sale.load_screens(openerp,module); - - openerp.point_of_sale.load_devices(openerp,module); - - openerp.point_of_sale.load_chrome(openerp,module); - - openerp.web.client_actions.add('pos.ui', 'openerp.point_of_sale.Chrome'); -}; +core.action_registry.add('pos.ui', chrome.Chrome); +}); diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index de51cc10169..15cca2d54fc 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1,1857 +1,1885 @@ -openerp.point_of_sale.load_models = function load_models(instance, module){ //module is instance.point_of_sale - "use strict"; +odoo.define('point_of_sale.models', ['barcodes.BarcodeParser', 'point_of_sale.DB', 'point_of_sale.devices', 'web.core', 'web.Model', 'web.session', 'web.time', 'web.utils'], function (require) { +"use strict"; - var QWeb = instance.web.qweb; - var _t = instance.web._t; +var BarcodeParser = require('barcodes.BarcodeParser'); +var PosDB = require('point_of_sale.DB'); +var devices = require('point_of_sale.devices'); +var core = require('web.core'); +var Model = require('web.Model'); +var session = require('web.session'); +var time = require('web.time'); +var utils = require('web.utils'); - var round_di = instance.web.round_decimals; - var round_pr = instance.web.round_precision - - // The PosModel contains the Point Of Sale's representation of the backend. - // Since the PoS must work in standalone ( Without connection to the server ) - // it must contains a representation of the server's PoS backend. - // (taxes, product list, configuration options, etc.) this representation - // is fetched and stored by the PosModel at the initialisation. - // this is done asynchronously, a ready deferred alows the GUI to wait interactively - // for the loading to be completed - // There is a single instance of the PosModel for each Front-End instance, it is usually called - // 'pos' and is available to all widgets extending PosWidget. +var QWeb = core.qweb; +var _t = core._t; +var Mutex = utils.Mutex; +var round_di = utils.round_decimals; +var round_pr = utils.round_precision; +var Backbone = window.Backbone; - module.PosModel = Backbone.Model.extend({ - initialize: function(session, attributes) { - Backbone.Model.prototype.initialize.call(this, attributes); - var self = this; - this.session = session; - this.flush_mutex = new $.Mutex(); // used to make sure the orders are sent to the server once at time - this.chrome = attributes.chrome; - this.gui = attributes.gui; +var exports = {}; - this.proxy = new module.ProxyDevice(this); // used to communicate to the hardware devices via a local proxy - this.barcode_reader = new module.BarcodeReader({'pos': this, proxy:this.proxy}); +// The PosModel contains the Point Of Sale's representation of the backend. +// Since the PoS must work in standalone ( Without connection to the server ) +// it must contains a representation of the server's PoS backend. +// (taxes, product list, configuration options, etc.) this representation +// is fetched and stored by the PosModel at the initialisation. +// this is done asynchronously, a ready deferred alows the GUI to wait interactively +// for the loading to be completed +// There is a single instance of the PosModel for each Front-End instance, it is usually called +// 'pos' and is available to all widgets extending PosWidget. - this.proxy_queue = new module.JobQueue(); // used to prevent parallels communications to the proxy - this.db = new module.PosDB(); // a local database used to search trough products and categories & store pending orders - this.debug = jQuery.deparam(jQuery.param.querystring()).debug !== undefined; //debug mode - - // Business data; loaded from the server at launch - this.company_logo = null; - this.company_logo_base64 = ''; - this.currency = null; - this.shop = null; - this.company = null; - this.user = null; - this.users = []; - this.partners = []; - this.cashier = null; - this.cashregisters = []; - this.taxes = []; - this.pos_session = null; - this.config = null; - this.units = []; - this.units_by_id = {}; - this.pricelist = null; - this.order_sequence = 1; - window.posmodel = this; +exports.PosModel = Backbone.Model.extend({ + initialize: function(session, attributes) { + Backbone.Model.prototype.initialize.call(this, attributes); + var self = this; + this.flush_mutex = new Mutex(); // used to make sure the orders are sent to the server once at time + this.chrome = attributes.chrome; + this.gui = attributes.gui; - // these dynamic attributes can be watched for change by other models or widgets - this.set({ - 'synch': { state:'connected', pending:0 }, - 'orders': new module.OrderCollection(), - 'selectedOrder': null, - }); + this.proxy = new devices.ProxyDevice(this); // used to communicate to the hardware devices via a local proxy + this.barcode_reader = new devices.BarcodeReader({'pos': this, proxy:this.proxy}); - this.bind('change:synch',function(pos,synch){ - clearTimeout(self.synch_timeout); - self.synch_timeout = setTimeout(function(){ - if(synch.state !== 'disconnected' && synch.pending > 0){ - self.set('synch',{state:'disconnected', pending:synch.pending}); - } - },3000); - }); - - this.get('orders').bind('remove', function(order,_unused_,options){ - self.on_removed_order(order,options.index,options.reason); - }); - - // We fetch the backend data on the server asynchronously. this is done only when the pos user interface is launched, - // Any change on this data made on the server is thus not reflected on the point of sale until it is relaunched. - // when all the data has loaded, we compute some stuff, and declare the Pos ready to be used. - this.ready = this.load_server_data().then(function(){ - return self.after_load_server_data(); - });; - }, - after_load_server_data: function(){ - this.barcode_reader.connect(); - this.load_orders(); - this.set_start_order(); - this.push_order(); - if(this.config.use_proxy){ - return this.connect_to_proxy(); - } - }, - // releases ressources holds by the model at the end of life of the posmodel - destroy: function(){ - // FIXME, should wait for flushing, return a deferred to indicate successfull destruction - // this.flush(); - this.proxy.close(); - this.barcode_reader.disconnect(); - this.barcode_reader.disconnect_from_proxy(); - }, - - connect_to_proxy: function(){ - var self = this; - var done = new $.Deferred(); - this.barcode_reader.disconnect_from_proxy(); - this.chrome.loading_message(_t('Connecting to the PosBox'),0); - this.chrome.loading_skip(function(){ - self.proxy.stop_searching(); - }); - this.proxy.autoconnect({ - force_ip: self.config.proxy_ip || undefined, - progress: function(prog){ - self.chrome.loading_progress(prog); - }, - }).then(function(){ - if(self.config.iface_scan_via_proxy){ - self.barcode_reader.connect_to_proxy(); - } - }).always(function(){ - done.resolve(); - }); - return done; - }, - - // Server side model loaders. This is the list of the models that need to be loaded from - // the server. The models are loaded one by one by this list's order. The 'loaded' callback - // is used to store the data in the appropriate place once it has been loaded. This callback - // can return a deferred that will pause the loading of the next module. - // a shared temporary dictionary is available for loaders to communicate private variables - // used during loading such as object ids, etc. - models: [ - { - model: 'res.users', - fields: ['name','company_id'], - ids: function(self){ return [self.session.uid]; }, - loaded: function(self,users){ self.user = users[0]; }, - },{ - model: 'res.company', - fields: [ 'currency_id', 'email', 'website', 'company_registry', 'vat', 'name', 'phone', 'partner_id' , 'country_id'], - ids: function(self){ return [self.user.company_id[0]] }, - loaded: function(self,companies){ self.company = companies[0]; }, - },{ - model: 'decimal.precision', - fields: ['name','digits'], - loaded: function(self,dps){ - self.dp = {}; - for (var i = 0; i < dps.length; i++) { - self.dp[dps[i].name] = dps[i].digits; - } - }, - },{ - model: 'product.uom', - fields: [], - domain: null, - loaded: function(self,units){ - self.units = units; - var units_by_id = {}; - for(var i = 0, len = units.length; i < len; i++){ - units_by_id[units[i].id] = units[i]; - units[i].groupable = ( units[i].category_id[0] === 1 ); - units[i].is_unit = ( units[i].id === 1 ); - } - self.units_by_id = units_by_id; - } - },{ - model: 'res.partner', - fields: ['name','street','city','state_id','country_id','vat','phone','zip','mobile','email','barcode','write_date'], - domain: [['customer','=',true]], - loaded: function(self,partners){ - self.partners = partners; - self.db.add_partners(partners); - }, - },{ - model: 'res.country', - fields: ['name'], - loaded: function(self,countries){ - self.countries = countries; - self.company.country = null; - for (var i = 0; i < countries.length; i++) { - if (countries[i].id === self.company.country_id[0]){ - self.company.country = countries[i]; - } - } - }, - },{ - model: 'account.tax', - fields: ['name','amount', 'price_include', 'include_base_amount', 'type'], - domain: null, - loaded: function(self,taxes){ - self.taxes = taxes; - self.taxes_by_id = {}; - - for (var i = 0; i < taxes.length; i++) { - self.taxes_by_id[taxes[i].id] = taxes[i]; - } - }, - },{ - model: 'pos.session', - fields: ['id', 'journal_ids','name','user_id','config_id','start_at','stop_at','sequence_number','login_number'], - domain: function(self){ return [['state','=','opened'],['user_id','=',self.session.uid]]; }, - loaded: function(self,pos_sessions){ - self.pos_session = pos_sessions[0]; - }, - },{ - model: 'pos.config', - fields: [], - domain: function(self){ return [['id','=', self.pos_session.config_id[0]]]; }, - loaded: function(self,configs){ - self.config = configs[0]; - self.config.use_proxy = self.config.iface_payment_terminal || - self.config.iface_electronic_scale || - self.config.iface_print_via_proxy || - self.config.iface_scan_via_proxy || - self.config.iface_cashdrawer; - - if (self.config.company_id[0] !== self.user.company_id[0]) { - throw new Error(_t("Error: The Point of Sale User must belong to the same company as the Point of Sale. You are probably trying to load the point of sale as an administrator in a multi-company setup, with the administrator account set to the wrong company.")); - } - - self.db.set_uuid(self.config.uuid); - - var orders = self.db.get_orders(); - for (var i = 0; i < orders.length; i++) { - self.pos_session.sequence_number = Math.max(self.pos_session.sequence_number, orders[i].data.sequence_number+1); - } - }, - },{ - model: 'res.users', - fields: ['name','pos_security_pin','groups_id','barcode'], - domain: function(self){ return [['company_id','=',self.user.company_id[0]],'|', ['groups_id','=', self.config.group_pos_manager_id[0]],['groups_id','=', self.config.group_pos_user_id[0]]]; }, - loaded: function(self,users){ - // we attribute a role to the user, 'cashier' or 'manager', depending - // on the group the user belongs. - var pos_users = []; - for (var i = 0; i < users.length; i++) { - var user = users[i]; - for (var j = 0; j < user.groups_id.length; j++) { - var group_id = user.groups_id[j]; - if (group_id === self.config.group_pos_manager_id[0]) { - user.role = 'manager'; - break; - } else if (group_id === self.config.group_pos_user_id[0]) { - user.role = 'cashier'; - } - } - if (user.role) { - pos_users.push(user); - } - // replace the current user with its updated version - if (user.id === self.user.id) { - self.user = user; - } - } - self.users = pos_users; - }, - },{ - model: 'stock.location', - fields: [], - ids: function(self){ return [self.config.stock_location_id[0]]; }, - loaded: function(self, locations){ self.shop = locations[0]; }, - },{ - model: 'product.pricelist', - fields: ['currency_id'], - ids: function(self){ return [self.config.pricelist_id[0]]; }, - loaded: function(self, pricelists){ self.pricelist = pricelists[0]; }, - },{ - model: 'res.currency', - fields: ['symbol','position','rounding','accuracy'], - ids: function(self){ return [self.pricelist.currency_id[0]]; }, - loaded: function(self, currencies){ - self.currency = currencies[0]; - if (self.currency.rounding > 0) { - self.currency.decimals = Math.ceil(Math.log(1.0 / self.currency.rounding) / Math.log(10)); - } else { - self.currency.decimals = 0; - } - - }, - },{ - model: 'product.packaging', - fields: ['barcode','product_tmpl_id'], - domain: null, - loaded: function(self, packagings){ - self.db.add_packagings(packagings); - }, - },{ - model: 'pos.category', - fields: ['id','name','parent_id','child_id'], - domain: null, - loaded: function(self, categories){ - self.db.add_categories(categories); - }, - },{ - model: 'product.product', - fields: ['display_name', 'list_price','price','pos_categ_id', 'taxes_id', 'barcode', 'default_code', - 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description', - 'product_tmpl_id'], - order: ['sequence','name'], - domain: [['sale_ok','=',true],['available_in_pos','=',true]], - context: function(self){ return { pricelist: self.pricelist.id, display_default_code: false }; }, - loaded: function(self, products){ - self.db.add_products(products); - }, - },{ - model: 'account.bank.statement', - fields: ['account_id','currency','journal_id','state','name','user_id','pos_session_id'], - domain: function(self){ return [['state', '=', 'open'],['pos_session_id', '=', self.pos_session.id]]; }, - loaded: function(self, cashregisters, tmp){ - self.cashregisters = cashregisters; - - tmp.journals = []; - _.each(cashregisters,function(statement){ - tmp.journals.push(statement.journal_id[0]); - }); - }, - },{ - model: 'account.journal', - fields: [], - domain: function(self,tmp){ return [['id','in',tmp.journals]]; }, - loaded: function(self, journals){ - self.journals = journals; - - // associate the bank statements with their journals. - var cashregisters = self.cashregisters; - for(var i = 0, ilen = cashregisters.length; i < ilen; i++){ - for(var j = 0, jlen = journals.length; j < jlen; j++){ - if(cashregisters[i].journal_id[0] === journals[j].id){ - cashregisters[i].journal = journals[j]; - } - } - } - - self.cashregisters_by_id = {}; - for (var i = 0; i < self.cashregisters.length; i++) { - self.cashregisters_by_id[self.cashregisters[i].id] = self.cashregisters[i]; - } - - self.cashregisters = self.cashregisters.sort(function(a,b){ - return a.journal.sequence - b.journal.sequence; - }); - - }, - }, { - label: 'fonts', - loaded: function(self){ - var fonts_loaded = new $.Deferred(); - // Waiting for fonts to be loaded to prevent receipt printing - // from printing empty receipt while loading Inconsolata - // ( The font used for the receipt ) - waitForWebfonts(['Lato','Inconsolata'], function(){ - fonts_loaded.resolve(); - }); - // The JS used to detect font loading is not 100% robust, so - // do not wait more than 5sec - setTimeout(function(){ - fonts_loaded.resolve(); - },5000); - - return fonts_loaded; - }, - },{ - label: 'pictures', - loaded: function(self){ - self.company_logo = new Image(); - var logo_loaded = new $.Deferred(); - self.company_logo.onload = function(){ - var img = self.company_logo; - var ratio = 1; - var targetwidth = 300; - var maxheight = 150; - if( img.width !== targetwidth ){ - ratio = targetwidth / img.width; - } - if( img.height * ratio > maxheight ){ - ratio = maxheight / img.height; - } - var width = Math.floor(img.width * ratio); - var height = Math.floor(img.height * ratio); - var c = document.createElement('canvas'); - c.width = width; - c.height = height - var ctx = c.getContext('2d'); - ctx.drawImage(self.company_logo,0,0, width, height); - - self.company_logo_base64 = c.toDataURL(); - logo_loaded.resolve(); - }; - self.company_logo.onerror = function(){ - logo_loaded.reject(); - }; - self.company_logo.crossOrigin = "anonymous"; - self.company_logo.src = '/web/binary/company_logo' +'?_'+Math.random(); - - return logo_loaded; - }, - }, { - label: 'barcodes', - loaded: function(self) { - var barcode_parser = new instance.barcodes.BarcodeParser({'nomenclature_id': self.config.barcode_nomenclature_id}); - self.barcode_reader.set_barcode_parser(barcode_parser); - return barcode_parser.is_loaded(); - }, - } - ], - - // loads all the needed data on the sever. returns a deferred indicating when all the data has loaded. - load_server_data: function(){ - var self = this; - var loaded = new $.Deferred(); - var progress = 0; - var progress_step = 1.0 / self.models.length; - var tmp = {}; // this is used to share a temporary state between models loaders - - function load_model(index){ - if(index >= self.models.length){ - loaded.resolve(); - }else{ - var model = self.models[index]; - self.chrome.loading_message(_t('Loading')+' '+(model.label || model.model || ''), progress); - - var cond = typeof model.condition === 'function' ? model.condition(self,tmp) : true; - if (!cond) { - load_model(index+1); - return; - } - - var fields = typeof model.fields === 'function' ? model.fields(self,tmp) : model.fields; - var domain = typeof model.domain === 'function' ? model.domain(self,tmp) : model.domain; - var context = typeof model.context === 'function' ? model.context(self,tmp) : model.context; - var ids = typeof model.ids === 'function' ? model.ids(self,tmp) : model.ids; - var order = typeof model.order === 'function' ? model.order(self,tmp): model.order; - progress += progress_step; - - - if( model.model ){ - if (model.ids) { - var records = new instance.web.Model(model.model).call('read',[ids,fields],context); - } else { - var records = new instance.web.Model(model.model).query(fields).filter(domain).order_by(order).context(context).all() - } - records.then(function(result){ - try{ // catching exceptions in model.loaded(...) - $.when(model.loaded(self,result,tmp)) - .then(function(){ load_model(index + 1); }, - function(err){ loaded.reject(err); }); - }catch(err){ - console.error(err.stack); - loaded.reject(err); - } - },function(err){ - loaded.reject(err); - }); - }else if( model.loaded ){ - try{ // catching exceptions in model.loaded(...) - $.when(model.loaded(self,tmp)) - .then( function(){ load_model(index +1); }, - function(err){ loaded.reject(err); }); - }catch(err){ - loaded.reject(err); - } - }else{ - load_model(index + 1); - } - } - } - - try{ - load_model(0); - }catch(err){ - loaded.reject(err); - } - - return loaded; - }, - - // reload the list of partner, returns as a deferred that resolves if there were - // updated partners, and fails if not - load_new_partners: function(){ - var self = this; - var def = new $.Deferred(); - var fields = _.find(this.models,function(model){ return model.model === 'res.partner'; }).fields; - new instance.web.Model('res.partner') - .query(fields) - .filter([['customer','=',true],['write_date','>',this.db.get_partner_write_date()]]) - .all({'timeout':3000, 'shadow': true}) - .then(function(partners){ - if (self.db.add_partners(partners)) { // check if the partners we got were real updates - def.resolve(); - } else { - def.reject(); - } - }, function(err,event){ event.preventDefault(); def.reject(); }); - return def; - }, - - // this is called when an order is removed from the order collection. It ensures that there is always an existing - // order and a valid selected order - on_removed_order: function(removed_order,index,reason){ - var order_list = this.get_order_list(); - if( (reason === 'abandon' || removed_order.temporary) && order_list.length > 0){ - // when we intentionally remove an unfinished order, and there is another existing one - this.set_order(order_list[index] || order_list[order_list.length -1]); - }else{ - // when the order was automatically removed after completion, - // or when we intentionally delete the only concurrent order - this.add_new_order(); - } - }, - - // returns the user who is currently the cashier for this point of sale - get_cashier: function(){ - return this.cashier || this.user; - }, - // changes the current cashier - set_cashier: function(user){ - this.cashier = user; - }, - //creates a new empty order and sets it as the current order - add_new_order: function(){ - var order = new module.Order({},{pos:this}); - this.get('orders').add(order); - this.set('selectedOrder', order); - return order; - }, - // load the locally saved unpaid orders for this session. - load_orders: function(){ - var jsons = this.db.get_unpaid_orders(); - var orders = []; - var not_loaded_count = 0; - - for (var i = 0; i < jsons.length; i++) { - var json = jsons[i]; - if (json.pos_session_id === this.pos_session.id) { - orders.push(new module.Order({},{ - pos: this, - json: json, - })); - } else { - not_loaded_count += 1; - } - } - - if (not_loaded_count) { - console.info('There are '+not_loaded_count+' locally saved unpaid orders belonging to another session'); - } - - orders = orders.sort(function(a,b){ - return a.sequence_number - b.sequence_number; - }); - - if (orders.length) { - this.get('orders').add(orders); - } - }, - - set_start_order: function(){ - var orders = this.get('orders').models; - - if (orders.length && !this.get('selectedOrder')) { - this.set('selectedOrder',orders[0]); - } else { - this.add_new_order(); - } - }, - - // return the current order - get_order: function(){ - return this.get('selectedOrder'); - }, - - // change the current order - set_order: function(order){ - this.set({ selectedOrder: order }); - }, + this.proxy_queue = new devices.JobQueue(); // used to prevent parallels communications to the proxy + this.db = new PosDB(); // a local database used to search trough products and categories & store pending orders + this.debug = core.debug; //debug mode - // return the list of unpaid orders - get_order_list: function(){ - return this.get('orders').models; - }, + // Business data; loaded from the server at launch + this.company_logo = null; + this.company_logo_base64 = ''; + this.currency = null; + this.shop = null; + this.company = null; + this.user = null; + this.users = []; + this.partners = []; + this.cashier = null; + this.cashregisters = []; + this.taxes = []; + this.pos_session = null; + this.config = null; + this.units = []; + this.units_by_id = {}; + this.pricelist = null; + this.order_sequence = 1; + window.posmodel = this; - //removes the current order - delete_current_order: function(){ - var order = this.get_order(); - if (order) { - order.destroy({'reason':'abandon'}); + // these dynamic attributes can be watched for change by other models or widgets + this.set({ + 'synch': { state:'connected', pending:0 }, + 'orders': new OrderCollection(), + 'selectedOrder': null, + }); + + this.bind('change:synch',function(pos,synch){ + clearTimeout(self.synch_timeout); + self.synch_timeout = setTimeout(function(){ + if(synch.state !== 'disconnected' && synch.pending > 0){ + self.set('synch',{state:'disconnected', pending:synch.pending}); + } + },3000); + }); + + this.get('orders').bind('remove', function(order,_unused_,options){ + self.on_removed_order(order,options.index,options.reason); + }); + + // We fetch the backend data on the server asynchronously. this is done only when the pos user interface is launched, + // Any change on this data made on the server is thus not reflected on the point of sale until it is relaunched. + // when all the data has loaded, we compute some stuff, and declare the Pos ready to be used. + this.ready = this.load_server_data().then(function(){ + return self.after_load_server_data(); + }); + }, + after_load_server_data: function(){ + this.barcode_reader.connect(); + this.load_orders(); + this.set_start_order(); + this.push_order(); + if(this.config.use_proxy){ + return this.connect_to_proxy(); + } + }, + // releases ressources holds by the model at the end of life of the posmodel + destroy: function(){ + // FIXME, should wait for flushing, return a deferred to indicate successfull destruction + // this.flush(); + this.proxy.close(); + this.barcode_reader.disconnect(); + this.barcode_reader.disconnect_from_proxy(); + }, + + connect_to_proxy: function(){ + var self = this; + var done = new $.Deferred(); + this.barcode_reader.disconnect_from_proxy(); + this.chrome.loading_message(_t('Connecting to the PosBox'),0); + this.chrome.loading_skip(function(){ + self.proxy.stop_searching(); + }); + this.proxy.autoconnect({ + force_ip: self.config.proxy_ip || undefined, + progress: function(prog){ + self.chrome.loading_progress(prog); + }, + }).then(function(){ + if(self.config.iface_scan_via_proxy){ + self.barcode_reader.connect_to_proxy(); + } + }).always(function(){ + done.resolve(); + }); + return done; + }, + + // Server side model loaders. This is the list of the models that need to be loaded from + // the server. The models are loaded one by one by this list's order. The 'loaded' callback + // is used to store the data in the appropriate place once it has been loaded. This callback + // can return a deferred that will pause the loading of the next module. + // a shared temporary dictionary is available for loaders to communicate private variables + // used during loading such as object ids, etc. + models: [ + { + model: 'res.users', + fields: ['name','company_id'], + ids: function(self){ return [session.uid]; }, + loaded: function(self,users){ self.user = users[0]; }, + },{ + model: 'res.company', + fields: [ 'currency_id', 'email', 'website', 'company_registry', 'vat', 'name', 'phone', 'partner_id' , 'country_id'], + ids: function(self){ return [self.user.company_id[0]]; }, + loaded: function(self,companies){ self.company = companies[0]; }, + },{ + model: 'decimal.precision', + fields: ['name','digits'], + loaded: function(self,dps){ + self.dp = {}; + for (var i = 0; i < dps.length; i++) { + self.dp[dps[i].name] = dps[i].digits; } }, - - // saves the order locally and try to send it to the backend. - // it returns a deferred that succeeds after having tried to send the order and all the other pending orders. - push_order: function(order) { - var self = this; - - if(order){ - this.db.add_order(order.export_as_JSON()); + },{ + model: 'product.uom', + fields: [], + domain: null, + loaded: function(self,units){ + self.units = units; + var units_by_id = {}; + for(var i = 0, len = units.length; i < len; i++){ + units_by_id[units[i].id] = units[i]; + units[i].groupable = ( units[i].category_id[0] === 1 ); + units[i].is_unit = ( units[i].id === 1 ); } + self.units_by_id = units_by_id; + } + },{ + model: 'res.partner', + fields: ['name','street','city','state_id','country_id','vat','phone','zip','mobile','email','barcode','write_date'], + domain: [['customer','=',true]], + loaded: function(self,partners){ + self.partners = partners; + self.db.add_partners(partners); + }, + },{ + model: 'res.country', + fields: ['name'], + loaded: function(self,countries){ + self.countries = countries; + self.company.country = null; + for (var i = 0; i < countries.length; i++) { + if (countries[i].id === self.company.country_id[0]){ + self.company.country = countries[i]; + } + } + }, + },{ + model: 'account.tax', + fields: ['name','amount', 'price_include', 'include_base_amount', 'type'], + domain: null, + loaded: function(self,taxes){ + self.taxes = taxes; + self.taxes_by_id = {}; - var pushed = new $.Deferred(); - - this.flush_mutex.exec(function(){ - var flushed = self._flush_orders(self.db.get_orders()); - - flushed.always(function(ids){ - pushed.resolve(); - }); - }); - return pushed; + for (var i = 0; i < taxes.length; i++) { + self.taxes_by_id[taxes[i].id] = taxes[i]; + } }, + },{ + model: 'pos.session', + fields: ['id', 'journal_ids','name','user_id','config_id','start_at','stop_at','sequence_number','login_number'], + domain: function(self){ return [['state','=','opened'],['user_id','=',session.uid]]; }, + loaded: function(self,pos_sessions){ + self.pos_session = pos_sessions[0]; + }, + },{ + model: 'pos.config', + fields: [], + domain: function(self){ return [['id','=', self.pos_session.config_id[0]]]; }, + loaded: function(self,configs){ + self.config = configs[0]; + self.config.use_proxy = self.config.iface_payment_terminal || + self.config.iface_electronic_scale || + self.config.iface_print_via_proxy || + self.config.iface_scan_via_proxy || + self.config.iface_cashdrawer; - // saves the order locally and try to send it to the backend and make an invoice - // returns a deferred that succeeds when the order has been posted and successfully generated - // an invoice. This method can fail in various ways: - // error-no-client: the order must have an associated partner_id. You can retry to make an invoice once - // this error is solved - // error-transfer: there was a connection error during the transfer. You can retry to make the invoice once - // the network connection is up - - push_and_invoice_order: function(order){ - var self = this; - var invoiced = new $.Deferred(); - - if(!order.get_client()){ - invoiced.reject('error-no-client'); - return invoiced; + if (self.config.company_id[0] !== self.user.company_id[0]) { + throw new Error(_t("Error: The Point of Sale User must belong to the same company as the Point of Sale. You are probably trying to load the point of sale as an administrator in a multi-company setup, with the administrator account set to the wrong company.")); } - var order_id = this.db.add_order(order.export_as_JSON()); + self.db.set_uuid(self.config.uuid); - this.flush_mutex.exec(function(){ - var done = new $.Deferred(); // holds the mutex - - // send the order to the server - // we have a 30 seconds timeout on this push. - // FIXME: if the server takes more than 30 seconds to accept the order, - // the client will believe it wasn't successfully sent, and very bad - // things will happen as a duplicate will be sent next time - // so we must make sure the server detects and ignores duplicated orders - - var transfer = self._flush_orders([self.db.get_order(order_id)], {timeout:30000, to_invoice:true}); - - transfer.fail(function(){ - invoiced.reject('error-transfer'); - done.reject(); - }); - - // on success, get the order id generated by the server - transfer.pipe(function(order_server_id){ - - // generate the pdf and download it - self.chrome.do_action('point_of_sale.pos_invoice_report',{additional_context:{ - active_ids:order_server_id, - }}); - - invoiced.resolve(); - done.resolve(); - }); - - return done; - - }); - - return invoiced; - }, - - // wrapper around the _save_to_server that updates the synch status widget - _flush_orders: function(orders, options) { - var self = this; - this.set('synch',{ state: 'connecting', pending: orders.length}); - - return self._save_to_server(orders, options).done(function (server_ids) { - var pending = self.db.get_orders().length; - - self.set('synch', { - state: pending ? 'connecting' : 'connected', - pending: pending - }); - - return server_ids; - }); - }, - - // send an array of orders to the server - // available options: - // - timeout: timeout for the rpc call in ms - // returns a deferred that resolves with the list of - // server generated ids for the sent orders - _save_to_server: function (orders, options) { - if (!orders || !orders.length) { - var result = $.Deferred(); - result.resolve([]); - return result; + var orders = self.db.get_orders(); + for (var i = 0; i < orders.length; i++) { + self.pos_session.sequence_number = Math.max(self.pos_session.sequence_number, orders[i].data.sequence_number+1); } - - options = options || {}; - - var self = this; - var timeout = typeof options.timeout === 'number' ? options.timeout : 7500 * orders.length; - - // we try to send the order. shadow prevents a spinner if it takes too long. (unless we are sending an invoice, - // then we want to notify the user that we are waiting on something ) - var posOrderModel = new instance.web.Model('pos.order'); - return posOrderModel.call('create_from_ui', - [_.map(orders, function (order) { - order.to_invoice = options.to_invoice || false; - return order; - })], - undefined, - { - shadow: !options.to_invoice, - timeout: timeout - } - ).then(function (server_ids) { - _.each(orders, function (order) { - self.db.remove_order(order.id); - }); - return server_ids; - }).fail(function (error, event){ - if(error.code === 200 ){ // Business Logic Error, not a connection problem - //if warning do not need to display traceback!! - if (error.data.exception_type == 'warning') { - delete error.data.debug; + }, + },{ + model: 'res.users', + fields: ['name','pos_security_pin','groups_id','barcode'], + domain: function(self){ return [['company_id','=',self.user.company_id[0]],'|', ['groups_id','=', self.config.group_pos_manager_id[0]],['groups_id','=', self.config.group_pos_user_id[0]]]; }, + loaded: function(self,users){ + // we attribute a role to the user, 'cashier' or 'manager', depending + // on the group the user belongs. + var pos_users = []; + for (var i = 0; i < users.length; i++) { + var user = users[i]; + for (var j = 0; j < user.groups_id.length; j++) { + var group_id = user.groups_id[j]; + if (group_id === self.config.group_pos_manager_id[0]) { + user.role = 'manager'; + break; + } else if (group_id === self.config.group_pos_user_id[0]) { + user.role = 'cashier'; } - self.gui.show_popup('error-traceback',{ - 'title': error.data.message, - 'body': error.data.debug - }); } - // prevent an error popup creation by the rpc failure - // we want the failure to be silent as we send the orders in the background - event.preventDefault(); - console.error('Failed to send orders:', orders); + if (user.role) { + pos_users.push(user); + } + // replace the current user with its updated version + if (user.id === self.user.id) { + self.user = user; + } + } + self.users = pos_users; + }, + },{ + model: 'stock.location', + fields: [], + ids: function(self){ return [self.config.stock_location_id[0]]; }, + loaded: function(self, locations){ self.shop = locations[0]; }, + },{ + model: 'product.pricelist', + fields: ['currency_id'], + ids: function(self){ return [self.config.pricelist_id[0]]; }, + loaded: function(self, pricelists){ self.pricelist = pricelists[0]; }, + },{ + model: 'res.currency', + fields: ['symbol','position','rounding','accuracy'], + ids: function(self){ return [self.pricelist.currency_id[0]]; }, + loaded: function(self, currencies){ + self.currency = currencies[0]; + if (self.currency.rounding > 0) { + self.currency.decimals = Math.ceil(Math.log(1.0 / self.currency.rounding) / Math.log(10)); + } else { + self.currency.decimals = 0; + } + + }, + },{ + model: 'product.packaging', + fields: ['barcode','product_tmpl_id'], + domain: null, + loaded: function(self, packagings){ + self.db.add_packagings(packagings); + }, + },{ + model: 'pos.category', + fields: ['id','name','parent_id','child_id'], + domain: null, + loaded: function(self, categories){ + self.db.add_categories(categories); + }, + },{ + model: 'product.product', + fields: ['display_name', 'list_price','price','pos_categ_id', 'taxes_id', 'barcode', 'default_code', + 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description', + 'product_tmpl_id'], + order: ['sequence','name'], + domain: [['sale_ok','=',true],['available_in_pos','=',true]], + context: function(self){ return { pricelist: self.pricelist.id, display_default_code: false }; }, + loaded: function(self, products){ + self.db.add_products(products); + }, + },{ + model: 'account.bank.statement', + fields: ['account_id','currency','journal_id','state','name','user_id','pos_session_id'], + domain: function(self){ return [['state', '=', 'open'],['pos_session_id', '=', self.pos_session.id]]; }, + loaded: function(self, cashregisters, tmp){ + self.cashregisters = cashregisters; + + tmp.journals = []; + _.each(cashregisters,function(statement){ + tmp.journals.push(statement.journal_id[0]); }); }, + },{ + model: 'account.journal', + fields: [], + domain: function(self,tmp){ return [['id','in',tmp.journals]]; }, + loaded: function(self, journals){ + var i; + self.journals = journals; - scan_product: function(parsed_code){ - var self = this; - var selectedOrder = this.get_order(); - var product = this.db.get_product_by_barcode(parsed_code.base_code); - - if(!product){ - return false; + // associate the bank statements with their journals. + var cashregisters = self.cashregisters; + var ilen = cashregisters.length; + for(i = 0; i < ilen; i++){ + for(var j = 0, jlen = journals.length; j < jlen; j++){ + if(cashregisters[i].journal_id[0] === journals[j].id){ + cashregisters[i].journal = journals[j]; + } + } } - if(parsed_code.type === 'price'){ - selectedOrder.add_product(product, {price:parsed_code.value}); - }else if(parsed_code.type === 'weight'){ - selectedOrder.add_product(product, {quantity:parsed_code.value, merge:false}); - }else if(parsed_code.type === 'discount'){ - selectedOrder.add_product(product, {discount:parsed_code.value, merge:false}); - }else{ - selectedOrder.add_product(product); + self.cashregisters_by_id = {}; + for (i = 0; i < self.cashregisters.length; i++) { + self.cashregisters_by_id[self.cashregisters[i].id] = self.cashregisters[i]; } - return true; + + self.cashregisters = self.cashregisters.sort(function(a,b){ + return a.journal.sequence - b.journal.sequence; + }); + }, - }); + }, { + label: 'fonts', + loaded: function(){ + var fonts_loaded = new $.Deferred(); + // Waiting for fonts to be loaded to prevent receipt printing + // from printing empty receipt while loading Inconsolata + // ( The font used for the receipt ) + waitForWebfonts(['Lato','Inconsolata'], function(){ + fonts_loaded.resolve(); + }); + // The JS used to detect font loading is not 100% robust, so + // do not wait more than 5sec + setTimeout(function(){ + fonts_loaded.resolve(); + },5000); - // Add fields to the list of read fields when a model is loaded - // by the point of sale. - // e.g: module.load_fields("product.product",['price','category']) + return fonts_loaded; + }, + },{ + label: 'pictures', + loaded: function(self){ + self.company_logo = new Image(); + var logo_loaded = new $.Deferred(); + self.company_logo.onload = function(){ + var img = self.company_logo; + var ratio = 1; + var targetwidth = 300; + var maxheight = 150; + if( img.width !== targetwidth ){ + ratio = targetwidth / img.width; + } + if( img.height * ratio > maxheight ){ + ratio = maxheight / img.height; + } + var width = Math.floor(img.width * ratio); + var height = Math.floor(img.height * ratio); + var c = document.createElement('canvas'); + c.width = width; + c.height = height; + var ctx = c.getContext('2d'); + ctx.drawImage(self.company_logo,0,0, width, height); - module.load_fields = function(model_name, fields) { - if (!(fields instanceof Array)) { - fields = [fields]; - } + self.company_logo_base64 = c.toDataURL(); + logo_loaded.resolve(); + }; + self.company_logo.onerror = function(){ + logo_loaded.reject(); + }; + self.company_logo.crossOrigin = "anonymous"; + self.company_logo.src = '/web/binary/company_logo' +'?_'+Math.random(); - var models = module.PosModel.prototype.models; - for (var i = 0; i < models.length; i++) { - var model = models[i]; - if (model.model === model_name) { - // if 'fields' is empty all fields are loaded, so we do not need - // to modify the array - if ((model.fields instanceof Array) && model.fields.length > 0) { - model.fields = model.fields.concat(fields || []); + return logo_loaded; + }, + }, { + label: 'barcodes', + loaded: function(self) { + var barcode_parser = new BarcodeParser({'nomenclature_id': self.config.barcode_nomenclature_id}); + self.barcode_reader.set_barcode_parser(barcode_parser); + return barcode_parser.is_loaded(); + }, + } + ], + + // loads all the needed data on the sever. returns a deferred indicating when all the data has loaded. + load_server_data: function(){ + var self = this; + var loaded = new $.Deferred(); + var progress = 0; + var progress_step = 1.0 / self.models.length; + var tmp = {}; // this is used to share a temporary state between models loaders + + function load_model(index){ + if(index >= self.models.length){ + loaded.resolve(); + }else{ + var model = self.models[index]; + self.chrome.loading_message(_t('Loading')+' '+(model.label || model.model || ''), progress); + + var cond = typeof model.condition === 'function' ? model.condition(self,tmp) : true; + if (!cond) { + load_model(index+1); + return; + } + + var fields = typeof model.fields === 'function' ? model.fields(self,tmp) : model.fields; + var domain = typeof model.domain === 'function' ? model.domain(self,tmp) : model.domain; + var context = typeof model.context === 'function' ? model.context(self,tmp) : model.context; + var ids = typeof model.ids === 'function' ? model.ids(self,tmp) : model.ids; + var order = typeof model.order === 'function' ? model.order(self,tmp): model.order; + progress += progress_step; + + var records; + if( model.model ){ + if (model.ids) { + records = new Model(model.model).call('read',[ids,fields],context); + } else { + records = new Model(model.model) + .query(fields) + .filter(domain) + .order_by(order) + .context(context) + .all(); + } + records.then(function(result){ + try{ // catching exceptions in model.loaded(...) + $.when(model.loaded(self,result,tmp)) + .then(function(){ load_model(index + 1); }, + function(err){ loaded.reject(err); }); + }catch(err){ + console.error(err.stack); + loaded.reject(err); + } + },function(err){ + loaded.reject(err); + }); + }else if( model.loaded ){ + try{ // catching exceptions in model.loaded(...) + $.when(model.loaded(self,tmp)) + .then( function(){ load_model(index +1); }, + function(err){ loaded.reject(err); }); + }catch(err){ + loaded.reject(err); + } + }else{ + load_model(index + 1); } } } - }; - // Loads openerp models at the point of sale startup. - // load_models take an array of model loader declarations. - // - The models will be loaded in the array order. - // - If no openerp model name is provided, no server data - // will be loaded, but the system can be used to preprocess - // data before load. - // - loader arguments can be functions that return a dynamic - // value. The function takes the PosModel as the first argument - // and a temporary object that is shared by all models, and can - // be used to store transient information between model loads. - // - There is no dependency management. The models must be loaded - // in the right order. Newly added models are loaded at the end - // but the after / before options can be used to load directly - // before / after another model. - // - // models: [{ - // model: [string] the name of the openerp model to load. - // label: [string] The label displayed during load. - // fields: [[string]|function] the list of fields to be loaded. - // Empty Array / Null loads all fields. - // order: [[string]|function] the models will be ordered by - // the provided fields - // domain: [domain|function] the domain that determines what - // models need to be loaded. Null loads everything - // ids: [[id]|function] the id list of the models that must - // be loaded. Overrides domain. - // context: [Dict|function] the openerp context for the model read - // condition: [function] do not load the models if it evaluates to - // false. - // loaded: [function(self,model)] this function is called once the - // models have been loaded, with the data as second argument - // if the function returns a deferred, the next model will - // wait until it resolves before loading. - // }] - // - // options: - // before: [string] The model will be loaded before the named models - // (applies to both model name and label) - // after: [string] The model will be loaded after the (last loaded) - // named model. (applies to both model name and label) - // - module.load_models = function(models,options) { - options = options || {}; - if (!(models instanceof Array)) { - models = [models]; + try{ + load_model(0); + }catch(err){ + loaded.reject(err); } - var pmodels = module.PosModel.prototype.models; - var index = pmodels.length; - if (options.before) { - for (var i = 0; i < pmodels.length; i++) { - if ( pmodels[i].model === options.before || - pmodels[i].label === options.before ){ - index = i; + return loaded; + }, + + // reload the list of partner, returns as a deferred that resolves if there were + // updated partners, and fails if not + load_new_partners: function(){ + var self = this; + var def = new $.Deferred(); + var fields = _.find(this.models,function(model){ return model.model === 'res.partner'; }).fields; + new Model('res.partner') + .query(fields) + .filter([['customer','=',true],['write_date','>',this.db.get_partner_write_date()]]) + .all({'timeout':3000, 'shadow': true}) + .then(function(partners){ + if (self.db.add_partners(partners)) { // check if the partners we got were real updates + def.resolve(); + } else { + def.reject(); + } + }, function(err,event){ event.preventDefault(); def.reject(); }); + return def; + }, + + // this is called when an order is removed from the order collection. It ensures that there is always an existing + // order and a valid selected order + on_removed_order: function(removed_order,index,reason){ + var order_list = this.get_order_list(); + if( (reason === 'abandon' || removed_order.temporary) && order_list.length > 0){ + // when we intentionally remove an unfinished order, and there is another existing one + this.set_order(order_list[index] || order_list[order_list.length -1]); + }else{ + // when the order was automatically removed after completion, + // or when we intentionally delete the only concurrent order + this.add_new_order(); + } + }, + + // returns the user who is currently the cashier for this point of sale + get_cashier: function(){ + return this.cashier || this.user; + }, + // changes the current cashier + set_cashier: function(user){ + this.cashier = user; + }, + //creates a new empty order and sets it as the current order + add_new_order: function(){ + var order = new exports.Order({},{pos:this}); + this.get('orders').add(order); + this.set('selectedOrder', order); + return order; + }, + // load the locally saved unpaid orders for this session. + load_orders: function(){ + var jsons = this.db.get_unpaid_orders(); + var orders = []; + var not_loaded_count = 0; + + for (var i = 0; i < jsons.length; i++) { + var json = jsons[i]; + if (json.pos_session_id === this.pos_session.id) { + orders.push(new exports.Order({},{ + pos: this, + json: json, + })); + } else { + not_loaded_count += 1; + } + } + + if (not_loaded_count) { + console.info('There are '+not_loaded_count+' locally saved unpaid orders belonging to another session'); + } + + orders = orders.sort(function(a,b){ + return a.sequence_number - b.sequence_number; + }); + + if (orders.length) { + this.get('orders').add(orders); + } + }, + + set_start_order: function(){ + var orders = this.get('orders').models; + + if (orders.length && !this.get('selectedOrder')) { + this.set('selectedOrder',orders[0]); + } else { + this.add_new_order(); + } + }, + + // return the current order + get_order: function(){ + return this.get('selectedOrder'); + }, + + // change the current order + set_order: function(order){ + this.set({ selectedOrder: order }); + }, + + // return the list of unpaid orders + get_order_list: function(){ + return this.get('orders').models; + }, + + //removes the current order + delete_current_order: function(){ + var order = this.get_order(); + if (order) { + order.destroy({'reason':'abandon'}); + } + }, + + // saves the order locally and try to send it to the backend. + // it returns a deferred that succeeds after having tried to send the order and all the other pending orders. + push_order: function(order) { + var self = this; + + if(order){ + this.db.add_order(order.export_as_JSON()); + } + + var pushed = new $.Deferred(); + + this.flush_mutex.exec(function(){ + var flushed = self._flush_orders(self.db.get_orders()); + + flushed.always(function(ids){ + pushed.resolve(); + }); + }); + return pushed; + }, + + // saves the order locally and try to send it to the backend and make an invoice + // returns a deferred that succeeds when the order has been posted and successfully generated + // an invoice. This method can fail in various ways: + // error-no-client: the order must have an associated partner_id. You can retry to make an invoice once + // this error is solved + // error-transfer: there was a connection error during the transfer. You can retry to make the invoice once + // the network connection is up + + push_and_invoice_order: function(order){ + var self = this; + var invoiced = new $.Deferred(); + + if(!order.get_client()){ + invoiced.reject('error-no-client'); + return invoiced; + } + + var order_id = this.db.add_order(order.export_as_JSON()); + + this.flush_mutex.exec(function(){ + var done = new $.Deferred(); // holds the mutex + + // send the order to the server + // we have a 30 seconds timeout on this push. + // FIXME: if the server takes more than 30 seconds to accept the order, + // the client will believe it wasn't successfully sent, and very bad + // things will happen as a duplicate will be sent next time + // so we must make sure the server detects and ignores duplicated orders + + var transfer = self._flush_orders([self.db.get_order(order_id)], {timeout:30000, to_invoice:true}); + + transfer.fail(function(){ + invoiced.reject('error-transfer'); + done.reject(); + }); + + // on success, get the order id generated by the server + transfer.pipe(function(order_server_id){ + + // generate the pdf and download it + self.chrome.do_action('point_of_sale.pos_invoice_report',{additional_context:{ + active_ids:order_server_id, + }}); + + invoiced.resolve(); + done.resolve(); + }); + + return done; + + }); + + return invoiced; + }, + + // wrapper around the _save_to_server that updates the synch status widget + _flush_orders: function(orders, options) { + var self = this; + this.set('synch',{ state: 'connecting', pending: orders.length}); + + return self._save_to_server(orders, options).done(function (server_ids) { + var pending = self.db.get_orders().length; + + self.set('synch', { + state: pending ? 'connecting' : 'connected', + pending: pending + }); + + return server_ids; + }); + }, + + // send an array of orders to the server + // available options: + // - timeout: timeout for the rpc call in ms + // returns a deferred that resolves with the list of + // server generated ids for the sent orders + _save_to_server: function (orders, options) { + if (!orders || !orders.length) { + var result = $.Deferred(); + result.resolve([]); + return result; + } + + options = options || {}; + + var self = this; + var timeout = typeof options.timeout === 'number' ? options.timeout : 7500 * orders.length; + + // we try to send the order. shadow prevents a spinner if it takes too long. (unless we are sending an invoice, + // then we want to notify the user that we are waiting on something ) + var posOrderModel = new Model('pos.order'); + return posOrderModel.call('create_from_ui', + [_.map(orders, function (order) { + order.to_invoice = options.to_invoice || false; + return order; + })], + undefined, + { + shadow: !options.to_invoice, + timeout: timeout + } + ).then(function (server_ids) { + _.each(orders, function (order) { + self.db.remove_order(order.id); + }); + return server_ids; + }).fail(function (error, event){ + if(error.code === 200 ){ // Business Logic Error, not a connection problem + //if warning do not need to display traceback!! + if (error.data.exception_type == 'warning') { + delete error.data.debug; + } + self.gui.show_popup('error-traceback',{ + 'title': error.data.message, + 'body': error.data.debug + }); + } + // prevent an error popup creation by the rpc failure + // we want the failure to be silent as we send the orders in the background + event.preventDefault(); + console.error('Failed to send orders:', orders); + }); + }, + + scan_product: function(parsed_code){ + var selectedOrder = this.get_order(); + var product = this.db.get_product_by_barcode(parsed_code.base_code); + + if(!product){ + return false; + } + + if(parsed_code.type === 'price'){ + selectedOrder.add_product(product, {price:parsed_code.value}); + }else if(parsed_code.type === 'weight'){ + selectedOrder.add_product(product, {quantity:parsed_code.value, merge:false}); + }else if(parsed_code.type === 'discount'){ + selectedOrder.add_product(product, {discount:parsed_code.value, merge:false}); + }else{ + selectedOrder.add_product(product); + } + return true; + }, +}); + +// Add fields to the list of read fields when a model is loaded +// by the point of sale. +// e.g: module.load_fields("product.product",['price','category']) + +exports.load_fields = function(model_name, fields) { + if (!(fields instanceof Array)) { + fields = [fields]; + } + + var models = exports.PosModel.prototype.models; + for (var i = 0; i < models.length; i++) { + var model = models[i]; + if (model.model === model_name) { + // if 'fields' is empty all fields are loaded, so we do not need + // to modify the array + if ((model.fields instanceof Array) && model.fields.length > 0) { + model.fields = model.fields.concat(fields || []); + } + } + } +}; + +// Loads openerp models at the point of sale startup. +// load_models take an array of model loader declarations. +// - The models will be loaded in the array order. +// - If no openerp model name is provided, no server data +// will be loaded, but the system can be used to preprocess +// data before load. +// - loader arguments can be functions that return a dynamic +// value. The function takes the PosModel as the first argument +// and a temporary object that is shared by all models, and can +// be used to store transient information between model loads. +// - There is no dependency management. The models must be loaded +// in the right order. Newly added models are loaded at the end +// but the after / before options can be used to load directly +// before / after another model. +// +// models: [{ +// model: [string] the name of the openerp model to load. +// label: [string] The label displayed during load. +// fields: [[string]|function] the list of fields to be loaded. +// Empty Array / Null loads all fields. +// order: [[string]|function] the models will be ordered by +// the provided fields +// domain: [domain|function] the domain that determines what +// models need to be loaded. Null loads everything +// ids: [[id]|function] the id list of the models that must +// be loaded. Overrides domain. +// context: [Dict|function] the openerp context for the model read +// condition: [function] do not load the models if it evaluates to +// false. +// loaded: [function(self,model)] this function is called once the +// models have been loaded, with the data as second argument +// if the function returns a deferred, the next model will +// wait until it resolves before loading. +// }] +// +// options: +// before: [string] The model will be loaded before the named models +// (applies to both model name and label) +// after: [string] The model will be loaded after the (last loaded) +// named model. (applies to both model name and label) +// +exports.load_models = function(models,options) { + options = options || {}; + if (!(models instanceof Array)) { + models = [models]; + } + + var pmodels = exports.PosModel.prototype.models; + var index = pmodels.length; + if (options.before) { + for (var i = 0; i < pmodels.length; i++) { + if ( pmodels[i].model === options.before || + pmodels[i].label === options.before ){ + index = i; + break; + } + } + } else if (options.after) { + for (var i = 0; i < pmodels.length; i++) { + if ( pmodels[i].model === options.after || + pmodels[i].label === options.after ){ + index = i + 1; + } + } + } + pmodels.splice.apply(pmodels,[index,0].concat(models)); +}; + +var orderline_id = 1; + +// An orderline represent one element of the content of a client's shopping cart. +// An orderline contains a product, its quantity, its price, discount. etc. +// An Order contains zero or more Orderlines. +exports.Orderline = Backbone.Model.extend({ + initialize: function(attr,options){ + this.pos = options.pos; + this.order = options.order; + if (options.json) { + this.init_from_JSON(options.json); + return; + } + this.product = options.product; + this.price = options.product.price; + this.quantity = 1; + this.quantityStr = '1'; + this.discount = 0; + this.discountStr = '0'; + this.type = 'unit'; + this.selected = false; + this.id = orderline_id++; + }, + init_from_JSON: function(json) { + this.product = this.pos.db.get_product_by_id(json.product_id); + if (!this.product) { + console.error('ERROR: attempting to recover product not available in the point of sale'); + } + this.price = json.price_unit; + this.set_discount(json.discount); + this.set_quantity(json.qty); + this.id = json.id; + orderline_id = Math.max(this.id+1,orderline_id); + }, + clone: function(){ + var orderline = new exports.Orderline({},{ + pos: this.pos, + order: null, + product: this.product, + price: this.price, + }); + orderline.quantity = this.quantity; + orderline.quantityStr = this.quantityStr; + orderline.discount = this.discount; + orderline.type = this.type; + orderline.selected = false; + return orderline; + }, + // sets a discount [0,100]% + set_discount: function(discount){ + var disc = Math.min(Math.max(parseFloat(discount) || 0, 0),100); + this.discount = disc; + this.discountStr = '' + disc; + this.trigger('change',this); + }, + // returns the discount [0,100]% + get_discount: function(){ + return this.discount; + }, + get_discount_str: function(){ + return this.discountStr; + }, + get_product_type: function(){ + return this.type; + }, + // sets the quantity of the product. The quantity will be rounded according to the + // product's unity of measure properties. Quantities greater than zero will not get + // rounded to zero + set_quantity: function(quantity){ + this.order.assert_editable(); + if(quantity === 'remove'){ + this.order.remove_orderline(this); + return; + }else{ + var quant = parseFloat(quantity) || 0; + var unit = this.get_unit(); + if(unit){ + if (unit.rounding) { + this.quantity = round_pr(quant, unit.rounding); + this.quantityStr = this.quantity.toFixed(Math.ceil(Math.log(1.0 / unit.rounding) / Math.log(10))); + } else { + this.quantity = round_pr(quant, 1); + this.quantityStr = this.quantity.toFixed(0); + } + }else{ + this.quantity = quant; + this.quantityStr = '' + this.quantity; + } + } + this.trigger('change',this); + }, + // return the quantity of product + get_quantity: function(){ + return this.quantity; + }, + get_quantity_str: function(){ + return this.quantityStr; + }, + get_quantity_str_with_unit: function(){ + var unit = this.get_unit(); + if(unit && !unit.is_unit){ + return this.quantityStr + ' ' + unit.name; + }else{ + return this.quantityStr; + } + }, + // return the unit of measure of the product + get_unit: function(){ + var unit_id = this.product.uom_id; + if(!unit_id){ + return undefined; + } + unit_id = unit_id[0]; + if(!this.pos){ + return undefined; + } + return this.pos.units_by_id[unit_id]; + }, + // return the product of this orderline + get_product: function(){ + return this.product; + }, + // selects or deselects this orderline + set_selected: function(selected){ + this.selected = selected; + this.trigger('change',this); + }, + // returns true if this orderline is selected + is_selected: function(){ + return this.selected; + }, + // when we add an new orderline we want to merge it with the last line to see reduce the number of items + // in the orderline. This returns true if it makes sense to merge the two + can_be_merged_with: function(orderline){ + if( this.get_product().id !== orderline.get_product().id){ //only orderline of the same product can be merged + return false; + }else if(!this.get_unit() || !this.get_unit().groupable){ + return false; + }else if(this.get_product_type() !== orderline.get_product_type()){ + return false; + }else if(this.get_discount() > 0){ // we don't merge discounted orderlines + return false; + }else if(this.price !== orderline.price){ + return false; + }else{ + return true; + } + }, + merge: function(orderline){ + this.order.assert_editable(); + this.set_quantity(this.get_quantity() + orderline.get_quantity()); + }, + export_as_JSON: function() { + return { + qty: this.get_quantity(), + price_unit: this.get_unit_price(), + discount: this.get_discount(), + product_id: this.get_product().id, + tax_ids: [[6, false, _.map(this.get_applicable_taxes(), function(tax){ return tax.id; })]], + id: this.id, + }; + }, + //used to create a json of the ticket, to be sent to the printer + export_for_printing: function(){ + return { + quantity: this.get_quantity(), + unit_name: this.get_unit().name, + price: this.get_unit_display_price(), + discount: this.get_discount(), + product_name: this.get_product().display_name, + price_display : this.get_display_price(), + price_with_tax : this.get_price_with_tax(), + price_without_tax: this.get_price_without_tax(), + tax: this.get_tax(), + product_description: this.get_product().description, + product_description_sale: this.get_product().description_sale, + }; + }, + // changes the base price of the product for this orderline + set_unit_price: function(price){ + this.order.assert_editable(); + this.price = round_di(parseFloat(price) || 0, this.pos.dp['Product Price']); + this.trigger('change',this); + }, + get_unit_price: function(){ + return this.price; + }, + get_unit_display_price: function(){ + if (this.pos.config.iface_tax_included) { + var quantity = this.quantity; + this.quantity = 1.0; + var price = this.get_all_prices().priceWithTax; + this.quantity = quantity; + return price; + } else { + return this.get_unit_price(); + } + }, + get_base_price: function(){ + var rounding = this.pos.currency.rounding; + return round_pr(this.get_unit_price() * this.get_quantity() * (1 - this.get_discount()/100), rounding); + }, + get_display_price: function(){ + return this.get_base_price(); + if (this.pos.config.iface_tax_included) { + return this.get_all_prices().priceWithTax; + } else { + return this.get_base_price(); + } + }, + get_price_without_tax: function(){ + return this.get_all_prices().priceWithoutTax; + }, + get_price_with_tax: function(){ + return this.get_all_prices().priceWithTax; + }, + get_tax: function(){ + return this.get_all_prices().tax; + }, + get_applicable_taxes: function(){ + var i; + // Shenaningans because we need + // to keep the taxes ordering. + var ptaxes_ids = this.get_product().taxes_id; + var ptaxes_set = {}; + for (i = 0; i < ptaxes_ids.length; i++) { + ptaxes_set[ptaxes_ids[i]] = true; + } + var taxes = []; + for (i = 0; i < this.pos.taxes.length; i++) { + if (ptaxes_set[this.pos.taxes[i].id]) { + taxes.push(this.pos.taxes[i]); + } + } + return taxes; + }, + get_tax_details: function(){ + return this.get_all_prices().taxDetails; + }, + get_taxes: function(){ + var taxes_ids = this.get_product().taxes_id; + var taxes = []; + for (var i = 0; i < taxes_ids.length; i++) { + taxes.push(this.pos.taxes_by_id[taxes_ids[i]]); + } + return taxes; + }, + get_all_prices: function(){ + var self = this; + var currency_rounding = this.pos.currency.rounding; + var base = this.get_base_price(); + var totalTax = base; + var totalNoTax = base; + + var product = this.get_product(); + var taxes = this.get_applicable_taxes(); + var taxtotal = 0; + var taxdetail = {}; + _.each(taxes, function(tax) { + var tmp; + if (tax.price_include) { + if (tax.type === "percent") { + tmp = base - round_pr(base / (1 + tax.amount),currency_rounding); + } else if (tax.type === "fixed") { + tmp = round_pr(tax.amount * self.get_quantity(),currency_rounding); + } else { + throw "This type of tax is not supported by the point of sale: " + tax.type; + } + tmp = round_pr(tmp,currency_rounding); + taxtotal += tmp; + totalNoTax -= tmp; + taxdetail[tax.id] = tmp; + } else { + if (tax.type === "percent") { + tmp = tax.amount * base; + } else if (tax.type === "fixed") { + tmp = tax.amount * self.get_quantity(); + } else { + throw "This type of tax is not supported by the point of sale: " + tax.type; + } + + tmp = round_pr(tmp,currency_rounding); + + if (tax.include_base_amount) { + base += tmp; + } + + taxtotal += tmp; + totalTax += tmp; + taxdetail[tax.id] = tmp; + } + }); + return { + "priceWithTax": totalTax, + "priceWithoutTax": totalNoTax, + "tax": taxtotal, + "taxDetails": taxdetail, + }; + }, +}); + +var OrderlineCollection = Backbone.Collection.extend({ + model: exports.Orderline, +}); + +// Every Paymentline contains a cashregister and an amount of money. +var Paymentline = Backbone.Model.extend({ + initialize: function(attributes, options) { + this.pos = options.pos; + this.order = options.order; + this.amount = 0; + this.selected = false; + if (options.json) { + this.init_from_JSON(options.json); + return; + } + this.cashregister = options.cashregister; + this.name = this.cashregister.journal_id[1]; + }, + init_from_JSON: function(json){ + this.amount = json.amount; + this.cashregister = this.pos.cashregisters_by_id[json.statement_id]; + this.name = this.cashregister.journal_id[1]; + }, + //sets the amount of money on this payment line + set_amount: function(value){ + this.order.assert_editable(); + this.amount = round_di(parseFloat(value) || 0, this.pos.currency.decimals); + this.trigger('change',this); + }, + // returns the amount of money on this paymentline + get_amount: function(){ + return this.amount; + }, + get_amount_str: function(){ + return this.amount.toFixed(this.pos.currency.decimals); + }, + set_selected: function(selected){ + if(this.selected !== selected){ + this.selected = selected; + this.trigger('change',this); + } + }, + // returns the payment type: 'cash' | 'bank' + get_type: function(){ + return this.cashregister.journal.type; + }, + // returns the associated cashregister + //exports as JSON for server communication + export_as_JSON: function(){ + return { + name: time.datetime_to_str(new Date()), + statement_id: this.cashregister.id, + account_id: this.cashregister.account_id[0], + journal_id: this.cashregister.journal_id[0], + amount: this.get_amount() + }; + }, + //exports as JSON for receipt printing + export_for_printing: function(){ + return { + amount: this.get_amount(), + journal: this.cashregister.journal_id[1], + }; + }, +}); + +var PaymentlineCollection = Backbone.Collection.extend({ + model: Paymentline, +}); + +// An order more or less represents the content of a client's shopping cart (the OrderLines) +// plus the associated payment information (the Paymentlines) +// there is always an active ('selected') order in the Pos, a new one is created +// automaticaly once an order is completed and sent to the server. +exports.Order = Backbone.Model.extend({ + initialize: function(attributes,options){ + Backbone.Model.prototype.initialize.apply(this, arguments); + options = options || {}; + + this.init_locked = true; + this.pos = options.pos; + this.selected_orderline = undefined; + this.selected_paymentline = undefined; + this.screen_data = {}; // see Gui + this.temporary = options.temporary || false; + this.creation_date = new Date(); + this.to_invoice = false; + this.orderlines = new OrderlineCollection(); + this.paymentlines = new PaymentlineCollection(); + this.pos_session_id = this.pos.pos_session.id; + this.finalized = false; // if true, cannot be modified. + + this.set({ client: null }); + + if (options.json) { + this.init_from_JSON(options.json); + } else { + this.sequence_number = this.pos.pos_session.sequence_number++; + this.uid = this.generate_unique_id(); + this.name = _t("Order ") + this.uid; + } + + this.on('change', function(){ this.save_to_db("order:change"); }, this); + this.orderlines.on('change', function(){ this.save_to_db("orderline:change"); }, this); + this.orderlines.on('add', function(){ this.save_to_db("orderline:add"); }, this); + this.orderlines.on('remove', function(){ this.save_to_db("orderline:remove"); }, this); + this.paymentlines.on('change', function(){ this.save_to_db("paymentline:change"); }, this); + this.paymentlines.on('add', function(){ this.save_to_db("paymentline:add"); }, this); + this.paymentlines.on('remove', function(){ this.save_to_db("paymentline:rem"); }, this); + + this.init_locked = false; + this.save_to_db(); + + return this; + }, + save_to_db: function(){ + if (!this.init_locked) { + this.pos.db.save_unpaid_order(this); + } + }, + init_from_JSON: function(json) { + var client; + this.sequence_number = json.sequence_number; + this.pos.pos_session.sequence_number = Math.max(this.sequence_number+1,this.pos.pos_session.sequence_number); + this.session_id = json.pos_session_id; + this.uid = json.uid; + this.name = _t("Order ") + this.uid; + if (json.partner_id) { + client = this.pos.db.get_partner_by_id(json.partner_id); + if (!client) { + console.error('ERROR: trying to load a parner not available in the pos'); + } + } else { + client = null; + } + this.set_client(client); + + this.temporary = false; // FIXME + this.to_invoice = false; // FIXME + + var orderlines = json.lines; + for (var i = 0; i < orderlines.length; i++) { + var orderline = orderlines[i][2]; + this.add_orderline(new exports.Orderline({}, {pos: this.pos, order: this, json: orderline})); + } + + var paymentlines = json.statement_ids; + for (var i = 0; i < paymentlines.length; i++) { + var paymentline = paymentlines[i][2]; + var newpaymentline = new Paymentline({},{pos: this.pos, order: this, json: paymentline}); + this.paymentlines.add(newpaymentline); + + if (i === paymentlines.length - 1) { + this.select_paymentline(newpaymentline); + } + } + }, + export_as_JSON: function() { + var orderLines, paymentLines; + orderLines = []; + this.orderlines.each(_.bind( function(item) { + return orderLines.push([0, 0, item.export_as_JSON()]); + }, this)); + paymentLines = []; + this.paymentlines.each(_.bind( function(item) { + return paymentLines.push([0, 0, item.export_as_JSON()]); + }, this)); + return { + name: this.get_name(), + amount_paid: this.get_total_paid(), + amount_total: this.get_total_with_tax(), + amount_tax: this.get_total_tax(), + amount_return: this.get_change(), + lines: orderLines, + statement_ids: paymentLines, + pos_session_id: this.pos_session_id, + partner_id: this.get_client() ? this.get_client().id : false, + user_id: this.pos.cashier ? this.pos.cashier.id : this.pos.user.id, + uid: this.uid, + sequence_number: this.sequence_number, + }; + }, + export_for_printing: function(){ + var orderlines = []; + var self = this; + + this.orderlines.each(function(orderline){ + orderlines.push(orderline.export_for_printing()); + }); + + var paymentlines = []; + this.paymentlines.each(function(paymentline){ + paymentlines.push(paymentline.export_for_printing()); + }); + var client = this.get('client'); + var cashier = this.pos.cashier || this.pos.user; + var company = this.pos.company; + var shop = this.pos.shop; + var date = new Date(); + + function is_xml(subreceipt){ + return subreceipt ? (subreceipt.split('\n')[0].indexOf('= 0) : false; + } + + function render_xml(subreceipt){ + if (!is_xml(subreceipt)) { + return subreceipt; + } else { + subreceipt = subreceipt.split('\n').slice(1).join('\n'); + var qweb = new QWeb2.Engine(); + qweb.debug = core.debug; + qweb.default_dict = _.clone(QWeb.default_dict); + qweb.add_template(''+subreceipt+''); + + return qweb.render('subreceipt',{'pos':self.pos,'widget':self.pos.chrome,'order':self, 'receipt': receipt}) ; + } + } + + var receipt = { + orderlines: orderlines, + paymentlines: paymentlines, + subtotal: this.get_subtotal(), + total_with_tax: this.get_total_with_tax(), + total_without_tax: this.get_total_without_tax(), + total_tax: this.get_total_tax(), + total_paid: this.get_total_paid(), + total_discount: this.get_total_discount(), + tax_details: this.get_tax_details(), + change: this.get_change(), + name : this.get_name(), + client: client ? client.name : null , + invoice_id: null, //TODO + cashier: cashier ? cashier.name : null, + header: this.pos.config.receipt_header || '', + footer: this.pos.config.receipt_footer || '', + precision: { + price: 2, + money: 2, + quantity: 3, + }, + date: { + year: date.getFullYear(), + month: date.getMonth(), + date: date.getDate(), // day of the month + day: date.getDay(), // day of the week + hour: date.getHours(), + minute: date.getMinutes() , + isostring: date.toISOString(), + localestring: date.toLocaleString(), + }, + company:{ + email: company.email, + website: company.website, + company_registry: company.company_registry, + contact_address: company.partner_id[1], + vat: company.vat, + name: company.name, + phone: company.phone, + logo: this.pos.company_logo_base64, + }, + shop:{ + name: shop.name, + }, + currency: this.pos.currency, + }; + + if (is_xml(this.pos.config.receipt_header)){ + receipt.header_xml = render_xml(this.pos.config.receipt_header); + } + + if (is_xml(this.pos.config.receipt_footer)){ + receipt.footer_xml = render_xml(this.pos.config.receipt_footer); + } + + return receipt; + }, + is_empty: function(){ + return this.orderlines.models.length === 0; + }, + generate_unique_id: function() { + // Generates a public identification number for the order. + // The generated number must be unique and sequential. They are made 12 digit long + // to fit into EAN-13 barcodes, should it be needed + + function zero_pad(num,size){ + var s = ""+num; + while (s.length < size) { + s = "0" + s; + } + return s; + } + return zero_pad(this.pos.pos_session.id,5) +'-'+ + zero_pad(this.pos.pos_session.login_number,3) +'-'+ + zero_pad(this.sequence_number,4); + }, + get_name: function() { + return this.name; + }, + assert_editable: function() { + if (this.finalized) { + throw new Error('Finalized Order cannot be modified'); + } + }, + /* ---- Order Lines --- */ + add_orderline: function(line){ + this.assert_editable(); + if(line.order){ + line.order.remove_orderline(line); + } + line.order = this; + this.orderlines.add(line); + this.select_orderline(this.get_last_orderline()); + }, + get_orderline: function(id){ + var orderlines = this.orderlines.models; + for(var i = 0; i < orderlines.length; i++){ + if(orderlines[i].id === id){ + return orderlines[i]; + } + } + return null; + }, + get_orderlines: function(){ + return this.orderlines.models; + }, + get_last_orderline: function(){ + return this.orderlines.at(this.orderlines.length -1); + }, + get_tip: function() { + var tip_product = this.pos.db.get_product_by_id(this.pos.config.tip_product_id[0]); + var lines = this.get_orderlines(); + if (!tip_product) { + return 0; + } else { + for (var i = 0; i < lines.length; i++) { + if (lines[i].get_product() === tip_product) { + return lines[i].get_unit_price(); + } + } + return 0; + } + }, + set_tip: function(tip) { + var tip_product = this.pos.db.get_product_by_id(this.pos.config.tip_product_id[0]); + var lines = this.get_orderlines(); + if (tip_product) { + for (var i = 0; i < lines.length; i++) { + if (lines[i].get_product() === tip_product) { + lines[i].set_unit_price(tip); + return; + } + } + this.add_product(tip_product, {quantity: 1, price: tip }); + } + }, + remove_orderline: function( line ){ + this.assert_editable(); + this.orderlines.remove(line); + this.select_orderline(this.get_last_orderline()); + }, + add_product: function(product, options){ + this.assert_editable(); + options = options || {}; + var attr = JSON.parse(JSON.stringify(product)); + attr.pos = this.pos; + attr.order = this; + var line = new exports.Orderline({}, {pos: this.pos, order: this, product: product}); + + if(options.quantity !== undefined){ + line.set_quantity(options.quantity); + } + if(options.price !== undefined){ + line.set_unit_price(options.price); + } + if(options.discount !== undefined){ + line.set_discount(options.discount); + } + + if(options.extras !== undefined){ + for (var prop in options.extras) { + line[prop] = options.extras[prop]; + } + } + + var last_orderline = this.get_last_orderline(); + if( last_orderline && last_orderline.can_be_merged_with(line) && options.merge !== false){ + last_orderline.merge(line); + }else{ + this.orderlines.add(line); + } + this.select_orderline(this.get_last_orderline()); + }, + get_selected_orderline: function(){ + return this.selected_orderline; + }, + select_orderline: function(line){ + if(line){ + if(line !== this.selected_orderline){ + if(this.selected_orderline){ + this.selected_orderline.set_selected(false); + } + this.selected_orderline = line; + this.selected_orderline.set_selected(true); + } + }else{ + this.selected_orderline = undefined; + } + }, + deselect_orderline: function(){ + if(this.selected_orderline){ + this.selected_orderline.set_selected(false); + this.selected_orderline = undefined; + } + }, + /* ---- Payment Lines --- */ + add_paymentline: function(cashregister) { + this.assert_editable(); + var newPaymentline = new Paymentline({},{order: this, cashregister:cashregister, pos: this.pos}); + if(cashregister.journal.type !== 'cash' || this.pos.config.iface_precompute_cash){ + newPaymentline.set_amount( Math.max(this.get_due(),0) ); + } + this.paymentlines.add(newPaymentline); + this.select_paymentline(newPaymentline); + + }, + get_paymentlines: function(){ + return this.paymentlines.models; + }, + remove_paymentline: function(line){ + this.assert_editable(); + if(this.selected_paymentline === line){ + this.select_paymentline(undefined); + } + this.paymentlines.remove(line); + }, + clean_empty_paymentlines: function() { + var lines = this.paymentlines.models; + var empty = []; + for ( var i = 0; i < lines.length; i++) { + if (!lines[i].get_amount()) { + empty.push(lines[i]); + } + } + for ( var i = 0; i < empty.length; i++) { + this.remove_paymentline(empty[i]); + } + }, + select_paymentline: function(line){ + if(line !== this.selected_paymentline){ + if(this.selected_paymentline){ + this.selected_paymentline.set_selected(false); + } + this.selected_paymentline = line; + if(this.selected_paymentline){ + this.selected_paymentline.set_selected(true); + } + this.trigger('change:selected_paymentline',this.selected_paymentline); + } + }, + /* ---- Payment Status --- */ + get_subtotal : function(){ + return this.orderlines.reduce((function(sum, orderLine){ + return sum + orderLine.get_display_price(); + }), 0); + }, + get_total_with_tax: function() { + return this.orderlines.reduce((function(sum, orderLine) { + return sum + orderLine.get_price_with_tax(); + }), 0); + }, + get_total_without_tax: function() { + return this.orderlines.reduce((function(sum, orderLine) { + return sum + orderLine.get_price_without_tax(); + }), 0); + }, + get_total_discount: function() { + return this.orderlines.reduce((function(sum, orderLine) { + return sum + (orderLine.get_unit_price() * (orderLine.get_discount()/100) * orderLine.get_quantity()); + }), 0); + }, + get_total_tax: function() { + return this.orderlines.reduce((function(sum, orderLine) { + return sum + orderLine.get_tax(); + }), 0); + }, + get_total_paid: function() { + return this.paymentlines.reduce((function(sum, paymentLine) { + return sum + paymentLine.get_amount(); + }), 0); + }, + get_tax_details: function(){ + var details = {}; + var fulldetails = []; + + this.orderlines.each(function(line){ + var ldetails = line.get_tax_details(); + for(var id in ldetails){ + if(ldetails.hasOwnProperty(id)){ + details[id] = (details[id] || 0) + ldetails[id]; + } + } + }); + + for(var id in details){ + if(details.hasOwnProperty(id)){ + fulldetails.push({amount: details[id], tax: this.pos.taxes_by_id[id], name: this.pos.taxes_by_id[id].name}); + } + } + + return fulldetails; + }, + // Returns a total only for the orderlines with products belonging to the category + get_total_for_category_with_tax: function(categ_id){ + var total = 0; + var self = this; + + if (categ_id instanceof Array) { + for (var i = 0; i < categ_id.length; i++) { + total += this.get_total_for_category_with_tax(categ_id[i]); + } + return total; + } + + this.orderlines.each(function(line){ + if ( self.pos.db.category_contains(categ_id,line.product.id) ) { + total += line.get_price_with_tax(); + } + }); + + return total; + }, + get_total_for_taxes: function(tax_id){ + var total = 0; + + if (!(tax_id instanceof Array)) { + tax_id = [tax_id]; + } + + var tax_set = {}; + + for (var i = 0; i < tax_id.length; i++) { + tax_set[tax_id[i]] = true; + } + + this.orderlines.each(function(line){ + var taxes_ids = line.get_product().taxes_id; + for (var i = 0; i < taxes_ids.length; i++) { + if (tax_set[taxes_ids[i]]) { + total += line.get_price_with_tax(); + return; + } + } + }); + + return total; + }, + get_change: function(paymentline) { + if (!paymentline) { + var change = this.get_total_paid() - this.get_total_with_tax(); + } else { + var change = -this.get_total_with_tax(); + var lines = this.paymentlines.models; + for (var i = 0; i < lines.length; i++) { + change += lines[i].get_amount(); + if (lines[i] === paymentline) { break; } } - } else if (options.after) { - for (var i = 0; i < pmodels.length; i++) { - if ( pmodels[i].model === options.after || - pmodels[i].label === options.after ){ - index = i + 1; + } + return round_pr(Math.max(0,change), this.pos.currency.rounding); + }, + get_due: function(paymentline) { + if (!paymentline) { + var due = this.get_total_with_tax() - this.get_total_paid(); + } else { + var due = this.get_total_with_tax(); + var lines = this.paymentlines.models; + for (var i = 0; i < lines.length; i++) { + if (lines[i] === paymentline) { + break; + } else { + due -= lines[i].get_amount(); } } } - pmodels.splice.apply(pmodels,[index,0].concat(models)); - }; - - var orderline_id = 1; - - // An orderline represent one element of the content of a client's shopping cart. - // An orderline contains a product, its quantity, its price, discount. etc. - // An Order contains zero or more Orderlines. - module.Orderline = Backbone.Model.extend({ - initialize: function(attr,options){ - this.pos = options.pos; - this.order = options.order; - if (options.json) { - this.init_from_JSON(options.json); - return; + return round_pr(Math.max(0,due), this.pos.currency.rounding); + }, + is_paid: function(){ + return this.get_due() === 0; + }, + is_paid_with_cash: function(){ + return !!this.paymentlines.find( function(pl){ + return pl.cashregister.journal.type === 'cash'; + }); + }, + finalize: function(){ + this.destroy(); + }, + destroy: function(){ + Backbone.Model.prototype.destroy.apply(this,arguments); + this.pos.db.remove_unpaid_order(this); + }, + /* ---- Invoice --- */ + set_to_invoice: function(to_invoice) { + this.assert_editable(); + this.to_invoice = to_invoice; + }, + is_to_invoice: function(){ + return this.to_invoice; + }, + /* ---- Client / Customer --- */ + // the client related to the current order. + set_client: function(client){ + this.assert_editable(); + this.set('client',client); + }, + get_client: function(){ + return this.get('client'); + }, + get_client_name: function(){ + var client = this.get('client'); + return client ? client.name : ""; + }, + /* ---- Screen Status --- */ + // the order also stores the screen status, as the PoS supports + // different active screens per order. This method is used to + // store the screen status. + set_screen_data: function(key,value){ + if(arguments.length === 2){ + this.screen_data[key] = value; + }else if(arguments.length === 1){ + for(var key in arguments[0]){ + this.screen_data[key] = arguments[0][key]; } - this.product = options.product; - this.price = options.product.price; - this.quantity = 1; - this.quantityStr = '1'; - this.discount = 0; - this.discountStr = '0'; - this.type = 'unit'; - this.selected = false; - this.id = orderline_id++; - }, - init_from_JSON: function(json) { - this.product = this.pos.db.get_product_by_id(json.product_id); - if (!this.product) { - console.error('ERROR: attempting to recover product not available in the point of sale'); - } - this.price = json.price_unit; - this.set_discount(json.discount); - this.set_quantity(json.qty); - this.id = json.id; - orderline_id = Math.max(this.id+1,orderline_id) - }, - clone: function(){ - var orderline = new module.Orderline({},{ - pos: this.pos, - order: null, - product: this.product, - price: this.price, + } + }, + //see set_screen_data + get_screen_data: function(key){ + return this.screen_data[key]; + }, +}); + +var OrderCollection = Backbone.Collection.extend({ + model: exports.Order, +}); + +/* + The numpad handles both the choice of the property currently being modified + (quantity, price or discount) and the edition of the corresponding numeric value. + */ +exports.NumpadState = Backbone.Model.extend({ + defaults: { + buffer: "0", + mode: "quantity" + }, + appendNewChar: function(newChar) { + var oldBuffer; + oldBuffer = this.get('buffer'); + if (oldBuffer === '0') { + this.set({ + buffer: newChar }); - orderline.quantity = this.quantity; - orderline.quantityStr = this.quantityStr; - orderline.discount = this.discount; - orderline.type = this.type; - orderline.selected = false; - return orderline; - }, - // sets a discount [0,100]% - set_discount: function(discount){ - var disc = Math.min(Math.max(parseFloat(discount) || 0, 0),100); - this.discount = disc; - this.discountStr = '' + disc; - this.trigger('change',this); - }, - // returns the discount [0,100]% - get_discount: function(){ - return this.discount; - }, - get_discount_str: function(){ - return this.discountStr; - }, - get_product_type: function(){ - return this.type; - }, - // sets the quantity of the product. The quantity will be rounded according to the - // product's unity of measure properties. Quantities greater than zero will not get - // rounded to zero - set_quantity: function(quantity){ - this.order.assert_editable(); - if(quantity === 'remove'){ - this.order.remove_orderline(this); - return; + } else if (oldBuffer === '-0') { + this.set({ + buffer: "-" + newChar + }); + } else { + this.set({ + buffer: (this.get('buffer')) + newChar + }); + } + this.trigger('set_value',this.get('buffer')); + }, + deleteLastChar: function() { + if(this.get('buffer') === ""){ + if(this.get('mode') === 'quantity'){ + this.trigger('set_value','remove'); }else{ - var quant = parseFloat(quantity) || 0; - var unit = this.get_unit(); - if(unit){ - if (unit.rounding) { - this.quantity = round_pr(quant, unit.rounding); - this.quantityStr = this.quantity.toFixed(Math.ceil(Math.log(1.0 / unit.rounding) / Math.log(10))); - } else { - this.quantity = round_pr(quant, 1); - this.quantityStr = this.quantity.toFixed(0); - } - }else{ - this.quantity = quant; - this.quantityStr = '' + this.quantity; - } - } - this.trigger('change',this); - }, - // return the quantity of product - get_quantity: function(){ - return this.quantity; - }, - get_quantity_str: function(){ - return this.quantityStr; - }, - get_quantity_str_with_unit: function(){ - var unit = this.get_unit(); - if(unit && !unit.is_unit){ - return this.quantityStr + ' ' + unit.name; - }else{ - return this.quantityStr; - } - }, - // return the unit of measure of the product - get_unit: function(){ - var unit_id = this.product.uom_id; - if(!unit_id){ - return undefined; - } - unit_id = unit_id[0]; - if(!this.pos){ - return undefined; - } - return this.pos.units_by_id[unit_id]; - }, - // return the product of this orderline - get_product: function(){ - return this.product; - }, - // selects or deselects this orderline - set_selected: function(selected){ - this.selected = selected; - this.trigger('change',this); - }, - // returns true if this orderline is selected - is_selected: function(){ - return this.selected; - }, - // when we add an new orderline we want to merge it with the last line to see reduce the number of items - // in the orderline. This returns true if it makes sense to merge the two - can_be_merged_with: function(orderline){ - if( this.get_product().id !== orderline.get_product().id){ //only orderline of the same product can be merged - return false; - }else if(!this.get_unit() || !this.get_unit().groupable){ - return false; - }else if(this.get_product_type() !== orderline.get_product_type()){ - return false; - }else if(this.get_discount() > 0){ // we don't merge discounted orderlines - return false; - }else if(this.price !== orderline.price){ - return false; - }else{ - return true; - } - }, - merge: function(orderline){ - this.order.assert_editable(); - this.set_quantity(this.get_quantity() + orderline.get_quantity()); - }, - export_as_JSON: function() { - return { - qty: this.get_quantity(), - price_unit: this.get_unit_price(), - discount: this.get_discount(), - product_id: this.get_product().id, - tax_ids: [[6, false, _.map(this.get_applicable_taxes(), function(tax){ return tax.id; })]], - id: this.id, - }; - }, - //used to create a json of the ticket, to be sent to the printer - export_for_printing: function(){ - return { - quantity: this.get_quantity(), - unit_name: this.get_unit().name, - price: this.get_unit_display_price(), - discount: this.get_discount(), - product_name: this.get_product().display_name, - price_display : this.get_display_price(), - price_with_tax : this.get_price_with_tax(), - price_without_tax: this.get_price_without_tax(), - tax: this.get_tax(), - product_description: this.get_product().description, - product_description_sale: this.get_product().description_sale, - }; - }, - // changes the base price of the product for this orderline - set_unit_price: function(price){ - this.order.assert_editable(); - this.price = round_di(parseFloat(price) || 0, this.pos.dp['Product Price']); - this.trigger('change',this); - }, - get_unit_price: function(){ - return this.price; - }, - get_unit_display_price: function(){ - if (this.pos.config.iface_tax_included) { - var quantity = this.quantity; - this.quantity = 1.0; - var price = this.get_all_prices().priceWithTax; - this.quantity = quantity; - return price; - } else { - return this.get_unit_price(); - } - }, - get_base_price: function(){ - var rounding = this.pos.currency.rounding; - return round_pr(this.get_unit_price() * this.get_quantity() * (1 - this.get_discount()/100), rounding); - }, - get_display_price: function(){ - return this.get_base_price(); - if (this.pos.config.iface_tax_included) { - return this.get_all_prices().priceWithTax; - } else { - return this.get_base_price(); - } - }, - get_price_without_tax: function(){ - return this.get_all_prices().priceWithoutTax; - }, - get_price_with_tax: function(){ - return this.get_all_prices().priceWithTax; - }, - get_tax: function(){ - return this.get_all_prices().tax; - }, - get_applicable_taxes: function(){ - // Shenaningans because we need - // to keep the taxes ordering. - var ptaxes_ids = this.get_product().taxes_id; - var ptaxes_set = {}; - for (var i = 0; i < ptaxes_ids.length; i++) { - ptaxes_set[ptaxes_ids[i]] = true; - } - var taxes = []; - for (var i = 0; i < this.pos.taxes.length; i++) { - if (ptaxes_set[this.pos.taxes[i].id]) { - taxes.push(this.pos.taxes[i]); - } - } - return taxes; - }, - get_tax_details: function(){ - return this.get_all_prices().taxDetails; - }, - get_taxes: function(){ - var taxes_ids = this.get_product().taxes_id; - var taxes = []; - for (var i = 0; i < taxes_ids.length; i++) { - taxes.push(this.pos.taxes_by_id[taxes_ids[i]]); - } - return taxes; - }, - get_all_prices: function(){ - var self = this; - var currency_rounding = this.pos.currency.rounding; - var base = this.get_base_price(); - var totalTax = base; - var totalNoTax = base; - - var product = this.get_product(); - var taxes = this.get_applicable_taxes(); - var taxtotal = 0; - var taxdetail = {}; - _.each(taxes, function(tax) { - if (tax.price_include) { - var tmp; - if (tax.type === "percent") { - tmp = base - round_pr(base / (1 + tax.amount),currency_rounding); - } else if (tax.type === "fixed") { - tmp = round_pr(tax.amount * self.get_quantity(),currency_rounding); - } else { - throw "This type of tax is not supported by the point of sale: " + tax.type; - } - tmp = round_pr(tmp,currency_rounding); - taxtotal += tmp; - totalNoTax -= tmp; - taxdetail[tax.id] = tmp; - } else { - var tmp; - if (tax.type === "percent") { - tmp = tax.amount * base; - } else if (tax.type === "fixed") { - tmp = tax.amount * self.get_quantity(); - } else { - throw "This type of tax is not supported by the point of sale: " + tax.type; - } - - tmp = round_pr(tmp,currency_rounding); - - if (tax.include_base_amount) { - base += tmp; - } - - taxtotal += tmp; - totalTax += tmp; - taxdetail[tax.id] = tmp; - } - }); - return { - "priceWithTax": totalTax, - "priceWithoutTax": totalNoTax, - "tax": taxtotal, - "taxDetails": taxdetail, - }; - }, - }); - - module.OrderlineCollection = Backbone.Collection.extend({ - model: module.Orderline, - }); - - // Every Paymentline contains a cashregister and an amount of money. - module.Paymentline = Backbone.Model.extend({ - initialize: function(attributes, options) { - this.pos = options.pos; - this.order = options.order; - this.amount = 0; - this.selected = false; - if (options.json) { - this.init_from_JSON(options.json); - return; - } - this.cashregister = options.cashregister; - this.name = this.cashregister.journal_id[1]; - }, - init_from_JSON: function(json){ - this.amount = json.amount; - this.cashregister = this.pos.cashregisters_by_id[json.statement_id]; - this.name = this.cashregister.journal_id[1]; - }, - //sets the amount of money on this payment line - set_amount: function(value){ - this.order.assert_editable(); - this.amount = round_di(parseFloat(value) || 0, this.pos.currency.decimals); - this.trigger('change',this); - }, - // returns the amount of money on this paymentline - get_amount: function(){ - return this.amount; - }, - get_amount_str: function(){ - return this.amount.toFixed(this.pos.currency.decimals); - }, - set_selected: function(selected){ - if(this.selected !== selected){ - this.selected = selected; - this.trigger('change',this); - } - }, - // returns the payment type: 'cash' | 'bank' - get_type: function(){ - return this.cashregister.journal.type - }, - // returns the associated cashregister - //exports as JSON for server communication - export_as_JSON: function(){ - return { - name: instance.web.datetime_to_str(new Date()), - statement_id: this.cashregister.id, - account_id: this.cashregister.account_id[0], - journal_id: this.cashregister.journal_id[0], - amount: this.get_amount() - }; - }, - //exports as JSON for receipt printing - export_for_printing: function(){ - return { - amount: this.get_amount(), - journal: this.cashregister.journal_id[1], - }; - }, - }); - - module.PaymentlineCollection = Backbone.Collection.extend({ - model: module.Paymentline, - }); - - // An order more or less represents the content of a client's shopping cart (the OrderLines) - // plus the associated payment information (the Paymentlines) - // there is always an active ('selected') order in the Pos, a new one is created - // automaticaly once an order is completed and sent to the server. - module.Order = Backbone.Model.extend({ - initialize: function(attributes,options){ - Backbone.Model.prototype.initialize.apply(this, arguments); - options = options || {}; - - this.init_locked = true; - this.pos = options.pos; - this.selected_orderline = undefined; - this.selected_paymentline = undefined; - this.screen_data = {}; // see Gui - this.temporary = options.temporary || false; - this.creation_date = new Date(); - this.to_invoice = false; - this.orderlines = new module.OrderlineCollection(); - this.paymentlines = new module.PaymentlineCollection(); - this.pos_session_id = this.pos.pos_session.id; - this.finalized = false; // if true, cannot be modified. - - this.set({ client: null }); - - if (options.json) { - this.init_from_JSON(options.json); - } else { - this.sequence_number = this.pos.pos_session.sequence_number++; - this.uid = this.generate_unique_id(); - this.name = _t("Order ") + this.uid; - } - - this.on('change', function(){ this.save_to_db("order:change"); }, this); - this.orderlines.on('change', function(){ this.save_to_db("orderline:change"); }, this); - this.orderlines.on('add', function(){ this.save_to_db("orderline:add"); }, this); - this.orderlines.on('remove', function(){ this.save_to_db("orderline:remove"); }, this); - this.paymentlines.on('change', function(){ this.save_to_db("paymentline:change"); }, this); - this.paymentlines.on('add', function(){ this.save_to_db("paymentline:add"); }, this); - this.paymentlines.on('remove', function(){ this.save_to_db("paymentline:rem"); }, this); - - this.init_locked = false; - this.save_to_db(); - - return this; - }, - save_to_db: function(){ - if (!this.init_locked) { - this.pos.db.save_unpaid_order(this); - } - }, - init_from_JSON: function(json) { - this.sequence_number = json.sequence_number; - this.pos.pos_session.sequence_number = Math.max(this.sequence_number+1,this.pos.pos_session.sequence_number); - this.session_id = json.pos_session_id; - this.uid = json.uid; - this.name = _t("Order ") + this.uid; - if (json.partner_id) { - var client = this.pos.db.get_partner_by_id(json.partner_id); - if (!client) { - console.error('ERROR: trying to load a parner not available in the pos'); - } - } else { - var client = null; - } - this.set_client(client); - - this.temporary = false; // FIXME - this.to_invoice = false; // FIXME - - var orderlines = json.lines; - for (var i = 0; i < orderlines.length; i++) { - var orderline = orderlines[i][2]; - this.add_orderline(new module.Orderline({}, {pos: this.pos, order: this, json: orderline})); - } - - var paymentlines = json.statement_ids; - for (var i = 0; i < paymentlines.length; i++) { - var paymentline = paymentlines[i][2]; - var newpaymentline = new module.Paymentline({},{pos: this.pos, order: this, json: paymentline}); - this.paymentlines.add(newpaymentline); - - if (i === paymentlines.length - 1) { - this.select_paymentline(newpaymentline); - } - } - }, - export_as_JSON: function() { - var orderLines, paymentLines; - orderLines = []; - this.orderlines.each(_.bind( function(item) { - return orderLines.push([0, 0, item.export_as_JSON()]); - }, this)); - paymentLines = []; - this.paymentlines.each(_.bind( function(item) { - return paymentLines.push([0, 0, item.export_as_JSON()]); - }, this)); - return { - name: this.get_name(), - amount_paid: this.get_total_paid(), - amount_total: this.get_total_with_tax(), - amount_tax: this.get_total_tax(), - amount_return: this.get_change(), - lines: orderLines, - statement_ids: paymentLines, - pos_session_id: this.pos_session_id, - partner_id: this.get_client() ? this.get_client().id : false, - user_id: this.pos.cashier ? this.pos.cashier.id : this.pos.user.id, - uid: this.uid, - sequence_number: this.sequence_number, - }; - }, - export_for_printing: function(){ - var orderlines = []; - var self = this; - - this.orderlines.each(function(orderline){ - orderlines.push(orderline.export_for_printing()); - }); - - var paymentlines = []; - this.paymentlines.each(function(paymentline){ - paymentlines.push(paymentline.export_for_printing()); - }); - var client = this.get('client'); - var cashier = this.pos.cashier || this.pos.user; - var company = this.pos.company; - var shop = this.pos.shop; - var date = new Date(); - - function is_xml(subreceipt){ - return subreceipt ? (subreceipt.split('\n')[0].indexOf('= 0) : false; - } - - function render_xml(subreceipt){ - if (!is_xml(subreceipt)) { - return subreceipt; - } else { - subreceipt = subreceipt.split('\n').slice(1).join('\n'); - var qweb = new QWeb2.Engine(); - qweb.debug = instance.session.debug; - qweb.default_dict = _.clone(QWeb.default_dict); - qweb.add_template(''+subreceipt+''); - - return qweb.render('subreceipt',{'pos':self.pos,'widget':self.pos.chrome,'order':self, 'receipt': receipt}) ; - } - } - - var receipt = { - orderlines: orderlines, - paymentlines: paymentlines, - subtotal: this.get_subtotal(), - total_with_tax: this.get_total_with_tax(), - total_without_tax: this.get_total_without_tax(), - total_tax: this.get_total_tax(), - total_paid: this.get_total_paid(), - total_discount: this.get_total_discount(), - tax_details: this.get_tax_details(), - change: this.get_change(), - name : this.get_name(), - client: client ? client.name : null , - invoice_id: null, //TODO - cashier: cashier ? cashier.name : null, - header: this.pos.config.receipt_header || '', - footer: this.pos.config.receipt_footer || '', - precision: { - price: 2, - money: 2, - quantity: 3, - }, - date: { - year: date.getFullYear(), - month: date.getMonth(), - date: date.getDate(), // day of the month - day: date.getDay(), // day of the week - hour: date.getHours(), - minute: date.getMinutes() , - isostring: date.toISOString(), - localestring: date.toLocaleString(), - }, - company:{ - email: company.email, - website: company.website, - company_registry: company.company_registry, - contact_address: company.partner_id[1], - vat: company.vat, - name: company.name, - phone: company.phone, - logo: this.pos.company_logo_base64, - }, - shop:{ - name: shop.name, - }, - currency: this.pos.currency, - }; - - if (is_xml(this.pos.config.receipt_header)){ - receipt.header_xml = render_xml(this.pos.config.receipt_header); - } - - if (is_xml(this.pos.config.receipt_footer)){ - receipt.footer_xml = render_xml(this.pos.config.receipt_footer); - } - - return receipt; - }, - is_empty: function(){ - return this.orderlines.models.length === 0; - }, - generate_unique_id: function() { - // Generates a public identification number for the order. - // The generated number must be unique and sequential. They are made 12 digit long - // to fit into EAN-13 barcodes, should it be needed - - function zero_pad(num,size){ - var s = ""+num; - while (s.length < size) { - s = "0" + s; - } - return s; - } - return zero_pad(this.pos.pos_session.id,5) +'-'+ - zero_pad(this.pos.pos_session.login_number,3) +'-'+ - zero_pad(this.sequence_number,4); - }, - get_name: function() { - return this.name; - }, - assert_editable: function() { - if (this.finalized) { - throw new Error('Finalized Order cannot be modified'); - } - }, - /* ---- Order Lines --- */ - add_orderline: function(line){ - this.assert_editable(); - if(line.order){ - line.order.remove_orderline(line); - } - line.order = this; - this.orderlines.add(line); - this.select_orderline(this.get_last_orderline()); - }, - get_orderline: function(id){ - var orderlines = this.orderlines.models; - for(var i = 0; i < orderlines.length; i++){ - if(orderlines[i].id === id){ - return orderlines[i]; - } - } - return null; - }, - get_orderlines: function(){ - return this.orderlines.models; - }, - get_last_orderline: function(){ - return this.orderlines.at(this.orderlines.length -1); - }, - get_tip: function() { - var tip_product = this.pos.db.get_product_by_id(this.pos.config.tip_product_id[0]); - var lines = this.get_orderlines(); - if (!tip_product) { - return 0; - } else { - for (var i = 0; i < lines.length; i++) { - if (lines[i].get_product() === tip_product) { - return lines[i].get_unit_price(); - } - } - return 0; - } - }, - set_tip: function(tip) { - var tip_product = this.pos.db.get_product_by_id(this.pos.config.tip_product_id[0]); - var lines = this.get_orderlines(); - if (tip_product) { - for (var i = 0; i < lines.length; i++) { - if (lines[i].get_product() === tip_product) { - lines[i].set_unit_price(tip); - return; - } - } - this.add_product(tip_product, {quantity: 1, price: tip }); - } - }, - remove_orderline: function( line ){ - this.assert_editable(); - this.orderlines.remove(line); - this.select_orderline(this.get_last_orderline()); - }, - add_product: function(product, options){ - this.assert_editable(); - options = options || {}; - var attr = JSON.parse(JSON.stringify(product)); - attr.pos = this.pos; - attr.order = this; - var line = new module.Orderline({}, {pos: this.pos, order: this, product: product}); - - if(options.quantity !== undefined){ - line.set_quantity(options.quantity); - } - if(options.price !== undefined){ - line.set_unit_price(options.price); - } - if(options.discount !== undefined){ - line.set_discount(options.discount); - } - - if(options.extras !== undefined){ - for (var prop in options.extras) { - line[prop] = options.extras[prop]; - } - } - - var last_orderline = this.get_last_orderline(); - if( last_orderline && last_orderline.can_be_merged_with(line) && options.merge !== false){ - last_orderline.merge(line); - }else{ - this.orderlines.add(line); - } - this.select_orderline(this.get_last_orderline()); - }, - get_selected_orderline: function(){ - return this.selected_orderline; - }, - select_orderline: function(line){ - if(line){ - if(line !== this.selected_orderline){ - if(this.selected_orderline){ - this.selected_orderline.set_selected(false); - } - this.selected_orderline = line; - this.selected_orderline.set_selected(true); - } - }else{ - this.selected_orderline = undefined; - } - }, - deselect_orderline: function(){ - if(this.selected_orderline){ - this.selected_orderline.set_selected(false); - this.selected_orderline = undefined; - } - }, - /* ---- Payment Lines --- */ - add_paymentline: function(cashregister) { - this.assert_editable(); - var newPaymentline = new module.Paymentline({},{order: this, cashregister:cashregister, pos: this.pos}); - if(cashregister.journal.type !== 'cash' || this.pos.config.iface_precompute_cash){ - newPaymentline.set_amount( Math.max(this.get_due(),0) ); - } - this.paymentlines.add(newPaymentline); - this.select_paymentline(newPaymentline); - - }, - get_paymentlines: function(){ - return this.paymentlines.models; - }, - remove_paymentline: function(line){ - this.assert_editable(); - if(this.selected_paymentline === line){ - this.select_paymentline(undefined); - } - this.paymentlines.remove(line); - }, - clean_empty_paymentlines: function() { - var lines = this.paymentlines.models; - var empty = []; - for ( var i = 0; i < lines.length; i++) { - if (!lines[i].get_amount()) { - empty.push(lines[i]); - } - } - for ( var i = 0; i < empty.length; i++) { - this.remove_paymentline(empty[i]); - } - }, - select_paymentline: function(line){ - if(line !== this.selected_paymentline){ - if(this.selected_paymentline){ - this.selected_paymentline.set_selected(false); - } - this.selected_paymentline = line; - if(this.selected_paymentline){ - this.selected_paymentline.set_selected(true); - } - this.trigger('change:selected_paymentline',this.selected_paymentline); - } - }, - /* ---- Payment Status --- */ - get_subtotal : function(){ - return this.orderlines.reduce((function(sum, orderLine){ - return sum + orderLine.get_display_price(); - }), 0); - }, - get_total_with_tax: function() { - return this.orderlines.reduce((function(sum, orderLine) { - return sum + orderLine.get_price_with_tax(); - }), 0); - }, - get_total_without_tax: function() { - return this.orderlines.reduce((function(sum, orderLine) { - return sum + orderLine.get_price_without_tax(); - }), 0); - }, - get_total_discount: function() { - return this.orderlines.reduce((function(sum, orderLine) { - return sum + (orderLine.get_unit_price() * (orderLine.get_discount()/100) * orderLine.get_quantity()); - }), 0); - }, - get_total_tax: function() { - return this.orderlines.reduce((function(sum, orderLine) { - return sum + orderLine.get_tax(); - }), 0); - }, - get_total_paid: function() { - return this.paymentlines.reduce((function(sum, paymentLine) { - return sum + paymentLine.get_amount(); - }), 0); - }, - get_tax_details: function(){ - var details = {}; - var fulldetails = []; - - this.orderlines.each(function(line){ - var ldetails = line.get_tax_details(); - for(var id in ldetails){ - if(ldetails.hasOwnProperty(id)){ - details[id] = (details[id] || 0) + ldetails[id]; - } - } - }); - - for(var id in details){ - if(details.hasOwnProperty(id)){ - fulldetails.push({amount: details[id], tax: this.pos.taxes_by_id[id], name: this.pos.taxes_by_id[id].name}); - } - } - - return fulldetails; - }, - // Returns a total only for the orderlines with products belonging to the category - get_total_for_category_with_tax: function(categ_id){ - var total = 0; - var self = this; - - if (categ_id instanceof Array) { - for (var i = 0; i < categ_id.length; i++) { - total += this.get_total_for_category_with_tax(categ_id[i]); - } - return total; - } - - this.orderlines.each(function(line){ - if ( self.pos.db.category_contains(categ_id,line.product.id) ) { - total += line.get_price_with_tax(); - } - }); - - return total; - }, - get_total_for_taxes: function(tax_id){ - var total = 0; - var self = this; - - if (!(tax_id instanceof Array)) { - tax_id = [tax_id]; - } - - var tax_set = {}; - - for (var i = 0; i < tax_id.length; i++) { - tax_set[tax_id[i]] = true; - } - - this.orderlines.each(function(line){ - var taxes_ids = line.get_product().taxes_id; - for (var i = 0; i < taxes_ids.length; i++) { - if (tax_set[taxes_ids[i]]) { - total += line.get_price_with_tax(); - return; - } - } - }); - - return total; - }, - get_change: function(paymentline) { - if (!paymentline) { - var change = this.get_total_paid() - this.get_total_with_tax(); - } else { - var change = -this.get_total_with_tax(); - var lines = this.paymentlines.models; - for (var i = 0; i < lines.length; i++) { - change += lines[i].get_amount(); - if (lines[i] === paymentline) { - break; - } - } - } - return round_pr(Math.max(0,change), this.pos.currency.rounding); - }, - get_due: function(paymentline) { - if (!paymentline) { - var due = this.get_total_with_tax() - this.get_total_paid(); - } else { - var due = this.get_total_with_tax(); - var lines = this.paymentlines.models; - for (var i = 0; i < lines.length; i++) { - if (lines[i] === paymentline) { - break; - } else { - due -= lines[i].get_amount(); - } - } - } - return round_pr(Math.max(0,due), this.pos.currency.rounding); - }, - is_paid: function(){ - return this.get_due() === 0; - }, - is_paid_with_cash: function(){ - return !!this.paymentlines.find( function(pl){ - return pl.cashregister.journal.type === 'cash'; - }); - }, - finalize: function(){ - this.destroy(); - }, - destroy: function(args){ - Backbone.Model.prototype.destroy.apply(this,arguments); - this.pos.db.remove_unpaid_order(this); - }, - /* ---- Invoice --- */ - set_to_invoice: function(to_invoice) { - this.assert_editable(); - this.to_invoice = to_invoice; - }, - is_to_invoice: function(){ - return this.to_invoice; - }, - /* ---- Client / Customer --- */ - // the client related to the current order. - set_client: function(client){ - this.assert_editable(); - this.set('client',client); - }, - get_client: function(){ - return this.get('client'); - }, - get_client_name: function(){ - var client = this.get('client'); - return client ? client.name : ""; - }, - /* ---- Screen Status --- */ - // the order also stores the screen status, as the PoS supports - // different active screens per order. This method is used to - // store the screen status. - set_screen_data: function(key,value){ - if(arguments.length === 2){ - this.screen_data[key] = value; - }else if(arguments.length === 1){ - for(var key in arguments[0]){ - this.screen_data[key] = arguments[0][key]; - } - } - }, - //see set_screen_data - get_screen_data: function(key){ - return this.screen_data[key]; - }, - }); - - module.OrderCollection = Backbone.Collection.extend({ - model: module.Order, - }); - - /* - The numpad handles both the choice of the property currently being modified - (quantity, price or discount) and the edition of the corresponding numeric value. - */ - module.NumpadState = Backbone.Model.extend({ - defaults: { - buffer: "0", - mode: "quantity" - }, - appendNewChar: function(newChar) { - var oldBuffer; - oldBuffer = this.get('buffer'); - if (oldBuffer === '0') { - this.set({ - buffer: newChar - }); - } else if (oldBuffer === '-0') { - this.set({ - buffer: "-" + newChar - }); - } else { - this.set({ - buffer: (this.get('buffer')) + newChar - }); - } - this.trigger('set_value',this.get('buffer')); - }, - deleteLastChar: function() { - if(this.get('buffer') === ""){ - if(this.get('mode') === 'quantity'){ - this.trigger('set_value','remove'); - }else{ - this.trigger('set_value',this.get('buffer')); - } - }else{ - var newBuffer = this.get('buffer').slice(0,-1) || ""; - this.set({ buffer: newBuffer }); this.trigger('set_value',this.get('buffer')); } - }, - switchSign: function() { - var oldBuffer; - oldBuffer = this.get('buffer'); - this.set({ - buffer: oldBuffer[0] === '-' ? oldBuffer.substr(1) : "-" + oldBuffer - }); + }else{ + var newBuffer = this.get('buffer').slice(0,-1) || ""; + this.set({ buffer: newBuffer }); this.trigger('set_value',this.get('buffer')); - }, - changeMode: function(newMode) { - this.set({ - buffer: "0", - mode: newMode - }); - }, - reset: function() { - this.set({ - buffer: "0", - mode: "quantity" - }); - }, - resetValue: function(){ - this.set({buffer:'0'}); - }, - }); -} + } + }, + switchSign: function() { + var oldBuffer; + oldBuffer = this.get('buffer'); + this.set({ + buffer: oldBuffer[0] === '-' ? oldBuffer.substr(1) : "-" + oldBuffer + }); + this.trigger('set_value',this.get('buffer')); + }, + changeMode: function(newMode) { + this.set({ + buffer: "0", + mode: newMode + }); + }, + reset: function() { + this.set({ + buffer: "0", + mode: "quantity" + }); + }, + resetValue: function(){ + this.set({buffer:'0'}); + }, +}); + +// exports = { +// PosModel: PosModel, +// NumpadState: NumpadState, +// load_fields: load_fields, +// load_models: load_models, +// Orderline: Orderline, +// Order: Order, +// }; +return exports; + +}); \ No newline at end of file diff --git a/addons/point_of_sale/static/src/js/popups.js b/addons/point_of_sale/static/src/js/popups.js index 7063b5aa7d7..c4f4fcc84c9 100644 --- a/addons/point_of_sale/static/src/js/popups.js +++ b/addons/point_of_sale/static/src/js/popups.js @@ -1,241 +1,241 @@ +odoo.define('point_of_sale.popups', ['point_of_sale.BaseWidget', 'point_of_sale.gui'], 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. -openerp.point_of_sale.load_popups = function load_popups(instance, module) { - "use strict"; - - module.PopupWidget = module.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 PosBaseWidget = require('point_of_sale.BaseWidget'); +var gui = require('point_of_sale.gui'); - module.ErrorPopupWidget = module.PopupWidget.extend({ - template:'ErrorPopupWidget', - show: function(options){ - this._super(options); - this.gui.play_sound('error'); - }, - }); - module.Gui.define_popup({name:'error', widget:module.ErrorPopupWidget}); +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', + }, - - module.ErrorTracebackPopupWidget = module.ErrorPopupWidget.extend({ - template:'ErrorTracebackPopupWidget', - }); - module.Gui.define_popup({name:'error-traceback', widget:module.ErrorTracebackPopupWidget}); - - - module.ErrorBarcodePopupWidget = module.ErrorPopupWidget.extend({ - template:'ErrorBarcodePopupWidget', - show: function(barcode){ - this._super({barcode: barcode}); - }, - }); - module.Gui.define_popup({name:'error-barcode', widget:module.ErrorBarcodePopupWidget}); - - - module.ConfirmPopupWidget = module.PopupWidget.extend({ - template: 'ConfirmPopupWidget', - }); - module.Gui.define_popup({name:'confirm', widget:module.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 - * } - * }); - */ - - module.SelectionPopupWidget = module.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; - } + // show the popup ! + show: function(options){ + if(this.$el){ + this.$el.removeClass('oe_hidden'); } - }); - module.Gui.define_popup({name:'selection', widget:module.SelectionPopupWidget}); + + 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}); - module.TextInputPopupWidget = module.PopupWidget.extend({ - template: 'TextInputPopupWidget', - show: function(options){ - options = options || {}; - var self = this; - this._super(options); +var ErrorTracebackPopupWidget = ErrorPopupWidget.extend({ + template:'ErrorTracebackPopupWidget', +}); +gui.define_popup({name:'error-traceback', widget: ErrorTracebackPopupWidget}); - 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); + +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); } - }, - }); - module.Gui.define_popup({name:'textinput', widget:module.TextInputPopupWidget}); + }); + }, + 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}); - module.TextAreaPopupWidget = module.TextInputPopupWidget.extend({ - template: 'TextAreaPopupWidget', - }); - module.Gui.define_popup({name:'textarea', widget:module.TextAreaPopupWidget}); +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}); - module.NumberPopupWidget = module.PopupWidget.extend({ - template: 'NumberPopupWidget', - show: function(options){ - options = options || {}; - var self = this; - this._super(options); +var TextAreaPopupWidget = TextInputPopupWidget.extend({ + template: 'TextAreaPopupWidget', +}); +gui.define_popup({name:'textarea', widget: TextAreaPopupWidget}); - 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(event){ - this.gui.close_popup(); - if( this.options.confirm ){ - this.options.confirm.call(this,this.inputbuffer); - } - }, - }); - module.Gui.define_popup({name:'number', widget:module.NumberPopupWidget}); +var NumberPopupWidget = PopupWidget.extend({ + template: 'NumberPopupWidget', + show: function(options){ + options = options || {}; + this._super(options); - module.PasswordPopupWidget = module.NumberPopupWidget.extend({ - renderElement: function(){ - this._super(); - this.$('.popup').addClass('popup-password'); - }, - }); - module.Gui.define_popup({name:'password', widget:module.PasswordPopupWidget}); + 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}); - module.UnsentOrdersPopupWidget = module.ConfirmPopupWidget.extend({ - template: 'UnsentOrdersPopupWidget', - }); - module.Gui.define_popup({name:'unsent-orders', widget:module.UnsentOrdersPopupWidget}); + 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}); - module.UnpaidOrdersPopupWidget = module.ConfirmPopupWidget.extend({ - template: 'UnpaidOrdersPopupWidget', - }); - module.Gui.define_popup({name:'unpaid-orders', widget:module.UnpaidOrdersPopupWidget}); -}; +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}); + +}); diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 2de500004d7..dfc8366e39c 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -1,4 +1,5 @@ - +odoo.define('point_of_sale.screens', ['point_of_sale.BaseWidget', 'point_of_sale.gui', 'point_of_sale.models', 'web.core', 'web.Model', 'web.utils'], function (require) { +"use strict"; // This file contains the Screens definitions. Screens are the // content of the right pane of the pos, containing the main functionalities. // @@ -26,1784 +27,1790 @@ // each screen display. A screen can be called with parameters, which are // to be used for the duration of the screen only. -openerp.point_of_sale.load_screens = function load_screens(instance, module){ //module is instance.point_of_sale - "use strict"; +var PosBaseWidget = require('point_of_sale.BaseWidget'); +var gui = require('point_of_sale.gui'); +var models = require('point_of_sale.models'); +var core = require('web.core'); +var Model = require('web.Model'); +var utils = require('web.utils'); - var QWeb = instance.web.qweb; - var _t = instance.web._t; +var QWeb = core.qweb; +var _t = core._t; - var round_pr = instance.web.round_precision +var round_pr = utils.round_precision; - /*--------------------------------------*\ - | THE SCREEN WIDGET | - \*======================================*/ - - // The screen widget is the base class inherited - // by all screens. +/*--------------------------------------*\ + | THE SCREEN WIDGET | +\*======================================*/ - module.ScreenWidget = module.PosBaseWidget.extend({ +// The screen widget is the base class inherited +// by all screens. +var ScreenWidget = PosBaseWidget.extend({ - init: function(parent,options){ - this._super(parent,options); - this.hidden = false; - }, + init: function(parent,options){ + this._super(parent,options); + this.hidden = false; + }, - barcode_product_screen: 'products', //if defined, this screen will be loaded when a product is scanned + barcode_product_screen: 'products', //if defined, this screen will be loaded when a product is scanned - // what happens when a product is scanned : - // it will add the product to the order and go to barcode_product_screen. - barcode_product_action: function(code){ - var self = this; - if(self.pos.scan_product(code)){ - if(self.barcode_product_screen){ - self.gui.show_screen(self.barcode_product_screen); - } - }else{ - self.gui.show_popup('error-barcode',code.code); + // what happens when a product is scanned : + // it will add the product to the order and go to barcode_product_screen. + barcode_product_action: function(code){ + var self = this; + if(self.pos.scan_product(code)){ + if(self.barcode_product_screen){ + self.gui.show_screen(self.barcode_product_screen); } - }, + }else{ + self.gui.show_popup('error-barcode',code.code); + } + }, - // what happens when a cashier id barcode is scanned. - // the default behavior is the following : - // - if there's a user with a matching barcode, put it as the active 'cashier', go to cashier mode, and return true - // - else : do nothing and return false. You probably want to extend this to show and appropriate error popup... - barcode_cashier_action: function(code){ - var users = this.pos.users; - for(var i = 0, len = users.length; i < len; i++){ - if(users[i].barcode === code.code){ - this.pos.set_cashier(users[i]); - this.chrome.widget.username.renderElement(); - return true; - } - } - this.gui.show_popup('error-barcode',code.code); - return false; - }, - - // what happens when a client id barcode is scanned. - // the default behavior is the following : - // - if there's a user with a matching barcode, put it as the active 'client' and return true - // - else : return false. - barcode_client_action: function(code){ - var partner = this.pos.db.get_partner_by_barcode(code.code); - if(partner){ - this.pos.get_order().set_client(partner); + // what happens when a cashier id barcode is scanned. + // the default behavior is the following : + // - if there's a user with a matching barcode, put it as the active 'cashier', go to cashier mode, and return true + // - else : do nothing and return false. You probably want to extend this to show and appropriate error popup... + barcode_cashier_action: function(code){ + var users = this.pos.users; + for(var i = 0, len = users.length; i < len; i++){ + if(users[i].barcode === code.code){ + this.pos.set_cashier(users[i]); + this.chrome.widget.username.renderElement(); return true; } - this.gui.show_popup('error-barcode',code.code); - return false; - }, - - // what happens when a discount barcode is scanned : the default behavior - // is to set the discount on the last order. - barcode_discount_action: function(code){ - var last_orderline = this.pos.get_order().get_last_orderline(); - if(last_orderline){ - last_orderline.set_discount(code.value) - } - }, - // What happens when an invalid barcode is scanned : shows an error popup. - barcode_error_action: function(code){ - this.gui.show_popup('error-barcode',code.code); - }, + } + this.gui.show_popup('error-barcode',code.code); + return false; + }, + + // what happens when a client id barcode is scanned. + // the default behavior is the following : + // - if there's a user with a matching barcode, put it as the active 'client' and return true + // - else : return false. + barcode_client_action: function(code){ + var partner = this.pos.db.get_partner_by_barcode(code.code); + if(partner){ + this.pos.get_order().set_client(partner); + return true; + } + this.gui.show_popup('error-barcode',code.code); + return false; + }, + + // what happens when a discount barcode is scanned : the default behavior + // is to set the discount on the last order. + barcode_discount_action: function(code){ + var last_orderline = this.pos.get_order().get_last_orderline(); + if(last_orderline){ + last_orderline.set_discount(code.value); + } + }, + // What happens when an invalid barcode is scanned : shows an error popup. + barcode_error_action: function(code){ + this.gui.show_popup('error-barcode',code.code); + }, - // this method shows the screen and sets up all the widget related to this screen. Extend this method - // if you want to alter the behavior of the screen. - show: function(){ - var self = this; + // this method shows the screen and sets up all the widget related to this screen. Extend this method + // if you want to alter the behavior of the screen. + show: function(){ + var self = this; - this.hidden = false; - if(this.$el){ - this.$el.removeClass('oe_hidden'); - } + this.hidden = false; + if(this.$el){ + this.$el.removeClass('oe_hidden'); + } - this.pos.barcode_reader.set_action_callback({ - 'cashier': self.barcode_cashier_action ? function(code){ self.barcode_cashier_action(code); } : undefined , - 'product': self.barcode_product_action ? function(code){ self.barcode_product_action(code); } : undefined , - 'client' : self.barcode_client_action ? function(code){ self.barcode_client_action(code); } : undefined , - 'discount': self.barcode_discount_action ? function(code){ self.barcode_discount_action(code); } : undefined, - 'error' : self.barcode_error_action ? function(code){ self.barcode_error_action(code); } : undefined, - }); - }, + this.pos.barcode_reader.set_action_callback({ + 'cashier': self.barcode_cashier_action ? function(code){ self.barcode_cashier_action(code); } : undefined , + 'product': self.barcode_product_action ? function(code){ self.barcode_product_action(code); } : undefined , + 'client' : self.barcode_client_action ? function(code){ self.barcode_client_action(code); } : undefined , + 'discount': self.barcode_discount_action ? function(code){ self.barcode_discount_action(code); } : undefined, + 'error' : self.barcode_error_action ? function(code){ self.barcode_error_action(code); } : undefined, + }); + }, - // this method is called when the screen is closed to make place for a new screen. this is a good place - // to put your cleanup stuff as it is guaranteed that for each show() there is one and only one close() - close: function(){ - if(this.pos.barcode_reader){ - this.pos.barcode_reader.reset_action_callbacks(); - } - }, + // this method is called when the screen is closed to make place for a new screen. this is a good place + // to put your cleanup stuff as it is guaranteed that for each show() there is one and only one close() + close: function(){ + if(this.pos.barcode_reader){ + this.pos.barcode_reader.reset_action_callbacks(); + } + }, - // this methods hides the screen. It's not a good place to put your cleanup stuff as it is called on the - // POS initialization. - hide: function(){ - this.hidden = true; + // this methods hides the screen. It's not a good place to put your cleanup stuff as it is called on the + // POS initialization. + hide: function(){ + this.hidden = true; + if(this.$el){ + this.$el.addClass('oe_hidden'); + } + }, + + // we need this because some screens re-render themselves when they are hidden + // (due to some events, or magic, or both...) we must make sure they remain hidden. + // the good solution would probably be to make them not re-render themselves when they + // are hidden. + renderElement: function(){ + this._super(); + if(this.hidden){ if(this.$el){ this.$el.addClass('oe_hidden'); } - }, + } + }, +}); - // we need this because some screens re-render themselves when they are hidden - // (due to some events, or magic, or both...) we must make sure they remain hidden. - // the good solution would probably be to make them not re-render themselves when they - // are hidden. - renderElement: function(){ - this._super(); - if(this.hidden){ - if(this.$el){ - this.$el.addClass('oe_hidden'); +/*--------------------------------------*\ + | THE DOM CACHE | +\*======================================*/ + +// The Dom Cache is used by various screens to improve +// their performances when displaying many time the +// same piece of DOM. +// +// It is a simple map from string 'keys' to DOM Nodes. +// +// The cache empties itself based on usage frequency +// stats, so you may not always get back what +// you put in. + +var DomCache = core.Class.extend({ + init: function(options){ + options = options || {}; + this.max_size = options.max_size || 2000; + + this.cache = {}; + this.access_time = {}; + this.size = 0; + }, + cache_node: function(key,node){ + var cached = this.cache[key]; + this.cache[key] = node; + this.access_time[key] = new Date().getTime(); + if(!cached){ + this.size++; + while(this.size >= this.max_size){ + var oldest_key = null; + var oldest_time = new Date().getTime(); + for(key in this.cache){ + var time = this.access_time[key]; + if(time <= oldest_time){ + oldest_time = time; + oldest_key = key; + } } + if(oldest_key){ + delete this.cache[oldest_key]; + delete this.access_time[oldest_key]; + } + this.size--; } - }, - }); - - /*--------------------------------------*\ - | THE DOM CACHE | - \*======================================*/ - - // The Dom Cache is used by various screens to improve - // their performances when displaying many time the - // same piece of DOM. - // - // It is a simple map from string 'keys' to DOM Nodes. - // - // The cache empties itself based on usage frequency - // stats, so you may not always get back what - // you put in. - - module.DomCache = instance.web.Class.extend({ - init: function(options){ - options = options || {}; - this.max_size = options.max_size || 2000; - - this.cache = {}; - this.access_time = {}; - this.size = 0; - }, - cache_node: function(key,node){ - var cached = this.cache[key]; - this.cache[key] = node; + } + return node; + }, + get_node: function(key){ + var cached = this.cache[key]; + if(cached){ this.access_time[key] = new Date().getTime(); - if(!cached){ - this.size++; - while(this.size >= this.max_size){ - var oldest_key = null; - var oldest_time = new Date().getTime(); - for(var key in this.cache){ - var time = this.access_time[key]; - if(time <= oldest_time){ - oldest_time = time; - oldest_key = key; - } - } - if(oldest_key){ - delete this.cache[oldest_key]; - delete this.access_time[oldest_key]; - } - this.size--; - } - } - return node; - }, - get_node: function(key){ - var cached = this.cache[key]; - if(cached){ - this.access_time[key] = new Date().getTime(); - } - return cached; - }, - }); + } + return cached; + }, +}); - /*--------------------------------------*\ - | THE SCALE SCREEN | - \*======================================*/ +/*--------------------------------------*\ + | THE SCALE SCREEN | +\*======================================*/ - // The scale screen displays the weight of - // a product on the electronic scale. +// The scale screen displays the weight of +// a product on the electronic scale. - module.ScaleScreenWidget = module.ScreenWidget.extend({ - template:'ScaleScreenWidget', +var ScaleScreenWidget = ScreenWidget.extend({ + template:'ScaleScreenWidget', - next_screen: 'products', - previous_screen: 'products', + next_screen: 'products', + previous_screen: 'products', - show: function(){ - this._super(); - var self = this; - var queue = this.pos.proxy_queue; + show: function(){ + this._super(); + var self = this; + var queue = this.pos.proxy_queue; - this.set_weight(0); - this.renderElement(); + this.set_weight(0); + this.renderElement(); - this.hotkey_handler = function(event){ - if(event.which === 13){ - self.order_product(); - self.gui.show_screen(self.next_screen); - }else if(event.which === 27){ - self.gui.show_screen(self.previous_screen); - } - }; - - $('body').on('keyup',this.hotkey_handler); - - this.$('.back').click(function(){ - self.gui.show_screen(self.previous_screen); - }); - - this.$('.next,.buy-product').click(function(){ + this.hotkey_handler = function(event){ + if(event.which === 13){ self.order_product(); self.gui.show_screen(self.next_screen); - }); - - queue.schedule(function(){ - return self.pos.proxy.scale_read().then(function(weight){ - self.set_weight(weight.weight); - }); - },{duration:50, repeat: true}); - - }, - get_product: function(){ - return this.gui.get_current_screen_param('product'); - }, - order_product: function(){ - this.pos.get_order().add_product(this.get_product(),{ quantity: this.weight }); - }, - get_product_name: function(){ - var product = this.get_product(); - return (product ? product.display_name : undefined) || 'Unnamed Product'; - }, - get_product_price: function(){ - var product = this.get_product(); - return (product ? product.price : 0) || 0; - }, - set_weight: function(weight){ - this.weight = weight; - this.$('.weight').text(this.get_product_weight_string()); - this.$('.computed-price').text(this.get_computed_price_string()); - }, - get_product_weight_string: function(){ - var product = this.get_product(); - var defaultstr = (this.weight || 0).toFixed(3) + ' Kg'; - if(!product || !this.pos){ - return defaultstr; + }else if(event.which === 27){ + self.gui.show_screen(self.previous_screen); } - var unit_id = product.uom_id; - if(!unit_id){ - return defaultstr; - } - var unit = this.pos.units_by_id[unit_id[0]]; - var weight = round_pr(this.weight || 0, unit.rounding); - var weightstr = weight.toFixed(Math.ceil(Math.log(1.0/unit.rounding) / Math.log(10) )); - weightstr += ' Kg'; - return weightstr; - }, - get_computed_price_string: function(){ - return this.format_currency(this.get_product_price() * this.weight); - }, - close: function(){ - var self = this; - this._super(); - $('body').off('keyup',this.hotkey_handler); + }; - this.pos.proxy_queue.clear(); - }, - }); - module.Gui.define_screen({name: 'scale', widget: module.ScaleScreenWidget}); + $('body').on('keyup',this.hotkey_handler); - /*--------------------------------------*\ - | THE PRODUCT SCREEN | - \*======================================*/ + this.$('.back').click(function(){ + self.gui.show_screen(self.previous_screen); + }); - // The product screen contains the list of products, - // The category selector and the order display. - // It is the default screen for orders and the - // startup screen for shops. - // - // There product screens uses many sub-widgets, - // the code follows. + this.$('.next,.buy-product').click(function(){ + self.order_product(); + self.gui.show_screen(self.next_screen); + }); - - /* ------------ The Numpad ------------ */ - - // The numpad that edits the order lines. - - module.NumpadWidget = module.PosBaseWidget.extend({ - template:'NumpadWidget', - init: function(parent, options) { - this._super(parent); - this.state = new module.NumpadState(); - var self = this; - }, - start: function() { - this.state.bind('change:mode', this.changedMode, this); - this.changedMode(); - this.$el.find('.numpad-backspace').click(_.bind(this.clickDeleteLastChar, this)); - this.$el.find('.numpad-minus').click(_.bind(this.clickSwitchSign, this)); - this.$el.find('.number-char').click(_.bind(this.clickAppendNewChar, this)); - this.$el.find('.mode-button').click(_.bind(this.clickChangeMode, this)); - }, - clickDeleteLastChar: function() { - return this.state.deleteLastChar(); - }, - clickSwitchSign: function() { - return this.state.switchSign(); - }, - clickAppendNewChar: function(event) { - var newChar; - newChar = event.currentTarget.innerText || event.currentTarget.textContent; - return this.state.appendNewChar(newChar); - }, - clickChangeMode: function(event) { - var newMode = event.currentTarget.attributes['data-mode'].nodeValue; - return this.state.changeMode(newMode); - }, - changedMode: function() { - var mode = this.state.get('mode'); - $('.selected-mode').removeClass('selected-mode'); - $(_.str.sprintf('.mode-button[data-mode="%s"]', mode), this.$el).addClass('selected-mode'); - }, - }); - - /* ---------- The Action Pad ---------- */ - - // The action pad contains the payment button and the - // customer selection button - - module.ActionpadWidget = module.PosBaseWidget.extend({ - template: 'ActionpadWidget', - renderElement: function() { - var self = this; - this._super(); - this.$('.pay').click(function(){ - self.gui.show_screen('payment'); - }); - this.$('.set-customer').click(function(){ - self.gui.show_screen('clientlist'); + queue.schedule(function(){ + return self.pos.proxy.scale_read().then(function(weight){ + self.set_weight(weight.weight); }); + },{duration:50, repeat: true}); + + }, + get_product: function(){ + return this.gui.get_current_screen_param('product'); + }, + order_product: function(){ + this.pos.get_order().add_product(this.get_product(),{ quantity: this.weight }); + }, + get_product_name: function(){ + var product = this.get_product(); + return (product ? product.display_name : undefined) || 'Unnamed Product'; + }, + get_product_price: function(){ + var product = this.get_product(); + return (product ? product.price : 0) || 0; + }, + set_weight: function(weight){ + this.weight = weight; + this.$('.weight').text(this.get_product_weight_string()); + this.$('.computed-price').text(this.get_computed_price_string()); + }, + get_product_weight_string: function(){ + var product = this.get_product(); + var defaultstr = (this.weight || 0).toFixed(3) + ' Kg'; + if(!product || !this.pos){ + return defaultstr; } - }); + var unit_id = product.uom_id; + if(!unit_id){ + return defaultstr; + } + var unit = this.pos.units_by_id[unit_id[0]]; + var weight = round_pr(this.weight || 0, unit.rounding); + var weightstr = weight.toFixed(Math.ceil(Math.log(1.0/unit.rounding) / Math.log(10) )); + weightstr += ' Kg'; + return weightstr; + }, + get_computed_price_string: function(){ + return this.format_currency(this.get_product_price() * this.weight); + }, + close: function(){ + this._super(); + $('body').off('keyup',this.hotkey_handler); - /* --------- The Order Widget --------- */ - - // Displays the current Order. + this.pos.proxy_queue.clear(); + }, +}); +gui.define_screen({name: 'scale', widget: ScaleScreenWidget}); - module.OrderWidget = module.PosBaseWidget.extend({ - template:'OrderWidget', - init: function(parent, options) { - var self = this; - this._super(parent,options); +/*--------------------------------------*\ + | THE PRODUCT SCREEN | +\*======================================*/ - this.numpad_state = options.numpad_state; - this.numpad_state.reset(); - this.numpad_state.bind('set_value', this.set_value, this); +// The product screen contains the list of products, +// The category selector and the order display. +// It is the default screen for orders and the +// startup screen for shops. +// +// There product screens uses many sub-widgets, +// the code follows. - this.pos.bind('change:selectedOrder', this.change_selected_order, this); - this.line_click_handler = function(event){ - self.click_line(this.orderline, event); - }; +/* ------------ The Numpad ------------ */ - if (this.pos.get_order()) { - this.bind_order_events(); +// The numpad that edits the order lines. + +var NumpadWidget = PosBaseWidget.extend({ + template:'NumpadWidget', + init: function(parent) { + this._super(parent); + this.state = new models.NumpadState(); + }, + start: function() { + this.state.bind('change:mode', this.changedMode, this); + this.changedMode(); + this.$el.find('.numpad-backspace').click(_.bind(this.clickDeleteLastChar, this)); + this.$el.find('.numpad-minus').click(_.bind(this.clickSwitchSign, this)); + this.$el.find('.number-char').click(_.bind(this.clickAppendNewChar, this)); + this.$el.find('.mode-button').click(_.bind(this.clickChangeMode, this)); + }, + clickDeleteLastChar: function() { + return this.state.deleteLastChar(); + }, + clickSwitchSign: function() { + return this.state.switchSign(); + }, + clickAppendNewChar: function(event) { + var newChar; + newChar = event.currentTarget.innerText || event.currentTarget.textContent; + return this.state.appendNewChar(newChar); + }, + clickChangeMode: function(event) { + var newMode = event.currentTarget.attributes['data-mode'].nodeValue; + return this.state.changeMode(newMode); + }, + changedMode: function() { + var mode = this.state.get('mode'); + $('.selected-mode').removeClass('selected-mode'); + $(_.str.sprintf('.mode-button[data-mode="%s"]', mode), this.$el).addClass('selected-mode'); + }, +}); + +/* ---------- The Action Pad ---------- */ + +// The action pad contains the payment button and the +// customer selection button + +var ActionpadWidget = PosBaseWidget.extend({ + template: 'ActionpadWidget', + renderElement: function() { + var self = this; + this._super(); + this.$('.pay').click(function(){ + self.gui.show_screen('payment'); + }); + this.$('.set-customer').click(function(){ + self.gui.show_screen('clientlist'); + }); + } +}); + +/* --------- The Order Widget --------- */ + +// Displays the current Order. + +var OrderWidget = PosBaseWidget.extend({ + template:'OrderWidget', + init: function(parent, options) { + var self = this; + this._super(parent,options); + + this.numpad_state = options.numpad_state; + this.numpad_state.reset(); + this.numpad_state.bind('set_value', this.set_value, this); + + this.pos.bind('change:selectedOrder', this.change_selected_order, this); + + this.line_click_handler = function(event){ + self.click_line(this.orderline, event); + }; + + if (this.pos.get_order()) { + this.bind_order_events(); + } + + }, + click_line: function(orderline, event) { + this.pos.get_order().select_orderline(orderline); + this.numpad_state.reset(); + }, + set_value: function(val) { + var order = this.pos.get_order(); + if (order.get_selected_orderline()) { + var mode = this.numpad_state.get('mode'); + if( mode === 'quantity'){ + order.get_selected_orderline().set_quantity(val); + }else if( mode === 'discount'){ + order.get_selected_orderline().set_discount(val); + }else if( mode === 'price'){ + order.get_selected_orderline().set_unit_price(val); } - - }, - click_line: function(orderline, event) { - this.pos.get_order().select_orderline(orderline); + } + }, + change_selected_order: function() { + if (this.pos.get_order()) { + this.bind_order_events(); this.numpad_state.reset(); - }, - set_value: function(val) { - var order = this.pos.get_order(); - if (order.get_selected_orderline()) { - var mode = this.numpad_state.get('mode'); - if( mode === 'quantity'){ - order.get_selected_orderline().set_quantity(val); - }else if( mode === 'discount'){ - order.get_selected_orderline().set_discount(val); - }else if( mode === 'price'){ - order.get_selected_orderline().set_unit_price(val); - } - } - }, - change_selected_order: function() { - if (this.pos.get_order()) { - this.bind_order_events(); - this.numpad_state.reset(); - this.renderElement(); - } - }, - orderline_add: function(){ - this.numpad_state.reset(); - this.renderElement('and_scroll_to_bottom'); - }, - orderline_remove: function(line){ - this.remove_orderline(line); - this.numpad_state.reset(); - this.update_summary(); - }, - orderline_change: function(line){ - this.rerender_orderline(line); - this.update_summary(); - }, - bind_order_events: function() { - var order = this.pos.get_order(); - order.unbind('change:client', this.update_summary, this); - order.bind('change:client', this.update_summary, this); - order.unbind('change', this.update_summary, this); - order.bind('change', this.update_summary, this); - - var lines = order.orderlines; - lines.unbind('add', this.orderline_add, this); - lines.bind('add', this.orderline_add, this); - lines.unbind('remove', this.orderline_remove, this); - lines.bind('remove', this.orderline_remove, this); - lines.unbind('change', this.orderline_change, this); - lines.bind('change', this.orderline_change, this); - - }, - render_orderline: function(orderline){ - var el_str = openerp.qweb.render('Orderline',{widget:this, line:orderline}); - var el_node = document.createElement('div'); - el_node.innerHTML = _.str.trim(el_str); - el_node = el_node.childNodes[0]; - el_node.orderline = orderline; - el_node.addEventListener('click',this.line_click_handler); - - orderline.node = el_node; - return el_node; - }, - remove_orderline: function(order_line){ - if(this.pos.get_order().get_orderlines().length === 0){ - this.renderElement(); - }else{ - order_line.node.parentNode.removeChild(order_line.node); - } - }, - rerender_orderline: function(order_line){ - var node = order_line.node; - var replacement_line = this.render_orderline(order_line); - node.parentNode.replaceChild(replacement_line,node); - }, - // overriding the openerp framework replace method for performance reasons - replace: function($target){ this.renderElement(); - var target = $target[0]; - target.parentNode.replaceChild(this.el,target); - }, - renderElement: function(scrollbottom){ - var order = this.pos.get_order(); - if (!order) { - return; - } - var orderlines = order.get_orderlines(); + } + }, + orderline_add: function(){ + this.numpad_state.reset(); + this.renderElement('and_scroll_to_bottom'); + }, + orderline_remove: function(line){ + this.remove_orderline(line); + this.numpad_state.reset(); + this.update_summary(); + }, + orderline_change: function(line){ + this.rerender_orderline(line); + this.update_summary(); + }, + bind_order_events: function() { + var order = this.pos.get_order(); + order.unbind('change:client', this.update_summary, this); + order.bind('change:client', this.update_summary, this); + order.unbind('change', this.update_summary, this); + order.bind('change', this.update_summary, this); - var el_str = openerp.qweb.render('OrderWidget',{widget:this, order:order, orderlines:orderlines}); + var lines = order.orderlines; + lines.unbind('add', this.orderline_add, this); + lines.bind('add', this.orderline_add, this); + lines.unbind('remove', this.orderline_remove, this); + lines.bind('remove', this.orderline_remove, this); + lines.unbind('change', this.orderline_change, this); + lines.bind('change', this.orderline_change, this); - var el_node = document.createElement('div'); - el_node.innerHTML = _.str.trim(el_str); - el_node = el_node.childNodes[0]; + }, + render_orderline: function(orderline){ + var el_str = QWeb.render('Orderline',{widget:this, line:orderline}); + var el_node = document.createElement('div'); + el_node.innerHTML = _.str.trim(el_str); + el_node = el_node.childNodes[0]; + el_node.orderline = orderline; + el_node.addEventListener('click',this.line_click_handler); - - var list_container = el_node.querySelector('.orderlines'); - for(var i = 0, len = orderlines.length; i < len; i++){ - var orderline = this.render_orderline(orderlines[i]); - list_container.appendChild(orderline); - } - - if(this.el && this.el.parentNode){ - this.el.parentNode.replaceChild(el_node,this.el); - } - this.el = el_node; - this.update_summary(); - - if(scrollbottom){ - this.el.querySelector('.order-scroller').scrollTop = 100 * orderlines.length; - } - }, - update_summary: function(){ - var order = this.pos.get_order(); - if (!order.get_orderlines().length) { - return; - } - - var total = order ? order.get_total_with_tax() : 0; - var taxes = order ? total - order.get_total_without_tax() : 0; - - this.el.querySelector('.summary .total > .value').textContent = this.format_currency(total); - this.el.querySelector('.summary .total .subentry .value').textContent = this.format_currency(taxes); - }, - }); - - /* ------ The Product Categories ------ */ - - // Display and navigate the product categories. - // Also handles searches. - // - set_category() to change the displayed category - // - reset_category() to go to the root category - // - perform_search() to search for products - // - clear_search() does what it says. - - module.ProductCategoriesWidget = module.PosBaseWidget.extend({ - template: 'ProductCategoriesWidget', - init: function(parent, options){ - var self = this; - this._super(parent,options); - this.product_type = options.product_type || 'all'; // 'all' | 'weightable' - this.onlyWeightable = options.onlyWeightable || false; - this.category = this.pos.root_category; - this.breadcrumb = []; - this.subcategories = []; - this.product_list_widget = options.product_list_widget || null; - this.category_cache = new module.DomCache(); - this.start_categ_id = this.pos.config.iface_start_categ_id ? this.pos.config.iface_start_categ_id[0] : 0; - this.set_category(this.pos.db.get_category_by_id(this.start_categ_id)); - - this.switch_category_handler = function(event){ - self.set_category(self.pos.db.get_category_by_id(Number(this.dataset['categoryId']))); - self.renderElement(); - }; - - this.clear_search_handler = function(event){ - self.clear_search(); - }; - - var search_timeout = null; - this.search_handler = function(event){ - clearTimeout(search_timeout); - - var query = this.value; - - search_timeout = setTimeout(function(){ - self.perform_search(self.category, query, event.which === 13); - },70); - }; - }, - - // changes the category. if undefined, sets to root category - set_category : function(category){ - var db = this.pos.db; - if(!category){ - this.category = db.get_category_by_id(db.root_category_id); - }else{ - this.category = category; - } - this.breadcrumb = []; - var ancestors_ids = db.get_category_ancestors_ids(this.category.id); - for(var i = 1; i < ancestors_ids.length; i++){ - this.breadcrumb.push(db.get_category_by_id(ancestors_ids[i])); - } - if(this.category.id !== db.root_category_id){ - this.breadcrumb.push(this.category); - } - this.subcategories = db.get_category_by_id(db.get_category_childs_ids(this.category.id)); - }, - - get_image_url: function(category){ - return window.location.origin + '/web/binary/image?model=pos.category&field=image_medium&id='+category.id; - }, - - render_category: function( category, with_image ){ - var cached = this.category_cache.get_node(category.id); - if(!cached){ - if(with_image){ - var image_url = this.get_image_url(category); - var category_html = QWeb.render('CategoryButton',{ - widget: this, - category: category, - image_url: this.get_image_url(category), - }); - category_html = _.str.trim(category_html); - var category_node = document.createElement('div'); - category_node.innerHTML = category_html; - category_node = category_node.childNodes[0]; - }else{ - var category_html = QWeb.render('CategorySimpleButton',{ - widget: this, - category: category, - }); - category_html = _.str.trim(category_html); - var category_node = document.createElement('div'); - category_node.innerHTML = category_html; - category_node = category_node.childNodes[0]; - } - this.category_cache.cache_node(category.id,category_node); - return category_node; - } - return cached; - }, - - replace: function($target){ + orderline.node = el_node; + return el_node; + }, + remove_orderline: function(order_line){ + if(this.pos.get_order().get_orderlines().length === 0){ this.renderElement(); - var target = $target[0]; - target.parentNode.replaceChild(this.el,target); - }, + }else{ + order_line.node.parentNode.removeChild(order_line.node); + } + }, + rerender_orderline: function(order_line){ + var node = order_line.node; + var replacement_line = this.render_orderline(order_line); + node.parentNode.replaceChild(replacement_line,node); + }, + // overriding the openerp framework replace method for performance reasons + replace: function($target){ + this.renderElement(); + var target = $target[0]; + target.parentNode.replaceChild(this.el,target); + }, + renderElement: function(scrollbottom){ + var order = this.pos.get_order(); + if (!order) { + return; + } + var orderlines = order.get_orderlines(); - renderElement: function(){ - var self = this; + var el_str = QWeb.render('OrderWidget',{widget:this, order:order, orderlines:orderlines}); - var el_str = openerp.qweb.render(this.template, {widget: this}); - var el_node = document.createElement('div'); - el_node.innerHTML = el_str; - el_node = el_node.childNodes[1]; + var el_node = document.createElement('div'); + el_node.innerHTML = _.str.trim(el_str); + el_node = el_node.childNodes[0]; - if(this.el && this.el.parentNode){ - this.el.parentNode.replaceChild(el_node,this.el); - } - this.el = el_node; + var list_container = el_node.querySelector('.orderlines'); + for(var i = 0, len = orderlines.length; i < len; i++){ + var orderline = this.render_orderline(orderlines[i]); + list_container.appendChild(orderline); + } - var hasimages = false; //if none of the subcategories have images, we don't display buttons with icons - /* - for(var i = 0; i < this.subcategories.length; i++){ - if(this.subcategories[i].image){ - hasimages = true; - break; - } - } - */ + if(this.el && this.el.parentNode){ + this.el.parentNode.replaceChild(el_node,this.el); + } + this.el = el_node; + this.update_summary(); - var list_container = el_node.querySelector('.category-list'); - if (list_container) { - if (!hasimages) { - list_container.classList.add('simple'); - } else { - list_container.classList.remove('simple'); - } - for(var i = 0, len = this.subcategories.length; i < len; i++){ - list_container.appendChild(this.render_category(this.subcategories[i],hasimages)); - }; - } + if(scrollbottom){ + this.el.querySelector('.order-scroller').scrollTop = 100 * orderlines.length; + } + }, + update_summary: function(){ + var order = this.pos.get_order(); + if (!order.get_orderlines().length) { + return; + } - var buttons = el_node.querySelectorAll('.js-category-switch'); - for(var i = 0; i < buttons.length; i++){ - buttons[i].addEventListener('click',this.switch_category_handler); - } + var total = order ? order.get_total_with_tax() : 0; + var taxes = order ? total - order.get_total_without_tax() : 0; - var products = this.pos.db.get_product_by_category(this.category.id); - this.product_list_widget.set_product_list(products); // FIXME: this should be moved elsewhere ... + this.el.querySelector('.summary .total > .value').textContent = this.format_currency(total); + this.el.querySelector('.summary .total .subentry .value').textContent = this.format_currency(taxes); + }, +}); - this.el.querySelector('.searchbox input').addEventListener('keyup',this.search_handler); +/* ------ The Product Categories ------ */ - this.el.querySelector('.search-clear').addEventListener('click',this.clear_search_handler); +// Display and navigate the product categories. +// Also handles searches. +// - set_category() to change the displayed category +// - reset_category() to go to the root category +// - perform_search() to search for products +// - clear_search() does what it says. - if(this.pos.config.iface_vkeyboard && this.chrome.widget.keyboard){ - this.chrome.widget.keyboard.connect($(this.el.querySelector('.searchbox input'))); - } - }, +var ProductCategoriesWidget = PosBaseWidget.extend({ + template: 'ProductCategoriesWidget', + init: function(parent, options){ + var self = this; + this._super(parent,options); + this.product_type = options.product_type || 'all'; // 'all' | 'weightable' + this.onlyWeightable = options.onlyWeightable || false; + this.category = this.pos.root_category; + this.breadcrumb = []; + this.subcategories = []; + this.product_list_widget = options.product_list_widget || null; + this.category_cache = new DomCache(); + this.start_categ_id = this.pos.config.iface_start_categ_id ? this.pos.config.iface_start_categ_id[0] : 0; + this.set_category(this.pos.db.get_category_by_id(this.start_categ_id)); - // resets the current category to the root category - reset_category: function(){ - this.set_category(this.pos.db.get_category_by_id(this.start_categ_id)); - this.renderElement(); - }, + this.switch_category_handler = function(event){ + self.set_category(self.pos.db.get_category_by_id(Number(this.dataset.categoryId))); + self.renderElement(); + }; + + this.clear_search_handler = function(event){ + self.clear_search(); + }; - // empties the content of the search box - clear_search: function(){ - var products = this.pos.db.get_product_by_category(this.category.id); - this.product_list_widget.set_product_list(products); - var input = this.el.querySelector('.searchbox input'); - input.value = ''; - input.focus(); - }, - perform_search: function(category, query, buy_result){ - if(query){ - var products = this.pos.db.search_product_in_category(category.id,query) - if(buy_result && products.length === 1){ - this.pos.get_order().add_product(products[0]); - this.clear_search(); - }else{ - this.product_list_widget.set_product_list(products); - } + var search_timeout = null; + this.search_handler = function(event){ + clearTimeout(search_timeout); + + var query = this.value; + + search_timeout = setTimeout(function(){ + self.perform_search(self.category, query, event.which === 13); + },70); + }; + }, + + // changes the category. if undefined, sets to root category + set_category : function(category){ + var db = this.pos.db; + if(!category){ + this.category = db.get_category_by_id(db.root_category_id); + }else{ + this.category = category; + } + this.breadcrumb = []; + var ancestors_ids = db.get_category_ancestors_ids(this.category.id); + for(var i = 1; i < ancestors_ids.length; i++){ + this.breadcrumb.push(db.get_category_by_id(ancestors_ids[i])); + } + if(this.category.id !== db.root_category_id){ + this.breadcrumb.push(this.category); + } + this.subcategories = db.get_category_by_id(db.get_category_childs_ids(this.category.id)); + }, + + get_image_url: function(category){ + return window.location.origin + '/web/binary/image?model=pos.category&field=image_medium&id='+category.id; + }, + + render_category: function( category, with_image ){ + var cached = this.category_cache.get_node(category.id); + if(!cached){ + if(with_image){ + var image_url = this.get_image_url(category); + var category_html = QWeb.render('CategoryButton',{ + widget: this, + category: category, + image_url: this.get_image_url(category), + }); + category_html = _.str.trim(category_html); + var category_node = document.createElement('div'); + category_node.innerHTML = category_html; + category_node = category_node.childNodes[0]; + }else{ + var category_html = QWeb.render('CategorySimpleButton',{ + widget: this, + category: category, + }); + category_html = _.str.trim(category_html); + var category_node = document.createElement('div'); + category_node.innerHTML = category_html; + category_node = category_node.childNodes[0]; + } + this.category_cache.cache_node(category.id,category_node); + return category_node; + } + return cached; + }, + + replace: function($target){ + this.renderElement(); + var target = $target[0]; + target.parentNode.replaceChild(this.el,target); + }, + + renderElement: function(){ + + var el_str = QWeb.render(this.template, {widget: this}); + var el_node = document.createElement('div'); + + el_node.innerHTML = el_str; + el_node = el_node.childNodes[1]; + + if(this.el && this.el.parentNode){ + this.el.parentNode.replaceChild(el_node,this.el); + } + + this.el = el_node; + + var hasimages = false; //if none of the subcategories have images, we don't display buttons with icons + /* + for(var i = 0; i < this.subcategories.length; i++){ + if(this.subcategories[i].image){ + hasimages = true; + break; + } + } + */ + + var list_container = el_node.querySelector('.category-list'); + if (list_container) { + if (!hasimages) { + list_container.classList.add('simple'); + } else { + list_container.classList.remove('simple'); + } + for(var i = 0, len = this.subcategories.length; i < len; i++){ + list_container.appendChild(this.render_category(this.subcategories[i],hasimages)); + } + } + + var buttons = el_node.querySelectorAll('.js-category-switch'); + for(var i = 0; i < buttons.length; i++){ + buttons[i].addEventListener('click',this.switch_category_handler); + } + + var products = this.pos.db.get_product_by_category(this.category.id); + this.product_list_widget.set_product_list(products); // FIXME: this should be moved elsewhere ... + + this.el.querySelector('.searchbox input').addEventListener('keyup',this.search_handler); + + this.el.querySelector('.search-clear').addEventListener('click',this.clear_search_handler); + + if(this.pos.config.iface_vkeyboard && this.chrome.widget.keyboard){ + this.chrome.widget.keyboard.connect($(this.el.querySelector('.searchbox input'))); + } + }, + + // resets the current category to the root category + reset_category: function(){ + this.set_category(this.pos.db.get_category_by_id(this.start_categ_id)); + this.renderElement(); + }, + + // empties the content of the search box + clear_search: function(){ + var products = this.pos.db.get_product_by_category(this.category.id); + this.product_list_widget.set_product_list(products); + var input = this.el.querySelector('.searchbox input'); + input.value = ''; + input.focus(); + }, + perform_search: function(category, query, buy_result){ + var products; + if(query){ + products = this.pos.db.search_product_in_category(category.id,query); + if(buy_result && products.length === 1){ + this.pos.get_order().add_product(products[0]); + this.clear_search(); }else{ - var products = this.pos.db.get_product_by_category(this.category.id); this.product_list_widget.set_product_list(products); } - }, + }else{ + products = this.pos.db.get_product_by_category(this.category.id); + this.product_list_widget.set_product_list(products); + } + }, - }); +}); - /* --------- The Product List --------- */ - - // Display the list of products. - // - change the list with .set_product_list() - // - click_product_action(), passed as an option, tells - // what to do when a product is clicked. +/* --------- The Product List --------- */ - module.ProductListWidget = module.PosBaseWidget.extend({ - template:'ProductListWidget', - init: function(parent, options) { - var self = this; - this._super(parent,options); - this.model = options.model; - this.productwidgets = []; - this.weight = options.weight || 0; - this.show_scale = options.show_scale || false; - this.next_screen = options.next_screen || false; +// Display the list of products. +// - change the list with .set_product_list() +// - click_product_action(), passed as an option, tells +// what to do when a product is clicked. - this.click_product_handler = function(event){ - var product = self.pos.db.get_product_by_id(this.dataset['productId']); - options.click_product_action(product); - }; +var ProductListWidget = PosBaseWidget.extend({ + template:'ProductListWidget', + init: function(parent, options) { + var self = this; + this._super(parent,options); + this.model = options.model; + this.productwidgets = []; + this.weight = options.weight || 0; + this.show_scale = options.show_scale || false; + this.next_screen = options.next_screen || false; - this.product_list = options.product_list || []; - this.product_cache = new module.DomCache(); - }, - set_product_list: function(product_list){ - this.product_list = product_list; - this.renderElement(); - }, - get_product_image_url: function(product){ - return window.location.origin + '/web/binary/image?model=product.product&field=image_medium&id='+product.id; - }, - replace: function($target){ - this.renderElement(); - var target = $target[0]; - target.parentNode.replaceChild(this.el,target); - }, + this.click_product_handler = function(){ + var product = self.pos.db.get_product_by_id(this.dataset.productId); + options.click_product_action(product); + }; - render_product: function(product){ - var cached = this.product_cache.get_node(product.id); - if(!cached){ - var image_url = this.get_product_image_url(product); - var product_html = QWeb.render('Product',{ - widget: this, - product: product, - image_url: this.get_product_image_url(product), - }); - var product_node = document.createElement('div'); - product_node.innerHTML = product_html; - product_node = product_node.childNodes[1]; - this.product_cache.cache_node(product.id,product_node); - return product_node; - } - return cached; - }, + this.product_list = options.product_list || []; + this.product_cache = new DomCache(); + }, + set_product_list: function(product_list){ + this.product_list = product_list; + this.renderElement(); + }, + get_product_image_url: function(product){ + return window.location.origin + '/web/binary/image?model=product.product&field=image_medium&id='+product.id; + }, + replace: function($target){ + this.renderElement(); + var target = $target[0]; + target.parentNode.replaceChild(this.el,target); + }, - renderElement: function() { - var self = this; + render_product: function(product){ + var cached = this.product_cache.get_node(product.id); + if(!cached){ + var image_url = this.get_product_image_url(product); + var product_html = QWeb.render('Product',{ + widget: this, + product: product, + image_url: this.get_product_image_url(product), + }); + var product_node = document.createElement('div'); + product_node.innerHTML = product_html; + product_node = product_node.childNodes[1]; + this.product_cache.cache_node(product.id,product_node); + return product_node; + } + return cached; + }, - // this._super() - var el_str = openerp.qweb.render(this.template, {widget: this}); - var el_node = document.createElement('div'); - el_node.innerHTML = el_str; - el_node = el_node.childNodes[1]; + renderElement: function() { + var el_str = QWeb.render(this.template, {widget: this}); + var el_node = document.createElement('div'); + el_node.innerHTML = el_str; + el_node = el_node.childNodes[1]; - if(this.el && this.el.parentNode){ - this.el.parentNode.replaceChild(el_node,this.el); - } - this.el = el_node; + if(this.el && this.el.parentNode){ + this.el.parentNode.replaceChild(el_node,this.el); + } + this.el = el_node; - var list_container = el_node.querySelector('.product-list'); - for(var i = 0, len = this.product_list.length; i < len; i++){ - var product_node = this.render_product(this.product_list[i]); - product_node.addEventListener('click',this.click_product_handler); - list_container.appendChild(product_node); - }; - }, - }); + var list_container = el_node.querySelector('.product-list'); + for(var i = 0, len = this.product_list.length; i < len; i++){ + var product_node = this.render_product(this.product_list[i]); + product_node.addEventListener('click',this.click_product_handler); + list_container.appendChild(product_node); + } + }, +}); - /* -------- The Action Buttons -------- */ +/* -------- The Action Buttons -------- */ - // Above the numpad and the actionpad, buttons - // for extra actions and controls by point of - // sale extensions modules. +// Above the numpad and the actionpad, buttons +// for extra actions and controls by point of +// sale extensions modules. - module.action_button_classes = []; - module.define_action_button = function(classe, options){ - options = options || {}; +var action_button_classes = []; +var define_action_button = function(classe, options){ + options = options || {}; - var classes = module.action_button_classes; - var index = classes.length; + var classes = action_button_classes; + var index = classes.length; + var i; - if (options.after) { - for (var i = 0; i < classes.length; i++) { - if (classes[i].name === options.after) { - index = i + 1; - } - } - } else if (options.before) { - for (var i = 0; i < classes.length; i++) { - if (classes[i].name === options.after) { - index = i; - break; - } + if (options.after) { + for (i = 0; i < classes.length; i++) { + if (classes[i].name === options.after) { + index = i + 1; } } - classes.splice(i,0,classe); - }; + } else if (options.before) { + for (i = 0; i < classes.length; i++) { + if (classes[i].name === options.after) { + index = i; + break; + } + } + } + classes.splice(i,0,classe); +}; - module.ActionButtonWidget = module.PosBaseWidget.extend({ - template: 'ActionButtonWidget', - label: _t('Button'), - renderElement: function(){ - var self = this; - this._super(); - this.$el.click(function(){ - self.button_click(); +var ActionButtonWidget = PosBaseWidget.extend({ + template: 'ActionButtonWidget', + label: _t('Button'), + renderElement: function(){ + var self = this; + this._super(); + this.$el.click(function(){ + self.button_click(); + }); + }, + button_click: function(){}, + highlight: function(highlight){ + this.$el.toggleClass('highlight',!!highlight); + }, +}); + +/* -------- The Product Screen -------- */ + +var ProductScreenWidget = ScreenWidget.extend({ + template:'ProductScreenWidget', + + start: function(){ + + var self = this; + + this.actionpad = new ActionpadWidget(this,{}); + this.actionpad.replace(this.$('.placeholder-ActionpadWidget')); + + this.numpad = new NumpadWidget(this,{}); + this.numpad.replace(this.$('.placeholder-NumpadWidget')); + + this.order_widget = new OrderWidget(this,{ + numpad_state: this.numpad.state, + }); + this.order_widget.replace(this.$('.placeholder-OrderWidget')); + + this.product_list_widget = new ProductListWidget(this,{ + click_product_action: function(product){ self.click_product(product); }, + product_list: this.pos.db.get_product_by_category(0) + }); + this.product_list_widget.replace(this.$('.placeholder-ProductListWidget')); + + this.product_categories_widget = new ProductCategoriesWidget(this,{ + product_list_widget: this.product_list_widget, + }); + this.product_categories_widget.replace(this.$('.placeholder-ProductCategoriesWidget')); + + this.action_buttons = {}; + var classes = action_button_classes; + for (var i = 0; i < classes.length; i++) { + var classe = classes[i]; + if ( !classe.condition || classe.condition.call(this) ) { + var widget = new classe.widget(this,{}); + widget.appendTo(this.$('.control-buttons')); + this.action_buttons[classe.name] = widget; + } + } + if (_.size(this.action_buttons)) { + this.$('.control-buttons').removeClass('oe_hidden'); + } + }, + + click_product: function(product) { + if(product.to_weight && this.pos.config.iface_electronic_scale){ + this.gui.show_screen('scale',{product: product}); + }else{ + this.pos.get_order().add_product(product); + } + }, + + show: function(){ + this._super(); + this.product_categories_widget.reset_category(); + this.numpad.state.reset(); + }, + + close: function(){ + this._super(); + if(this.pos.config.iface_vkeyboard && this.chrome.widget.keyboard){ + this.chrome.widget.keyboard.hide(); + } + }, +}); +gui.define_screen({name:'products', widget: ProductScreenWidget}); + +/*--------------------------------------*\ + | THE CLIENT LIST | +\*======================================*/ + +// The clientlist displays the list of customer, +// and allows the cashier to create, edit and assign +// customers. + +var ClientListScreenWidget = ScreenWidget.extend({ + template: 'ClientListScreenWidget', + + init: function(parent, options){ + this._super(parent, options); + this.partner_cache = new DomCache(); + }, + + auto_back: true, + + show: function(){ + var self = this; + this._super(); + + this.renderElement(); + this.details_visible = false; + this.old_client = this.pos.get_order().get_client(); + this.new_client = this.old_client; + + this.$('.back').click(function(){ + self.gui.back(); + }); + + this.$('.next').click(function(){ + self.save_changes(); + self.gui.back(); // FIXME HUH ? + }); + + this.$('.new-customer').click(function(){ + self.display_client_details('edit',{ + 'country_id': self.pos.company.country_id, }); - }, - button_click: function(){}, - highlight: function(highlight){ - this.$el.toggleClass('highlight',!!highlight); - }, - }); + }); - /* -------- The Product Screen -------- */ - - module.ProductScreenWidget = module.ScreenWidget.extend({ - template:'ProductScreenWidget', - - start: function(){ - - var self = this; - - this.actionpad = new module.ActionpadWidget(this,{}); - this.actionpad.replace(this.$('.placeholder-ActionpadWidget')); - - this.numpad = new module.NumpadWidget(this,{}); - this.numpad.replace(this.$('.placeholder-NumpadWidget')); - - this.order_widget = new module.OrderWidget(this,{ - numpad_state: this.numpad.state, - }); - this.order_widget.replace(this.$('.placeholder-OrderWidget')); - - this.product_list_widget = new module.ProductListWidget(this,{ - click_product_action: function(product){ self.click_product(product); }, - product_list: this.pos.db.get_product_by_category(0) - }); - this.product_list_widget.replace(this.$('.placeholder-ProductListWidget')); - - this.product_categories_widget = new module.ProductCategoriesWidget(this,{ - product_list_widget: this.product_list_widget, - }); - this.product_categories_widget.replace(this.$('.placeholder-ProductCategoriesWidget')); - - this.action_buttons = {}; - var classes = module.action_button_classes; - for (var i = 0; i < classes.length; i++) { - var classe = classes[i]; - if ( !classe.condition || classe.condition.call(this) ) { - var widget = new classe.widget(this,{}); - widget.appendTo(this.$('.control-buttons')); - this.action_buttons[classe.name] = widget; - } - } - if (_.size(this.action_buttons)) { - this.$('.control-buttons').removeClass('oe_hidden'); - } - }, - - click_product: function(product) { - if(product.to_weight && this.pos.config.iface_electronic_scale){ - this.gui.show_screen('scale',{product: product}); - }else{ - this.pos.get_order().add_product(product); - } - }, - - show: function(){ - this._super(); - this.product_categories_widget.reset_category(); - this.numpad.state.reset(); - }, - - close: function(){ - this._super(); - if(this.pos.config.iface_vkeyboard && this.chrome.widget.keyboard){ - this.chrome.widget.keyboard.hide(); - } - }, - }); - module.Gui.define_screen({name:'products', widget:module.ProductScreenWidget}); - - /*--------------------------------------*\ - | THE CLIENT LIST | - \*======================================*/ - - // The clientlist displays the list of customer, - // and allows the cashier to create, edit and assign - // customers. - - module.ClientListScreenWidget = module.ScreenWidget.extend({ - template: 'ClientListScreenWidget', - - init: function(parent, options){ - this._super(parent, options); - this.partner_cache = new module.DomCache(); - }, - - auto_back: true, - - show: function(){ - var self = this; - this._super(); - - this.renderElement(); - this.details_visible = false; - this.old_client = this.pos.get_order().get_client() - this.new_client = this.old_client; - - this.$('.back').click(function(){ - self.gui.back(); - }); - - this.$('.next').click(function(){ - self.save_changes(); - self.gui.back(); // FIXME HUH ? - }); - - this.$('.new-customer').click(function(){ - self.display_client_details('edit',{ - 'country_id': self.pos.company.country_id, - }); - }); - - var partners = this.pos.db.get_partners_sorted(1000); - this.render_list(partners); - - this.reload_partners(); - - if( this.old_client ){ - this.display_client_details('show',this.old_client,0); - } - - this.$('.client-list-contents').delegate('.client-line','click',function(event){ - self.line_select(event,$(this),parseInt($(this).data('id'))); - }); - - var search_timeout = null; - - if(this.pos.config.iface_vkeyboard && this.chrome.widget.keyboard){ - this.chrome.widget.keyboard.connect(this.$('.searchbox input')); - } - - this.$('.searchbox input').on('keyup',function(event){ - clearTimeout(search_timeout); - - var query = this.value; - - search_timeout = setTimeout(function(){ - self.perform_search(query,event.which === 13); - },70); - }); - - this.$('.searchbox .search-clear').click(function(){ - self.clear_search(); - }); - }, - barcode_client_action: function(code){ - if (this.editing_client) { - this.$('.detail.barcode').val(code.code); - } else if (this.pos.db.get_partner_by_barcode(code.code)) { - this.display_client_details('show',this.pos.db.get_partner_by_barcode(code.code)); - } - }, - perform_search: function(query, associate_result){ - if(query){ - var customers = this.pos.db.search_partner(query); - this.display_client_details('hide'); - if ( associate_result && customers.length === 1){ - this.new_client = customers[0]; - this.save_changes(); - this.gui.back(); - } - this.render_list(customers); - }else{ - var customers = this.pos.db.get_partners_sorted(); - this.render_list(customers); - } - }, - clear_search: function(){ - var customers = this.pos.db.get_partners_sorted(1000); - this.render_list(customers); - this.$('.searchbox input')[0].value = ''; - this.$('.searchbox input').focus(); - }, - render_list: function(partners){ - var contents = this.$el[0].querySelector('.client-list-contents'); - contents.innerHTML = ""; - for(var i = 0, len = Math.min(partners.length,1000); i < len; i++){ - var partner = partners[i]; - var clientline = this.partner_cache.get_node(partner.id); - if(!clientline){ - var clientline_html = QWeb.render('ClientLine',{widget: this, partner:partners[i]}); - var clientline = document.createElement('tbody'); - clientline.innerHTML = clientline_html; - clientline = clientline.childNodes[1]; - this.partner_cache.cache_node(partner.id,clientline); - } - if( partners === this.new_client ){ - clientline.classList.add('highlight'); - }else{ - clientline.classList.remove('highlight'); - } - contents.appendChild(clientline); - } - }, - save_changes: function(){ - if( this.has_client_changed() ){ - this.pos.get_order().set_client(this.new_client); - } - }, - has_client_changed: function(){ - if( this.old_client && this.new_client ){ - return this.old_client.id !== this.new_client.id; - }else{ - return !!this.old_client !== !!this.new_client; - } - }, - toggle_save_button: function(){ - var $button = this.$('.button.next'); - if (this.editing_client) { - $button.addClass('oe_hidden'); - return; - } else if( this.new_client ){ - if( !this.old_client){ - $button.text(_t('Set Customer')); - }else{ - $button.text(_t('Change Customer')); - } - }else{ - $button.text(_t('Deselect Customer')); - } - $button.toggleClass('oe_hidden',!this.has_client_changed()); - }, - line_select: function(event,$line,id){ - var partner = this.pos.db.get_partner_by_id(id); - this.$('.client-list .lowlight').removeClass('lowlight'); - if ( $line.hasClass('highlight') ){ - $line.removeClass('highlight'); - $line.addClass('lowlight'); - this.display_client_details('hide',partner); - this.new_client = null; - this.toggle_save_button(); - }else{ - this.$('.client-list .highlight').removeClass('highlight'); - $line.addClass('highlight'); - var y = event.pageY - $line.parent().offset().top - this.display_client_details('show',partner,y); - this.new_client = partner; - this.toggle_save_button(); - } - }, - partner_icon_url: function(id){ - return '/web/binary/image?model=res.partner&id='+id+'&field=image_small'; - }, - - // ui handle for the 'edit selected customer' action - edit_client_details: function(partner) { - this.display_client_details('edit',partner); - }, - - // ui handle for the 'cancel customer edit changes' action - undo_client_details: function(partner) { - if (!partner.id) { - this.display_client_details('hide'); - } else { - this.display_client_details('show',partner); - } - }, - - // what happens when we save the changes on the client edit form -> we fetch the fields, sanitize them, - // send them to the backend for update, and call saved_client_details() when the server tells us the - // save was successfull. - save_client_details: function(partner) { - var self = this; - - var fields = {} - this.$('.client-details-contents .detail').each(function(idx,el){ - fields[el.name] = el.value; - }); - - if (!fields.name) { - this.gui.show_popup('error',_t('A Customer Name Is Required')); - return; - } - - if (this.uploaded_picture) { - fields.image = this.uploaded_picture; - } - - fields.id = partner.id || false; - fields.country_id = fields.country_id || false; - fields.barcode = fields.barcode ? this.pos.barcode_reader.sanitize_ean(fields.barcode) : false; - - new instance.web.Model('res.partner').call('create_from_ui',[fields]).then(function(partner_id){ - self.saved_client_details(partner_id); - },function(err,event){ - event.preventDefault(); - self.gui.show_popup('error',{ - 'title': _t('Error: Could not Save Changes'), - 'body': _t('Your Internet connection is probably down.'), - }); - }); - }, + var partners = this.pos.db.get_partners_sorted(1000); + this.render_list(partners); - // what happens when we've just pushed modifications for a partner of id partner_id - saved_client_details: function(partner_id){ - var self = this; - this.reload_partners().then(function(){ - var partner = self.pos.db.get_partner_by_id(partner_id); - if (partner) { - self.new_client = partner; - self.toggle_save_button(); - self.display_client_details('show',partner); - } else { - // should never happen, because create_from_ui must return the id of the partner it - // has created, and reload_partner() must have loaded the newly created partner. - self.display_client_details('hide'); - } + this.reload_partners(); + + if( this.old_client ){ + this.display_client_details('show',this.old_client,0); + } + + this.$('.client-list-contents').delegate('.client-line','click',function(event){ + self.line_select(event,$(this),parseInt($(this).data('id'))); + }); + + var search_timeout = null; + + if(this.pos.config.iface_vkeyboard && this.chrome.widget.keyboard){ + this.chrome.widget.keyboard.connect(this.$('.searchbox input')); + } + + this.$('.searchbox input').on('keyup',function(event){ + clearTimeout(search_timeout); + + var query = this.value; + + search_timeout = setTimeout(function(){ + self.perform_search(query,event.which === 13); + },70); + }); + + this.$('.searchbox .search-clear').click(function(){ + self.clear_search(); + }); + }, + barcode_client_action: function(code){ + if (this.editing_client) { + this.$('.detail.barcode').val(code.code); + } else if (this.pos.db.get_partner_by_barcode(code.code)) { + this.display_client_details('show',this.pos.db.get_partner_by_barcode(code.code)); + } + }, + perform_search: function(query, associate_result){ + var customers; + if(query){ + customers = this.pos.db.search_partner(query); + this.display_client_details('hide'); + if ( associate_result && customers.length === 1){ + this.new_client = customers[0]; + this.save_changes(); + this.gui.back(); + } + this.render_list(customers); + }else{ + customers = this.pos.db.get_partners_sorted(); + this.render_list(customers); + } + }, + clear_search: function(){ + var customers = this.pos.db.get_partners_sorted(1000); + this.render_list(customers); + this.$('.searchbox input')[0].value = ''; + this.$('.searchbox input').focus(); + }, + render_list: function(partners){ + var contents = this.$el[0].querySelector('.client-list-contents'); + contents.innerHTML = ""; + for(var i = 0, len = Math.min(partners.length,1000); i < len; i++){ + var partner = partners[i]; + var clientline = this.partner_cache.get_node(partner.id); + if(!clientline){ + var clientline_html = QWeb.render('ClientLine',{widget: this, partner:partners[i]}); + var clientline = document.createElement('tbody'); + clientline.innerHTML = clientline_html; + clientline = clientline.childNodes[1]; + this.partner_cache.cache_node(partner.id,clientline); + } + if( partners === this.new_client ){ + clientline.classList.add('highlight'); + }else{ + clientline.classList.remove('highlight'); + } + contents.appendChild(clientline); + } + }, + save_changes: function(){ + if( this.has_client_changed() ){ + this.pos.get_order().set_client(this.new_client); + } + }, + has_client_changed: function(){ + if( this.old_client && this.new_client ){ + return this.old_client.id !== this.new_client.id; + }else{ + return !!this.old_client !== !!this.new_client; + } + }, + toggle_save_button: function(){ + var $button = this.$('.button.next'); + if (this.editing_client) { + $button.addClass('oe_hidden'); + return; + } else if( this.new_client ){ + if( !this.old_client){ + $button.text(_t('Set Customer')); + }else{ + $button.text(_t('Change Customer')); + } + }else{ + $button.text(_t('Deselect Customer')); + } + $button.toggleClass('oe_hidden',!this.has_client_changed()); + }, + line_select: function(event,$line,id){ + var partner = this.pos.db.get_partner_by_id(id); + this.$('.client-list .lowlight').removeClass('lowlight'); + if ( $line.hasClass('highlight') ){ + $line.removeClass('highlight'); + $line.addClass('lowlight'); + this.display_client_details('hide',partner); + this.new_client = null; + this.toggle_save_button(); + }else{ + this.$('.client-list .highlight').removeClass('highlight'); + $line.addClass('highlight'); + var y = event.pageY - $line.parent().offset().top; + this.display_client_details('show',partner,y); + this.new_client = partner; + this.toggle_save_button(); + } + }, + partner_icon_url: function(id){ + return '/web/binary/image?model=res.partner&id='+id+'&field=image_small'; + }, + + // ui handle for the 'edit selected customer' action + edit_client_details: function(partner) { + this.display_client_details('edit',partner); + }, + + // ui handle for the 'cancel customer edit changes' action + undo_client_details: function(partner) { + if (!partner.id) { + this.display_client_details('hide'); + } else { + this.display_client_details('show',partner); + } + }, + + // what happens when we save the changes on the client edit form -> we fetch the fields, sanitize them, + // send them to the backend for update, and call saved_client_details() when the server tells us the + // save was successfull. + save_client_details: function(partner) { + var self = this; + + var fields = {}; + this.$('.client-details-contents .detail').each(function(idx,el){ + fields[el.name] = el.value; + }); + + if (!fields.name) { + this.gui.show_popup('error',_t('A Customer Name Is Required')); + return; + } + + if (this.uploaded_picture) { + fields.image = this.uploaded_picture; + } + + fields.id = partner.id || false; + fields.country_id = fields.country_id || false; + fields.barcode = fields.barcode ? this.pos.barcode_reader.sanitize_ean(fields.barcode) : false; + + new Model('res.partner').call('create_from_ui',[fields]).then(function(partner_id){ + self.saved_client_details(partner_id); + },function(err,event){ + event.preventDefault(); + self.gui.show_popup('error',{ + 'title': _t('Error: Could not Save Changes'), + 'body': _t('Your Internet connection is probably down.'), }); - }, - - // resizes an image, keeping the aspect ratio intact, - // the resize is useful to avoid sending 12Mpixels jpegs - // over a wireless connection. - resize_image_to_dataurl: function(img, maxwidth, maxheight, callback){ - img.onload = function(){ - var png = new Image(); - var canvas = document.createElement('canvas'); - var ctx = canvas.getContext('2d'); - var ratio = 1; - - if (img.width > maxwidth) { - ratio = maxwidth / img.width; - } - if (img.height * ratio > maxheight) { - ratio = maxheight / img.height; - } - var width = Math.floor(img.width * ratio); - var height = Math.floor(img.height * ratio); - - canvas.width = width; - canvas.height = height; - ctx.drawImage(img,0,0,width,height); - - var dataurl = canvas.toDataURL(); - callback(dataurl); + }); + }, + + // what happens when we've just pushed modifications for a partner of id partner_id + saved_client_details: function(partner_id){ + var self = this; + this.reload_partners().then(function(){ + var partner = self.pos.db.get_partner_by_id(partner_id); + if (partner) { + self.new_client = partner; + self.toggle_save_button(); + self.display_client_details('show',partner); + } else { + // should never happen, because create_from_ui must return the id of the partner it + // has created, and reload_partner() must have loaded the newly created partner. + self.display_client_details('hide'); } - }, + }); + }, - // Loads and resizes a File that contains an image. - // callback gets a dataurl in case of success. - load_image_file: function(file, callback){ - var self = this; - if (!file.type.match(/image.*/)) { - this.gui.show_popup('error',{ - title: _t('Unsupported File Format'), - body: _t('Only web-compatible Image formats such as .png or .jpeg are supported'), - }); - return; + // resizes an image, keeping the aspect ratio intact, + // the resize is useful to avoid sending 12Mpixels jpegs + // over a wireless connection. + resize_image_to_dataurl: function(img, maxwidth, maxheight, callback){ + img.onload = function(){ + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d'); + var ratio = 1; + + if (img.width > maxwidth) { + ratio = maxwidth / img.width; } + if (img.height * ratio > maxheight) { + ratio = maxheight / img.height; + } + var width = Math.floor(img.width * ratio); + var height = Math.floor(img.height * ratio); + + canvas.width = width; + canvas.height = height; + ctx.drawImage(img,0,0,width,height); + + var dataurl = canvas.toDataURL(); + callback(dataurl); + }; + }, + + // Loads and resizes a File that contains an image. + // callback gets a dataurl in case of success. + load_image_file: function(file, callback){ + var self = this; + if (!file.type.match(/image.*/)) { + this.gui.show_popup('error',{ + title: _t('Unsupported File Format'), + body: _t('Only web-compatible Image formats such as .png or .jpeg are supported'), + }); + return; + } + + var reader = new FileReader(); + reader.onload = function(event){ + var dataurl = event.target.result; + var img = new Image(); + img.src = dataurl; + self.resize_image_to_dataurl(img,800,600,callback); + }; + reader.onerror = function(){ + self.gui.show_popup('error',{ + title :_t('Could Not Read Image'), + body :_t('The provided file could not be read due to an unknown error'), + }); + }; + reader.readAsDataURL(file); + }, + + // This fetches partner changes on the server, and in case of changes, + // rerenders the affected views + reload_partners: function(){ + var self = this; + return this.pos.load_new_partners().then(function(){ + self.render_list(self.pos.db.get_partners_sorted(1000)); - var reader = new FileReader(); - reader.onload = function(event){ - var dataurl = event.target.result; - var img = new Image(); - img.src = dataurl; - self.resize_image_to_dataurl(img,800,600,callback); + // update the currently assigned client if it has been changed in db. + var curr_client = self.pos.get_order().get_client(); + if (curr_client) { + self.pos.get_order().set_client(self.pos.db.get_partner_by_id(curr_client.id)); } - reader.onerror = function(){ - self.gui.show_popup('error',{ - title :_t('Could Not Read Image'), - body :_t('The provided file could not be read due to an unknown error'), - }); - }; - reader.readAsDataURL(file); - }, + }); + }, - // This fetches partner changes on the server, and in case of changes, - // rerenders the affected views - reload_partners: function(){ - var self = this; - return this.pos.load_new_partners().then(function(){ - self.render_list(self.pos.db.get_partners_sorted(1000)); - - // update the currently assigned client if it has been changed in db. - var curr_client = self.pos.get_order().get_client(); - if (curr_client) { - self.pos.get_order().set_client(self.pos.db.get_partner_by_id(curr_client.id)); + // Shows,hides or edit the customer details box : + // visibility: 'show', 'hide' or 'edit' + // partner: the partner object to show or edit + // clickpos: the height of the click on the list (in pixel), used + // to maintain consistent scroll. + display_client_details: function(visibility,partner,clickpos){ + var self = this; + var contents = this.$('.client-details-contents'); + var parent = this.$('.client-list').parent(); + var scroll = parent.scrollTop(); + var height = contents.height(); + + contents.off('click','.button.edit'); + contents.off('click','.button.save'); + contents.off('click','.button.undo'); + contents.on('click','.button.edit',function(){ self.edit_client_details(partner); }); + contents.on('click','.button.save',function(){ self.save_client_details(partner); }); + contents.on('click','.button.undo',function(){ self.undo_client_details(partner); }); + this.editing_client = false; + this.uploaded_picture = null; + + if(visibility === 'show'){ + contents.empty(); + contents.append($(QWeb.render('ClientDetails',{widget:this,partner:partner}))); + + var new_height = contents.height(); + + if(!this.details_visible){ + if(clickpos < scroll + new_height + 20 ){ + parent.scrollTop( clickpos - 20 ); + }else{ + parent.scrollTop(parent.scrollTop() + new_height); } - }); - }, + }else{ + parent.scrollTop(parent.scrollTop() - height + new_height); + } - // Shows,hides or edit the customer details box : - // visibility: 'show', 'hide' or 'edit' - // partner: the partner object to show or edit - // clickpos: the height of the click on the list (in pixel), used - // to maintain consistent scroll. - display_client_details: function(visibility,partner,clickpos){ - var self = this; - var contents = this.$('.client-details-contents'); - var parent = this.$('.client-list').parent(); - var scroll = parent.scrollTop(); - var height = contents.height(); + this.details_visible = true; + this.toggle_save_button(); + } else if (visibility === 'edit') { + this.editing_client = true; + contents.empty(); + contents.append($(QWeb.render('ClientDetailsEdit',{widget:this,partner:partner}))); + this.toggle_save_button(); - contents.off('click','.button.edit'); - contents.off('click','.button.save'); - contents.off('click','.button.undo'); - contents.on('click','.button.edit',function(){ self.edit_client_details(partner); }); - contents.on('click','.button.save',function(){ self.save_client_details(partner); }); - contents.on('click','.button.undo',function(){ self.undo_client_details(partner); }); - this.editing_client = false; - this.uploaded_picture = null; - - if(visibility === 'show'){ - contents.empty(); - contents.append($(QWeb.render('ClientDetails',{widget:this,partner:partner}))); - - var new_height = contents.height(); - - if(!this.details_visible){ - if(clickpos < scroll + new_height + 20 ){ - parent.scrollTop( clickpos - 20 ); - }else{ - parent.scrollTop(parent.scrollTop() + new_height); + contents.find('.image-uploader').on('change',function(){ + self.load_image_file(event.target.files[0],function(res){ + if (res) { + contents.find('.client-picture img, .client-picture .fa').remove(); + contents.find('.client-picture').append(""); + contents.find('.detail.picture').remove(); + self.uploaded_picture = res; } - }else{ - parent.scrollTop(parent.scrollTop() - height + new_height); - } - - this.details_visible = true; - this.toggle_save_button(); - } else if (visibility === 'edit') { - this.editing_client = true; - contents.empty(); - contents.append($(QWeb.render('ClientDetailsEdit',{widget:this,partner:partner}))); - this.toggle_save_button(); - - contents.find('.image-uploader').on('change',function(){ - self.load_image_file(event.target.files[0],function(res){ - if (res) { - contents.find('.client-picture img, .client-picture .fa').remove(); - contents.find('.client-picture').append(""); - contents.find('.detail.picture').remove(); - self.uploaded_picture = res; - } - }); }); - } else if (visibility === 'hide') { - contents.empty(); - if( height > scroll ){ - contents.css({height:height+'px'}); - contents.animate({height:0},400,function(){ - contents.css({height:''}); - }); - }else{ - parent.scrollTop( parent.scrollTop() - height); - } - this.details_visible = false; - this.toggle_save_button(); - } - }, - close: function(){ - this._super(); - }, - }); - module.Gui.define_screen({name:'clientlist', widget: module.ClientListScreenWidget}); - - /*--------------------------------------*\ - | THE RECEIPT SCREEN | - \*======================================*/ - - // The receipt screen displays the order's - // receipt and allows it to be printed in a web browser. - // The receipt screen is not shown if the point of sale - // is set up to print with the proxy. Altough it could - // be useful to do so... - - module.ReceiptScreenWidget = module.ScreenWidget.extend({ - template: 'ReceiptScreenWidget', - - show: function(){ - this._super(); - var self = this; - - this.refresh(); - - if (!this.pos.get_order()._printed && this.pos.config.iface_print_auto) { - this.print(); - } - - // The problem is that in chrome the print() is asynchronous and doesn't - // execute until all rpc are finished. So it conflicts with the rpc used - // to send the orders to the backend, and the user is able to go to the next - // screen before the printing dialog is opened. The problem is that what's - // printed is whatever is in the page when the dialog is opened and not when it's called, - // and so you end up printing the product list instead of the receipt... - // - // Fixing this would need a re-architecturing - // of the code to postpone sending of orders after printing. - // - // But since the print dialog also blocks the other asynchronous calls, the - // button enabling in the setTimeout() is blocked until the printing dialog is - // closed. But the timeout has to be big enough or else it doesn't work - // 2 seconds is the same as the default timeout for sending orders and so the dialog - // should have appeared before the timeout... so yeah that's not ultra reliable. - - this.lock_screen(true); - setTimeout(function(){ - self.lock_screen(false); - }, 2000); - }, - lock_screen: function(locked) { - this._locked = locked; - if (locked) { - this.$('.next').removeClass('highlight'); - } else { - this.$('.next').addClass('highlight'); - } - }, - print: function() { - this.pos.get_order()._printed = true; - window.print(); - }, - finish_order: function() { - if (!this._locked) { - this.pos.get_order().finalize(); - } - }, - renderElement: function() { - var self = this; - this._super(); - this.$('.next').click(function(){ - self.finish_order(); }); - this.$('.button.print').click(function(){ - self.print(); - }); - }, - refresh: function() { - var order = this.pos.get_order(); - this.$('.pos-receipt-container').html(QWeb.render('PosTicket',{ - widget:this, - order: order, - receipt: order.export_for_printing(), - orderlines: order.get_orderlines(), - paymentlines: order.get_paymentlines(), - })); - }, - }); - module.Gui.define_screen({name:'receipt', widget:module.ReceiptScreenWidget}); + } else if (visibility === 'hide') { + contents.empty(); + if( height > scroll ){ + contents.css({height:height+'px'}); + contents.animate({height:0},400,function(){ + contents.css({height:''}); + }); + }else{ + parent.scrollTop( parent.scrollTop() - height); + } + this.details_visible = false; + this.toggle_save_button(); + } + }, + close: function(){ + this._super(); + }, +}); +gui.define_screen({name:'clientlist', widget: ClientListScreenWidget}); - /*--------------------------------------*\ - | THE PAYMENT SCREEN | - \*======================================*/ +/*--------------------------------------*\ + | THE RECEIPT SCREEN | +\*======================================*/ - // The Payment Screen handles the payments, and - // it is unfortunately quite complicated. +// The receipt screen displays the order's +// receipt and allows it to be printed in a web browser. +// The receipt screen is not shown if the point of sale +// is set up to print with the proxy. Altough it could +// be useful to do so... - module.PaymentScreenWidget = module.ScreenWidget.extend({ - template: 'PaymentScreenWidget', - back_screen: 'product', - next_screen: 'receipt', - init: function(parent, options) { - var self = this; - this._super(parent, options); +var ReceiptScreenWidget = ScreenWidget.extend({ + template: 'ReceiptScreenWidget', - this.pos.bind('change:selectedOrder',function(){ - this.renderElement(); - this.watch_order_changes(); - },this); - this.watch_order_changes(); + show: function(){ + this._super(); + var self = this; - this.inputbuffer = ""; - this.firstinput = true; - - // This is a keydown handler that prevents backspace from - // doing a back navigation - this.keyboard_no_backnav = function(event){ - if (event.keyCode === 8) { // Backspace - event.preventDefault(); - } - }; - - // This keyboard handler is on keyup to prevent keypress repeats - // but it thus cannot prevent the back navigation - this.keyboard_handler = function(event){ - var key = ''; - if ( event.keyCode === 13 ) { // Enter - self.validate_order(); - } else if ( event.keyCode === 190 ) { // Dot - key = '.'; - } else if ( event.keyCode === 46 ) { // Delete - key = 'CLEAR'; - } else if ( event.keyCode === 8 ) { // Backspace - key = 'BACKSPACE'; - } else if ( event.keyCode >= 48 && event.keyCode <= 57 ){ // Numbers - key = '' + (event.keyCode - 48); - } else if ( event.keyCode >= 96 && event.keyCode <= 105 ){ // Numpad Numbers - key = '' + (event.keyCode - 96); - } else if ( event.keyCode === 189 || event.keyCode === 109 ) { // Minus - key = '-'; - } else if ( event.keyCode === 107 ) { // Plus - key = '+'; - } + this.refresh(); - self.payment_input(key); + if (!this.pos.get_order()._printed && this.pos.config.iface_print_auto) { + this.print(); + } + + // The problem is that in chrome the print() is asynchronous and doesn't + // execute until all rpc are finished. So it conflicts with the rpc used + // to send the orders to the backend, and the user is able to go to the next + // screen before the printing dialog is opened. The problem is that what's + // printed is whatever is in the page when the dialog is opened and not when it's called, + // and so you end up printing the product list instead of the receipt... + // + // Fixing this would need a re-architecturing + // of the code to postpone sending of orders after printing. + // + // But since the print dialog also blocks the other asynchronous calls, the + // button enabling in the setTimeout() is blocked until the printing dialog is + // closed. But the timeout has to be big enough or else it doesn't work + // 2 seconds is the same as the default timeout for sending orders and so the dialog + // should have appeared before the timeout... so yeah that's not ultra reliable. + + this.lock_screen(true); + setTimeout(function(){ + self.lock_screen(false); + }, 2000); + }, + lock_screen: function(locked) { + this._locked = locked; + if (locked) { + this.$('.next').removeClass('highlight'); + } else { + this.$('.next').addClass('highlight'); + } + }, + print: function() { + this.pos.get_order()._printed = true; + window.print(); + }, + finish_order: function() { + if (!this._locked) { + this.pos.get_order().finalize(); + } + }, + renderElement: function() { + var self = this; + this._super(); + this.$('.next').click(function(){ + self.finish_order(); + }); + this.$('.button.print').click(function(){ + self.print(); + }); + }, + refresh: function() { + var order = this.pos.get_order(); + this.$('.pos-receipt-container').html(QWeb.render('PosTicket',{ + widget:this, + order: order, + receipt: order.export_for_printing(), + orderlines: order.get_orderlines(), + paymentlines: order.get_paymentlines(), + })); + }, +}); +gui.define_screen({name:'receipt', widget: ReceiptScreenWidget}); + +/*--------------------------------------*\ + | THE PAYMENT SCREEN | +\*======================================*/ + +// The Payment Screen handles the payments, and +// it is unfortunately quite complicated. + +var PaymentScreenWidget = ScreenWidget.extend({ + template: 'PaymentScreenWidget', + back_screen: 'product', + next_screen: 'receipt', + init: function(parent, options) { + var self = this; + this._super(parent, options); + + this.pos.bind('change:selectedOrder',function(){ + this.renderElement(); + this.watch_order_changes(); + },this); + this.watch_order_changes(); + + this.inputbuffer = ""; + this.firstinput = true; + + // This is a keydown handler that prevents backspace from + // doing a back navigation + this.keyboard_no_backnav = function(event){ + if (event.keyCode === 8) { // Backspace event.preventDefault(); - }; - }, - // resets the current input buffer - reset_input: function(){ - var line = this.pos.get_order().selected_paymentline; - this.firstinput = true; - if (line) { - this.inputbuffer = this.format_currency_no_symbol(line.get_amount()); - } else { - this.inputbuffer = ""; } - }, - // handle both keyboard and numpad input. Accepts - // a string that represents the key pressed. - payment_input: function(input) { - var oldbuf = this.inputbuffer.slice(0); - var newbuf = this.gui.numpad_input(this.inputbuffer, input, {'firstinput': this.firstinput}); + }; + + // This keyboard handler is on keyup to prevent keypress repeats + // but it thus cannot prevent the back navigation + this.keyboard_handler = function(event){ + var key = ''; + if ( event.keyCode === 13 ) { // Enter + self.validate_order(); + } else if ( event.keyCode === 190 ) { // Dot + key = '.'; + } else if ( event.keyCode === 46 ) { // Delete + key = 'CLEAR'; + } else if ( event.keyCode === 8 ) { // Backspace + key = 'BACKSPACE'; + } else if ( event.keyCode >= 48 && event.keyCode <= 57 ){ // Numbers + key = '' + (event.keyCode - 48); + } else if ( event.keyCode >= 96 && event.keyCode <= 105 ){ // Numpad Numbers + key = '' + (event.keyCode - 96); + } else if ( event.keyCode === 189 || event.keyCode === 109 ) { // Minus + key = '-'; + } else if ( event.keyCode === 107 ) { // Plus + key = '+'; + } - this.firstinput = (newbuf.length === 0); + self.payment_input(key); + event.preventDefault(); + }; + }, + // resets the current input buffer + reset_input: function(){ + var line = this.pos.get_order().selected_paymentline; + this.firstinput = true; + if (line) { + this.inputbuffer = this.format_currency_no_symbol(line.get_amount()); + } else { + this.inputbuffer = ""; + } + }, + // handle both keyboard and numpad input. Accepts + // a string that represents the key pressed. + payment_input: function(input) { + var newbuf = this.gui.numpad_input(this.inputbuffer, input, {'firstinput': this.firstinput}); - // popup block inputs to prevent sneak editing. - if (this.gui.has_popup()) { - return; + this.firstinput = (newbuf.length === 0); + + // popup block inputs to prevent sneak editing. + if (this.gui.has_popup()) { + return; + } + + if (newbuf !== this.inputbuffer) { + this.inputbuffer = newbuf; + var order = this.pos.get_order(); + if (order.selected_paymentline) { + order.selected_paymentline.set_amount(parseFloat(this.inputbuffer)); + this.order_changes(); + this.render_paymentlines(); + this.$('.paymentline.selected .edit').text(this.inputbuffer); } - - if (newbuf !== this.inputbuffer) { - this.inputbuffer = newbuf; - var order = this.pos.get_order(); - if (order.selected_paymentline) { - order.selected_paymentline.set_amount(parseFloat(this.inputbuffer)); - this.order_changes(); - this.render_paymentlines(); - this.$('.paymentline.selected .edit').text(this.inputbuffer); - } - } - }, - click_numpad: function(button) { + } + }, + click_numpad: function(button) { if (!this.pos.get_order().get_paymentlines().length) { this.pos.get_order().add_paymentline( this.pos.cashregisters[0]); this.render_paymentlines(); } - this.payment_input(button.data('action')); - }, - render_numpad: function() { - var self = this; - var numpad = $(QWeb.render('PaymentScreen-Numpad', { widget:this })); - numpad.on('click','button',function(){ - self.click_numpad($(this)); - }); - return numpad; - }, - click_delete_paymentline: function(cid){ - var lines = this.pos.get_order().get_paymentlines(); - for ( var i = 0; i < lines.length; i++ ) { - if (lines[i].cid === cid) { - this.pos.get_order().remove_paymentline(lines[i]); - this.reset_input(); - this.render_paymentlines(); - return; - } - } - }, - click_paymentline: function(cid){ - var lines = this.pos.get_order().get_paymentlines(); - for ( var i = 0; i < lines.length; i++ ) { - if (lines[i].cid === cid) { - this.pos.get_order().select_paymentline(lines[i]); - this.reset_input(); - this.render_paymentlines(); - return; - } - } - }, - render_paymentlines: function() { - var self = this; - var order = this.pos.get_order(); - if (!order) { + this.payment_input(button.data('action')); + }, + render_numpad: function() { + var self = this; + var numpad = $(QWeb.render('PaymentScreen-Numpad', { widget:this })); + numpad.on('click','button',function(){ + self.click_numpad($(this)); + }); + return numpad; + }, + click_delete_paymentline: function(cid){ + var lines = this.pos.get_order().get_paymentlines(); + for ( var i = 0; i < lines.length; i++ ) { + if (lines[i].cid === cid) { + this.pos.get_order().remove_paymentline(lines[i]); + this.reset_input(); + this.render_paymentlines(); return; } - - var lines = order.get_paymentlines(); - var due = order.get_due(); - var extradue = 0; - if (due && lines.length && due !== order.get_due(lines[lines.length-1])) { - extradue = due; - } - - - this.$('.paymentlines-container').empty(); - var lines = $(QWeb.render('PaymentScreen-Paymentlines', { - widget: this, - order: order, - paymentlines: lines, - extradue: extradue, - })); - - lines.on('click','.delete-button',function(){ - self.click_delete_paymentline($(this).data('cid')); - }); - - lines.on('click','.paymentline',function(){ - self.click_paymentline($(this).data('cid')); - }); - - lines.appendTo(this.$('.paymentlines-container')); - }, - click_paymentmethods: function(id) { - var cashregister = null; - for ( var i = 0; i < this.pos.cashregisters.length; i++ ) { - if ( this.pos.cashregisters[i].journal_id[0] === id ){ - cashregister = this.pos.cashregisters[i]; - break; - } - } - this.pos.get_order().add_paymentline( cashregister ); - this.reset_input(); - this.render_paymentlines(); - }, - render_paymentmethods: function() { - var self = this; - var methods = $(QWeb.render('PaymentScreen-Paymentmethods', { widget:this })); - methods.on('click','.paymentmethod',function(){ - self.click_paymentmethods($(this).data('id')); - }); - return methods; - }, - click_invoice: function(){ - var order = this.pos.get_order(); - order.set_to_invoice(!order.is_to_invoice()); - if (order.is_to_invoice()) { - this.$('.js_invoice').addClass('highlight'); - } else { - this.$('.js_invoice').removeClass('highlight'); - } - }, - click_tip: function(){ - var self = this; - var order = this.pos.get_order(); - var tip = order.get_tip(); - var change = order.get_change(); - var value = tip; - - if (tip === 0 && change > 0 ) { - value = change; - } - - this.gui.show_popup('number',{ - 'title': tip ? _t('Change Tip') : _t('Add Tip'), - 'value': value, - 'confirm': function(value) { - order.set_tip(Number(value)); - self.order_changes(); - self.render_paymentlines(); - } - }); - }, - click_set_customer: function(){ - this.gui.show_screen('clientlist'); - }, - click_back: function(){ - this.gui.show_screen('products'); - }, - renderElement: function() { - var self = this; - this._super(); - - var numpad = this.render_numpad(); - numpad.appendTo(this.$('.payment-numpad')); - - var methods = this.render_paymentmethods(); - methods.appendTo(this.$('.paymentmethods-container')); - - this.render_paymentlines(); - - this.$('.back').click(function(){ - self.click_back(); - }); - - this.$('.next').click(function(){ - self.validate_order(); - }); - - this.$('.js_set_customer').click(function(){ - self.click_set_customer(); - }); - - this.$('.js_tip').click(function(){ - self.click_tip(); - }); - this.$('.js_invoice').click(function(){ - self.click_invoice(); - }); - - this.$('.js_cashdrawer').click(function(){ - self.pos.proxy.open_cashbox(); - }); - - }, - show: function(){ - this.pos.get_order().clean_empty_paymentlines(); - this.reset_input(); - this.render_paymentlines(); - this.order_changes(); - window.document.body.addEventListener('keyup',this.keyboard_handler); - window.document.body.addEventListener('keydown',this.keyboard_no_backnav); - this._super(); - }, - hide: function(){ - window.document.body.removeEventListener('keyup',this.keyboard_handler); - window.document.body.removeEventListener('keydown',this.keyboard_no_backnav); - this._super(); - }, - // sets up listeners to watch for order changes - watch_order_changes: function() { - var self = this; - var order = this.pos.get_order(); - if (!order) { + } + }, + click_paymentline: function(cid){ + var lines = this.pos.get_order().get_paymentlines(); + for ( var i = 0; i < lines.length; i++ ) { + if (lines[i].cid === cid) { + this.pos.get_order().select_paymentline(lines[i]); + this.reset_input(); + this.render_paymentlines(); return; } - if(this.old_order){ - this.old_order.unbind(null,null,this); + } + }, + render_paymentlines: function() { + var self = this; + var order = this.pos.get_order(); + if (!order) { + return; + } + + var lines = order.get_paymentlines(); + var due = order.get_due(); + var extradue = 0; + if (due && lines.length && due !== order.get_due(lines[lines.length-1])) { + extradue = due; + } + + + this.$('.paymentlines-container').empty(); + var lines = $(QWeb.render('PaymentScreen-Paymentlines', { + widget: this, + order: order, + paymentlines: lines, + extradue: extradue, + })); + + lines.on('click','.delete-button',function(){ + self.click_delete_paymentline($(this).data('cid')); + }); + + lines.on('click','.paymentline',function(){ + self.click_paymentline($(this).data('cid')); + }); + + lines.appendTo(this.$('.paymentlines-container')); + }, + click_paymentmethods: function(id) { + var cashregister = null; + for ( var i = 0; i < this.pos.cashregisters.length; i++ ) { + if ( this.pos.cashregisters[i].journal_id[0] === id ){ + cashregister = this.pos.cashregisters[i]; + break; } - order.bind('all',function(){ + } + this.pos.get_order().add_paymentline( cashregister ); + this.reset_input(); + this.render_paymentlines(); + }, + render_paymentmethods: function() { + var self = this; + var methods = $(QWeb.render('PaymentScreen-Paymentmethods', { widget:this })); + methods.on('click','.paymentmethod',function(){ + self.click_paymentmethods($(this).data('id')); + }); + return methods; + }, + click_invoice: function(){ + var order = this.pos.get_order(); + order.set_to_invoice(!order.is_to_invoice()); + if (order.is_to_invoice()) { + this.$('.js_invoice').addClass('highlight'); + } else { + this.$('.js_invoice').removeClass('highlight'); + } + }, + click_tip: function(){ + var self = this; + var order = this.pos.get_order(); + var tip = order.get_tip(); + var change = order.get_change(); + var value = tip; + + if (tip === 0 && change > 0 ) { + value = change; + } + + this.gui.show_popup('number',{ + 'title': tip ? _t('Change Tip') : _t('Add Tip'), + 'value': value, + 'confirm': function(value) { + order.set_tip(Number(value)); self.order_changes(); + self.render_paymentlines(); + } + }); + }, + click_set_customer: function(){ + this.gui.show_screen('clientlist'); + }, + click_back: function(){ + this.gui.show_screen('products'); + }, + renderElement: function() { + var self = this; + this._super(); + + var numpad = this.render_numpad(); + numpad.appendTo(this.$('.payment-numpad')); + + var methods = this.render_paymentmethods(); + methods.appendTo(this.$('.paymentmethods-container')); + + this.render_paymentlines(); + + this.$('.back').click(function(){ + self.click_back(); + }); + + this.$('.next').click(function(){ + self.validate_order(); + }); + + this.$('.js_set_customer').click(function(){ + self.click_set_customer(); + }); + + this.$('.js_tip').click(function(){ + self.click_tip(); + }); + this.$('.js_invoice').click(function(){ + self.click_invoice(); + }); + + this.$('.js_cashdrawer').click(function(){ + self.pos.proxy.open_cashbox(); + }); + + }, + show: function(){ + this.pos.get_order().clean_empty_paymentlines(); + this.reset_input(); + this.render_paymentlines(); + this.order_changes(); + window.document.body.addEventListener('keyup',this.keyboard_handler); + window.document.body.addEventListener('keydown',this.keyboard_no_backnav); + this._super(); + }, + hide: function(){ + window.document.body.removeEventListener('keyup',this.keyboard_handler); + window.document.body.removeEventListener('keydown',this.keyboard_no_backnav); + this._super(); + }, + // sets up listeners to watch for order changes + watch_order_changes: function() { + var self = this; + var order = this.pos.get_order(); + if (!order) { + return; + } + if(this.old_order){ + this.old_order.unbind(null,null,this); + } + order.bind('all',function(){ + self.order_changes(); + }); + this.old_order = order; + }, + // called when the order is changed, used to show if + // the order is paid or not + order_changes: function(){ + var self = this; + var order = this.pos.get_order(); + if (!order) { + return; + } else if (order.is_paid()) { + self.$('.next').addClass('highlight'); + }else{ + self.$('.next').removeClass('highlight'); + } + }, + print_escpos_receipt: function(){ + var env = { + widget: this, + pos: this.pos, + order: this.pos.get_order(), + receipt: this.pos.get_order().export_for_printing(), + }; + + this.pos.proxy.print_receipt(QWeb.render('XmlReceipt',env)); + }, + + // Check if the order is paid, then sends it to the backend, + // and complete the sale process + validate_order: function(force_validation) { + var self = this; + + var order = this.pos.get_order(); + + // FIXME: this check is there because the backend is unable to + // process empty orders. This is not the right place to fix it. + if (order.get_orderlines().length === 0) { + this.gui.show_popup('error',{ + 'title': _t('Empty Order'), + 'body': _t('There must be at least one product in your order before it can be validated'), }); - this.old_order = order; - }, - // called when the order is changed, used to show if - // the order is paid or not - order_changes: function(){ - var self = this; - var order = this.pos.get_order(); - if (!order) { + return; + } + + var plines = order.get_paymentlines(); + for (var i = 0; i < plines.length; i++) { + if (plines[i].get_type() === 'bank' && plines[i].get_amount() < 0) { + this.pos_widget.screen_selector.show_popup('error',{ + 'message': _t('Negative Bank Payment'), + 'comment': _t('You cannot have a negative amount in a Bank payment. Use a cash payment method to return money to the customer.'), + }); return; - } else if (order.is_paid()) { - self.$('.next').addClass('highlight'); - }else{ - self.$('.next').removeClass('highlight'); } - }, - print_escpos_receipt: function(){ - var env = { - widget: this, - pos: this.pos, - order: this.pos.get_order(), - receipt: this.pos.get_order().export_for_printing(), - }; + } - this.pos.proxy.print_receipt(QWeb.render('XmlReceipt',env)); - }, + if (!order.is_paid() || this.invoicing) { + return; + } - // Check if the order is paid, then sends it to the backend, - // and complete the sale process - validate_order: function(force_validation) { - var self = this; - - var order = this.pos.get_order(); - - // FIXME: this check is there because the backend is unable to - // process empty orders. This is not the right place to fix it. - if (order.get_orderlines().length === 0) { + // The exact amount must be paid if there is no cash payment method defined. + if (Math.abs(order.get_total_with_tax() - order.get_total_paid()) > 0.00001) { + var cash = false; + for (var i = 0; i < this.pos.cashregisters.length; i++) { + cash = cash || (this.pos.cashregisters[i].journal.type === 'cash'); + } + if (!cash) { this.gui.show_popup('error',{ - 'title': _t('Empty Order'), - 'body': _t('There must be at least one product in your order before it can be validated'), + title: _t('Cannot return change without a cash payment method'), + body: _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'), }); return; } + } - var plines = order.get_paymentlines(); - for (var i = 0; i < plines.length; i++) { - if (plines[i].get_type() === 'bank' && plines[i].get_amount() < 0) { - this.pos_widget.screen_selector.show_popup('error',{ - 'message': _t('Negative Bank Payment'), - 'comment': _t('You cannot have a negative amount in a Bank payment. Use a cash payment method to return money to the customer.'), + // if the change is too large, it's probably an input error, make the user confirm. + if (!force_validation && (order.get_total_with_tax() * 1000 < order.get_total_paid())) { + this.gui.show_popup('confirm',{ + title: _t('Please Confirm Large Amount'), + body: _t('Are you sure that the customer wants to pay') + + ' ' + + this.format_currency(order.get_total_paid()) + + ' ' + + _t('for an order of') + + ' ' + + this.format_currency(order.get_total_with_tax()) + + ' ' + + _t('? Clicking "Confirm" will validate the payment.'), + confirm: function() { + self.validate_order('confirm'); + }, + }); + return; + } + + if (order.is_paid_with_cash() && this.pos.config.iface_cashdrawer) { + + this.pos.proxy.open_cashbox(); + } + + if (order.is_to_invoice()) { + var invoiced = this.pos.push_and_invoice_order(order); + this.invoicing = true; + + invoiced.fail(function(error){ + self.invoicing = false; + if (error === 'error-no-client') { + self.gui.show_popup('confirm',{ + 'title': _t('Please select the Customer'), + 'body': _t('You need to select the customer before you can invoice an order.'), + confirm: function(){ + self.gui.show_screen('clientlist'); + }, }); - return; - } - } - - if (!order.is_paid() || this.invoicing) { - return; - } - - // The exact amount must be paid if there is no cash payment method defined. - if (Math.abs(order.get_total_with_tax() - order.get_total_paid()) > 0.00001) { - var cash = false; - for (var i = 0; i < this.pos.cashregisters.length; i++) { - cash = cash || (this.pos.cashregisters[i].journal.type === 'cash'); - } - if (!cash) { - this.gui.show_popup('error',{ - title: _t('Cannot return change without a cash payment method'), - body: _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'), - }); - return; - } - } - - // if the change is too large, it's probably an input error, make the user confirm. - if (!force_validation && (order.get_total_with_tax() * 1000 < order.get_total_paid())) { - this.gui.show_popup('confirm',{ - title: _t('Please Confirm Large Amount'), - body: _t('Are you sure that the customer wants to pay') + - ' ' + - this.format_currency(order.get_total_paid()) + - ' ' + - _t('for an order of') + - ' ' + - this.format_currency(order.get_total_with_tax()) + - ' ' + - _t('? Clicking "Confirm" will validate the payment.'), - confirm: function() { - self.validate_order('confirm'); - }, - }); - return; - } - - if (order.is_paid_with_cash() && this.pos.config.iface_cashdrawer) { - - this.pos.proxy.open_cashbox(); - } - - if (order.is_to_invoice()) { - var invoiced = this.pos.push_and_invoice_order(order); - this.invoicing = true; - - invoiced.fail(function(error){ - self.invoicing = false; - if (error === 'error-no-client') { - self.gui.show_popup('confirm',{ - 'title': _t('Please select the Customer'), - 'body': _t('You need to select the customer before you can invoice an order.'), - confirm: function(){ - self.gui.show_screen('clientlist'); - }, - }); - } else { - self.gui.show_popup('error',{ - 'title': _t('The order could not be sent'), - 'body': _t('Check your internet connection and try again.'), - }); - } - }); - - invoiced.done(function(){ - self.invoicing = false; - order.finalize(); - }); - } else { - this.pos.push_order(order) - if (this.pos.config.iface_print_via_proxy) { - this.print_escpos_receipt(); - order.finalize(); //finish order and go back to scan screen } else { - this.gui.show_screen(this.next_screen); + self.gui.show_popup('error',{ + 'title': _t('The order could not be sent'), + 'body': _t('Check your internet connection and try again.'), + }); } - } - }, - }); - module.Gui.define_screen({name:'payment', widget:module.PaymentScreenWidget}); + }); + invoiced.done(function(){ + self.invoicing = false; + order.finalize(); + }); + } else { + this.pos.push_order(order); + if (this.pos.config.iface_print_via_proxy) { + this.print_escpos_receipt(); + order.finalize(); //finish order and go back to scan screen + } else { + this.gui.show_screen(this.next_screen); + } + } + }, +}); +gui.define_screen({name:'payment', widget: PaymentScreenWidget}); + +return { + ActionButtonWidget: ActionButtonWidget, + define_action_button: define_action_button, + ScreenWidget: ScreenWidget, + OrderWidget: OrderWidget, }; +}); + diff --git a/addons/point_of_sale/static/src/js/tests.js b/addons/point_of_sale/static/src/js/tests.js index 08aa6e87213..b41488756bb 100644 --- a/addons/point_of_sale/static/src/js/tests.js +++ b/addons/point_of_sale/static/src/js/tests.js @@ -1,89 +1,90 @@ -(function() { - 'use strict'; +odoo.define('point_of_sale.Tour', ['web.Tour'], function (require) { +"use strict"; - openerp.Tour.register({ - id: 'pos_basic_order', - name: 'Complete a basic order trough the Front-End', - path: '/web#model=pos.session.opening&action=point_of_sale.action_pos_session_opening', - mode: 'test', - steps: [ - { - title: 'Wait fot the bloody screen to be ready', - wait: 200, - }, - { - title: 'Load the Session', - waitNot: '.oe_loading:visible', - element: 'span:contains("Resume Session"),span:contains("Start Session")', - }, - { - title: 'Loading the Loading Screen', - waitFor: '.loader' - }, - { - title: 'Waiting for the end of loading...', - waitFor: '.loader:hidden', - }, - { - title: 'Loading The Point of Sale', - waitFor: '.pos', - }, - { - title: 'On va manger des CHIPS!', - element: '.product-list .product-name:contains("250g Lays Pickels")', - }, - { - title: 'The chips have been added to the Order', - waitFor: '.order .product-name:contains("250g Lays Pickels")', - }, - { - title: 'The order total has been updated to the correct value', - wait: 2000, - waitFor: '.order .total .value:contains("1.48 €")', - }, - { - title: "Let's buy more chips", - element: '.product-list .product-name:contains("250g Lays Pickels")', - }, - { - title: "Let's veryify we pay the correct price for two bags of chips", - waitFor: '.order .total .value:contains("2.96 €")', - }, - { - title: "Let's pay with a debit card", - element: ".paypad-button:contains('Bank')", - }, - { - title: "Let's accept the payment", - onload: function(){ - // The test cannot validate or cancel the print() ... so we replace it by a noop !. - window._print = window.print; - window.print = function(){ console.log('Print!') }; - }, - element: ".button .iconlabel:contains('Validate'):visible", - }, - { - title: "Let's finish the order", - element: ".button:not(.disabled) .iconlabel:contains('Next'):visible", - }, - { - onload: function(){ - window.print = window._print; - window._print = undefined; - }, - title: "Let's wait for the order posting", - waitFor: ".oe_status.js_synch .js_connected:visible", - }, - { - title: "Let's close the Point of Sale", - element: ".header-button:contains('Close')", - }, - { - title: "Wait for the backend to ready itself", - element: 'span:contains("Resume Session"),span:contains("Start Session")', - }, - ], - }); +var Tour = require('web.Tour'); -})(); +Tour.register({ + id: 'pos_basic_order', + name: 'Complete a basic order trough the Front-End', + path: '/web#model=pos.session.opening&action=point_of_sale.action_pos_session_opening', + mode: 'test', + steps: [ + { + title: 'Wait fot the bloody screen to be ready', + wait: 200, + }, + { + title: 'Load the Session', + waitNot: '.oe_loading:visible', + element: 'span:contains("Resume Session"),span:contains("Start Session")', + }, + { + title: 'Loading the Loading Screen', + waitFor: '.loader' + }, + { + title: 'Waiting for the end of loading...', + waitFor: '.loader:hidden', + }, + { + title: 'Loading The Point of Sale', + waitFor: '.pos', + }, + { + title: 'On va manger des CHIPS!', + element: '.product-list .product-name:contains("250g Lays Pickels")', + }, + { + title: 'The chips have been added to the Order', + waitFor: '.order .product-name:contains("250g Lays Pickels")', + }, + { + title: 'The order total has been updated to the correct value', + wait: 2000, + waitFor: '.order .total .value:contains("1.48 €")', + }, + { + title: "Let's buy more chips", + element: '.product-list .product-name:contains("250g Lays Pickels")', + }, + { + title: "Let's veryify we pay the correct price for two bags of chips", + waitFor: '.order .total .value:contains("2.96 €")', + }, + { + title: "Let's pay with a debit card", + element: ".paypad-button:contains('Bank')", + }, + { + title: "Let's accept the payment", + onload: function(){ + // The test cannot validate or cancel the print() ... so we replace it by a noop !. + window._print = window.print; + window.print = function(){ console.log('Print!') }; + }, + element: ".button .iconlabel:contains('Validate'):visible", + }, + { + title: "Let's finish the order", + element: ".button:not(.disabled) .iconlabel:contains('Next'):visible", + }, + { + onload: function(){ + window.print = window._print; + window._print = undefined; + }, + title: "Let's wait for the order posting", + waitFor: ".oe_status.js_synch .js_connected:visible", + }, + { + title: "Let's close the Point of Sale", + element: ".header-button:contains('Close')", + }, + { + title: "Wait for the backend to ready itself", + element: 'span:contains("Resume Session"),span:contains("Start Session")', + }, + ], +}); +}); diff --git a/addons/point_of_sale/static/src/js/widget_base.js b/addons/point_of_sale/static/src/js/widget_base.js index a391b3d19d3..fae2b29f1cb 100644 --- a/addons/point_of_sale/static/src/js/widget_base.js +++ b/addons/point_of_sale/static/src/js/widget_base.js @@ -1,80 +1,84 @@ -openerp.point_of_sale.load_basewidget = function load_basewidget(instance, module){ //module is instance.point_of_sale - "use strict"; +odoo.define('point_of_sale.BaseWidget', ['web.utils', 'web.Widget'], function (require) { +"use strict"; - var round_di = instance.web.round_decimals; - var round_pr = instance.web.round_precision; +var utils = require('web.utils'); +var Widget = require('web.Widget'); - // This is a base class for all Widgets in the POS. It exposes relevant data to the - // templates : - // - widget.currency : { symbol: '$' | '€' | ..., position: 'before' | 'after } - // - widget.format_currency(amount) : this method returns a formatted string based on the - // symbol, the position, and the amount of money. - // if the PoS is not fully loaded when you instanciate the widget, the currency might not - // yet have been initialized. Use __build_currency_template() to recompute with correct values - // before rendering. +var round_di = utils.round_decimals; +var round_pr = utils.round_precision; - module.PosBaseWidget = instance.web.Widget.extend({ - init:function(parent,options){ - this._super(parent); - options = options || {}; - this.pos = options.pos || (parent ? parent.pos : undefined); - this.chrome = options.chrome || (parent ? parent.chrome : undefined); - this.gui = options.gui || (parent ? parent.gui : undefined); - }, - format_currency: function(amount,precision){ - var currency = (this.pos && this.pos.currency) ? this.pos.currency : {symbol:'$', position: 'after', rounding: 0.01, decimals: 2}; +// This is a base class for all Widgets in the POS. It exposes relevant data to the +// templates : +// - widget.currency : { symbol: '$' | '€' | ..., position: 'before' | 'after } +// - widget.format_currency(amount) : this method returns a formatted string based on the +// symbol, the position, and the amount of money. +// if the PoS is not fully loaded when you instanciate the widget, the currency might not +// yet have been initialized. Use __build_currency_template() to recompute with correct values +// before rendering. +var PosBaseWidget = Widget.extend({ + init:function(parent,options){ + this._super(parent); + options = options || {}; + this.pos = options.pos || (parent ? parent.pos : undefined); + this.chrome = options.chrome || (parent ? parent.chrome : undefined); + this.gui = options.gui || (parent ? parent.gui : undefined); + }, + format_currency: function(amount,precision){ + var currency = (this.pos && this.pos.currency) ? this.pos.currency : {symbol:'$', position: 'after', rounding: 0.01, decimals: 2}; - amount = this.format_currency_no_symbol(amount,precision); + amount = this.format_currency_no_symbol(amount,precision); - if (currency.position === 'after') { - return amount + ' ' + (currency.symbol || ''); - } else { - return (currency.symbol || '') + ' ' + amount; - } - }, - format_currency_no_symbol: function(amount, precision) { - var currency = (this.pos && this.pos.currency) ? this.pos.currency : {symbol:'$', position: 'after', rounding: 0.01, decimals: 2}; - var decimals = currency.decimals; + if (currency.position === 'after') { + return amount + ' ' + (currency.symbol || ''); + } else { + return (currency.symbol || '') + ' ' + amount; + } + }, + format_currency_no_symbol: function(amount, precision) { + var currency = (this.pos && this.pos.currency) ? this.pos.currency : {symbol:'$', position: 'after', rounding: 0.01, decimals: 2}; + var decimals = currency.decimals; - if (precision && (typeof this.pos.dp[precision]) !== undefined) { - decimals = this.pos.dp[precision]; - } - - this.format_currency_no_symbol = function(amount){ - amount = round_pr(amount,currency.rounding); - amount = amount.toFixed(decimals); - return amount; - }; - - if (typeof amount === 'number') { - amount = round_di(amount,decimals).toFixed(decimals); - } + if (precision && (typeof this.pos.dp[precision]) !== undefined) { + decimals = this.pos.dp[precision]; + } + this.format_currency_no_symbol = function(amount){ + amount = round_pr(amount,currency.rounding); + amount = amount.toFixed(decimals); return amount; - }, - show: function(){ - this.$el.removeClass('oe_hidden'); - }, - hide: function(){ - this.$el.addClass('oe_hidden'); - }, - format_pr: function(value,precision){ - var decimals = precision > 0 ? Math.max(0,Math.ceil(Math.log(1.0/precision) / Math.log(10))) : 0; - return value.toFixed(decimals); - }, - format_fixed: function(value,integer_width,decimal_width){ - value = value.toFixed(decimal_width || 0); - var width = value.indexOf('.'); - if (width < 0 ) { - width = value.length; - } - var missing = integer_width - width; - while (missing > 0) { - value = '0' + value; - missing--; - } - return value; - }, - }); + }; -} + if (typeof amount === 'number') { + amount = round_di(amount,decimals).toFixed(decimals); + } + + return amount; + }, + show: function(){ + this.$el.removeClass('oe_hidden'); + }, + hide: function(){ + this.$el.addClass('oe_hidden'); + }, + format_pr: function(value,precision){ + var decimals = precision > 0 ? Math.max(0,Math.ceil(Math.log(1.0/precision) / Math.log(10))) : 0; + return value.toFixed(decimals); + }, + format_fixed: function(value,integer_width,decimal_width){ + value = value.toFixed(decimal_width || 0); + var width = value.indexOf('.'); + if (width < 0 ) { + width = value.length; + } + var missing = integer_width - width; + while (missing > 0) { + value = '0' + value; + missing--; + } + return value; + }, +}); + +return PosBaseWidget; + +}); diff --git a/addons/point_of_sale/views/templates.xml b/addons/point_of_sale/views/templates.xml index 9a9f7f1d5c1..9fb5a8f5beb 100644 --- a/addons/point_of_sale/views/templates.xml +++ b/addons/point_of_sale/views/templates.xml @@ -31,17 +31,22 @@