~pexego/openobject-addons/6.1-pexego-new-addons

« back to all changes in this revision

Viewing changes to django_pos/static/js/pos.js

  • Committer: Santi Argueso (Pexego)
  • Date: 2013-08-28 18:19:09 UTC
  • mfrom: (14.1.14 6.1)
  • Revision ID: santiago@pexego.es-20130828181909-wds7dw2n9r44d1hb
MERGE

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
openerp.point_of_sale = function(db) {
2
 
    
3
 
    db.point_of_sale = {};
4
 
 
5
 
    var QWeb = db.web.qweb;
6
 
    var qweb_template = function(template) {
7
 
        return function(ctx) {
8
 
            return QWeb.render(template, _.extend({}, ctx,{
9
 
                'currency': pos.get('currency'),
10
 
                'format_amount': function(amount) {
11
 
                    if (pos.get('currency').position == 'after') {
12
 
                        return amount + ' ' + pos.get('currency').symbol;
13
 
                    } else {
14
 
                        return pos.get('currency').symbol + ' ' + amount;
15
 
                    }
16
 
                },
17
 
                }));
18
 
        };
19
 
    };
20
 
    var _t = db.web._t;
21
 
 
22
 
    /*
23
 
     Local store access. Read once from localStorage upon construction and persist on every change.
24
 
     There should only be one store active at any given time to ensure data consistency.
25
 
     */
26
 
    var Store = db.web.Class.extend({
27
 
        init: function() {
28
 
            this.data = {};
29
 
        },
30
 
        get: function(key, _default) {
31
 
            if (this.data[key] === undefined) {
32
 
                var stored = localStorage['oe_pos_' + key];
33
 
                if (stored)
34
 
                    this.data[key] = JSON.parse(stored);
35
 
                else
36
 
                    return _default;
37
 
            }
38
 
            return this.data[key];
39
 
        },
40
 
        set: function(key, value) {
41
 
            this.data[key] = value;
42
 
            localStorage['oe_pos_' + key] = JSON.stringify(value);
43
 
        },
44
 
    });
45
 
    /*
46
 
     Gets all the necessary data from the OpenERP web client (session, shop data etc.)
47
 
     */
48
 
    var Pos = Backbone.Model.extend({
49
 
        initialize: function(session, attributes) {
50
 
            Backbone.Model.prototype.initialize.call(this, attributes);
51
 
            this.store = new Store();
52
 
            this.ready = $.Deferred();
53
 
            this.flush_mutex = new $.Mutex();
54
 
            this.build_tree = _.bind(this.build_tree, this);
55
 
            this.session = session;
56
 
            var attributes = {
57
 
                'pending_operations': [],
58
 
                'currency': {symbol: '$', position: 'after'},
59
 
                'shop': {},
60
 
                'company': {},
61
 
                'user': {},
62
 
            };
63
 
            _.each(attributes, _.bind(function(def, attr) {
64
 
                var to_set = {};
65
 
                to_set[attr] = this.store.get(attr, def);
66
 
                this.set(to_set);
67
 
                this.bind('change:' + attr, _.bind(function(unused, val) {
68
 
                    this.store.set(attr, val);
69
 
                }, this));
70
 
            }, this));
71
 
            $.when(this.fetch('pos.category', ['name', 'parent_id', 'child_id']),
72
 
                this.fetch('product.product', ['name', 'list_price', 'pos_categ_id', 'taxes_id', 'product_image_small', 'ean13', 'id'], [['pos_categ_id', '!=', false]]),
73
 
                this.fetch('product.packaging', ['product_id', 'ean']),
74
 
                this.fetch('account.bank.statement', ['account_id', 'currency', 'journal_id', 'state', 'name'],
75
 
                    [['state', '=', 'open'], ['user_id', '=', this.session.uid]]),
76
 
                this.fetch('account.journal', ['auto_cash', 'check_dtls', 'currency', 'name', 'type']),
77
 
                this.fetch('account.tax', ['amount', 'price_include', 'type']),
78
 
                this.get_app_data())
79
 
                .pipe(_.bind(this.build_tree, this));
80
 
        },
81
 
        fetch: function(osvModel, fields, domain) {
82
 
            var dataSetSearch;
83
 
            var self = this;
84
 
            dataSetSearch = new db.web.DataSetSearch(this, osvModel, {}, domain);
85
 
            return dataSetSearch.read_slice(fields, 0).then(function(result) {
86
 
                return self.store.set(osvModel, result);
87
 
            });
88
 
        },
89
 
        get_app_data: function() {
90
 
            var self = this;
91
 
            return $.when(new db.web.Model("sale.shop").get_func("search_read")([]).pipe(function(result) {
92
 
                self.set({'shop': result[0]});
93
 
                var company_id = result[0]['company_id'][0];
94
 
                return new db.web.Model("res.company").get_func("read")(company_id, ['currency_id', 'name', 'phone']).pipe(function(result) {
95
 
                    self.set({'company': result});
96
 
                    var currency_id = result['currency_id'][0]
97
 
                    return new db.web.Model("res.currency").get_func("read")([currency_id],
98
 
                            ['symbol', 'position']).pipe(function(result) {
99
 
                        self.set({'currency': result[0]});
100
 
                        
101
 
                    });
102
 
                });
103
 
            }), new db.web.Model("res.users").get_func("read")(this.session.uid, ['name']).pipe(function(result) {
104
 
                self.set({'user': result});
105
 
            }));
106
 
        },
107
 
        pushOrder: function(record) {
108
 
            var ops = _.clone(this.get('pending_operations'));
109
 
            ops.push(record);
110
 
            this.set({pending_operations: ops});
111
 
            return this.flush();
112
 
        },
113
 
        flush: function() {
114
 
            return this.flush_mutex.exec(_.bind(function() {
115
 
                return this._int_flush();
116
 
            }, this));
117
 
        },
118
 
        _int_flush : function() {
119
 
            var ops = this.get('pending_operations');
120
 
            if (ops.length === 0)
121
 
                return $.when();
122
 
            var op = ops[0];
123
 
            /* we prevent the default error handler and assume errors
124
 
             * are a normal use case, except we stop the current iteration
125
 
             */
126
 
            return new db.web.Model("pos.order").get_func("create_from_ui")([op]).fail(function(unused, event) {
127
 
                event.preventDefault();
128
 
            }).pipe(_.bind(function() {
129
 
                console.debug('saved 1 record');
130
 
                var ops2 = this.get('pending_operations');
131
 
                this.set({'pending_operations': _.without(ops2, op)});
132
 
                return this._int_flush();
133
 
            }, this), function() {return $.when()});
134
 
        },
135
 
        categories: {},
136
 
        build_tree: function() {
137
 
            var c, id, _i, _len, _ref, _ref2;
138
 
            _ref = this.store.get('pos.category');
139
 
            for (_i = 0, _len = _ref.length; _i < _len; _i++) {
140
 
                c = _ref[_i];
141
 
                this.categories[c.id] = {
142
 
                    id: c.id,
143
 
                    name: c.name,
144
 
                    children: c.child_id,
145
 
                    parent: c.parent_id[0],
146
 
                    ancestors: [c.id],
147
 
                    subtree: [c.id]
148
 
                };
149
 
            }
150
 
            _ref2 = this.categories;
151
 
            for (id in _ref2) {
152
 
                c = _ref2[id];
153
 
                this.current_category = c;
154
 
                this.build_ancestors(c.parent);
155
 
                this.build_subtree(c);
156
 
            }
157
 
            this.categories[0] = {
158
 
                ancestors: [],
159
 
                children: (function() {
160
 
                    var _j, _len2, _ref3, _results;
161
 
                    _ref3 = this.store.get('pos.category');
162
 
                    _results = [];
163
 
                    for (_j = 0, _len2 = _ref3.length; _j < _len2; _j++) {
164
 
                        c = _ref3[_j];
165
 
                        if (!(c.parent_id[0] != null)) {
166
 
                            _results.push(c.id);
167
 
                        }
168
 
                    }
169
 
                    return _results;
170
 
                }).call(this),
171
 
                subtree: (function() {
172
 
                    var _j, _len2, _ref3, _results;
173
 
                    _ref3 = this.store.get('pos.category');
174
 
                    _results = [];
175
 
                    for (_j = 0, _len2 = _ref3.length; _j < _len2; _j++) {
176
 
                        c = _ref3[_j];
177
 
                        _results.push(c.id);
178
 
                    }
179
 
                    return _results;
180
 
                }).call(this)
181
 
            };
182
 
            return this.ready.resolve();
183
 
        },
184
 
        build_ancestors: function(parent) {
185
 
            if (parent != null) {
186
 
                this.current_category.ancestors.unshift(parent);
187
 
                return this.build_ancestors(this.categories[parent].parent);
188
 
            }
189
 
        },
190
 
        build_subtree: function(category) {
191
 
            var c, _i, _len, _ref, _results;
192
 
            _ref = category.children;
193
 
            _results = [];
194
 
            for (_i = 0, _len = _ref.length; _i < _len; _i++) {
195
 
                c = _ref[_i];
196
 
                this.current_category.subtree.push(c);
197
 
                _results.push(this.build_subtree(this.categories[c]));
198
 
            }
199
 
            return _results;
200
 
        }
201
 
    });
202
 
 
203
 
    /* global variable */
204
 
    var pos;
205
 
 
206
 
    /*
207
 
     ---
208
 
     Models
209
 
     ---
210
 
     */
211
 
 
212
 
    var CashRegister = Backbone.Model.extend({
213
 
    });
214
 
 
215
 
    var CashRegisterCollection = Backbone.Collection.extend({
216
 
        model: CashRegister,
217
 
    });
218
 
 
219
 
    var Product = Backbone.Model.extend({
220
 
    });
221
 
 
222
 
    var ProductCollection = Backbone.Collection.extend({
223
 
        model: Product,
224
 
    });
225
 
 
226
 
    var Category = Backbone.Model.extend({
227
 
    });
228
 
 
229
 
    var CategoryCollection = Backbone.Collection.extend({
230
 
        model: Category,
231
 
    });
232
 
 
233
 
    /*
234
 
     Each Order contains zero or more Orderlines (i.e. the content of the "shopping cart".)
235
 
     There should only ever be one Orderline per distinct product in an Order.
236
 
     To add more of the same product, just update the quantity accordingly.
237
 
     The Order also contains payment information.
238
 
     */
239
 
    var Orderline = Backbone.Model.extend({
240
 
        defaults: {
241
 
            quantity: 1,
242
 
            list_price: 0,
243
 
            discount: 0
244
 
        },
245
 
        initialize: function(attributes) {
246
 
            Backbone.Model.prototype.initialize.apply(this, arguments);
247
 
            this.bind('change:quantity', function(unused, qty) {
248
 
                if (qty == 0)
249
 
                    this.trigger('killme');
250
 
            }, this);
251
 
        },
252
 
        incrementQuantity: function() {
253
 
            return this.set({
254
 
                quantity: (this.get('quantity')) + 1
255
 
            });
256
 
        },
257
 
        getPriceWithoutTax: function() {
258
 
            return this.getAllPrices().priceWithoutTax;
259
 
        },
260
 
        getPriceWithTax: function() {
261
 
            return this.getAllPrices().priceWithTax;
262
 
        },
263
 
        getTax: function() {
264
 
            return this.getAllPrices().tax;
265
 
        },
266
 
        getAllPrices: function() {
267
 
            var self = this;
268
 
            var base = (this.get('quantity')) * (this.get('list_price')) * (1 - (this.get('discount')) / 100);
269
 
            var totalTax = base;
270
 
            var totalNoTax = base;
271
 
            
272
 
            var products = pos.store.get('product.product');
273
 
            var product = _.detect(products, function(el) {return el.id === self.get('id');});
274
 
            var taxes_ids = product.taxes_id;
275
 
            var taxes =  pos.store.get('account.tax');
276
 
            var taxtotal = 0;
277
 
            _.each(taxes_ids, function(el) {
278
 
                var tax = _.detect(taxes, function(t) {return t.id === el;});
279
 
                if (tax.price_include) {
280
 
                    var tmp;
281
 
                    if (tax.type === "percent") {
282
 
                        tmp =  base - (base / (1 + tax.amount));
283
 
                    } else if (tax.type === "fixed") {
284
 
                        tmp = tax.amount * self.get('quantity');
285
 
                    } else {
286
 
                        throw "This type of tax is not supported by the point of sale: " + tax.type;
287
 
                    }
288
 
                    taxtotal += tmp;
289
 
                    totalNoTax -= tmp;
290
 
                } else {
291
 
                    var tmp;
292
 
                    if (tax.type === "percent") {
293
 
                        tmp = tax.amount * base;
294
 
                    } else if (tax.type === "fixed") {
295
 
                        tmp = tax.amount * self.get('quantity');
296
 
                    } else {
297
 
                        throw "This type of tax is not supported by the point of sale: " + tax.type;
298
 
                    }
299
 
                    taxtotal += tmp;
300
 
                    totalTax += tmp;
301
 
                }
302
 
            });
303
 
            return {
304
 
                "priceWithTax": totalTax,
305
 
                "priceWithoutTax": totalNoTax,
306
 
                "tax": taxtotal,
307
 
            };
308
 
        },
309
 
        exportAsJSON: function() {
310
 
            return {
311
 
                qty: this.get('quantity'),
312
 
                price_unit: this.get('list_price'),
313
 
                discount: this.get('discount'),
314
 
                product_id: this.get('id')
315
 
            };
316
 
        },
317
 
    });
318
 
 
319
 
    var OrderlineCollection = Backbone.Collection.extend({
320
 
        model: Orderline,
321
 
    });
322
 
 
323
 
    // Every PaymentLine has all the attributes of the corresponding CashRegister.
324
 
    var Paymentline = Backbone.Model.extend({
325
 
        defaults: { 
326
 
            amount: 0,
327
 
        },
328
 
        initialize: function(attributes) {
329
 
            Backbone.Model.prototype.initialize.apply(this, arguments);
330
 
        },
331
 
        getAmount: function(){
332
 
            return this.get('amount');
333
 
        },
334
 
        exportAsJSON: function(){
335
 
            return {
336
 
                name: db.web.datetime_to_str(new Date()),
337
 
                statement_id: this.get('id'),
338
 
                account_id: (this.get('account_id'))[0],
339
 
                journal_id: (this.get('journal_id'))[0],
340
 
                amount: this.getAmount()
341
 
            };
342
 
        },
343
 
    });
344
 
 
345
 
    var PaymentlineCollection = Backbone.Collection.extend({
346
 
        model: Paymentline,
347
 
    });
348
 
    
349
 
    var Order = Backbone.Model.extend({
350
 
        defaults:{
351
 
            validated: false,
352
 
            step: 'products',
353
 
        },
354
 
        initialize: function(attributes){
355
 
            Backbone.Model.prototype.initialize.apply(this, arguments);
356
 
            this.set({
357
 
                creationDate:   new Date,
358
 
                orderLines:     new OrderlineCollection,
359
 
                paymentLines:   new PaymentlineCollection,
360
 
                name:           "Order " + this.generateUniqueId(),
361
 
            });
362
 
            this.bind('change:validated', this.validatedChanged);
363
 
            return this;
364
 
        },
365
 
        events: {
366
 
            'change:validated': 'validatedChanged'
367
 
        },
368
 
        validatedChanged: function() {
369
 
            if (this.get("validated") && !this.previous("validated")) {
370
 
                this.set({'step': 'receipt'});
371
 
            }
372
 
        },
373
 
        generateUniqueId: function() {
374
 
            return new Date().getTime();
375
 
        },
376
 
        addProduct: function(product) {
377
 
            var existing;
378
 
            existing = (this.get('orderLines')).get(product.id);
379
 
            if (existing != null) {
380
 
                existing.incrementQuantity();
381
 
            } else {
382
 
                var line = new Orderline(product.toJSON());
383
 
                this.get('orderLines').add(line);
384
 
                line.bind('killme', function() {
385
 
                    this.get('orderLines').remove(line);
386
 
                }, this);
387
 
            }
388
 
        },
389
 
        addPaymentLine: function(cashRegister) {
390
 
            var newPaymentline;
391
 
            newPaymentline = new Paymentline(cashRegister);
392
 
            /* TODO: Should be 0 for cash-like accounts */
393
 
            newPaymentline.set({
394
 
                amount: this.getDueLeft()
395
 
            });
396
 
            return (this.get('paymentLines')).add(newPaymentline);
397
 
        },
398
 
        getName: function() {
399
 
            return this.get('name');
400
 
        },
401
 
        getTotal: function() {
402
 
            return (this.get('orderLines')).reduce((function(sum, orderLine) {
403
 
                return sum + orderLine.getPriceWithTax();
404
 
            }), 0);
405
 
        },
406
 
        getTotalTaxExcluded: function() {
407
 
            return (this.get('orderLines')).reduce((function(sum, orderLine) {
408
 
                return sum + orderLine.getPriceWithoutTax();
409
 
            }), 0);
410
 
        },
411
 
        getTax: function() {
412
 
            return (this.get('orderLines')).reduce((function(sum, orderLine) {
413
 
                return sum + orderLine.getTax();
414
 
            }), 0);
415
 
        },
416
 
        getPaidTotal: function() {
417
 
            return (this.get('paymentLines')).reduce((function(sum, paymentLine) {
418
 
                return sum + paymentLine.getAmount();
419
 
            }), 0);
420
 
        },
421
 
        getChange: function() {
422
 
            return this.getPaidTotal() - this.getTotal();
423
 
        },
424
 
        getDueLeft: function() {
425
 
            return this.getTotal() - this.getPaidTotal();
426
 
        },
427
 
        exportAsJSON: function() {
428
 
            var orderLines, paymentLines;
429
 
            orderLines = [];
430
 
            (this.get('orderLines')).each(_.bind( function(item) {
431
 
                return orderLines.push([0, 0, item.exportAsJSON()]);
432
 
            }, this));
433
 
            paymentLines = [];
434
 
            (this.get('paymentLines')).each(_.bind( function(item) {
435
 
                return paymentLines.push([0, 0, item.exportAsJSON()]);
436
 
            }, this));
437
 
            return {
438
 
                name: this.getName(),
439
 
                amount_paid: this.getPaidTotal(),
440
 
                amount_total: this.getTotal(),
441
 
                amount_tax: this.getTax(),
442
 
                amount_return: this.getChange(),
443
 
                lines: orderLines,
444
 
                statement_ids: paymentLines
445
 
            };
446
 
        },
447
 
    });
448
 
 
449
 
    var OrderCollection = Backbone.Collection.extend({
450
 
        model: Order,
451
 
    });
452
 
 
453
 
    var Shop = Backbone.Model.extend({
454
 
        initialize: function() {
455
 
            this.set({
456
 
                orders: new OrderCollection(),
457
 
                products: new ProductCollection()
458
 
            });
459
 
            this.set({
460
 
                cashRegisters: new CashRegisterCollection(pos.store.get('account.bank.statement')),
461
 
            });
462
 
            return (this.get('orders')).bind('remove', _.bind( function(removedOrder) {
463
 
                if ((this.get('orders')).isEmpty()) {
464
 
                    this.addAndSelectOrder(new Order);
465
 
                }
466
 
                if ((this.get('selectedOrder')) === removedOrder) {
467
 
                    return this.set({
468
 
                        selectedOrder: (this.get('orders')).last()
469
 
                    });
470
 
                }
471
 
            }, this));
472
 
        },
473
 
        addAndSelectOrder: function(newOrder) {
474
 
            (this.get('orders')).add(newOrder);
475
 
            return this.set({
476
 
                selectedOrder: newOrder
477
 
            });
478
 
        },
479
 
    });
480
 
 
481
 
    /*
482
 
     The numpad handles both the choice of the property currently being modified
483
 
     (quantity, price or discount) and the edition of the corresponding numeric value.
484
 
     */
485
 
    var NumpadState = Backbone.Model.extend({
486
 
        defaults: {
487
 
            buffer: "0",
488
 
            mode: "quantity"
489
 
        },
490
 
        appendNewChar: function(newChar) {
491
 
            var oldBuffer;
492
 
            oldBuffer = this.get('buffer');
493
 
            if (oldBuffer === '0') {
494
 
                this.set({
495
 
                    buffer: newChar
496
 
                });
497
 
            } else if (oldBuffer === '-0') {
498
 
                this.set({
499
 
                    buffer: "-" + newChar
500
 
                });
501
 
            } else {
502
 
                this.set({
503
 
                    buffer: (this.get('buffer')) + newChar
504
 
                });
505
 
            }
506
 
            this.updateTarget();
507
 
        },
508
 
        deleteLastChar: function() {
509
 
            var tempNewBuffer;
510
 
            tempNewBuffer = (this.get('buffer')).slice(0, -1) || "0";
511
 
            if (isNaN(tempNewBuffer)) {
512
 
                tempNewBuffer = "0";
513
 
            }
514
 
            this.set({
515
 
                buffer: tempNewBuffer
516
 
            });
517
 
            this.updateTarget();
518
 
        },
519
 
        switchSign: function() {
520
 
            var oldBuffer;
521
 
            oldBuffer = this.get('buffer');
522
 
            this.set({
523
 
                buffer: oldBuffer[0] === '-' ? oldBuffer.substr(1) : "-" + oldBuffer
524
 
            });
525
 
            this.updateTarget();
526
 
        },
527
 
        changeMode: function(newMode) {
528
 
            this.set({
529
 
                buffer: "0",
530
 
                mode: newMode
531
 
            });
532
 
        },
533
 
        reset: function() {
534
 
            this.set({
535
 
                buffer: "0",
536
 
                mode: "quantity"
537
 
            });
538
 
        },
539
 
        updateTarget: function() {
540
 
            var bufferContent, params;
541
 
            bufferContent = this.get('buffer');
542
 
            if (bufferContent && !isNaN(bufferContent)) {
543
 
                this.trigger('setValue', parseFloat(bufferContent));
544
 
            }
545
 
        },
546
 
    });
547
 
 
548
 
    /*
549
 
     ---
550
 
     Views
551
 
     ---
552
 
     */
553
 
    var NumpadWidget = db.web.OldWidget.extend({
554
 
        init: function(parent, options) {
555
 
            this._super(parent);
556
 
            this.state = new NumpadState();
557
 
        },
558
 
        start: function() {
559
 
            this.state.bind('change:mode', this.changedMode, this);
560
 
            this.changedMode();
561
 
            this.$element.find('button#numpad-backspace').click(_.bind(this.clickDeleteLastChar, this));
562
 
            this.$element.find('button#numpad-minus').click(_.bind(this.clickSwitchSign, this));
563
 
            this.$element.find('button.number-char').click(_.bind(this.clickAppendNewChar, this));
564
 
            this.$element.find('button.mode-button').click(_.bind(this.clickChangeMode, this));
565
 
        },
566
 
        clickDeleteLastChar: function() {
567
 
            return this.state.deleteLastChar();
568
 
        },
569
 
        clickSwitchSign: function() {
570
 
            return this.state.switchSign();
571
 
        },
572
 
        clickAppendNewChar: function(event) {
573
 
            var newChar;
574
 
            newChar = event.currentTarget.innerText || event.currentTarget.textContent;
575
 
            return this.state.appendNewChar(newChar);
576
 
        },
577
 
        clickChangeMode: function(event) {
578
 
            var newMode = event.currentTarget.attributes['data-mode'].nodeValue;
579
 
            return this.state.changeMode(newMode);
580
 
        },
581
 
        changedMode: function() {
582
 
            var mode = this.state.get('mode');
583
 
            $('.selected-mode').removeClass('selected-mode');
584
 
            $(_.str.sprintf('.mode-button[data-mode="%s"]', mode), this.$element).addClass('selected-mode');
585
 
        },
586
 
    });
587
 
    /*
588
 
     Gives access to the payment methods (aka. 'cash registers')
589
 
     */
590
 
    var PaypadWidget = db.web.OldWidget.extend({
591
 
        init: function(parent, options) {
592
 
            this._super(parent);
593
 
            this.shop = options.shop;
594
 
        },
595
 
        start: function() {
596
 
            this.$element.find('button').click(_.bind(this.performPayment, this));
597
 
        },
598
 
        performPayment: function(event) {
599
 
            if (this.shop.get('selectedOrder').get('step') === 'receipt')
600
 
                return;
601
 
            var cashRegister, cashRegisterCollection, cashRegisterId;
602
 
            /* set correct view */
603
 
            this.shop.get('selectedOrder').set({'step': 'payment'});
604
 
 
605
 
            cashRegisterId = event.currentTarget.attributes['cash-register-id'].nodeValue;
606
 
            cashRegisterCollection = this.shop.get('cashRegisters');
607
 
            cashRegister = cashRegisterCollection.find(_.bind( function(item) {
608
 
                return (item.get('id')) === parseInt(cashRegisterId, 10);
609
 
            }, this));
610
 
            return (this.shop.get('selectedOrder')).addPaymentLine(cashRegister);
611
 
        },
612
 
        render_element: function() {
613
 
            this.$element.empty();
614
 
            return (this.shop.get('cashRegisters')).each(_.bind( function(cashRegister) {
615
 
                var button = new PaymentButtonWidget();
616
 
                button.model = cashRegister;
617
 
                button.appendTo(this.$element);
618
 
            }, this));
619
 
        }
620
 
    });
621
 
    var PaymentButtonWidget = db.web.OldWidget.extend({
622
 
        template_fct: qweb_template('pos-payment-button-template'),
623
 
        render_element: function() {
624
 
            this.$element.html(this.template_fct({
625
 
                id: this.model.get('id'),
626
 
                name: (this.model.get('journal_id'))[1]
627
 
            }));
628
 
            return this;
629
 
        }
630
 
    });
631
 
    /*
632
 
     There are 3 steps in a POS workflow:
633
 
     1. prepare the order (i.e. chose products, quantities etc.)
634
 
     2. choose payment method(s) and amount(s)
635
 
     3. validae order and print receipt
636
 
     It should be possible to go back to any step as long as step 3 hasn't been completed.
637
 
     Modifying an order after validation shouldn't be allowed.
638
 
     */
639
 
    var StepSwitcher = db.web.OldWidget.extend({
640
 
        init: function(parent, options) {
641
 
            this._super(parent);
642
 
            this.shop = options.shop;
643
 
            this.change_order();
644
 
            this.shop.bind('change:selectedOrder', this.change_order, this);
645
 
        },
646
 
        change_order: function() {
647
 
            if (this.selected_order) {
648
 
                this.selected_order.unbind('change:step', this.change_step);
649
 
            }
650
 
            this.selected_order = this.shop.get('selectedOrder');
651
 
            if (this.selected_order) {
652
 
                this.selected_order.bind('change:step', this.change_step, this);
653
 
            }
654
 
            this.change_step();
655
 
        },
656
 
        change_step: function() {
657
 
            var new_step = this.selected_order ? this.selected_order.get('step') : 'products';
658
 
            $('.step-screen').hide();
659
 
            $('#' + new_step + '-screen').show();
660
 
        },
661
 
    });
662
 
    /*
663
 
     Shopping carts.
664
 
     */
665
 
    var OrderlineWidget = db.web.OldWidget.extend({
666
 
        tag_name: 'tr',
667
 
        template_fct: qweb_template('pos-orderline-template'),
668
 
        init: function(parent, options) {
669
 
            this._super(parent);
670
 
            this.model = options.model;
671
 
            this.model.bind('change', _.bind( function() {
672
 
                this.refresh();
673
 
            }, this));
674
 
            this.model.bind('remove', _.bind( function() {
675
 
                this.$element.remove();
676
 
            }, this));
677
 
            this.order = options.order;
678
 
        },
679
 
        start: function() {
680
 
            this.$element.click(_.bind(this.clickHandler, this));
681
 
            this.refresh();
682
 
        },
683
 
        clickHandler: function() {
684
 
            this.select();
685
 
        },
686
 
        render_element: function() {
687
 
            this.$element.html(this.template_fct(this.model.toJSON()));
688
 
            this.select();
689
 
        },
690
 
        refresh: function() {
691
 
            this.render_element();
692
 
            var heights = _.map(this.$element.prevAll(), function(el) {return $(el).outerHeight();});
693
 
            heights.push($('#current-order thead').outerHeight());
694
 
            var position = _.reduce(heights, function(memo, num){ return memo + num; }, 0);
695
 
            $('#current-order').scrollTop(position);
696
 
        },
697
 
        select: function() {
698
 
            $('tr.selected').removeClass('selected');
699
 
            this.$element.addClass('selected');
700
 
            this.order.selected = this.model;
701
 
            this.on_selected();
702
 
        },
703
 
        on_selected: function() {},
704
 
    });
705
 
 
706
 
    var OrderWidget = db.web.OldWidget.extend({
707
 
        init: function(parent, options) {
708
 
            this._super(parent);
709
 
            this.shop = options.shop;
710
 
            this.setNumpadState(options.numpadState);
711
 
            this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this);
712
 
            this.bindOrderLineEvents();
713
 
        },
714
 
        setNumpadState: function(numpadState) {
715
 
                if (this.numpadState) {
716
 
                        this.numpadState.unbind('setValue', this.setValue);
717
 
                }
718
 
                this.numpadState = numpadState;
719
 
                if (this.numpadState) {
720
 
                        this.numpadState.bind('setValue', this.setValue, this);
721
 
                        this.numpadState.reset();
722
 
                }
723
 
        },
724
 
        setValue: function(val) {
725
 
                var param = {};
726
 
                param[this.numpadState.get('mode')] = val;
727
 
                var order = this.shop.get('selectedOrder');
728
 
                if (order.get('orderLines').length !== 0) {
729
 
                   order.selected.set(param);
730
 
                } else {
731
 
                    this.shop.get('selectedOrder').destroy();
732
 
                }
733
 
        },
734
 
        changeSelectedOrder: function() {
735
 
            this.currentOrderLines.unbind();
736
 
            this.bindOrderLineEvents();
737
 
            this.render_element();
738
 
        },
739
 
        bindOrderLineEvents: function() {
740
 
            this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines');
741
 
            this.currentOrderLines.bind('add', this.addLine, this);
742
 
            this.currentOrderLines.bind('remove', this.render_element, this);
743
 
        },
744
 
        addLine: function(newLine) {
745
 
            var line = new OrderlineWidget(null, {
746
 
                    model: newLine,
747
 
                    order: this.shop.get('selectedOrder')
748
 
            });
749
 
            line.on_selected.add(_.bind(this.selectedLine, this));
750
 
            this.selectedLine();
751
 
            line.appendTo(this.$element);
752
 
            this.updateSummary();
753
 
        },
754
 
        selectedLine: function() {
755
 
                var reset = false;
756
 
                if (this.currentSelected !== this.shop.get('selectedOrder').selected) {
757
 
                        reset = true;
758
 
                }
759
 
                this.currentSelected = this.shop.get('selectedOrder').selected;
760
 
                if (reset && this.numpadState)
761
 
                        this.numpadState.reset();
762
 
            this.updateSummary();
763
 
        },
764
 
        render_element: function() {
765
 
            this.$element.empty();
766
 
            this.currentOrderLines.each(_.bind( function(orderLine) {
767
 
                var line = new OrderlineWidget(null, {
768
 
                        model: orderLine,
769
 
                        order: this.shop.get('selectedOrder')
770
 
                });
771
 
                line.on_selected.add(_.bind(this.selectedLine, this));
772
 
                line.appendTo(this.$element);
773
 
            }, this));
774
 
            this.updateSummary();
775
 
        },
776
 
        updateSummary: function() {
777
 
            var currentOrder, tax, total, totalTaxExcluded;
778
 
            currentOrder = this.shop.get('selectedOrder');
779
 
            total = currentOrder.getTotal();
780
 
            totalTaxExcluded = currentOrder.getTotalTaxExcluded();
781
 
            tax = currentOrder.getTax();
782
 
            $('#subtotal').html(totalTaxExcluded.toFixed(2)).hide().fadeIn();
783
 
            $('#tax').html(tax.toFixed(2)).hide().fadeIn();
784
 
            $('#total').html(total.toFixed(2)).hide().fadeIn();
785
 
        },
786
 
    });
787
 
 
788
 
    /*
789
 
     "Products" step.
790
 
     */
791
 
    var CategoryWidget = db.web.OldWidget.extend({
792
 
        start: function() {
793
 
            this.$element.find(".oe-pos-categories-list a").click(_.bind(this.changeCategory, this));
794
 
            $("#products-screen-ol").css("top",$("#products-screen-categories").height()+"px");
795
 
        },
796
 
        template_fct: qweb_template('pos-category-template'),
797
 
        render_element: function() {
798
 
            var self = this;
799
 
            var c;
800
 
            this.$element.html(this.template_fct({
801
 
                breadcrumb: (function() {
802
 
                    var _i, _len, _results;
803
 
                    _results = [];
804
 
                    for (_i = 0, _len = self.ancestors.length; _i < _len; _i++) {
805
 
                        c = self.ancestors[_i];
806
 
                        _results.push(pos.categories[c]);
807
 
                    }
808
 
                    return _results;
809
 
                })(),
810
 
                categories: (function() {
811
 
                    var _i, _len, _results;
812
 
                    _results = [];
813
 
                    for (_i = 0, _len = self.children.length; _i < _len; _i++) {
814
 
                        c = self.children[_i];
815
 
                        _results.push(pos.categories[c]);
816
 
                    }
817
 
                    return _results;
818
 
                })()
819
 
            }));
820
 
        },
821
 
        changeCategory: function(a) {
822
 
            var id = $(a.target).data("category-id");
823
 
            this.on_change_category(id);
824
 
        },
825
 
        on_change_category: function(id) {},
826
 
    });
827
 
 
828
 
    var ProductWidget = db.web.OldWidget.extend({
829
 
        tag_name:'li',
830
 
        template_fct: qweb_template('pos-product-template'),
831
 
        init: function(parent, options) {
832
 
            this._super(parent);
833
 
            this.model = options.model;
834
 
            this.shop = options.shop;
835
 
        },
836
 
        start: function(options) {
837
 
            $("a", this.$element).click(_.bind(this.addToOrder, this));
838
 
        },
839
 
        addToOrder: function(event) {
840
 
            /* Preserve the category URL */
841
 
            event.preventDefault();
842
 
            return (this.shop.get('selectedOrder')).addProduct(this.model);
843
 
        },
844
 
        render_element: function() {
845
 
            this.$element.addClass("product");
846
 
            this.$element.html(this.template_fct(this.model.toJSON()));
847
 
            return this;
848
 
        },
849
 
    });
850
 
 
851
 
    var ProductListWidget = db.web.OldWidget.extend({
852
 
        init: function(parent, options) {
853
 
            this._super(parent);
854
 
            this.model = options.model;
855
 
            this.shop = options.shop;
856
 
            this.shop.get('products').bind('reset', this.render_element, this);
857
 
        },
858
 
        render_element: function() {
859
 
            this.$element.empty();
860
 
            (this.shop.get('products')).each(_.bind( function(product) {
861
 
                var p = new ProductWidget(null, {
862
 
                        model: product,
863
 
                        shop: this.shop
864
 
                });
865
 
                p.appendTo(this.$element);
866
 
            }, this));
867
 
            return this;
868
 
        },
869
 
    });
870
 
    /*
871
 
     "Payment" step.
872
 
     */
873
 
    var PaymentlineWidget = db.web.OldWidget.extend({
874
 
        tag_name: 'tr',
875
 
        template_fct: qweb_template('pos-paymentline-template'),
876
 
        init: function(parent, options) {
877
 
            this._super(parent);
878
 
            this.model = options.model;
879
 
            this.model.bind('change', this.changedAmount, this);
880
 
        },
881
 
        on_delete: function() {},
882
 
        changeAmount: function(event) {
883
 
            var newAmount;
884
 
            newAmount = event.currentTarget.value;
885
 
            if (newAmount && !isNaN(newAmount)) {
886
 
                this.amount = parseFloat(newAmount);
887
 
                this.model.set({
888
 
                    amount: this.amount,
889
 
                });
890
 
            }
891
 
        },
892
 
        changedAmount: function() {
893
 
                if (this.amount !== this.model.get('amount'))
894
 
                        this.render_element();
895
 
        },
896
 
        render_element: function() {
897
 
                this.amount = this.model.get('amount');
898
 
            this.$element.html(this.template_fct({
899
 
                name: (this.model.get('journal_id'))[1],
900
 
                amount: this.amount,
901
 
            }));
902
 
            this.$element.addClass('paymentline');
903
 
            $('input', this.$element).keyup(_.bind(this.changeAmount, this));
904
 
            $('.delete-payment-line', this.$element).click(this.on_delete);
905
 
        },
906
 
    });
907
 
    var PaymentWidget = db.web.OldWidget.extend({
908
 
        init: function(parent, options) {
909
 
            this._super(parent);
910
 
            this.model = options.model;
911
 
            this.shop = options.shop;
912
 
            this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this);
913
 
            this.bindPaymentLineEvents();
914
 
            this.bindOrderLineEvents();
915
 
        },
916
 
        paymentLineList: function() {
917
 
            return this.$element.find('#paymentlines');
918
 
        },
919
 
        start: function() {
920
 
            $('button#validate-order', this.$element).click(_.bind(this.validateCurrentOrder, this));
921
 
            $('.oe-back-to-products', this.$element).click(_.bind(this.back, this));
922
 
        },
923
 
        back: function() {
924
 
            this.shop.get('selectedOrder').set({"step": "products"});
925
 
        },
926
 
        validateCurrentOrder: function() {
927
 
            var callback, currentOrder;
928
 
            currentOrder = this.shop.get('selectedOrder');
929
 
            $('button#validate-order', this.$element).attr('disabled', 'disabled');
930
 
            pos.pushOrder(currentOrder.exportAsJSON()).then(_.bind(function() {
931
 
                $('button#validate-order', this.$element).removeAttr('disabled');
932
 
                return currentOrder.set({
933
 
                    validated: true
934
 
                });
935
 
            }, this));
936
 
        },
937
 
        bindPaymentLineEvents: function() {
938
 
            this.currentPaymentLines = (this.shop.get('selectedOrder')).get('paymentLines');
939
 
            this.currentPaymentLines.bind('add', this.addPaymentLine, this);
940
 
            this.currentPaymentLines.bind('remove', this.render_element, this);
941
 
            this.currentPaymentLines.bind('all', this.updatePaymentSummary, this);
942
 
        },
943
 
        bindOrderLineEvents: function() {
944
 
            this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines');
945
 
            this.currentOrderLines.bind('all', this.updatePaymentSummary, this);
946
 
        },
947
 
        changeSelectedOrder: function() {
948
 
            this.currentPaymentLines.unbind();
949
 
            this.bindPaymentLineEvents();
950
 
            this.currentOrderLines.unbind();
951
 
            this.bindOrderLineEvents();
952
 
            this.render_element();
953
 
        },
954
 
        addPaymentLine: function(newPaymentLine) {
955
 
            var x = new PaymentlineWidget(null, {
956
 
                    model: newPaymentLine
957
 
                });
958
 
            x.on_delete.add(_.bind(this.deleteLine, this, x));
959
 
            x.appendTo(this.paymentLineList());
960
 
        },
961
 
        render_element: function() {
962
 
            this.paymentLineList().empty();
963
 
            this.currentPaymentLines.each(_.bind( function(paymentLine) {
964
 
                this.addPaymentLine(paymentLine);
965
 
            }, this));
966
 
            this.updatePaymentSummary();
967
 
        },
968
 
        deleteLine: function(lineWidget) {
969
 
                this.currentPaymentLines.remove([lineWidget.model]);
970
 
        },
971
 
        updatePaymentSummary: function() {
972
 
            var currentOrder, dueTotal, paidTotal, remaining, remainingAmount;
973
 
            currentOrder = this.shop.get('selectedOrder');
974
 
            paidTotal = currentOrder.getPaidTotal();
975
 
            dueTotal = currentOrder.getTotal();
976
 
            this.$element.find('#payment-due-total').html(dueTotal.toFixed(2));
977
 
            this.$element.find('#payment-paid-total').html(paidTotal.toFixed(2));
978
 
            remainingAmount = dueTotal - paidTotal;
979
 
            remaining = remainingAmount > 0 ? 0 : (-remainingAmount).toFixed(2);
980
 
            $('#payment-remaining').html(remaining);
981
 
        },
982
 
        setNumpadState: function(numpadState) {
983
 
                if (this.numpadState) {
984
 
                        this.numpadState.unbind('setValue', this.setValue);
985
 
                        this.numpadState.unbind('change:mode', this.setNumpadMode);
986
 
                }
987
 
                this.numpadState = numpadState;
988
 
                if (this.numpadState) {
989
 
                        this.numpadState.bind('setValue', this.setValue, this);
990
 
                        this.numpadState.bind('change:mode', this.setNumpadMode, this);
991
 
                        this.numpadState.reset();
992
 
                        this.setNumpadMode();
993
 
                }
994
 
        },
995
 
        setNumpadMode: function() {
996
 
                this.numpadState.set({mode: 'payment'});
997
 
        },
998
 
        setValue: function(val) {
999
 
                this.currentPaymentLines.last().set({amount: val});
1000
 
        },
1001
 
    });
1002
 
 
1003
 
    var ReceiptWidget = db.web.OldWidget.extend({
1004
 
        init: function(parent, options) {
1005
 
            this._super(parent);
1006
 
            this.model = options.model;
1007
 
            this.shop = options.shop;
1008
 
            this.user = pos.get('user');
1009
 
            this.company = pos.get('company');
1010
 
            this.shop_obj = pos.get('shop');
1011
 
        },
1012
 
        start: function() {
1013
 
            this.shop.bind('change:selectedOrder', this.changeSelectedOrder, this);
1014
 
            this.changeSelectedOrder();
1015
 
        },
1016
 
        render_element: function() {
1017
 
            this.$element.html(qweb_template('pos-receipt-view'));
1018
 
            $('button#pos-finish-order', this.$element).click(_.bind(this.finishOrder, this));
1019
 
            $('button#print-the-ticket', this.$element).click(_.bind(this.print, this));
1020
 
        },
1021
 
        print: function() {
1022
 
            window.print();
1023
 
        },
1024
 
        finishOrder: function() {
1025
 
            this.shop.get('selectedOrder').destroy();
1026
 
        },
1027
 
        changeSelectedOrder: function() {
1028
 
            if (this.currentOrderLines)
1029
 
                this.currentOrderLines.unbind();
1030
 
            this.currentOrderLines = (this.shop.get('selectedOrder')).get('orderLines');
1031
 
            this.currentOrderLines.bind('add', this.refresh, this);
1032
 
            this.currentOrderLines.bind('change', this.refresh, this);
1033
 
            this.currentOrderLines.bind('remove', this.refresh, this);
1034
 
            if (this.currentPaymentLines)
1035
 
                this.currentPaymentLines.unbind();
1036
 
            this.currentPaymentLines = (this.shop.get('selectedOrder')).get('paymentLines');
1037
 
            this.currentPaymentLines.bind('all', this.refresh, this);
1038
 
            this.refresh();
1039
 
        },
1040
 
        refresh: function() {
1041
 
            this.currentOrder = this.shop.get('selectedOrder');
1042
 
            $('.pos-receipt-container', this.$element).html(qweb_template('pos-ticket')({widget:this}));
1043
 
        },
1044
 
    });
1045
 
 
1046
 
    var OrderButtonWidget = db.web.OldWidget.extend({
1047
 
        tag_name: 'li',
1048
 
        template_fct: qweb_template('pos-order-selector-button-template'),
1049
 
        init: function(parent, options) {
1050
 
            this._super(parent);
1051
 
            this.order = options.order;
1052
 
            this.shop = options.shop;
1053
 
            this.order.bind('destroy', _.bind( function() {
1054
 
                return this.stop();
1055
 
            }, this));
1056
 
            this.shop.bind('change:selectedOrder', _.bind( function(shop) {
1057
 
                var selectedOrder;
1058
 
                selectedOrder = shop.get('selectedOrder');
1059
 
                if (this.order === selectedOrder) {
1060
 
                    this.setButtonSelected();
1061
 
                }
1062
 
            }, this));
1063
 
        },
1064
 
        start: function() {
1065
 
            $('button.select-order', this.$element).click(_.bind(this.selectOrder, this));
1066
 
            $('button.close-order', this.$element).click(_.bind(this.closeOrder, this));
1067
 
        },
1068
 
        selectOrder: function(event) {
1069
 
            this.shop.set({
1070
 
                selectedOrder: this.order
1071
 
            });
1072
 
        },
1073
 
        setButtonSelected: function() {
1074
 
            $('.selected-order').removeClass('selected-order');
1075
 
            this.$element.addClass('selected-order');
1076
 
        },
1077
 
        closeOrder: function(event) {
1078
 
            this.order.destroy();
1079
 
        },
1080
 
        render_element: function() {
1081
 
            this.$element.html(this.template_fct({widget:this}));
1082
 
            this.$element.addClass('order-selector-button');
1083
 
        }
1084
 
    });
1085
 
 
1086
 
    var ShopWidget = db.web.OldWidget.extend({
1087
 
        init: function(parent, options) {
1088
 
            this._super(parent);
1089
 
            this.shop = options.shop;
1090
 
        },
1091
 
        start: function() {
1092
 
            $('button#neworder-button', this.$element).click(_.bind(this.createNewOrder, this));
1093
 
 
1094
 
            (this.shop.get('orders')).bind('add', this.orderAdded, this);
1095
 
            (this.shop.get('orders')).add(new Order);
1096
 
            this.productListView = new ProductListWidget(null, {
1097
 
                shop: this.shop
1098
 
            });
1099
 
            this.productListView.$element = $("#products-screen-ol");
1100
 
            this.productListView.render_element();
1101
 
            this.productListView.start();
1102
 
            this.paypadView = new PaypadWidget(null, {
1103
 
                shop: this.shop
1104
 
            });
1105
 
            this.paypadView.$element = $('#paypad');
1106
 
            this.paypadView.render_element();
1107
 
            this.paypadView.start();
1108
 
            this.numpadView = new NumpadWidget(null);
1109
 
            this.numpadView.$element = $('#numpad');
1110
 
            this.numpadView.start();
1111
 
            this.orderView = new OrderWidget(null, {
1112
 
                shop: this.shop,
1113
 
            });
1114
 
            this.orderView.$element = $('#current-order-content');
1115
 
            this.orderView.start();
1116
 
            this.paymentView = new PaymentWidget(null, {
1117
 
                shop: this.shop
1118
 
            });
1119
 
            this.paymentView.$element = $('#payment-screen');
1120
 
            this.paymentView.render_element();
1121
 
            this.paymentView.start();
1122
 
            this.receiptView = new ReceiptWidget(null, {
1123
 
                shop: this.shop,
1124
 
            });
1125
 
            this.receiptView.replace($('#receipt-screen'));
1126
 
            this.stepSwitcher = new StepSwitcher(this, {shop: this.shop});
1127
 
            this.shop.bind('change:selectedOrder', this.changedSelectedOrder, this);
1128
 
            this.changedSelectedOrder();
1129
 
        },
1130
 
        createNewOrder: function() {
1131
 
            var newOrder;
1132
 
            newOrder = new Order;
1133
 
            (this.shop.get('orders')).add(newOrder);
1134
 
            this.shop.set({
1135
 
                selectedOrder: newOrder
1136
 
            });
1137
 
        },
1138
 
        orderAdded: function(newOrder) {
1139
 
            var newOrderButton;
1140
 
            newOrderButton = new OrderButtonWidget(null, {
1141
 
                order: newOrder,
1142
 
                shop: this.shop
1143
 
            });
1144
 
            newOrderButton.appendTo($('#orders'));
1145
 
            newOrderButton.selectOrder();
1146
 
        },
1147
 
        changedSelectedOrder: function() {
1148
 
                if (this.currentOrder) {
1149
 
                        this.currentOrder.unbind('change:step', this.changedStep);
1150
 
                }
1151
 
                this.currentOrder = this.shop.get('selectedOrder');
1152
 
                this.currentOrder.bind('change:step', this.changedStep, this);
1153
 
                this.changedStep();
1154
 
        },
1155
 
        changedStep: function() {
1156
 
                var step = this.currentOrder.get('step');
1157
 
                this.orderView.setNumpadState(null);
1158
 
                this.paymentView.setNumpadState(null);
1159
 
                if (step === 'products') {
1160
 
                        this.orderView.setNumpadState(this.numpadView.state);
1161
 
                } else if (step === 'payment') {
1162
 
                        this.paymentView.setNumpadState(this.numpadView.state);
1163
 
                }
1164
 
        },
1165
 
    });
1166
 
 
1167
 
    var App = (function() {
1168
 
 
1169
 
        function App($element) {
1170
 
            this.initialize($element);
1171
 
        }
1172
 
 
1173
 
        App.prototype.initialize = function($element) {
1174
 
            this.shop = new Shop;
1175
 
            this.shopView = new ShopWidget(null, {
1176
 
                shop: this.shop
1177
 
            });
1178
 
            this.shopView.$element = $element;
1179
 
            this.shopView.start();
1180
 
            this.categoryView = new CategoryWidget(null, 'products-screen-categories');
1181
 
            this.categoryView.on_change_category.add_last(_.bind(this.category, this));
1182
 
            this.category();
1183
 
        };
1184
 
 
1185
 
        App.prototype.category = function(id) {
1186
 
            var c, products, self = this;
1187
 
 
1188
 
            id = !id ? 0 : id; 
1189
 
 
1190
 
            c = pos.categories[id];
1191
 
            this.categoryView.ancestors = c.ancestors;
1192
 
            this.categoryView.children = c.children;
1193
 
            this.categoryView.render_element();
1194
 
            this.categoryView.start();
1195
 
            allProducts = pos.store.get('product.product');
1196
 
            allPackages = pos.store.get('product.packaging');
1197
 
            products = pos.store.get('product.product').filter( function(p) {
1198
 
                var _ref;
1199
 
                return _ref = p.pos_categ_id[0], _.indexOf(c.subtree, _ref) >= 0;
1200
 
            });
1201
 
            (this.shop.get('products')).reset(products);
1202
 
 
1203
 
 
1204
 
            //returns true if the code is a valid EAN codebar number by checking the control digit.
1205
 
            var checkEan = function(code) {
1206
 
                var st1 = code.slice();
1207
 
                var st2 = st1.slice(0,st1.length-1).reverse();
1208
 
                // some EAN13 barcodes have a length of 12, as they start by 0
1209
 
                while (st2.length < 12) {
1210
 
                    st2.push(0);
1211
 
                }
1212
 
                var countSt3 = 1;
1213
 
                var st3 = 0;
1214
 
                $.each(st2, function() {
1215
 
                    if (countSt3%2 === 1) {
1216
 
                        st3 +=  this;
1217
 
                    }
1218
 
                    countSt3 ++;
1219
 
                });
1220
 
                st3 *= 3;
1221
 
                var st4 = 0;
1222
 
                var countSt4 = 1;
1223
 
                $.each(st2, function() {
1224
 
                    if (countSt4%2 === 0) {
1225
 
                        st4 += this;
1226
 
                    }
1227
 
                    countSt4 ++;
1228
 
                });
1229
 
                var st5 = st3 + st4;
1230
 
                var cd = (10 - (st5%10)) % 10;
1231
 
                return code[code.length-1] === cd;
1232
 
            }
1233
 
 
1234
 
            var codeNumbers = [];
1235
 
 
1236
 
            // returns a product that has a packaging with an EAN matching to provided ean string. 
1237
 
            // returns undefined if no such product is found.
1238
 
            var getProductByEAN = function(ean) {
1239
 
                var prefix = ean.substring(0,2);
1240
 
                var scannedProductModel = undefined;
1241
 
                if (prefix in {'02':'', '22':'', '24':'', '26':'', '28':''}) {
1242
 
                    // PRICE barcode
1243
 
                    var itemCode = ean.substring(0,7);
1244
 
                    var scannedPackaging = _.detect(allPackages, function(pack) { return pack.ean !== undefined && pack.ean.substring(0,7) === itemCode;});
1245
 
                    if (scannedPackaging !== undefined) {
1246
 
                        scannedProductModel = _.detect(allProducts, function(pc) { return pc.id === scannedPackaging.product_id[0];});
1247
 
                        scannedProductModel.list_price = Number(ean.substring(7,12))/100;
1248
 
                    }
1249
 
                } else if (prefix in {'21':'','23':'','27':'','29':'','25':''}) {
1250
 
                    // WEIGHT barcode
1251
 
                    var weight = Number(barcode.substring(7,12))/1000;
1252
 
                    var itemCode = ean.substring(0,7);
1253
 
                    var scannedPackaging = _.detect(allPackages, function(pack) { return pack.ean !== undefined && pack.ean.substring(0,7) === itemCode;});
1254
 
                    if (scannedPackaging !== undefined) {
1255
 
                        scannedProductModel = _.detect(allProducts, function(pc) { return pc.id === scannedPackaging.product_id[0];});
1256
 
                        scannedProductModel.list_price *= weight;
1257
 
                        scannedProductModel.name += ' - ' + weight + ' Kg.';
1258
 
                    }
1259
 
                } else {
1260
 
                    // UNIT barcode
1261
 
                    scannedProductModel = _.detect(allProducts, function(pc) { return pc.ean13 === ean;});   //TODO DOES NOT SCALE
1262
 
                }
1263
 
                return scannedProductModel;
1264
 
            }
1265
 
 
1266
 
            // The barcode readers acts as a keyboard, we catch all keyup events and try to find a 
1267
 
            // barcode sequence in the typed keys, then act accordingly.
1268
 
            $('body').delegate('','keyup', function (e){
1269
 
 
1270
 
                //We only care about numbers
1271
 
                if (!isNaN(Number(String.fromCharCode(e.keyCode)))) {
1272
 
 
1273
 
                    // The barcode reader sends keystrokes with a specific interval.
1274
 
                    // We look if the typed keys fit in the interval. 
1275
 
                    if (codeNumbers.length==0) {
1276
 
                        timeStamp = new Date().getTime();
1277
 
                    } else {
1278
 
                        if (lastTimeStamp + 30 < new Date().getTime()) {
1279
 
                            // not a barcode reader
1280
 
                            codeNumbers = [];
1281
 
                            timeStamp = new Date().getTime();
1282
 
                        }
1283
 
                    }
1284
 
                    codeNumbers.push(e.keyCode - 48);
1285
 
                    lastTimeStamp = new Date().getTime();
1286
 
                    if (codeNumbers.length == 13) {
1287
 
                        // a barcode reader
1288
 
                        if (!checkEan(codeNumbers)) {
1289
 
                            // barcode read error, raise warning
1290
 
                            $(QWeb.render('pos-scan-warning')).dialog({
1291
 
                                resizable: false,
1292
 
                                height:220,
1293
 
                                modal: true,
1294
 
                                title: "Warning",
1295
 
                                buttons: {
1296
 
                                    "OK": function() {
1297
 
                                        $( this ).dialog( "close" );
1298
 
                                        return;
1299
 
                                    },
1300
 
                                }
1301
 
                            });
1302
 
                        }
1303
 
                        var selectedOrder = self.shop.get('selectedOrder');
1304
 
                        var scannedProductModel = getProductByEAN(codeNumbers.join(''));
1305
 
                        if (scannedProductModel === undefined) {
1306
 
                            // product not recognized, raise warning
1307
 
                            $(QWeb.render('pos-scan-warning')).dialog({
1308
 
                                resizable: false,
1309
 
                                height:220,
1310
 
                                modal: true,
1311
 
                                title: "Warning",
1312
 
                                buttons: {
1313
 
                                    "OK": function() {
1314
 
                                        $( this ).dialog( "close" );
1315
 
                                        return;
1316
 
                                    },
1317
 
                                }
1318
 
                            });
1319
 
                        } else {
1320
 
                            selectedOrder.addProduct(new Product(scannedProductModel));
1321
 
                        }
1322
 
 
1323
 
                        codeNumbers = [];
1324
 
                    }
1325
 
                } else {
1326
 
                    // NaN
1327
 
                    codeNumbers = [];
1328
 
                }
1329
 
            });
1330
 
 
1331
 
            $('.searchbox input').keyup(function() {
1332
 
                var m, s;
1333
 
                s = $(this).val().toLowerCase();
1334
 
                if (s) {
1335
 
                    m = products.filter( function(p) {
1336
 
                        return p.name.toLowerCase().indexOf(s) != -1;
1337
 
                    });
1338
 
                    $('.search-clear').fadeIn();
1339
 
                } else {
1340
 
                    m = products;
1341
 
                    $('.search-clear').fadeOut();
1342
 
                }
1343
 
                return (self.shop.get('products')).reset(m);
1344
 
            });
1345
 
            return $('.search-clear').click( function() {
1346
 
                (self.shop.get('products')).reset(products);
1347
 
                $('.searchbox input').val('').focus();
1348
 
                return $('.search-clear').fadeOut();
1349
 
            });
1350
 
        };
1351
 
        return App;
1352
 
    })();
1353
 
    
1354
 
    db.point_of_sale.SynchNotification = db.web.OldWidget.extend({
1355
 
        template: "pos-synch-notification",
1356
 
        init: function() {
1357
 
            this._super.apply(this, arguments);
1358
 
            this.nbr_pending = 0;
1359
 
        },
1360
 
        render_element: function() {
1361
 
            this._super.apply(this, arguments);
1362
 
            $('.oe_pos_synch-notification-button', this.$element).click(this.on_synch);
1363
 
        },
1364
 
        on_change_nbr_pending: function(nbr_pending) {
1365
 
            this.nbr_pending = nbr_pending;
1366
 
            this.render_element();
1367
 
        },
1368
 
        on_synch: function() {}
1369
 
    });
1370
 
 
1371
 
    db.web.client_actions.add('pos.ui', 'db.point_of_sale.PointOfSale');
1372
 
    db.point_of_sale.PointOfSale = db.web.OldWidget.extend({
1373
 
        init: function() {
1374
 
            this._super.apply(this, arguments);
1375
 
 
1376
 
            if (pos)
1377
 
                throw "It is not possible to instantiate multiple instances "+
1378
 
                    "of the point of sale at the same time.";
1379
 
            pos = new Pos(this.session);
1380
 
        },
1381
 
        start: function() {
1382
 
            var self = this;
1383
 
            return pos.ready.then(_.bind(function() {
1384
 
                this.render_element();
1385
 
                this.synch_notification = new db.point_of_sale.SynchNotification(this);
1386
 
                this.synch_notification.replace($('.oe_pos_synch-notification', this.$element));
1387
 
                this.synch_notification.on_synch.add(_.bind(pos.flush, pos));
1388
 
                
1389
 
                pos.bind('change:pending_operations', this.changed_pending_operations, this);
1390
 
                this.changed_pending_operations();
1391
 
                
1392
 
                this.$element.find("#loggedas button").click(function() {
1393
 
                    self.try_close();
1394
 
                });
1395
 
 
1396
 
                pos.app = new App(self.$element);
1397
 
                $('.oe_toggle_secondary_menu').hide();
1398
 
                $('.oe_footer').hide();
1399
 
                
1400
 
                if (pos.store.get('account.bank.statement').length === 0)
1401
 
                    return new db.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_open_statement']], ['res_id']).pipe(
1402
 
                            _.bind(function(res) {
1403
 
                        return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) {
1404
 
                            var action = result.result;
1405
 
                            this.do_action(action);
1406
 
                        }, this));
1407
 
                    }, this));
1408
 
            }, this));
1409
 
        },
1410
 
        render: function() {
1411
 
            return qweb_template("PointOfSale")();
1412
 
        },
1413
 
        changed_pending_operations: function () {
1414
 
            this.synch_notification.on_change_nbr_pending(pos.get('pending_operations').length);
1415
 
        },
1416
 
        try_close: function() {
1417
 
            pos.flush().then(_.bind(function() {
1418
 
                var close = _.bind(this.close, this);
1419
 
                if (pos.get('pending_operations').length > 0) {
1420
 
                    var confirm = false;
1421
 
                    $(QWeb.render('pos-close-warning')).dialog({
1422
 
                        resizable: false,
1423
 
                        height:160,
1424
 
                        modal: true,
1425
 
                        title: "Warning",
1426
 
                        buttons: {
1427
 
                            "Yes": function() {
1428
 
                                confirm = true;
1429
 
                                $( this ).dialog( "close" );
1430
 
                            },
1431
 
                            "No": function() {
1432
 
                                $( this ).dialog( "close" );
1433
 
                            }
1434
 
                        },
1435
 
                        close: function() {
1436
 
                            if (confirm)
1437
 
                                close();
1438
 
                        }
1439
 
                    });
1440
 
                } else {
1441
 
                    close();
1442
 
                }
1443
 
            }, this));
1444
 
        },
1445
 
        close: function() {
1446
 
            // remove barcode reader event listener
1447
 
            $('body').undelegate('', 'keyup')
1448
 
 
1449
 
            return new db.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_close_statement']], ['res_id']).pipe(
1450
 
                    _.bind(function(res) {
1451
 
                return this.rpc('/web/action/load', {'action_id': res[0]['res_id']}).pipe(_.bind(function(result) {
1452
 
                    var action = result.result;
1453
 
                    action.context = _.extend(action.context || {}, {'cancel_action': {type: 'ir.actions.client', tag: 'default_home'}});
1454
 
                    this.do_action(action);
1455
 
                }, this));
1456
 
            }, this));
1457
 
        },
1458
 
        stop: function() {
1459
 
            $('.oe_footer').show();
1460
 
            $('.oe_toggle_secondary_menu').show();
1461
 
            pos = undefined;
1462
 
            this._super();
1463
 
        }
1464
 
    });
1465
 
}