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 @@
The scanned product was not recognized Please wait, a cashier is on the way
-
-
-
@@ -361,6 +361,33 @@
+
+
+
+
An anonymous order cannot be invoiced
+
+
+
+
+
+
+
+
+
The Order could not be sent to the server for invoicing. Invoices cannot be generated
+ in offline mode. Please check your internet connection and try again.