Merge lp:~openerp-dev/openobject-addons/7.0-pos-backport-from-trunk into lp:openobject-addons/7.0

Proposed by van der Essen Frédéric (OpenERP)
Status: Needs review
Proposed branch: lp:~openerp-dev/openobject-addons/7.0-pos-backport-from-trunk
Merge into: lp:openobject-addons/7.0
Diff against target: 1892 lines (+694/-323)
13 files modified
point_of_sale/__openerp__.py (+1/-0)
point_of_sale/controllers/main.py (+12/-0)
point_of_sale/static/src/css/pos.css (+32/-13)
point_of_sale/static/src/js/db.js (+22/-3)
point_of_sale/static/src/js/devices.js (+135/-31)
point_of_sale/static/src/js/main.js (+3/-1)
point_of_sale/static/src/js/models.js (+126/-49)
point_of_sale/static/src/js/screens.js (+109/-92)
point_of_sale/static/src/js/tests.js (+92/-0)
point_of_sale/static/src/js/widget_base.js (+2/-2)
point_of_sale/static/src/js/widget_scrollbar.js (+9/-13)
point_of_sale/static/src/js/widgets.js (+106/-102)
point_of_sale/static/src/xml/pos.xml (+45/-17)
To merge this branch: bzr merge lp:~openerp-dev/openobject-addons/7.0-pos-backport-from-trunk
Reviewer Review Type Date Requested Status
OpenERP Core Team Pending
Review via email: mp+186834@code.launchpad.net

Description of the change

This is a Backport from all the fixes and improvements made to the point of sale client in trunk since the last few month. The invoicing support has been left out as it would introduce database changes.

Changes:
--------

- Two New Proxy Methods :
test_connection <- used to test the presence of the proxy
log <- used to log posted orders via the proxy, so that they can be retrieved in the event of a browser localStorage destruction.

The other changes are in the JS/CSS/Templates and do not impact the public APIs:

- Fixed memory leaks
- Improved product list performance
- Improved weighting scale reactivity
- Skip the weighting invite screen in cashier mode for faster scaling
- Prevent parallel calls to the proxy, and prevent call reordering
- Improved parallel order workflow. (You can now Destroy them, and they are kept aside while you process new orders)
- Removed hover effects that would interact badly with the mouse cursor on resistive screens
- Lots of other small fixes

To post a comment you must log in.
9463. By van der Essen Frédéric (OpenERP)

[MERGE] point_of_sale: fix for the closing confirmation dialog in self checkout mode

Unmerged revisions

9463. By van der Essen Frédéric (OpenERP)

[MERGE] point_of_sale: fix for the closing confirmation dialog in self checkout mode

9462. By van der Essen Frédéric (OpenERP)

[BACKPORT] point_of_sale: backporting the point of sale client from trunk. Invoicing support has not been included

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'point_of_sale/__openerp__.py'
2--- point_of_sale/__openerp__.py 2013-01-31 18:41:41 +0000
3+++ point_of_sale/__openerp__.py 2013-09-23 15:13:26 +0000
4@@ -98,6 +98,7 @@
5 'static/src/js/widgets.js',
6 'static/src/js/devices.js',
7 'static/src/js/screens.js',
8+ 'static/src/js/tests.js',
9 'static/src/js/main.js',
10 ],
11 'css': [
12
13=== modified file 'point_of_sale/controllers/main.py'
14--- point_of_sale/controllers/main.py 2012-11-29 22:26:45 +0000
15+++ point_of_sale/controllers/main.py 2013-09-23 15:13:26 +0000
16@@ -6,6 +6,8 @@
17
18 from openerp.addons.web.controllers.main import manifest_list, module_boot, html_template
19
20+_logger = logging.getLogger(__name__)
21+
22 class PointOfSaleController(openerp.addons.web.http.Controller):
23 _cp_path = '/pos'
24
25@@ -167,4 +169,14 @@
26 print 'print_pdf_invoice' + str(pdfinvoice)
27 return
28
29+ @openerp.addons.web.http.jsonrequest
30+ def test_connection(self, request):
31+ print 'test_connection'
32+
33+ @openerp.addons.web.http.jsonrequest
34+ def log(self, request, arguments):
35+ _logger.info(' '.join(str(v) for v in arguments))
36+
37+
38+
39
40
41=== modified file 'point_of_sale/static/src/css/pos.css'
42--- point_of_sale/static/src/css/pos.css 2013-09-12 15:54:09 +0000
43+++ point_of_sale/static/src/css/pos.css 2013-09-23 15:13:26 +0000
44@@ -21,6 +21,10 @@
45 user-select: none;
46 }
47
48+.point-of-sale .oe_hidden{
49+ display: none !important;
50+}
51+
52 .point-of-sale ul, .point-of-sale li {
53 margin: 0;
54 padding: 0;
55@@ -175,10 +179,9 @@
56 background: linear-gradient(#b2b3d7, #7f82ac);
57 }
58
59-.point-of-sale #rightheader button.neworder-button {
60+.point-of-sale #rightheader button.square{
61 width: 32px;
62 margin-left:4px;
63- margin-right:4px;
64 }
65
66 .point-of-sale div#order-selector {
67@@ -186,12 +189,20 @@
68 }
69 .point-of-sale ol#orders {
70 display: inline;
71+ margin-left: 8px;
72 }
73 .point-of-sale li.order-selector-button {
74 display: inline;
75 }
76 .point-of-sale li.selected-order button {
77 font-weight: 900;
78+ background: #7174A8 !important;
79+ color: rgb(236, 237, 255) !important;
80+ text-shadow: 0px 1px rgba(0, 0, 0, 0.31);
81+ -webkit-box-shadow: 0px 1px 2px rgb(63, 66, 139) inset;
82+ -moz-box-shadow: 0px 1px 2px rgb(63, 66, 139) inset;
83+ -ms-box-shadow: 0px 1px 2px rgb(63, 66, 139) inset;
84+ box-shadow: 0px 1px 2px rgb(63, 66, 139) inset;
85 }
86
87 /* c) The session buttons */
88@@ -215,7 +226,7 @@
89 .point-of-sale #rightheader .header-button:last-child{
90 border-left: 1px solid #3a3a3a;
91 }
92-.point-of-sale #rightheader .header-button:hover{
93+.point-of-sale #rightheader .header-button:active{
94 background: rgba(0,0,0,0.2);
95 text-shadow: #000 0px 0px 3px;
96 color:#EEE;
97@@ -291,10 +302,15 @@
98 display: inline-block;
99 text-align: center;
100 vertical-align: top;
101+ width: 205px;
102+ max-height: 232px;
103+ overflow-y: auto;
104+ overflow-x: hidden;
105 }
106 .point-of-sale #paypad button {
107 height: 50px;
108- width: 208px;
109+ display: block;
110+ width: 100%;
111 margin: 0px -6px 4px -2px;
112 font-weight: bold;
113 vertical-align: middle;
114@@ -302,7 +318,10 @@
115 border-top: 1px solid #efefef;
116 font-size: 14px;
117 }
118-.point-of-sale #paypad button:hover, .point-of-sale #numpad button:hover, .point-of-sale #numpad .selected-mode, .point-of-sale .popup button:hover {
119+.point-of-sale #paypad button:active,
120+.point-of-sale #numpad button:active,
121+.point-of-sale #numpad .selected-mode,
122+.point-of-sale .popup button:active{
123 border: none;
124 color: white;
125 background: #7f82ac;
126@@ -502,7 +521,7 @@
127 -moz-box-shadow: 0px 2px 2px rgba(0,0,0, 0.1);
128 box-shadow: 0px 2px 2px rgba(0,0,0, 0.1);
129 }
130-.point-of-sale .category-simple-button:hover {
131+.point-of-sale .category-simple-button:active{
132 color: white;
133 background: #7f82ac;
134 border: 1px solid #7f82ac;
135@@ -1040,13 +1059,13 @@
136 -moz-transition: background 250ms ease-in-out;
137 transition: background 250ms ease-in-out;
138 }
139-.point-of-sale .order .orderline:hover{
140+.point-of-sale .order .orderline:active{
141 background: rgba(140,143,183,0.05);
142 -webkit-transition: background 50ms ease-in-out;
143 -moz-transition: background 50ms ease-in-out;
144 transition: background 50ms ease-in-out;
145 }
146-.point-of-sale .order .orderline.empty:hover{
147+.point-of-sale .order .orderline.empty:active{
148 background: transparent;
149 cursor: default;
150 }
151@@ -1148,7 +1167,7 @@
152 .point-of-sale .pos-actionbar .button .icon{
153 margin-top: 10px;
154 }
155-.point-of-sale .pos-actionbar .button:hover {
156+.point-of-sale .pos-actionbar .button:active{
157 color: white;
158 background: #7f82ac;
159 border: 1px solid #7f82ac;
160@@ -1165,7 +1184,7 @@
161 .point-of-sale .pos-actionbar .button.disabled *{
162 opacity: 0.5;
163 }
164-.point-of-sale .pos-actionbar .button.disabled:hover{
165+.point-of-sale .pos-actionbar .button.disabled:active{
166 border: 1px solid #cacaca;
167 border-radius: 4px;
168 color: #555;
169@@ -1234,7 +1253,7 @@
170 display: block;
171 cursor:pointer;
172 }
173-.point-of-sale .debug-widget .button:hover{
174+.point-of-sale .debug-widget .button:active{
175 background: rgba(96,21,177,0.45);
176 }
177 .point-of-sale .debug-widget input{
178@@ -1322,7 +1341,7 @@
179 -moz-box-shadow: 0px 2px 2px rgba(0,0,0, 0.3);
180 box-shadow: 0px 2px 2px rgba(0,0,0, 0.3);
181 }
182-.point-of-sale .popup .button:hover {
183+.point-of-sale .popup .button:active{
184 color: white;
185 background: #7f82ac;
186 border: 1px solid #7f82ac;
187@@ -1390,7 +1409,7 @@
188 -moz-transition: all 250ms ease-in-out;
189 transition: all 250ms ease-in-out;
190 }
191-.point-of-sale .scrollbar .button:hover{
192+.point-of-sale .scrollbar .button:active{
193 text-shadow: rgba(255,255,255,0.8) 0px 0px 15px;
194 }
195 .point-of-sale .scrollbar .button.disabled{
196
197=== added file 'point_of_sale/static/src/img/minus.png'
198Binary files point_of_sale/static/src/img/minus.png 1970-01-01 00:00:00 +0000 and point_of_sale/static/src/img/minus.png 2013-09-23 15:13:26 +0000 differ
199=== added file 'point_of_sale/static/src/img/plus.png'
200Binary files point_of_sale/static/src/img/plus.png 1970-01-01 00:00:00 +0000 and point_of_sale/static/src/img/plus.png 2013-09-23 15:13:26 +0000 differ
201=== modified file 'point_of_sale/static/src/js/db.js'
202--- point_of_sale/static/src/js/db.js 2013-09-12 14:12:08 +0000
203+++ point_of_sale/static/src/js/db.js 2013-09-23 15:13:26 +0000
204@@ -270,11 +270,21 @@
205 return results;
206 },
207 add_order: function(order){
208- var last_id = this.load('last_order_id',0);
209+ var order_id = order.uid;
210 var orders = this.load('orders',[]);
211- orders.push({id: last_id + 1, data: order});
212- this.save('last_order_id',last_id+1);
213+
214+ // if the order was already stored, we overwrite its data
215+ for(var i = 0, len = orders.length; i < len; i++){
216+ if(orders[i].id === order_id){
217+ orders[i].data = order;
218+ this.save('orders',orders);
219+ return order_id;
220+ }
221+ }
222+
223+ orders.push({id: order_id, data: order});
224 this.save('orders',orders);
225+ return order_id;
226 },
227 remove_order: function(order_id){
228 var orders = this.load('orders',[]);
229@@ -286,5 +296,14 @@
230 get_orders: function(){
231 return this.load('orders',[]);
232 },
233+ get_order: function(order_id){
234+ var orders = this.get_orders();
235+ for(var i = 0, len = orders.length; i < len; i++){
236+ if(orders[i].id === order_id){
237+ return orders[i];
238+ }
239+ }
240+ return undefined;
241+ },
242 });
243 }
244
245=== modified file 'point_of_sale/static/src/js/devices.js'
246--- point_of_sale/static/src/js/devices.js 2013-09-18 15:33:47 +0000
247+++ point_of_sale/static/src/js/devices.js 2013-09-23 15:13:26 +0000
248@@ -1,16 +1,92 @@
249
250 function openerp_pos_devices(instance,module){ //module is instance.point_of_sale
251
252+ // the JobQueue schedules a sequence of 'jobs'. each job is
253+ // a function returning a deferred. the queue waits for each job to finish
254+ // before launching the next. Each job can also be scheduled with a delay.
255+ // the is used to prevent parallel requests to the proxy.
256+
257+ module.JobQueue = function(){
258+ var queue = [];
259+ var running = false;
260+ var scheduled_end_time = 0;
261+ var end_of_queue = (new $.Deferred()).resolve();
262+ var stoprepeat = false;
263+
264+ var run = function(){
265+ if(end_of_queue.state() === 'resolved'){
266+ end_of_queue = new $.Deferred();
267+ }
268+ if(queue.length > 0){
269+ running = true;
270+ var job = queue[0];
271+ if(!job.opts.repeat || stoprepeat){
272+ queue.shift();
273+ stoprepeat = false;
274+ }
275+
276+ // the time scheduled for this job
277+ scheduled_end_time = (new Date()).getTime() + (job.opts.duration || 0);
278+
279+ // we run the job and put in def when it finishes
280+ var def = job.fun() || (new $.Deferred()).resolve();
281+
282+ // we don't care if a job fails ...
283+ def.always(function(){
284+ // we run the next job after the scheduled_end_time, even if it finishes before
285+ setTimeout(function(){
286+ run();
287+ }, Math.max(0, scheduled_end_time - (new Date()).getTime()) );
288+ });
289+ }else{
290+ running = false;
291+ scheduled_end_time = 0;
292+ end_of_queue.resolve();
293+ }
294+ };
295+
296+ // adds a job to the schedule.
297+ // opts : {
298+ // duration : the job is guaranteed to finish no quicker than this (milisec)
299+ // repeat : if true, the job will be endlessly repeated
300+ // important : if true, the scheduled job cannot be canceled by a queue.clear()
301+ // }
302+ this.schedule = function(fun, opts){
303+ queue.push({fun:fun, opts:opts || {}});
304+ if(!running){
305+ run();
306+ }
307+ }
308+
309+ // remove all jobs from the schedule (except the ones marked as important)
310+ this.clear = function(){
311+ queue = _.filter(queue,function(job){job.opts.important === true});
312+ };
313+
314+ // end the repetition of the current job
315+ this.stoprepeat = function(){
316+ stoprepeat = true;
317+ };
318+
319+ // returns a deferred that resolves when all scheduled
320+ // jobs have been run.
321+ // ( jobs added after the call to this method are considered as well )
322+ this.finished = function(){
323+ return end_of_queue;
324+ }
325+
326+ };
327+
328 // this object interfaces with the local proxy to communicate to the various hardware devices
329 // connected to the Point of Sale. As the communication only goes from the POS to the proxy,
330 // methods are used both to signal an event, and to fetch information.
331
332 module.ProxyDevice = instance.web.Class.extend({
333 init: function(options){
334+ var self = this;
335 options = options || {};
336 url = options.url || 'http://localhost:8069';
337
338- this.weight = 0;
339 this.weighting = false;
340 this.debug_weight = 0;
341 this.use_debug_weight = false;
342@@ -25,26 +101,37 @@
343 };
344 this.custom_payment_status = this.default_payment_status;
345
346+ this.notifications = {};
347+ this.bypass_proxy = false;
348+
349 this.connection = new instance.web.JsonRPC();
350 this.connection.setup(url);
351 this.connection.session_id = _.uniqueId('posproxy');
352- this.bypass_proxy = false;
353- this.notifications = {};
354+ this.test_connection();
355+ window.proxy = this;
356
357 },
358+ close: function(){
359+ this.connection.destroy();
360+ },
361 message : function(name,params){
362- var ret = new $.Deferred();
363 var callbacks = this.notifications[name] || [];
364 for(var i = 0; i < callbacks.length; i++){
365 callbacks[i](params);
366 }
367-
368- this.connection.rpc('/pos/' + name, params || {}).done(function(result) {
369- ret.resolve(result);
370- }).fail(function(error) {
371- ret.reject(error);
372- });
373- return ret;
374+ if(this.connected){
375+ return this.connection.rpc('/pos/' + name, params || {});
376+ }else{
377+ return (new $.Deferred()).reject();
378+ }
379+ },
380+ test_connection: function(){
381+ var self = this;
382+ this.connected = true;
383+ return this.message('test_connection').fail(function(){
384+ self.connected = false;
385+ console.error('Could not connect to the Proxy');
386+ });
387 },
388
389 // this allows the client to be notified when a proxy call is made. The notification
390@@ -55,6 +142,8 @@
391 }
392 this.notifications[name].push(callback);
393 },
394+
395+
396
397 //a product has been scanned and recognized with success
398 // ean is a parsed ean object
399@@ -80,13 +169,32 @@
400
401 //the client is starting to weight
402 weighting_start: function(){
403+ var ret = new $.Deferred();
404 if(!this.weighting){
405 this.weighting = true;
406- if(!this.bypass_proxy){
407- this.weight = 0;
408- return this.message('weighting_start');
409- }
410- }
411+ this.message('weighting_start').always(function(){
412+ ret.resolve();
413+ });
414+ }else{
415+ console.error('Weighting already started!!!');
416+ ret.resolve();
417+ }
418+ return ret;
419+ },
420+
421+ // the client has finished weighting products
422+ weighting_end: function(){
423+ var ret = new $.Deferred();
424+ if(this.weighting){
425+ this.weighting = false;
426+ this.message('weighting_end').always(function(){
427+ ret.resolve();
428+ });
429+ }else{
430+ console.error('Weighting already ended !!!');
431+ ret.resolve();
432+ }
433+ return ret;
434 },
435
436 //returns the weight on the scale.
437@@ -94,17 +202,14 @@
438 // and a weighting_end()
439 weighting_read_kg: function(){
440 var self = this;
441+ var ret = new $.Deferred();
442 this.message('weighting_read_kg',{})
443- .done(function(weight){
444- if(self.weighting){
445- if(self.use_debug_weight){
446- self.weight = self.debug_weight;
447- }else{
448- self.weight = weight;
449- }
450- }
451+ .then(function(weight){
452+ ret.resolve(self.use_debug_weight ? self.debug_weight : weight);
453+ }, function(){ //failed to read weight
454+ ret.resolve(self.use_debug_weight ? self.debug_weight : 0.0);
455 });
456- return this.weight;
457+ return ret;
458 },
459
460 // sets a custom weight, ignoring the proxy returned value.
461@@ -119,12 +224,6 @@
462 this.debug_weight = 0;
463 },
464
465- // the client has finished weighting products
466- weighting_end: function(){
467- this.weight = 0;
468- this.weighting = false;
469- this.message('weighting_end');
470- },
471
472 // the pos asks the client to pay 'price' units
473 payment_request: function(price){
474@@ -237,6 +336,11 @@
475 return this.message('print_receipt',{receipt: receipt});
476 },
477
478+ // asks the proxy to log some information, as with the debug.log you can provide several arguments.
479+ log: function(){
480+ return this.message('log',{'arguments': _.toArray(arguments)});
481+ },
482+
483 // asks the proxy to print an invoice in pdf form ( used to print invoices generated by the server )
484 print_pdf_invoice: function(pdfinvoice){
485 return this.message('print_pdf_invoice',{pdfinvoice: pdfinvoice});
486
487=== modified file 'point_of_sale/static/src/js/main.js'
488--- point_of_sale/static/src/js/main.js 2012-11-29 22:26:45 +0000
489+++ point_of_sale/static/src/js/main.js 2013-09-23 15:13:26 +0000
490@@ -16,10 +16,12 @@
491 openerp_pos_scrollbar(instance,module); // import pos_scrollbar_widget.js
492
493 openerp_pos_screens(instance,module); // import pos_screens.js
494+
495+ openerp_pos_devices(instance,module); // import pos_devices.js
496
497 openerp_pos_widgets(instance,module); // import pos_widgets.js
498
499- openerp_pos_devices(instance,module); // import pos_devices.js
500+ openerp_pos_tests(instance,module); // import pos_tests.js
501
502 instance.web.client_actions.add('pos.ui', 'instance.point_of_sale.PosWidget');
503 };
504
505=== modified file 'point_of_sale/static/src/js/models.js'
506--- point_of_sale/static/src/js/models.js 2013-09-18 12:28:36 +0000
507+++ point_of_sale/static/src/js/models.js 2013-09-23 15:13:26 +0000
508@@ -24,6 +24,7 @@
509
510 this.barcode_reader = new module.BarcodeReader({'pos': this}); // used to read barcodes
511 this.proxy = new module.ProxyDevice(); // used to communicate to the hardware devices via a local proxy
512+ this.proxy_queue = new module.JobQueue(); // used to prevent parallels communications to the proxy
513 this.db = new module.PosLS(); // a database used to store the products and categories
514 this.db.clear('products','categories');
515 this.debug = jQuery.deparam(jQuery.param.querystring()).debug !== undefined; //debug mode
516@@ -56,7 +57,9 @@
517 'selectedOrder': null,
518 });
519
520- this.get('orders').bind('remove', function(){ self.on_removed_order(); });
521+ this.get('orders').bind('remove', function(order,_unused_,options){
522+ self.on_removed_order(order,options.index,options.reason);
523+ });
524
525 // We fetch the backend data on the server asynchronously. this is done only when the pos user interface is launched,
526 // Any change on this data made on the server is thus not reflected on the point of sale until it is relaunched.
527@@ -72,6 +75,14 @@
528 });
529 },
530
531+ // releases ressources holds by the model at the end of life of the posmodel
532+ destroy: function(){
533+ // FIXME, should wait for flushing, return a deferred to indicate successfull destruction
534+ // this.flush();
535+ this.proxy.close();
536+ this.barcode_reader.disconnect();
537+ },
538+
539 // helper function to load data from the server
540 fetch: function(model, fields, domain, ctx){
541 return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all()
542@@ -138,10 +149,10 @@
543
544 return self.fetch(
545 'pos.config',
546- ['name','journal_ids','shop_id','journal_id',
547+ ['name','journal_ids','shop_id','journal_id','pricelist_id',
548 'iface_self_checkout', 'iface_led', 'iface_cashdrawer',
549 'iface_payment_terminal', 'iface_electronic_scale', 'iface_barscan', 'iface_vkeyboard',
550- 'iface_print_via_proxy','iface_cashdrawer','state','sequence_id','session_ids'],
551+ 'iface_print_via_proxy','iface_cashdrawer','iface_invoicing','state','sequence_id','session_ids'],
552 [['id','=', self.get('pos_session').config_id[0]]]
553 );
554 }).then(function(configs){
555@@ -178,7 +189,7 @@
556 ['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13', 'default_code',
557 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description'],
558 [['sale_ok','=',true],['available_in_pos','=',true]],
559- {pricelist: self.get('shop').pricelist_id[0]} // context for price
560+ {pricelist: self.get('pricelist').id} // context for price
561 );
562 }).then(function(products){
563 self.db.add_products(products);
564@@ -235,20 +246,17 @@
565
566 // this is called when an order is removed from the order collection. It ensures that there is always an existing
567 // order and a valid selected order
568- on_removed_order: function(removed_order){
569- if( this.get('orders').isEmpty()){
570+ on_removed_order: function(removed_order,index,reason){
571+ if(reason === 'abandon' && this.get('orders').size() > 0){
572+ // when we intentionally remove an unfinished order, and there is another existing one
573+ this.set({'selectedOrder' : this.get('orders').at(index) || this.get('orders').last()});
574+ }else{
575+ // when the order was automatically removed after completion,
576+ // or when we intentionally delete the only concurrent order
577 this.add_new_order();
578- }else{
579- this.set({ selectedOrder: this.get('orders').last() });
580 }
581 },
582
583- // saves the order locally and try to send it to the backend. 'record' is a bizzarely defined JSON version of the Order
584- push_order: function(record) {
585- this.db.add_order(record);
586- this.flush();
587- },
588-
589 //creates a new empty order and sets it as the current order
590 add_new_order: function(){
591 var order = new module.Order({pos:this});
592@@ -256,45 +264,112 @@
593 this.set('selectedOrder', order);
594 },
595
596+ //removes the current order
597+ delete_current_order: function(){
598+ this.get('selectedOrder').destroy({'reason':'abandon'});
599+ },
600+
601+ // saves the order locally and try to send it to the backend.
602+ // it returns a deferred that succeeds after having tried to send the order and all the other pending orders.
603+ push_order: function(order) {
604+ var self = this;
605+ this.proxy.log('push_order',order.export_as_JSON());
606+ var order_id = this.db.add_order(order.export_as_JSON());
607+ var pushed = new $.Deferred();
608+
609+ this.set('nbr_pending_operations',self.db.get_orders().length);
610+
611+ this.flush_mutex.exec(function(){
612+ var flushed = self._flush_all_orders();
613+
614+ flushed.always(function(){
615+ pushed.resolve();
616+ });
617+
618+ return flushed;
619+ });
620+ return pushed;
621+ },
622+
623 // attemps to send all pending orders ( stored in the pos_db ) to the server,
624 // and remove the successfully sent ones from the db once
625 // it has been confirmed that they have been sent correctly.
626 flush: function() {
627- //TODO make the mutex work
628- //this makes sure only one _int_flush is called at the same time
629- /*
630- return this.flush_mutex.exec(_.bind(function() {
631- return this._flush(0);
632- }, this));
633- */
634- this._flush(0);
635- },
636- // attempts to send an order of index 'index' in the list of order to send. The index
637- // is used to skip orders that failed. do not call this method outside the mutex provided
638- // by flush()
639- _flush: function(index){
640+ var self = this;
641+ var flushed = new $.Deferred();
642+
643+ this.flush_mutex.exec(function(){
644+ var done = new $.Deferred();
645+
646+ self._flush_all_orders()
647+ .done( function(){ flushed.resolve();})
648+ .fail( function(){ flushed.reject(); })
649+ .always(function(){ done.resolve(); });
650+
651+ return done;
652+ });
653+
654+ return flushed;
655+ },
656+
657+ // attempts to send the locally stored order of id 'order_id'
658+ // the sending is asynchronous and can take some time to decide if it is successful or not
659+ // it is therefore important to only call this method from inside a mutex
660+ // this method returns a deferred indicating wether the sending was successful or not
661+ // there is a timeout parameter which is set to 2 seconds by default.
662+ _flush_order: function(order_id, options){
663+ var self = this;
664+ options = options || {};
665+ timeout = typeof options.timeout === 'number' ? options.timeout : 5000;
666+
667+ var order = this.db.get_order(order_id);
668+ order.to_invoice = options.to_invoice || false;
669+
670+ if(!order){
671+ // flushing a non existing order always fails
672+ return (new $.Deferred()).reject();
673+ }
674+
675+ // we try to send the order. shadow prevents a spinner if it takes too long. (unless we are sending an invoice,
676+ // then we want to notify the user that we are waiting on something )
677+ var rpc = (new instance.web.Model('pos.order')).call('create_from_ui',[[order]],undefined,{shadow: !options.to_invoice, timeout:timeout});
678+
679+ rpc.fail(function(unused,event){
680+ // prevent an error popup creation by the rpc failure
681+ // we want the failure to be silent as we send the orders in the background
682+ event.preventDefault();
683+ console.error('Failed to send order:',order);
684+ });
685+
686+ rpc.done(function(){
687+ self.db.remove_order(order_id);
688+ self.set('nbr_pending_operations',self.db.get_orders().length);
689+ });
690+
691+ return rpc;
692+ },
693+
694+ // attempts to send all the locally stored orders. As with _flush_order, it should only be
695+ // called from within a mutex.
696+ // this method returns a deferred that always succeeds when all orders have been tried to be sent,
697+ // even if none of them could actually be sent.
698+ _flush_all_orders: function(){
699 var self = this;
700 var orders = this.db.get_orders();
701- self.set('nbr_pending_operations',orders.length);
702+ var tried_all = new $.Deferred();
703
704- var order = orders[index];
705- if(!order){
706- return;
707+ function rec_flush(index){
708+ if(index < orders.length){
709+ self._flush_order(orders[index].id).always(function(){
710+ rec_flush(index+1);
711+ })
712+ }else{
713+ tried_all.resolve();
714+ }
715 }
716- //try to push an order to the server
717- // shadow : true is to prevent a spinner to appear in case of timeout
718- (new instance.web.Model('pos.order')).call('create_from_ui',[[order]],undefined,{ shadow:true })
719- .fail(function(unused, event){
720- //don't show error popup if it fails
721- event.preventDefault();
722- console.error('Failed to send order:',order);
723- self._flush(index+1);
724- })
725- .done(function(){
726- //remove from db if success
727- self.db.remove_order(order.id);
728- self._flush(index);
729- });
730+ rec_flush(0);
731+
732+ return tried_all;
733 },
734
735 scan_product: function(parsed_ean){
736@@ -326,7 +401,7 @@
737
738 module.Product = Backbone.Model.extend({
739 get_image_url: function(){
740- return instance.session.url('/web/binary/image', {model: 'product.product', field: 'image', id: this.get('id')});
741+ return instance.session.url('/web/binary/image', {model: 'product.product', field: 'image_medium', id: this.get('id')});
742 },
743 });
744
745@@ -593,11 +668,12 @@
746 module.Order = Backbone.Model.extend({
747 initialize: function(attributes){
748 Backbone.Model.prototype.initialize.apply(this, arguments);
749+ this.uid = this.generateUniqueId();
750 this.set({
751 creationDate: new Date(),
752 orderLines: new module.OrderlineCollection(),
753 paymentLines: new module.PaymentlineCollection(),
754- name: "Order " + this.generateUniqueId(),
755+ name: "Order " + this.uid,
756 client: null,
757 });
758 this.pos = attributes.pos;
759@@ -773,7 +849,7 @@
760 currency: this.pos.get('currency'),
761 };
762 },
763- exportAsJSON: function() {
764+ export_as_JSON: function() {
765 var orderLines, paymentLines;
766 orderLines = [];
767 (this.get('orderLines')).each(_.bind( function(item) {
768@@ -792,8 +868,9 @@
769 lines: orderLines,
770 statement_ids: paymentLines,
771 pos_session_id: this.pos.get('pos_session').id,
772- partner_id: this.get('client') ? this.get('client').id : undefined,
773+ partner_id: this.get_client() ? this.get_client().id : false,
774 user_id: this.pos.get('cashier') ? this.pos.get('cashier').id : this.pos.get('user').id,
775+ uid: this.uid,
776 };
777 },
778 getSelectedLine: function(){
779
780=== modified file 'point_of_sale/static/src/js/screens.js'
781--- point_of_sale/static/src/js/screens.js 2013-09-12 15:13:34 +0000
782+++ point_of_sale/static/src/js/screens.js 2013-09-23 15:13:26 +0000
783@@ -253,7 +253,7 @@
784
785 this.hidden = false;
786 if(this.$el){
787- this.$el.show();
788+ this.$el.removeClass('oe_hidden');
789 }
790
791 if(this.pos_widget.action_bar.get_button_count() > 0){
792@@ -271,19 +271,19 @@
793 });
794
795 var self = this;
796- var cashier_mode = this.pos_widget.screen_selector.get_user_mode() === 'cashier';
797+ this.cashier_mode = this.pos_widget.screen_selector.get_user_mode() === 'cashier';
798
799- this.pos_widget.set_numpad_visible(this.show_numpad && cashier_mode);
800+ this.pos_widget.set_numpad_visible(this.show_numpad && this.cashier_mode);
801 this.pos_widget.set_leftpane_visible(this.show_leftpane);
802- this.pos_widget.set_left_action_bar_visible(this.show_leftpane && !cashier_mode);
803- this.pos_widget.set_cashier_controls_visible(cashier_mode);
804+ this.pos_widget.set_left_action_bar_visible(this.show_leftpane && !this.cashier_mode);
805+ this.pos_widget.set_cashier_controls_visible(this.cashier_mode);
806
807- if(cashier_mode && this.pos.iface_self_checkout){
808+ if(this.cashier_mode && this.pos.iface_self_checkout){
809 this.pos_widget.client_button.show();
810 }else{
811 this.pos_widget.client_button.hide();
812 }
813- if(cashier_mode){
814+ if(this.cashier_mode){
815 this.pos_widget.close_button.show();
816 }else{
817 this.pos_widget.close_button.hide();
818@@ -314,7 +314,7 @@
819 hide: function(){
820 this.hidden = true;
821 if(this.$el){
822- this.$el.hide();
823+ this.$el.addClass('oe_hidden');
824 }
825 },
826
827@@ -326,7 +326,7 @@
828 this._super();
829 if(this.hidden){
830 if(this.$el){
831- this.$el.hide();
832+ this.$el.addClass('oe_hidden');
833 }
834 }
835 },
836@@ -335,7 +335,7 @@
837 module.PopUpWidget = module.PosBaseWidget.extend({
838 show: function(){
839 if(this.$el){
840- this.$el.show();
841+ this.$el.removeClass('oe_hidden');
842 }
843 },
844 /* called before hide, when a popup is closed */
845@@ -345,7 +345,7 @@
846 * pos instantiation, so you don't want to do anything fancy in here */
847 hide: function(){
848 if(this.$el){
849- this.$el.hide();
850+ this.$el.addClass('oe_hidden');
851 }
852 },
853 });
854@@ -434,6 +434,14 @@
855 template:'ErrorNegativePricePopupWidget',
856 });
857
858+ module.ErrorNoClientPopupWidget = module.ErrorPopupWidget.extend({
859+ template: 'ErrorNoClientPopupWidget',
860+ });
861+
862+ module.ErrorInvoiceTransferPopupWidget = module.ErrorPopupWidget.extend({
863+ template: 'ErrorInvoiceTransferPopupWidget',
864+ });
865+
866 module.ScaleInviteScreenWidget = module.ScreenWidget.extend({
867 template:'ScaleInviteScreenWidget',
868
869@@ -443,30 +451,35 @@
870 show: function(){
871 this._super();
872 var self = this;
873-
874- self.pos.proxy.weighting_start();
875-
876- this.intervalID = setInterval(function(){
877- var weight = self.pos.proxy.weighting_read_kg();
878- if(weight > 0.001){
879- clearInterval(this.intervalID);
880- self.pos_widget.screen_selector.set_current_screen(self.next_screen);
881- }
882- },500);
883+ var queue = this.pos.proxy_queue;
884+
885+ queue.schedule(function(){
886+ return self.pos.proxy.weighting_start();
887+ },{ important: true });
888+
889+ queue.schedule(function(){
890+ return self.pos.proxy.weighting_read_kg().then(function(weight){
891+ if(weight > 0.001){
892+ self.pos_widget.screen_selector.set_current_screen(self.next_screen);
893+ }
894+ });
895+ },{duration: 100, repeat: true});
896
897 this.add_action_button({
898 label: _t('Back'),
899 icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
900 click: function(){
901- clearInterval(this.intervalID);
902 self.pos_widget.screen_selector.set_current_screen(self.previous_screen);
903 }
904 });
905 },
906 close: function(){
907 this._super();
908- clearInterval(this.intervalID);
909- this.pos.proxy.weighting_end();
910+ var self = this;
911+ this.pos.proxy_queue.clear();
912+ this.pos.proxy_queue.schedule(function(){
913+ return self.pos.proxy.weighting_end();
914+ },{ important: true });
915 },
916 });
917
918@@ -478,9 +491,11 @@
919
920 show: function(){
921 this._super();
922+ var self = this;
923+ var queue = this.pos.proxy_queue;
924+
925+ this.set_weight(0);
926 this.renderElement();
927- var self = this;
928-
929
930 this.add_action_button({
931 label: _t('Back'),
932@@ -499,14 +514,16 @@
933 },
934 });
935
936- this.pos.proxy.weighting_start();
937- this.intervalID = setInterval(function(){
938- var weight = self.pos.proxy.weighting_read_kg();
939- if(weight != self.weight){
940- self.weight = weight;
941- self.renderElement();
942- }
943- },200);
944+ queue.schedule(function(){
945+ return self.pos.proxy.weighting_start()
946+ },{ important: true });
947+
948+ queue.schedule(function(){
949+ return self.pos.proxy.weighting_read_kg().then(function(weight){
950+ self.set_weight(weight);
951+ });
952+ },{duration:50, repeat: true});
953+
954 },
955 renderElement: function(){
956 var self = this;
957@@ -525,8 +542,7 @@
958 }
959 },
960 order_product: function(){
961- var weight = this.pos.proxy.weighting_read_kg();
962- this.pos.get('selectedOrder').addProduct(this.get_product(),{ quantity:weight });
963+ this.pos.get('selectedOrder').addProduct(this.get_product(),{ quantity: this.weight });
964 },
965 get_product_name: function(){
966 var product = this.get_product();
967@@ -536,54 +552,24 @@
968 var product = this.get_product();
969 return (product ? product.get('price') : 0) || 0;
970 },
971- get_product_weight: function(){
972- return this.weight || 0;
973+ set_weight: function(weight){
974+ this.weight = weight;
975+ this.$('.js-weight').text(this.get_product_weight_string());
976+ },
977+ get_product_weight_string: function(){
978+ return (this.weight || 0).toFixed(3) + ' Kg';
979 },
980 close: function(){
981+ var self = this;
982 this._super();
983- clearInterval(this.intervalID);
984- this.pos.proxy.weighting_end();
985+
986+ this.pos.proxy_queue.clear();
987+ this.pos.proxy_queue.schedule(function(){
988+ self.pos.proxy.weighting_end();
989+ },{ important: true });
990 },
991 });
992
993- // the JobQueue schedules a sequence of 'jobs'. each job is
994- // a function returning a deferred. the queue waits for each job to finish
995- // before launching the next. Each job can also be scheduled with a delay.
996- // the queue jobqueue is used to prevent parallel requests to the payment terminal.
997-
998- module.JobQueue = function(){
999- var queue = [];
1000- var running = false;
1001- var run = function(){
1002- if(queue.length > 0){
1003- running = true;
1004- var job = queue.shift();
1005- setTimeout(function(){
1006- var def = job.fun();
1007- if(def){
1008- def.done(run);
1009- }else{
1010- run();
1011- }
1012- },job.delay || 0);
1013- }else{
1014- running = false;
1015- }
1016- };
1017-
1018- // adds a job to the schedule.
1019- this.schedule = function(fun, delay){
1020- queue.push({fun:fun, delay:delay});
1021- if(!running){
1022- run();
1023- }
1024- }
1025-
1026- // remove all jobs from the schedule
1027- this.clear = function(){
1028- queue = [];
1029- };
1030- };
1031
1032 module.ClientPaymentScreenWidget = module.ScreenWidget.extend({
1033 template:'ClientPaymentScreenWidget',
1034@@ -639,7 +625,7 @@
1035
1036 var cashregister = selfCheckoutRegisters[0] || self.pos.get('cashRegisters').models[0];
1037 currentOrder.addPaymentLine(cashregister);
1038- self.pos.push_order(currentOrder.exportAsJSON())
1039+ self.pos.push_order(currentOrder)
1040 currentOrder.destroy();
1041 self.pos.proxy.transaction_end();
1042 self.pos_widget.screen_selector.set_current_screen(self.next_screen);
1043@@ -705,7 +691,7 @@
1044 barcode_client_action: function(ean){
1045 this.pos.proxy.transaction_start();
1046 this._super(ean);
1047- $('.goodbye-message').hide();
1048+ $('.goodbye-message').addClass('oe_hidden');
1049 this.pos_widget.screen_selector.show_popup('choose-receipt');
1050 },
1051
1052@@ -717,14 +703,14 @@
1053 label: _t('Help'),
1054 icon: '/point_of_sale/static/src/img/icons/png48/help.png',
1055 click: function(){
1056- $('.goodbye-message').css({opacity:1}).hide();
1057+ $('.goodbye-message').css({opacity:1}).addClass('oe_hidden');
1058 self.help_button_action();
1059 },
1060 });
1061
1062- $('.goodbye-message').css({opacity:1}).show();
1063+ $('.goodbye-message').css({opacity:1}).removeClass('oe_hidden');
1064 setTimeout(function(){
1065- $('.goodbye-message').animate({opacity:0},500,'swing',function(){$('.goodbye-message').hide();});
1066+ $('.goodbye-message').animate({opacity:0},500,'swing',function(){$('.goodbye-message').addClass('oe_hidden');});
1067 },5000);
1068 },
1069 });
1070@@ -732,7 +718,8 @@
1071 module.ProductScreenWidget = module.ScreenWidget.extend({
1072 template:'ProductScreenWidget',
1073
1074- scale_screen: 'scale_invite',
1075+ scale_screen: 'scale',
1076+ client_scale_screen : 'scale_invite',
1077 client_next_screen: 'client_payment',
1078
1079 show_numpad: true,
1080@@ -746,7 +733,7 @@
1081 this.product_list_widget = new module.ProductListWidget(this,{
1082 click_product_action: function(product){
1083 if(product.get('to_weight') && self.pos.iface_electronic_scale){
1084- self.pos_widget.screen_selector.set_current_screen(self.scale_screen, {product: product});
1085+ self.pos_widget.screen_selector.set_current_screen( self.cashier_mode ? self.scale_screen : self.client_scale_screen, {product: product});
1086 }else{
1087 self.pos.get('selectedOrder').addProduct(product);
1088 }
1089@@ -807,19 +794,42 @@
1090 this._super();
1091 var self = this;
1092
1093- this.add_action_button({
1094+ var print_button = this.add_action_button({
1095 label: _t('Print'),
1096 icon: '/point_of_sale/static/src/img/icons/png48/printer.png',
1097 click: function(){ self.print(); },
1098 });
1099
1100- this.add_action_button({
1101+ var finish_button = this.add_action_button({
1102 label: _t('Next Order'),
1103 icon: '/point_of_sale/static/src/img/icons/png48/go-next.png',
1104 click: function() { self.finishOrder(); },
1105 });
1106
1107 this.print();
1108+
1109+ // THIS IS THE HACK OF THE CENTURY
1110+ //
1111+ // The problem is that in chrome the print() is asynchronous and doesn't
1112+ // execute until all rpc are finished. So it conflicts with the rpc used
1113+ // to send the orders to the backend, and the user is able to go to the next
1114+ // screen before the printing dialog is opened. The problem is that what's
1115+ // printed is whatever is in the page when the dialog is opened and not when it's called,
1116+ // and so you end up printing the product list instead of the receipt...
1117+ //
1118+ // Fixing this would need a re-architecturing
1119+ // of the code to postpone sending of orders after printing.
1120+ //
1121+ // But since the print dialog also blocks the other asynchronous calls, the
1122+ // button enabling in the setTimeout() is blocked until the printing dialog is
1123+ // closed. But the timeout has to be big enough or else it doesn't work
1124+ // 2 seconds is the same as the default timeout for sending orders and so the dialog
1125+ // should have appeared before the timeout... so yeah that's not ultra reliable.
1126+
1127+ finish_button.set_disabled(true);
1128+ setTimeout(function(){
1129+ finish_button.set_disabled(false);
1130+ }, 2000);
1131 },
1132 print: function() {
1133 window.print();
1134@@ -869,15 +879,20 @@
1135
1136 this.set_numpad_state(this.pos_widget.numpad.state);
1137
1138- this.back_button = this.add_action_button({
1139+ this.add_action_button({
1140 label: _t('Back'),
1141 icon: '/point_of_sale/static/src/img/icons/png48/go-previous.png',
1142 click: function(){
1143+ _.each(self.paymentlinewidgets, function(widget){
1144+ if( widget.payment_line.get_amount() === 0 ){
1145+ widget.payment_line.destroy();
1146+ }
1147+ });
1148 self.pos_widget.screen_selector.set_current_screen(self.back_screen);
1149 },
1150 });
1151-
1152- this.validate_button = this.add_action_button({
1153+
1154+ this.add_action_button({
1155 label: _t('Validate'),
1156 name: 'validation',
1157 icon: '/point_of_sale/static/src/img/icons/png48/validate.png',
1158@@ -885,7 +900,7 @@
1159 self.validateCurrentOrder();
1160 },
1161 });
1162-
1163+
1164 this.updatePaymentSummary();
1165 this.line_refocus();
1166 },
1167@@ -898,9 +913,11 @@
1168 this.pos_widget.screen_selector.set_current_screen(self.back_screen);
1169 },
1170 validateCurrentOrder: function() {
1171+ var self = this;
1172+
1173 var currentOrder = this.pos.get('selectedOrder');
1174
1175- this.pos.push_order(currentOrder.exportAsJSON())
1176+ this.pos.push_order(currentOrder)
1177 if(this.pos.iface_print_via_proxy){
1178 this.pos.proxy.print_receipt(currentOrder.export_for_printing());
1179 this.pos.get('selectedOrder').destroy(); //finish order and go back to scan screen
1180
1181=== added file 'point_of_sale/static/src/js/tests.js'
1182--- point_of_sale/static/src/js/tests.js 1970-01-01 00:00:00 +0000
1183+++ point_of_sale/static/src/js/tests.js 2013-09-23 15:13:26 +0000
1184@@ -0,0 +1,92 @@
1185+function openerp_pos_tests(instance, module){ //module is instance.point_of_sale
1186+
1187+ // Various UI Tests to measure performance and memory leaks.
1188+ module.UiTester = function(){
1189+ var running = false;
1190+ var queue = new module.JobQueue();
1191+
1192+ // stop the currently running test
1193+ this.stop = function(){
1194+ queue.clear();
1195+ };
1196+
1197+ // randomly switch product categories
1198+ this.category_switch = function(interval){
1199+ queue.schedule(function(){
1200+ var breadcrumbs = $('.breadcrumb a');
1201+ var categories = $('li.category-button');
1202+ if(categories.length > 0){
1203+ var rnd = Math.floor(Math.random()*categories.length);
1204+ categories.eq(rnd).click();
1205+ }else{
1206+ var rnd = Math.floor(Math.random()*breadcrumbs.length);
1207+ breadcrumbs.eq(rnd).click();
1208+ }
1209+ },{repeat:true, duration:interval});
1210+ };
1211+
1212+ // randomly order products then resets the order
1213+ this.order_products = function(interval){
1214+
1215+ queue.schedule(function(){
1216+ var def = new $.Deferred();
1217+ var order_queue = new module.JobQueue();
1218+ var order_size = 1 + Math.floor(Math.random()*10);
1219+
1220+ while(order_size--){
1221+ order_queue.schedule(function(){
1222+ var products = $('.product a');
1223+ if(products.length > 0){
1224+ var rnd = Math.floor(Math.random()*products.length);
1225+ products.eq(rnd).click();
1226+ }
1227+ },{duration:250});
1228+ }
1229+ order_queue.finished().then(function(){
1230+ $('.deleteorder-button').click();
1231+ def.resolve();
1232+ });
1233+ return def;
1234+ },{repeat:true, duration: interval});
1235+
1236+ };
1237+
1238+ // makes a complete product order cycle ( print via proxy must be activated, and scale deactivated )
1239+ this.full_order_cycle = function(interval){
1240+ queue.schedule(function(){
1241+ var def = new $.Deferred();
1242+ var order_queue = new module.JobQueue();
1243+ var order_size = 1 + Math.floor(Math.random()*50);
1244+
1245+ while(order_size--){
1246+ order_queue.schedule(function(){
1247+ var products = $('.product a');
1248+ if(products.length > 0){
1249+ var rnd = Math.floor(Math.random()*products.length);
1250+ products.eq(rnd).click();
1251+ }
1252+ },{duration:250});
1253+ }
1254+ order_queue.schedule(function(){
1255+ $('.paypad-button:first').click();
1256+ },{duration:250});
1257+ order_queue.schedule(function(){
1258+ $('.paymentline-amount input:first').val(10000);
1259+ $('.paymentline-amount input:first').keyup();
1260+ },{duration:250});
1261+ order_queue.schedule(function(){
1262+ $('.pos-actionbar-button-list .button:eq(2)').click();
1263+ },{duration:250});
1264+ order_queue.schedule(function(){
1265+ def.resolve();
1266+ });
1267+ return def;
1268+ },{repeat: true, duration: interval});
1269+ };
1270+ };
1271+
1272+ if(jQuery.deparam(jQuery.param.querystring()).debug !== undefined){
1273+ window.pos_test_ui = new module.UiTester();
1274+ }
1275+
1276+}
1277
1278=== modified file 'point_of_sale/static/src/js/widget_base.js'
1279--- point_of_sale/static/src/js/widget_base.js 2013-01-28 17:18:00 +0000
1280+++ point_of_sale/static/src/js/widget_base.js 2013-09-23 15:13:26 +0000
1281@@ -41,10 +41,10 @@
1282
1283 },
1284 show: function(){
1285- this.$el.show();
1286+ this.$el.removeClass('oe_hidden');
1287 },
1288 hide: function(){
1289- this.$el.hide();
1290+ this.$el.addClass('oe_hidden');
1291 },
1292 });
1293
1294
1295=== modified file 'point_of_sale/static/src/js/widget_scrollbar.js'
1296--- point_of_sale/static/src/js/widget_scrollbar.js 2012-11-29 22:26:45 +0000
1297+++ point_of_sale/static/src/js/widget_scrollbar.js 2013-09-23 15:13:26 +0000
1298@@ -81,7 +81,7 @@
1299 $(window).unbind('resize',this.resize_handler);
1300 $(window).bind('resize',this.resize_handler);
1301
1302- this.target().unbind('mousewheel',this.target_mousweheel_handler);
1303+ this.target().unbind('mousewheel',this.target_mousewheel_handler);
1304 this.target().bind('mousewheel',this.target_mousewheel_handler);
1305
1306 // because the rendering is asynchronous we must wait for the next javascript update
1307@@ -93,22 +93,18 @@
1308 },0);
1309 },
1310
1311- // binds the window resize and the target scrolling events.
1312- // it is good advice not to bind these multiple_times
1313- bind_events:function(){
1314- $(window).resize(function(){
1315- });
1316- this.target().bind('mousewheel',function(event,delta){
1317- self.scroll(delta*self.wheel_step);
1318- });
1319+ destroy: function(){
1320+ $(window).unbind('resize',this.resize_handler);
1321+ this.target().unbind('mousewheel',this.target_mousewheel_handler);
1322+ this._super();
1323 },
1324
1325 // shows the scrollbar. if animated is true, it will do it in an animated fashion
1326 show: function(animated){ //FIXME: animated show and hide don't work ... ?
1327 if(animated){
1328- this.$el.show().animate({'width':'48px'}, 500, 'swing');
1329+ this.$el.removeClass('oe_hidden').animate({'width':'48px'}, 500, 'swing');
1330 }else{
1331- this.$el.show().css('width','48px');
1332+ this.$el.removeClass('oe_hidden').css('width','48px');
1333 }
1334 this.on_show(this);
1335 },
1336@@ -117,9 +113,9 @@
1337 hide: function(animated){
1338 var self = this;
1339 if(animated){
1340- this.$el.animate({'width':'0px'}, 500, 'swing', function(){ self.$el.hide();});
1341+ this.$el.animate({'width':'0px'}, 500, 'swing', function(){ self.$el.addClass('oe_hidden');});
1342 }else{
1343- this.$el.hide().css('width','0px');
1344+ this.$el.addClass('oe_hidden').css('width','0px');
1345 }
1346 this.on_hide(this);
1347 },
1348
1349=== modified file 'point_of_sale/static/src/js/widgets.js'
1350--- point_of_sale/static/src/js/widgets.js 2013-09-18 13:42:08 +0000
1351+++ point_of_sale/static/src/js/widgets.js 2013-09-23 15:13:26 +0000
1352@@ -62,10 +62,10 @@
1353 start: function() {
1354 this.state.bind('change:mode', this.changedMode, this);
1355 this.changedMode();
1356- this.$el.find('button#numpad-backspace').click(_.bind(this.clickDeleteLastChar, this));
1357- this.$el.find('button#numpad-minus').click(_.bind(this.clickSwitchSign, this));
1358- this.$el.find('button.number-char').click(_.bind(this.clickAppendNewChar, this));
1359- this.$el.find('button.mode-button').click(_.bind(this.clickChangeMode, this));
1360+ this.$el.find('.numpad-backspace').click(_.bind(this.clickDeleteLastChar, this));
1361+ this.$el.find('.numpad-minus').click(_.bind(this.clickSwitchSign, this));
1362+ this.$el.find('.number-char').click(_.bind(this.clickAppendNewChar, this));
1363+ this.$el.find('.mode-button').click(_.bind(this.clickChangeMode, this));
1364 },
1365 clickDeleteLastChar: function() {
1366 return this.state.deleteLastChar();
1367@@ -137,17 +137,15 @@
1368 this.model = options.model;
1369 this.order = options.order;
1370
1371- this.model.bind('change', _.bind( function() {
1372- this.refresh();
1373- }, this));
1374- },
1375- click_handler: function() {
1376- this.order.selectLine(this.model);
1377- this.trigger('order_line_selected');
1378+ this.model.bind('change', this.refresh, this);
1379 },
1380 renderElement: function() {
1381+ var self = this;
1382 this._super();
1383- this.$el.click(_.bind(this.click_handler, this));
1384+ this.$el.click(function(){
1385+ self.order.selectLine(this.model);
1386+ self.trigger('order_line_selected');
1387+ });
1388 if(this.model.is_selected()){
1389 this.$el.addClass('selected');
1390 }
1391@@ -156,6 +154,10 @@
1392 this.renderElement();
1393 this.trigger('order_line_refreshed');
1394 },
1395+ destroy: function(){
1396+ this.model.unbind('change',this.refresh,this);
1397+ this._super();
1398+ },
1399 });
1400
1401 module.OrderWidget = module.PosBaseWidget.extend({
1402@@ -189,8 +191,6 @@
1403 }else if( mode === 'price'){
1404 order.getSelectedLine().set_unit_price(val);
1405 }
1406- } else {
1407- this.pos.get('selectedOrder').destroy();
1408 }
1409 },
1410 change_selected_order: function() {
1411@@ -283,27 +283,6 @@
1412 },
1413 });
1414
1415- module.ProductWidget = module.PosBaseWidget.extend({
1416- template: 'ProductWidget',
1417- init: function(parent, options) {
1418- this._super(parent,options);
1419- this.model = options.model;
1420- this.model.attributes.weight = options.weight;
1421- this.next_screen = options.next_screen; //when a product is clicked, this screen is set
1422- this.click_product_action = options.click_product_action;
1423- },
1424- // returns the url of the product thumbnail
1425- renderElement: function() {
1426- this._super();
1427- this.$('img').replaceWith(this.pos_widget.image_cache.get_image(this.model.get_image_url()));
1428- var self = this;
1429- $("a", this.$el).click(function(e){
1430- if(self.click_product_action){
1431- self.click_product_action(self.model);
1432- }
1433- });
1434- },
1435- });
1436
1437 module.PaymentlineWidget = module.PosBaseWidget.extend({
1438 template: 'PaymentlineWidget',
1439@@ -320,6 +299,14 @@
1440 this.payment_line.set_amount(amount);
1441 }
1442 },
1443+ checkAmount: function(e){
1444+ if (e.which !== 0 && e.charCode !== 0) {
1445+ if(isNaN(String.fromCharCode(e.charCode))){
1446+ return (String.fromCharCode(e.charCode) === "." && e.currentTarget.value.toString().split(".").length < 2)?true:false;
1447+ }
1448+ }
1449+ return true
1450+ },
1451 changedAmount: function() {
1452 if (this.amount !== this.payment_line.get_amount()){
1453 this.renderElement();
1454@@ -329,7 +316,8 @@
1455 var self = this;
1456 this.name = this.payment_line.get_cashregister().get('journal_id')[1];
1457 this._super();
1458- this.$('input').keyup(function(event){
1459+ this.$('input').keypress(_.bind(this.checkAmount, this))
1460+ .keyup(function(event){
1461 self.changeAmount(event);
1462 });
1463 this.$('.delete-payment-line').click(function() {
1464@@ -351,32 +339,30 @@
1465 var self = this;
1466
1467 this.order = options.order;
1468- this.order.bind('destroy',function(){ self.destroy(); });
1469- this.order.bind('change', function(){ self.renderElement(); });
1470- this.pos.bind('change:selectedOrder', _.bind( function(pos) {
1471- var selectedOrder;
1472- selectedOrder = pos.get('selectedOrder');
1473- if (this.order === selectedOrder) {
1474- this.setButtonSelected();
1475- }
1476- }, this));
1477+ this.order.bind('destroy',this.destroy, this );
1478+ this.order.bind('change', this.renderElement, this );
1479+ this.pos.bind('change:selectedOrder', this.renderElement,this );
1480 },
1481 renderElement:function(){
1482 this._super();
1483- this.$('button.select-order').off('click').click(_.bind(this.selectOrder, this));
1484- this.$('button.close-order').off('click').click(_.bind(this.closeOrder, this));
1485+ var self = this;
1486+ this.$el.click(function(){
1487+ self.selectOrder();
1488+ });
1489+ if( this.order === this.pos.get('selectedOrder') ){
1490+ this.$el.addClass('selected-order');
1491+ }
1492 },
1493 selectOrder: function(event) {
1494 this.pos.set({
1495 selectedOrder: this.order
1496 });
1497 },
1498- setButtonSelected: function() {
1499- $('.selected-order').removeClass('selected-order');
1500- this.$el.addClass('selected-order');
1501- },
1502- closeOrder: function(event) {
1503- this.order.destroy();
1504+ destroy: function(){
1505+ this.order.unbind('destroy', this.destroy, this);
1506+ this.order.unbind('change', this.renderElement, this);
1507+ this.pos.unbind('change:selectedOrder', this.renderElement, this);
1508+ this._super();
1509 },
1510 });
1511
1512@@ -420,9 +406,9 @@
1513 if(visible != this.visibility[element]){
1514 this.visibility[element] = !!visible;
1515 if(visible){
1516- this.$('.'+element).show();
1517+ this.$('.'+element).removeClass('oe_hidden');
1518 }else{
1519- this.$('.'+element).hide();
1520+ this.$('.'+element).addClass('oe_hidden');
1521 }
1522 }
1523 if(visible && action){
1524@@ -457,10 +443,10 @@
1525 return button;
1526 },
1527 show:function(){
1528- this.$el.show();
1529+ this.$el.removeClass('oe_hidden');
1530 },
1531 hide:function(){
1532- this.$el.hide();
1533+ this.$el.addClass('oe_hidden');
1534 },
1535 });
1536
1537@@ -499,7 +485,7 @@
1538 },
1539
1540 get_image_url: function(category){
1541- return instance.session.url('/web/binary/image', {model: 'pos.category', field: 'image', id: category.id});
1542+ return instance.session.url('/web/binary/image', {model: 'pos.category', field: 'image_medium', id: category.id});
1543 },
1544
1545 renderElement: function(){
1546@@ -622,25 +608,19 @@
1547 renderElement: function() {
1548 var self = this;
1549 this._super();
1550-
1551- // free subwidgets memory from previous renders
1552
1553- for(var i = 0, len = this.productwidgets.length; i < len; i++){
1554- this.productwidgets[i].destroy();
1555- }
1556- this.productwidgets = [];
1557 if(this.scrollbar){
1558 this.scrollbar.destroy();
1559 }
1560 var products = this.pos.get('products').models || [];
1561- for(var i = 0, len = products.length; i < len; i++){
1562- var product = new module.ProductWidget(self, {
1563- model: products[i],
1564- click_product_action: this.click_product_action,
1565- });
1566- this.productwidgets.push(product);
1567- product.appendTo(this.$('.product-list'));
1568- }
1569+
1570+ _.each(products,function(product,i){
1571+ var $product = $(QWeb.render('Product',{ widget:self, product: products[i] }));
1572+ $product.find('img').replaceWith(self.pos_widget.image_cache.get_image(products[i].get_image_url()));
1573+ $product.find('a').click(function(){ self.click_product_action(product); });
1574+ $product.appendTo(self.$('.product-list'));
1575+ });
1576+
1577 this.scrollbar = new module.ScrollbarWidget(this,{
1578 target_widget: this,
1579 target_selector: '.product-list-scroller',
1580@@ -698,11 +678,13 @@
1581 var self = this;
1582 this._super();
1583 if(this.action){
1584- this.$el.click(function(){ self.action(); });
1585+ this.$el.click(function(){
1586+ self.action();
1587+ });
1588 }
1589 },
1590- show: function(){ this.$el.show(); },
1591- hide: function(){ this.$el.hide(); },
1592+ show: function(){ this.$el.removeClass('oe_hidden'); },
1593+ hide: function(){ this.$el.addClass('oe_hidden'); },
1594 });
1595
1596 // The debug widget lets the user control and monitor the hardware and software status
1597@@ -843,6 +825,7 @@
1598 instance.web.blockUI();
1599
1600 this.pos = new module.PosModel(this.session);
1601+ this.pos.pos_widget = this;
1602 this.pos_widget = this; //So that pos_widget's childs have pos_widget set automatically
1603
1604 this.numpad_visible = true;
1605@@ -851,17 +834,24 @@
1606 this.leftpane_width = '440px';
1607 this.cashier_controls_visible = true;
1608 this.image_cache = new module.ImageCache(); // for faster products image display
1609+
1610 },
1611
1612 start: function() {
1613 var self = this;
1614 return self.pos.ready.done(function() {
1615+ $('.oe_tooltip').remove(); // remove tooltip from the start session button
1616+
1617 self.build_currency_template();
1618 self.renderElement();
1619
1620 self.$('.neworder-button').click(function(){
1621 self.pos.add_new_order();
1622 });
1623+
1624+ self.$('.deleteorder-button').click(function(){
1625+ self.pos.delete_current_order();
1626+ });
1627
1628 //when a new order is created, add an order button widget
1629 self.pos.get('orders').bind('add', function(new_order){
1630@@ -873,13 +863,12 @@
1631 new_order_button.selectOrder();
1632 }, self);
1633
1634- self.pos.get('orders').add(new module.Order({ pos: self.pos }));
1635+ self.pos.add_new_order();
1636
1637 self.build_widgets();
1638
1639 self.screen_selector.set_default_screen();
1640
1641- window.screen_selector = self.screen_selector;
1642
1643 self.pos.barcode_reader.connect();
1644
1645@@ -892,7 +881,7 @@
1646 }
1647
1648 instance.web.unblockUI();
1649- self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').hide();});
1650+ self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').addClass('oe_hidden');});
1651
1652 self.pos.flush();
1653
1654@@ -957,6 +946,12 @@
1655 this.error_negative_price_popup = new module.ErrorNegativePricePopupWidget(this, {});
1656 this.error_negative_price_popup.appendTo($('.point-of-sale'));
1657
1658+ this.error_no_client_popup = new module.ErrorNoClientPopupWidget(this, {});
1659+ this.error_no_client_popup.appendTo($('.point-of-sale'));
1660+
1661+ this.error_invoice_transfer_popup = new module.ErrorInvoiceTransferPopupWidget(this, {});
1662+ this.error_invoice_transfer_popup.appendTo($('.point-of-sale'));
1663+
1664 // -------- Misc ---------
1665
1666 this.notification = new module.SynchNotificationWidget(this,{});
1667@@ -987,7 +982,7 @@
1668
1669 this.close_button = new module.HeaderButtonWidget(this,{
1670 label: _t('Close'),
1671- action: function(){ self.try_close(); },
1672+ action: function(){ self.close(); },
1673 });
1674 this.close_button.appendTo(this.$('#rightheader'));
1675
1676@@ -1018,6 +1013,8 @@
1677 'error-session': this.error_session_popup,
1678 'error-negative-price': this.error_negative_price_popup,
1679 'choose-receipt': this.choose_receipt_popup,
1680+ 'error-no-client': this.error_no_client_popup,
1681+ 'error-invoice-transfer': this.error_invoice_transfer_popup,
1682 },
1683 default_client_screen: 'welcome',
1684 default_cashier_screen: 'products',
1685@@ -1073,11 +1070,11 @@
1686 if(visible !== this.leftpane_visible){
1687 this.leftpane_visible = visible;
1688 if(visible){
1689- $('#leftpane').show().animate({'width':this.leftpane_width},500,'swing');
1690+ $('#leftpane').removeClass('oe_hidden').animate({'width':this.leftpane_width},500,'swing');
1691 $('#rightpane').animate({'left':this.leftpane_width},500,'swing');
1692 }else{
1693 var leftpane = $('#leftpane');
1694- $('#leftpane').animate({'width':'0px'},500,'swing', function(){ leftpane.hide(); });
1695+ $('#leftpane').animate({'width':'0px'},500,'swing', function(){ leftpane.addClass('oe_hidden'); });
1696 $('#rightpane').animate({'left':'0px'},500,'swing');
1697 }
1698 }
1699@@ -1087,36 +1084,43 @@
1700 if(visible !== this.cashier_controls_visible){
1701 this.cashier_controls_visible = visible;
1702 if(visible){
1703- $('#loggedas').show();
1704- $('#rightheader').show();
1705+ $('#loggedas').removeClass('oe_hidden');
1706+ $('#rightheader').removeClass('oe_hidden');
1707 }else{
1708- $('#loggedas').hide();
1709- $('#rightheader').hide();
1710+ $('#loggedas').addClass('oe_hidden');
1711+ $('#rightheader').addClass('oe_hidden');
1712 }
1713 }
1714 },
1715- try_close: function() {
1716- var self = this;
1717- //TODO : do the close after the flush...
1718- self.pos.flush()
1719- self.close();
1720- },
1721 close: function() {
1722 var self = this;
1723- this.pos.barcode_reader.disconnect();
1724- return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_client_pos_menu']], ['res_id']).pipe(
1725- _.bind(function(res) {
1726- return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) {
1727- var action = result;
1728- action.context = _.extend(action.context || {}, {'cancel_action': {type: 'ir.actions.client', tag: 'reload'}});
1729- //self.destroy();
1730- this.do_action(action);
1731- }, this));
1732- }, this));
1733+
1734+ function close(){
1735+ return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_client_pos_menu']], ['res_id']).pipe(
1736+ _.bind(function(res) {
1737+ return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) {
1738+ var action = result;
1739+ action.context = _.extend(action.context || {}, {'cancel_action': {type: 'ir.actions.client', tag: 'reload'}});
1740+ //self.destroy();
1741+ this.do_action(action);
1742+ }, this));
1743+ }, self));
1744+ }
1745+
1746+ var draft_order = _.find( self.pos.get('orders').models, function(order){
1747+ return order.get('orderLines').length !== 0 && order.get('paymentLines').length === 0;
1748+ });
1749+ if(draft_order){
1750+ if (confirm(_t("Pending orders will be lost.\nAre you sure you want to leave this session?"))) {
1751+ return close();
1752+ }
1753+ }else{
1754+ return close();
1755+ }
1756 },
1757 destroy: function() {
1758+ this.pos.destroy();
1759 instance.webclient.set_content_full_screen(false);
1760- self.pos = undefined;
1761 this._super();
1762 }
1763 });
1764
1765=== modified file 'point_of_sale/static/src/xml/pos.xml'
1766--- point_of_sale/static/src/xml/pos.xml 2013-09-12 15:54:09 +0000
1767+++ point_of_sale/static/src/xml/pos.xml 2013-09-23 15:13:26 +0000
1768@@ -12,7 +12,8 @@
1769 </div>
1770 <div id="rightheader">
1771 <div id="order-selector">
1772- <button class="neworder-button">+</button>
1773+ <button class="neworder-button square"><img src='/point_of_sale/static/src/img/plus.png' /></button>
1774+ <button class="deleteorder-button square"><img src='/point_of_sale/static/src/img/minus.png' /></button>
1775 <ol id="orders"></ol>
1776 </div>
1777 <!-- here goes header buttons -->
1778@@ -93,10 +94,10 @@
1779 <button class="input-button number-char">9</button>
1780 <button class="mode-button" data-mode='price'>Price</button>
1781 <br />
1782- <button class="input-button" id="numpad-minus" >+/-</button>
1783+ <button class="input-button numpad-minus" >+/-</button>
1784 <button class="input-button number-char">0</button>
1785 <button class="input-button number-char">.</button>
1786- <button class="input-button" id="numpad-backspace">
1787+ <button class="input-button numpad-backspace">
1788 <img src="/point_of_sale/static/src/img/backspace.png" width="24" height="21" />
1789 </button>
1790 <br />
1791@@ -191,8 +192,9 @@
1792 <div class="display">
1793 <span class="weight">
1794 <p>
1795- <t t-esc="widget.get_product_weight().toFixed(3)" />
1796- Kg
1797+ <span class='js-weight'>
1798+ <t t-esc="widget.get_product_weight_string()" />
1799+ </span>
1800 </p>
1801 </span>
1802 <span class="product-name">
1803@@ -336,11 +338,11 @@
1804 <div class="modal-dialog">
1805 <div class="popup popup-help">
1806 <p class="message">The scanned product was not recognized<br /> Please wait, a cashier is on the way</p>
1807- </div>
1808- </div>
1809- <div class="footer">
1810- <div class="button">
1811- Ok
1812+ <div class="footer">
1813+ <div class="button">
1814+ Ok
1815+ </div>
1816+ </div>
1817 </div>
1818 </div>
1819 </t>
1820@@ -361,6 +363,33 @@
1821 </div>
1822 </t>
1823
1824+ <t t-name="ErrorNoClientPopupWidget">
1825+ <div class="modal-dialog">
1826+ <div class="popup popup-help">
1827+ <p class="message">An anonymous order cannot be invoiced</p>
1828+ <div class="footer">
1829+ <div class="button">
1830+ Ok
1831+ </div>
1832+ </div>
1833+ </div>
1834+ </div>
1835+ </t>
1836+
1837+ <t t-name="ErrorInvoiceTransferPopupWidget">
1838+ <div class="modal-dialog">
1839+ <div class="popup popup-help">
1840+ <p class="message">The Order could not be sent to the server for invoicing. Invoices cannot be generated
1841+ in offline mode. Please check your internet connection and try again.</p>
1842+ <div class="footer">
1843+ <div class="button">
1844+ Ok
1845+ </div>
1846+ </div>
1847+ </div>
1848+ </div>
1849+ </t>
1850+
1851 <t t-name="ErrorPopupWidget">
1852 <div class="modal-dialog">
1853 <div class="popup popup-help">
1854@@ -370,24 +399,24 @@
1855 </div>
1856 </t>
1857
1858- <t t-name="ProductWidget">
1859+ <t t-name="Product">
1860 <li class='product'>
1861 <a href="#">
1862 <div class="product-img">
1863 <img src='' /> <!-- the product thumbnail -->
1864- <t t-if="!widget.model.get('to_weight')">
1865+ <t t-if="!product.get('to_weight')">
1866 <span class="price-tag">
1867- <t t-esc="widget.format_currency(widget.model.get('price'))"/>
1868+ <t t-esc="widget.format_currency(product.get('price'))"/>
1869 </span>
1870 </t>
1871- <t t-if="widget.model.get('to_weight')">
1872+ <t t-if="product.get('to_weight')">
1873 <span class="price-tag">
1874- <t t-esc="widget.format_currency(widget.model.get('price'))+'/Kg'"/>
1875+ <t t-esc="widget.format_currency(product.get('price'))+'/Kg'"/>
1876 </span>
1877 </t>
1878 </div>
1879 <div class="product-name">
1880- <t t-esc="widget.model.get('name')"/>
1881+ <t t-esc="product.get('name')"/>
1882 </div>
1883 </a>
1884 </li>
1885@@ -538,7 +567,6 @@
1886 <button class="paypad-button" t-att-cash-register-id="widget.cashRegister.get('id')">
1887 <t t-esc="widget.cashRegister.get('journal').name"/>
1888 </button>
1889- <br />
1890 </t>
1891
1892 <t t-name="OrderButtonWidget">