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