Merge lp:~openerp-dev/openobject-addons/7.0-pos-backport-from-trunk into lp:openobject-addons/7.0
- 7.0-pos-backport-from-trunk
- Merge into 7.0
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
OpenERP Core Team | Pending | ||
Review via email: mp+186834@code.launchpad.net |
Commit message
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
- 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
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' |
198 | Binary 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' |
200 | Binary 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"> |