diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py index a4a06e94eaa..db5d5a25018 100644 --- a/addons/point_of_sale/controllers/main.py +++ b/addons/point_of_sale/controllers/main.py @@ -4,27 +4,31 @@ import simplejson import os import openerp +from openerp.addons.web import http +from openerp.addons.web.http import request from openerp.addons.web.controllers.main import manifest_list, module_boot, html_template -class PointOfSaleController(openerp.addons.web.http.Controller): - _cp_path = '/pos' +class PointOfSaleController(http.Controller): - @openerp.addons.web.http.httprequest - def app(self, req, s_action=None, **kw): - js = "\n ".join('' % i for i in manifest_list(req, None, 'js')) - css = "\n ".join('' % i for i in manifest_list(req, None, 'css')) + @http.route('/pos/app', type='http', auth='admin') + def app(self): + js = "\n ".join('' % i for i in manifest_list('js',db=request.db)) + css = "\n ".join('' % i for i in manifest_list('css',db=request.db)) + + cookie = request.httprequest.cookies.get("instance0|session_id") + session_id = cookie.replace("%22","") + template = html_template.replace(' + diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index d9a4d6d00a9..e328054aa28 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -291,10 +291,15 @@ display: inline-block; text-align: center; vertical-align: top; + width: 205px; + max-height: 232px; + overflow-y: auto; + overflow-x: hidden; } .point-of-sale #paypad button { height: 50px; - width: 208px; + display: block; + width: 100%; margin: 0px -6px 4px -2px; font-weight: bold; vertical-align: middle; @@ -302,6 +307,17 @@ border-top: 1px solid #efefef; font-size: 14px; } +.point-of-sale #paypad button, .point-of-sale #numpad button, .point-of-sale .popup button{ + position: relative; + top: 0; + -webkit-transition: top 150ms linear; + -moz-transition: top 150ms linear; + -ms-transition: top 150ms linear; + transition: top 150ms linear; +} +.point-of-sale #paypad button:active, .point-of-sale #numpad button:active, .point-of-sale .popup button:active{ + top:3px; +} .point-of-sale #paypad button:hover, .point-of-sale #numpad button:hover, .point-of-sale #numpad .selected-mode, .point-of-sale .popup button:hover { border: none; color: white; diff --git a/addons/point_of_sale/static/src/img/icons/png48/invoice.png b/addons/point_of_sale/static/src/img/icons/png48/invoice.png new file mode 100644 index 00000000000..e65d2ec5a67 Binary files /dev/null and b/addons/point_of_sale/static/src/img/icons/png48/invoice.png differ diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index e47ce66f648..d16ba5a53dd 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -267,11 +267,21 @@ function openerp_pos_db(instance, module){ return results; }, add_order: function(order){ - var last_id = this.load('last_order_id',0); + var order_id = order.uid; var orders = this.load('orders',[]); - orders.push({id: last_id + 1, data: order}); - this.save('last_order_id',last_id+1); + + // 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',[]); @@ -283,5 +293,14 @@ function openerp_pos_db(instance, module){ 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; + }, }); } diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 8cb88303dd1..8375377b31c 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -104,7 +104,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal return self.fetch('res.currency',['symbol','position','rounding','accuracy'],[['id','=',self.get('company').currency_id[0]]]); }).then(function(currencies){ - console.log('Currency:',currencies[0]); self.set('currency',currencies[0]); return self.fetch('product.uom', null, null); @@ -145,7 +144,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal ['name','journal_ids','warehouse_id','journal_id','pricelist_id', 'iface_self_checkout', 'iface_led', 'iface_cashdrawer', 'iface_payment_terminal', 'iface_electronic_scale', 'iface_barscan', 'iface_vkeyboard', - 'iface_print_via_proxy','iface_cashdrawer','state','sequence_id','session_ids'], + 'iface_print_via_proxy','iface_cashdrawer','iface_invoicing','state','sequence_id','session_ids'], [['id','=', self.get('pos_session').config_id[0]]] ); }).then(function(configs){ @@ -156,6 +155,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal self.iface_vkeyboard = !!pos_config.iface_vkeyboard; self.iface_self_checkout = !!pos_config.iface_self_checkout; self.iface_cashdrawer = !!pos_config.iface_cashdrawer; + self.iface_invoicing = !!pos_config.iface_invoicing; return self.fetch('stock.warehouse',[],[['id','=',pos_config.warehouse_id[0]]]); }).then(function(shops){ @@ -240,12 +240,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal } }, - // saves the order locally and try to send it to the backend. 'record' is a bizzarely defined JSON version of the Order - push_order: function(record) { - this.db.add_order(record); - this.flush(); - }, - //creates a new empty order and sets it as the current order add_new_order: function(){ var order = new module.Order({pos:this}); @@ -253,45 +247,161 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal this.set('selectedOrder', order); }, + // 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; + var order_id = this.db.add_order(order.export_as_JSON()); + var pushed = new $.Deferred(); + + this.set('nbr_pending_operations',self.db.get_orders().length); + + this.flush_mutex.exec(function(){ + var flushed = self._flush_all_orders(); + + flushed.always(function(){ + pushed.resolve(); + }); + + return flushed; + }); + 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.set('nbr_pending_operations',self.db.get_orders().length); + + 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_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.pos_widget.do_action('point_of_sale.pos_invoice_report',{additional_context:{ + active_ids:order_server_id, + }}); + invoiced.resolve(); + done.resolve(); + }); + + return done; + + }); + + return invoiced; + }, + // attemps to send all pending orders ( stored in the pos_db ) to the server, // and remove the successfully sent ones from the db once // it has been confirmed that they have been sent correctly. flush: function() { - //TODO make the mutex work - //this makes sure only one _int_flush is called at the same time - /* - return this.flush_mutex.exec(_.bind(function() { - return this._flush(0); - }, this)); - */ - this._flush(0); + var self = this; + var flushed = new $.Deferred(); + + this.flush_mutex.exec(function(){ + var done = new $.Deferred(); + + self._flush_all_orders() + .done( function(){ flushed.resolve();}) + .fail( function(){ flushed.reject(); }) + .always(function(){ done.resolve(); }); + + return done; + }); + + return flushed; }, - // attempts to send an order of index 'index' in the list of order to send. The index - // is used to skip orders that failed. do not call this method outside the mutex provided - // by flush() - _flush: function(index){ + + // attempts to send the locally stored order of id 'order_id' + // the sending is asynchronous and can take some time to decide if it is successful or not + // it is therefore important to only call this method from inside a mutex + // this method returns a deferred indicating wether the sending was successful or not + // there is a timeout parameter which is set to 2 seconds by default. + _flush_order: function(order_id, options){ + var self = this; + options = options || {}; + timeout = typeof options.timeout === 'number' ? options.timeout : 5000; + + var order = this.db.get_order(order_id); + order.to_invoice = options.to_invoice || false; + + if(!order){ + // flushing a non existing order always fails + return (new $.Deferred()).reject(); + } + + // 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 rpc = (new instance.web.Model('pos.order')).call('create_from_ui',[[order]],undefined,{shadow: !options.to_invoice, timeout:timeout}); + + rpc.fail(function(unused,event){ + // 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 order:',order); + }); + + rpc.done(function(){ + self.db.remove_order(order_id); + self.set('nbr_pending_operations',self.db.get_orders().length); + }); + + return rpc; + }, + + // attempts to send all the locally stored orders. As with _flush_order, it should only be + // called from within a mutex. + // this method returns a deferred that always succeeds when all orders have been tried to be sent, + // even if none of them could actually be sent. + _flush_all_orders: function(){ var self = this; var orders = this.db.get_orders(); - self.set('nbr_pending_operations',orders.length); + var tried_all = new $.Deferred(); - var order = orders[index]; - if(!order){ - return; + function rec_flush(index){ + if(index < orders.length){ + self._flush_order(orders[index].id).always(function(){ + rec_flush(index+1); + }) + }else{ + tried_all.resolve(); + } } - //try to push an order to the server - // shadow : true is to prevent a spinner to appear in case of timeout - (new instance.web.Model('pos.order')).call('create_from_ui',[[order]],undefined,{ shadow:true }) - .fail(function(unused, event){ - //don't show error popup if it fails - event.preventDefault(); - console.error('Failed to send order:',order); - self._flush(index+1); - }) - .done(function(){ - //remove from db if success - self.db.remove_order(order.id); - self._flush(index); - }); + rec_flush(0); + + return tried_all; }, scan_product: function(parsed_ean){ @@ -590,11 +700,12 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal module.Order = Backbone.Model.extend({ initialize: function(attributes){ Backbone.Model.prototype.initialize.apply(this, arguments); + this.uid = this.generateUniqueId(); this.set({ creationDate: new Date(), orderLines: new module.OrderlineCollection(), paymentLines: new module.PaymentlineCollection(), - name: "Order " + this.generateUniqueId(), + name: "Order " + this.uid, client: null, }); this.pos = attributes.pos; @@ -770,7 +881,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal currency: this.pos.get('currency'), }; }, - exportAsJSON: function() { + export_as_JSON: function() { var orderLines, paymentLines; orderLines = []; (this.get('orderLines')).each(_.bind( function(item) { @@ -789,8 +900,9 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal lines: orderLines, statement_ids: paymentLines, pos_session_id: this.pos.get('pos_session').id, - partner_id: this.pos.get('client') ? this.pos.get('client').id : undefined, + partner_id: this.get_client() ? this.get_client().id : false, user_id: this.pos.get('cashier') ? this.pos.get('cashier').id : this.pos.get('user').id, + uid: this.uid, }; }, getSelectedLine: function(){ diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 7a01732c353..e0a8ba4b71c 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -434,6 +434,14 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa template:'ErrorNegativePricePopupWidget', }); + module.ErrorNoClientPopupWidget = module.ErrorPopupWidget.extend({ + template: 'ErrorNoClientPopupWidget', + }); + + module.ErrorInvoiceTransferPopupWidget = module.ErrorPopupWidget.extend({ + template: 'ErrorInvoiceTransferPopupWidget', + }); + module.ScaleInviteScreenWidget = module.ScreenWidget.extend({ template:'ScaleInviteScreenWidget', @@ -452,7 +460,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa clearInterval(this.intervalID); self.pos_widget.screen_selector.set_current_screen(self.next_screen); } - },500); + },100); this.add_action_button({ label: _t('Back'), @@ -507,7 +515,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa self.weight = weight; self.renderElement(); } - },200); + },100); }, renderElement: function(){ var self = this; @@ -640,7 +648,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa var cashregister = selfCheckoutRegisters[0] || self.pos.get('cashRegisters').models[0]; currentOrder.addPaymentLine(cashregister); - self.pos.push_order(currentOrder.exportAsJSON()) + self.pos.push_order(currentOrder) currentOrder.destroy(); self.pos.proxy.transaction_end(); self.pos_widget.screen_selector.set_current_screen(self.next_screen); @@ -808,19 +816,42 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this._super(); var self = this; - this.add_action_button({ + var print_button = this.add_action_button({ label: _t('Print'), icon: '/point_of_sale/static/src/img/icons/png48/printer.png', click: function(){ self.print(); }, }); - this.add_action_button({ + var finish_button = this.add_action_button({ label: _t('Next Order'), icon: '/point_of_sale/static/src/img/icons/png48/go-next.png', click: function() { self.finishOrder(); }, }); window.print(); + + // THIS IS THE HACK OF THE CENTURY + // + // 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. + + finish_button.set_disabled(true); + setTimeout(function(){ + finish_button.set_disabled(false); + }, 2000); }, print: function() { window.print(); @@ -870,15 +901,15 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.set_numpad_state(this.pos_widget.numpad.state); - this.back_button = this.add_action_button({ + this.add_action_button({ label: _t('Back'), icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png', click: function(){ self.pos_widget.screen_selector.set_current_screen(self.back_screen); }, }); - - this.validate_button = this.add_action_button({ + + this.add_action_button({ label: _t('Validate'), name: 'validation', icon: '/point_of_sale/static/src/img/icons/png48/validate.png', @@ -886,6 +917,17 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa self.validateCurrentOrder(); }, }); + + if(this.pos.iface_invoicing){ + this.add_action_button({ + label: 'Invoice', + name: 'invoice', + icon: '/point_of_sale/static/src/img/icons/png48/invoice.png', + click: function(){ + self.validateCurrentOrder({invoice: true}); + }, + }); + } this.updatePaymentSummary(); this.line_refocus(); @@ -898,15 +940,44 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa back: function() { this.pos_widget.screen_selector.set_current_screen(self.back_screen); }, - validateCurrentOrder: function() { + validateCurrentOrder: function(options) { + var self = this; + options = options || {}; + var currentOrder = this.pos.get('selectedOrder'); - this.pos.push_order(currentOrder.exportAsJSON()) - if(this.pos.iface_print_via_proxy){ - this.pos.proxy.print_receipt(currentOrder.export_for_printing()); - this.pos.get('selectedOrder').destroy(); //finish order and go back to scan screen + + if(options.invoice){ + // deactivate the validation button while we try to send the order + this.pos_widget.action_bar.set_button_disabled('validation',true); + this.pos_widget.action_bar.set_button_disabled('invoice',true); + + var invoiced = this.pos.push_and_invoice_order(currentOrder); + + invoiced.fail(function(error){ + if(error === 'error-no-client'){ + self.pos_widget.screen_selector.show_popup('error-no-client'); + }else{ + self.pos_widget.screen_selector.show_popup('error-invoice-transfer'); + } + self.pos_widget.action_bar.set_button_disabled('validation',false); + self.pos_widget.action_bar.set_button_disabled('invoice',false); + }); + + invoiced.done(function(){ + self.pos_widget.action_bar.set_button_disabled('validation',false); + self.pos_widget.action_bar.set_button_disabled('invoice',false); + self.pos.get('selectedOrder').destroy(); + }); + }else{ - this.pos_widget.screen_selector.set_current_screen(this.next_screen); + this.pos.push_order(currentOrder) + if(this.pos.iface_print_via_proxy){ + this.pos.proxy.print_receipt(currentOrder.export_for_printing()); + this.pos.get('selectedOrder').destroy(); //finish order and go back to scan screen + }else{ + this.pos_widget.screen_selector.set_current_screen(this.next_screen); + } } }, bindPaymentLineEvents: function() { @@ -985,6 +1056,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa if(this.pos_widget.action_bar){ this.pos_widget.action_bar.set_button_disabled('validation', remaining > 0.000001); + this.pos_widget.action_bar.set_button_disabled('invoice', remaining > 0.000001); } }, set_numpad_state: function(numpadState) { diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index 5d56943de73..911d8cd321b 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -838,6 +838,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa instance.web.blockUI(); this.pos = new module.PosModel(this.session); + this.pos.pos_widget = this; this.pos_widget = this; //So that pos_widget's childs have pos_widget set automatically this.numpad_visible = true; @@ -952,6 +953,12 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa this.error_negative_price_popup = new module.ErrorNegativePricePopupWidget(this, {}); this.error_negative_price_popup.appendTo($('.point-of-sale')); + this.error_no_client_popup = new module.ErrorNoClientPopupWidget(this, {}); + this.error_no_client_popup.appendTo($('.point-of-sale')); + + this.error_invoice_transfer_popup = new module.ErrorInvoiceTransferPopupWidget(this, {}); + this.error_invoice_transfer_popup.appendTo($('.point-of-sale')); + // -------- Misc --------- this.notification = new module.SynchNotificationWidget(this,{}); @@ -1013,6 +1020,8 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa 'error-session': this.error_session_popup, 'error-negative-price': this.error_negative_price_popup, 'choose-receipt': this.choose_receipt_popup, + 'error-no-client': this.error_no_client_popup, + 'error-invoice-transfer': this.error_invoice_transfer_popup, }, default_client_screen: 'welcome', default_cashier_screen: 'products', diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 3da76ec12d8..e28ea1c06b0 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -336,11 +336,11 @@ - @@ -361,6 +361,33 @@ + + + + + + + +