diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index 9ee5b944718..9363afe01ea 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -483,7 +483,10 @@ class pos_order(osv.osv): #_logger.info("orders: %r", orders) order_ids = [] for tmp_order in orders: + to_invoice = tmp_order['to_invoice'] order = tmp_order['data'] + + order_id = self.create(cr, uid, { 'name': order['name'], 'user_id': order['user_id'] or False, @@ -522,6 +525,13 @@ class pos_order(osv.osv): order_ids.append(order_id) wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr) + + if to_invoice: + self.action_invoice(cr, uid, [order_id], context) + order_obj = self.browse(cr, uid, order_id, context) + wf_service = netsvc.LocalService('workflow') + wf_service.trg_validate(uid,'account.invoice', order_obj.invoice_id.id,'invoice_open',cr) + return order_ids def unlink(self, cr, uid, ids, context=None): @@ -859,6 +869,7 @@ class pos_order(osv.osv): inv_line_ref.create(cr, uid, inv_line, context=context) inv_ref.button_reset_taxes(cr, uid, [inv_id], context=context) wf_service.trg_validate(uid, 'pos.order', order.id, 'invoice', cr) + wf_service.trg_validate(uid, 'account.invoice', inv_id, 'validate', cr) if not inv_ids: return {} diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index c28c08950db..aad6ed8ca69 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -267,10 +267,19 @@ function openerp_pos_db(instance, module){ return results; }, add_order: function(order){ - var order_id = this.load('last_order_id',0) + 1; + 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('last_order_id', order_id); this.save('orders',orders); return order_id; }, diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 426bea9a9b7..ec4924c6068 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -262,36 +262,80 @@ 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. 'record' is a bizzarely defined JSON version of the Order - // it returns a deferred that succeeds or fail when the pushed order is successfully posted on the server, previously failed orders - // will try to be re-sent but don't make this method return false if they fail. ( So that in the unlikely case of an order making - // the server crash, the other orders can still be sent ) - // payment process ) - push_order: function(record) { + // 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(record); + 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(); - //first we try to push all orders (the one we added and the one that were not sent) - var tried_all = self._flush_all_orders(); - var done = new $.Deferred(); + flushed.always(function(){ + pushed.resolve(); + }); - tried_all.always(function(){ - // then we verify that the one we just added has been sent successfuly. - self._flush_order(order_id) - .done( function(){ pushed.resolve();}) - .fail( function(){ pushed.reject(); }) - .always( function(){ done.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 pushed; + return invoiced; }, // attemps to send all pending orders ( stored in the pos_db ) to the server, @@ -316,23 +360,30 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal }, // attempts to send the locally stored order of id 'order_id' - // the sending is asynchronous and can take a long time to decide if it is successful or not (60s) + // 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 - _flush_order: function(order_id){ + // 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 succeeds - return (new $.Deferred()).resolve(); + // 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. - var rpc = (new instance.web.Model('pos.order')).call('create_from_ui',[[order]],undefined,{shadow: true, timeout: 2000}); + // 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 + // 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); }); @@ -664,11 +715,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; @@ -844,7 +896,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) { @@ -865,6 +917,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal pos_session_id: this.pos.get('pos_session').id, 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 a99de66988d..6c96d51748b 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -433,6 +433,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', @@ -639,7 +647,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); @@ -807,19 +815,40 @@ 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: '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: '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 this is not reliable ... if the timeout is too slow it doesn't work + + finish_button.set_disabled(true); + setTimeout(function(){ + finish_button.set_disabled(false); + }, 2000); }, print: function() { window.print(); @@ -896,7 +925,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa }); this.updatePaymentSummary(); - this.line_refocus(); + this.line_refocus();this }, close: function(){ this._super(); @@ -907,18 +936,43 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa this.pos_widget.screen_selector.set_current_screen(self.back_screen); }, validateCurrentOrder: function(options) { + var self = this; options = options || {}; var currentOrder = this.pos.get('selectedOrder'); - this.pos.push_order(currentOrder.exportAsJSON()) + if(options.invoice){ - console.log('send invoice'); - }else 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 + // 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() { diff --git a/addons/point_of_sale/static/src/js/widgets.js b/addons/point_of_sale/static/src/js/widgets.js index cec48dbe72f..6a606652a8e 100644 --- a/addons/point_of_sale/static/src/js/widgets.js +++ b/addons/point_of_sale/static/src/js/widgets.js @@ -828,6 +828,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; @@ -942,6 +943,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,{}); @@ -1003,6 +1010,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 1dce368b9cb..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 @@ + + + + + + + +