diff --git a/addons/point_of_sale/static/src/js/chrome.js b/addons/point_of_sale/static/src/js/chrome.js index 74f9d96fce4..ad806ac71c4 100644 --- a/addons/point_of_sale/static/src/js/chrome.js +++ b/addons/point_of_sale/static/src/js/chrome.js @@ -4,255 +4,10 @@ openerp.point_of_sale.load_chrome = function load_chrome(instance, module){ //mo var QWeb = instance.web.qweb; var _t = instance.web._t; - 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; - 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; - }, - }); - - module.NumpadWidget = module.PosBaseWidget.extend({ - template:'NumpadWidget', - init: function(parent, options) { - this._super(parent); - this.state = new module.NumpadState(); - window.numpadstate = this.state; - 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 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'); - }); - } - }); - - module.OrderWidget = module.PosBaseWidget.extend({ - template:'OrderWidget', - init: function(parent, options) { - var self = this; - this._super(parent,options); - this.editable = false; - this.pos.bind('change:selectedOrder', this.change_selected_order, this); - this.line_click_handler = function(event){ - if(!self.editable){ - return; - } - self.pos.get_order().select_orderline(this.orderline); - self.chrome.widget.numpad.state.reset(); - }; - this.client_change_handler = function(event){ - self.update_summary(); - } - if (this.pos.get_order()) { - this.bind_order_events(); - } - }, - enable_numpad: function(){ - this.disable_numpad(); //ensure we don't register the callbacks twice - this.numpad_state = this.chrome.widget.numpad.state; - if(this.numpad_state){ - this.numpad_state.reset(); - this.numpad_state.bind('set_value', this.set_value, this); - } - - }, - disable_numpad: function(){ - if(this.numpad_state){ - this.numpad_state.unbind('set_value', this.set_value); - this.numpad_state.reset(); - } - }, - set_value: function(val) { - var order = this.pos.get_order(); - if (this.editable && 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.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.client_change_handler); - order.bind('change:client', this.client_change_handler); - - 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){ - this.chrome.widget.numpad.state.reset(); //FIXME WTF - - var order = this.pos.get_order(); - if (!order) { - return; - } - var orderlines = order.get_orderlines(); - - var el_str = openerp.qweb.render('OrderWidget',{widget:this, order:order, orderlines:orderlines}); - - var el_node = document.createElement('div'); - el_node.innerHTML = _.str.trim(el_str); - el_node = el_node.childNodes[0]; - - - 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(); - 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 Order Selector -------- */ + + // Allows the cashier to create / delete and + // switch between orders. module.OrderSelectorWidget = module.PosBaseWidget.extend({ template: 'OrderSelectorWidget', @@ -311,254 +66,10 @@ openerp.point_of_sale.load_chrome = function load_chrome(instance, module){ //mo }, }); - 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.set_category(); - - 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){ - this.renderElement(); - var target = $target[0]; - target.parentNode.replaceChild(this.el,target); - }, - - renderElement: function(){ - var self = this; - - 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]; - - 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); - - 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.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){ - 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); - } - }else{ - var products = this.pos.db.get_product_by_category(this.category.id); - this.product_list_widget.set_product_list(products); - } - }, - - }); - - 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; - - this.click_product_handler = function(event){ - var product = self.pos.db.get_product_by_id(this.dataset['productId']); - options.click_product_action(product); - }; - - 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); - }, - - 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; - }, - - renderElement: function() { - var self = this; - - // 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]; - - 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); - }; - }, - }); + /* ------- The User Name Widget ------- */ + + // Displays the current cashier's name and allows + // to switch between cashiers. module.UsernameWidget = module.PosBaseWidget.extend({ template: 'UsernameWidget', @@ -595,6 +106,12 @@ openerp.point_of_sale.load_chrome = function load_chrome(instance, module){ //mo }, }); + /* -------- 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){ @@ -616,8 +133,14 @@ openerp.point_of_sale.load_chrome = function load_chrome(instance, module){ //mo hide: function(){ this.$el.addClass('oe_hidden'); }, }); - // The debug widget lets the user control and monitor the hardware and software status - // without the use of the proxy + /* --------- 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:{ @@ -760,7 +283,10 @@ openerp.point_of_sale.load_chrome = function load_chrome(instance, module){ //mo }, }); -// ---------- Main Point of Sale Widget ---------- + /* --------- 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'], @@ -779,7 +305,11 @@ openerp.point_of_sale.load_chrome = function load_chrome(instance, module){ //mo }, }); - // this is used to notify the user that data is being synchronized on the network + /* ------- Synch. Notifications ------- */ + + // Displays if there are orders that could + // not be submitted, and how many. + module.SynchNotificationWidget = module.StatusWidget.extend({ template: 'SynchNotificationWidget', start: function(){ @@ -793,7 +323,11 @@ openerp.point_of_sale.load_chrome = function load_chrome(instance, module){ //mo }, }); - // this is used to notify the user if the pos is connected to the proxy + /* --------- 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){ @@ -846,14 +380,27 @@ openerp.point_of_sale.load_chrome = function load_chrome(instance, module){ //mo }); - // The Chrome is the main widget that contains all other widgets in the PointOfSale. + /*--------------------------------------*\ + | THE CHROME | + \*======================================*/ + + // Displays the status of the hardware proxy + // 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) + // - a leftpane, containing the list of bought + // products (orderlines) + // - a rightpane, containing the screens + // (see pos_screens.js) // - popups // - an onscreen keyboard - // a gui which controls the switching between screens and the showing/closing of popups + // - .gui which controls the switching between + // screens and the showing/closing of popups module.Chrome = module.PosBaseWidget.extend({ template: 'Chrome', diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 32794c66bf7..f553a16b3bb 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -34,6 +34,13 @@ openerp.point_of_sale.load_screens = function load_screens(instance, module){ // var round_pr = instance.web.round_precision + /*--------------------------------------*\ + | THE SCREEN WIDGET | + \*======================================*/ + + // The screen widget is the base class inherited + // by all screens. + module.ScreenWidget = module.PosBaseWidget.extend({ show_numpad: true, @@ -158,6 +165,69 @@ openerp.point_of_sale.load_screens = function load_screens(instance, module){ // }, }); + /*--------------------------------------*\ + | 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; + 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; + }, + }); + + /*--------------------------------------*\ + | THE SCALE SCREEN | + \*======================================*/ + + // The scale screen displays the weight of + // a product on the electronic scale. module.ScaleScreenWidget = module.ScreenWidget.extend({ template:'ScaleScreenWidget', @@ -249,6 +319,505 @@ openerp.point_of_sale.load_screens = function load_screens(instance, module){ // }, }); + /*--------------------------------------*\ + | THE PRODUCT 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. + + + /* ------------ 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(); + window.numpadstate = this.state; + 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'); + }); + } + }); + + /* --------- The Order Widget --------- */ + + // Displays the current Order. + + module.OrderWidget = module.PosBaseWidget.extend({ + template:'OrderWidget', + init: function(parent, options) { + var self = this; + this._super(parent,options); + this.editable = false; + this.pos.bind('change:selectedOrder', this.change_selected_order, this); + this.line_click_handler = function(event){ + if(!self.editable){ + return; + } + self.pos.get_order().select_orderline(this.orderline); + self.chrome.widget.numpad.state.reset(); + }; + this.client_change_handler = function(event){ + self.update_summary(); + } + if (this.pos.get_order()) { + this.bind_order_events(); + } + }, + enable_numpad: function(){ + this.disable_numpad(); //ensure we don't register the callbacks twice + this.numpad_state = this.chrome.widget.numpad.state; + if(this.numpad_state){ + this.numpad_state.reset(); + this.numpad_state.bind('set_value', this.set_value, this); + } + + }, + disable_numpad: function(){ + if(this.numpad_state){ + this.numpad_state.unbind('set_value', this.set_value); + this.numpad_state.reset(); + } + }, + set_value: function(val) { + var order = this.pos.get_order(); + if (this.editable && 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.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.client_change_handler); + order.bind('change:client', this.client_change_handler); + + 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){ + this.chrome.widget.numpad.state.reset(); //FIXME WTF + + var order = this.pos.get_order(); + if (!order) { + return; + } + var orderlines = order.get_orderlines(); + + var el_str = openerp.qweb.render('OrderWidget',{widget:this, order:order, orderlines:orderlines}); + + var el_node = document.createElement('div'); + el_node.innerHTML = _.str.trim(el_str); + el_node = el_node.childNodes[0]; + + + 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(); + 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.set_category(); + + 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){ + this.renderElement(); + var target = $target[0]; + target.parentNode.replaceChild(this.el,target); + }, + + renderElement: function(){ + var self = this; + + 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]; + + 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.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){ + 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); + } + }else{ + var 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. + + 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; + + this.click_product_handler = function(event){ + var product = self.pos.db.get_product_by_id(this.dataset['productId']); + options.click_product_action(product); + }; + + 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); + }, + + 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; + }, + + renderElement: function() { + var self = this; + + // 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]; + + 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); + }; + }, + }); + + /* -------- The Product Screen -------- */ + module.ProductScreenWidget = module.ScreenWidget.extend({ template:'ProductScreenWidget', @@ -290,6 +859,14 @@ openerp.point_of_sale.load_screens = function load_screens(instance, module){ // }, }); + /*--------------------------------------*\ + | 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', @@ -669,6 +1246,16 @@ openerp.point_of_sale.load_screens = function load_screens(instance, module){ // }, }); + /*--------------------------------------*\ + | 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_numpad: false, @@ -744,6 +1331,13 @@ openerp.point_of_sale.load_screens = function load_screens(instance, module){ // }, }); + /*--------------------------------------*\ + | THE PAYMENT SCREEN | + \*======================================*/ + + // The Payment Screen handles the payments, and + // it is unfortunately quite complicated. + module.PaymentScreenWidget = module.ScreenWidget.extend({ template: 'PaymentScreenWidget', back_screen: 'product',