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