barcode scanning fixes
[odoo/odoo.git] / addons / point_of_sale / static / src / js / pos_models.js
1 function openerp_pos_models(module, instance){ //module is instance.point_of_sale
2     var QWeb = instance.web.qweb;
3
4     module.LocalStorageDAO = instance.web.Class.extend({
5         add_operation: function(operation) {
6             var self = this;
7             return $.async_when().pipe(function() {
8                 var tmp = self._get('oe_pos_operations', []);
9                 var last_id = self._get('oe_pos_operations_sequence', 1);
10                 tmp.push({'id': last_id, 'data': operation});
11                 self._set('oe_pos_operations', tmp);
12                 self._set('oe_pos_operations_sequence', last_id + 1);
13             });
14         },
15         remove_operation: function(id) {
16             var self = this;
17             return $.async_when().pipe(function() {
18                 var tmp = self._get('oe_pos_operations', []);
19                 tmp = _.filter(tmp, function(el) {
20                     return el.id !== id;
21                 });
22                 self._set('oe_pos_operations', tmp);
23             });
24         },
25         get_operations: function() {
26             var self = this;
27             return $.async_when().pipe(function() {
28                 return self._get('oe_pos_operations', []);
29             });
30         },
31         _get: function(key, default_) {
32             var txt = localStorage[key];
33             if (! txt)
34                 return default_;
35             return JSON.parse(txt);
36         },
37         _set: function(key, value) {
38             localStorage[key] = JSON.stringify(value);
39         },
40     });
41
42     var fetch = function(osvModel, fields, domain){
43         var dataSetSearch = new instance.web.DataSetSearch(null, osvModel, {}, domain);
44         return dataSetSearch.read_slice(fields, 0);
45     };
46     
47     /*
48      Gets all the necessary data from the OpenERP web client (instance, shop data etc.)
49      */
50     module.PosModel = Backbone.Model.extend({
51         initialize: function(session, attributes) {
52             Backbone.Model.prototype.initialize.call(this, attributes);
53             var  self = this;
54             this.dao = new module.LocalStorageDAO();
55             this.ready = $.Deferred();
56             this.flush_mutex = new $.Mutex();
57             this.build_tree = _.bind(this.build_tree, this);
58             this.session = session;
59             this.categories = {};
60             this.barcode_reader = new module.BarcodeReader({'pos': this});
61             this.proxy = new module.ProxyDevice({'pos': this});
62             this.set({
63                 'nbr_pending_operations': 0,
64                 'currency': {symbol: '$', position: 'after'},
65                 'shop': {},
66                 'company': {},
67                 'user': {},
68                 'orders': new module.OrderCollection(),
69                 'products': new module.ProductCollection(),
70                 //'cashRegisters': [], // new module.CashRegisterCollection(this.pos.get('bank_statements')),
71                 'selectedOrder': undefined,
72             });
73             
74             var cat_def = fetch('pos.category', ['name', 'parent_id', 'child_id'])
75                 .pipe(function(result){
76                     return self.set({'categories': result});
77                 });
78             
79             var prod_def = fetch( 
80                 'product.product', 
81                 ['name', 'list_price', 'pos_categ_id', 'taxes_id','product_image_small'],
82                 [['pos_categ_id','!=', false]] 
83                 ).then(function(result){
84                     console.log('product_list:',result);
85                     return self.set({'product_list': result});
86                 });
87
88             var bank_def = fetch(
89                 'account.bank.statement', 
90                 ['account_id', 'currency', 'journal_id', 'state', 'name'],
91                 [['state','=','open'], ['user_id', '=', this.session.uid]]
92                 ).then(function(result){
93                     console.log('bank_statements:',result);
94                     return self.set({'bank_statements': result});
95                 });
96
97             var tax_def = fetch('account.tax', ['amount','price_include','type'])
98                 .then(function(result){
99                     console.log('taxes:',result);
100                     return self.set({'taxes': result});
101                 });
102
103             $.when(cat_def,prod_def,bank_def,tax_def,this.get_app_data(), this.flush())
104                 .pipe(_.bind(this.build_tree, this))
105                 .pipe(function(){
106                     self.set({'cashRegisters': new module.CashRegisterCollection(self.get('bank_statements')) });
107                     self.ready.resolve();
108                 });
109
110             return (this.get('orders')).bind('remove', _.bind( function(removedOrder) {
111                 if ((this.get('orders')).isEmpty()) {
112                     this.addAndSelectOrder(new module.Order({pos: self}));
113                 }
114                 if ((this.get('selectedOrder')) === removedOrder) {
115                     return this.set({
116                         selectedOrder: (this.get('orders')).last()
117                     });
118                 }
119             }, this));
120
121         },
122
123         get_app_data: function() {
124             var self = this;
125             return $.when(new instance.web.Model("sale.shop").get_func("search_read")([]).pipe(function(result) {
126                 self.set({'shop': result[0]});
127                 var company_id = result[0]['company_id'][0];
128                 return new instance.web.Model("res.company").get_func("read")(company_id, ['currency_id', 'name', 'phone']).pipe(function(result) {
129                     self.set({'company': result});
130                     var currency_id = result['currency_id'][0]
131                     return new instance.web.Model("res.currency").get_func("read")([currency_id],
132                             ['symbol', 'position']).pipe(function(result) {
133                         self.set({'currency': result[0]});
134                         
135                     });
136                 });
137             }), new instance.web.Model("res.users").get_func("read")(this.session.uid, ['name']).pipe(function(result) {
138                 self.set({'user': result});
139             }));
140         },
141         push_order: function(record) {
142             var self = this;
143             return this.dao.add_operation(record).pipe(function(){
144                     return self.flush();
145             });
146         },
147         addAndSelectOrder: function(newOrder) {
148             (this.get('orders')).add(newOrder);
149             return this.set({
150                 selectedOrder: newOrder
151             });
152         },
153         flush: function() {
154             return this.flush_mutex.exec(_.bind(function() {
155                 return this._int_flush();
156             }, this));
157         },
158         _int_flush : function() {
159             var self = this;
160
161             this.dao.get_operations().pipe(function(operations) {
162                 self.set( {'nbr_pending_operations':operations.length} );
163                 if(operations.length === 0){
164                     return $.when();
165                 }
166                 var op = operations[0];
167
168                  // we prevent the default error handler and assume errors
169                  // are a normal use case, except we stop the current iteration
170
171                  return new instance.web.Model('pos.order').get_func('create_from_ui')([op])
172                             .fail(function(unused, event){
173                                 event.preventDefault();
174                             })
175                             .pipe(function(){
176                                 console.debug('saved 1 record'); //TODO Debug this
177                                 self.dao.remove_operation(operations[0].id).pipe(function(){
178                                     return self._int_flush();
179                                 });
180                             }, function(){
181                                 return $.when();
182                             });
183             });
184         },
185         build_tree: function() {
186             var c, id, _i, _len, _ref, _ref2;
187             _ref = this.get('categories');
188             for (_i = 0, _len = _ref.length; _i < _len; _i++) {
189                 c = _ref[_i];
190                 this.categories[c.id] = {
191                     id: c.id,
192                     name: c.name,
193                     children: c.child_id,
194                     parent: c.parent_id[0],
195                     ancestors: [c.id],
196                     subtree: [c.id]
197                 };
198             }
199             _ref2 = this.categories;
200             for (id in _ref2) {
201                 c = _ref2[id];
202                 this.current_category = c;
203                 this.build_ancestors(c.parent);
204                 this.build_subtree(c);
205             }
206             this.categories[0] = {
207                 ancestors: [],
208                 children: (function() {
209                     var _j, _len2, _ref3, _results;
210                     _ref3 = this.get('categories');
211                     _results = [];
212                     for (_j = 0, _len2 = _ref3.length; _j < _len2; _j++) {
213                         c = _ref3[_j];
214                         if (!(c.parent_id[0] != null)) {
215                             _results.push(c.id);
216                         }
217                     }
218                     return _results;
219                 }).call(this),
220                 subtree: (function() {
221                     var _j, _len2, _ref3, _results;
222                     _ref3 = this.get('categories');
223                     _results = [];
224                     for (_j = 0, _len2 = _ref3.length; _j < _len2; _j++) {
225                         c = _ref3[_j];
226                         _results.push(c.id);
227                     }
228                     return _results;
229                 }).call(this)
230             };
231         },
232         build_ancestors: function(parent) {
233             if (parent != null) {
234                 this.current_category.ancestors.unshift(parent);
235                 return this.build_ancestors(this.categories[parent].parent);
236             }
237         },
238         build_subtree: function(category) {
239             var c, _i, _len, _ref, _results;
240             _ref = category.children;
241             _results = [];
242             for (_i = 0, _len = _ref.length; _i < _len; _i++) {
243                 c = _ref[_i];
244                 this.current_category.subtree.push(c);
245                 _results.push(this.build_subtree(this.categories[c]));
246             }
247             return _results;
248         }
249     });
250
251     module.CashRegister = Backbone.Model.extend({
252     });
253
254     module.CashRegisterCollection = Backbone.Collection.extend({
255         model: module.CashRegister,
256     });
257
258     module.Product = Backbone.Model.extend({
259     });
260
261     module.ProductCollection = Backbone.Collection.extend({
262         model: module.Product,
263     });
264
265     /*
266      Each Order contains zero or more Orderlines (i.e. the content of the "shopping cart".)
267      There should only ever be one Orderline per distinct product in an Order.
268      To add more of the same product, just update the quantity accordingly.
269      The Order also contains payment information.
270      */
271     module.Orderline = Backbone.Model.extend({
272         defaults: {
273             quantity: 1,
274             list_price: 0,
275             discount: 0,
276             weighted: false,
277         },
278         initialize: function(attributes) {
279             this.pos = attributes.pos;
280             console.log(attributes);
281             Backbone.Model.prototype.initialize.apply(this, arguments);
282
283             if(attributes.weight){
284                 this.setWeight(attributes.weight);
285                 this.set({weighted: true});
286             }
287
288             this.bind('change:quantity', function(unused, qty) {
289                 if (qty == 0)
290                     this.trigger('killme');
291             }, this);
292         },
293         setWeight: function(weight){
294             return this.set({
295                 quantity: weight,
296             });
297         },
298         incrementQuantity: function() {
299             return this.set({
300                 quantity: (this.get('quantity')) + 1
301             });
302         },
303         incrementWeight: function(weight){
304             return this.set({
305                 quantity: (this.get('quantity')) + weight,
306             });
307         },
308         getPriceWithoutTax: function() {
309             return this.getAllPrices().priceWithoutTax;
310         },
311         getPriceWithTax: function() {
312             return this.getAllPrices().priceWithTax;
313         },
314         getTax: function() {
315             return this.getAllPrices().tax;
316         },
317         getAllPrices: function() {
318             var self = this;
319             var base = (this.get('quantity')) * (this.get('list_price')) * (1 - (this.get('discount')) / 100);
320             var totalTax = base;
321             var totalNoTax = base;
322             
323             var product_list = self.pos.get('product_list');
324             var product = _.detect(product_list, function(el) {return el.id === self.get('id');});
325             var taxes_ids = product.taxes_id;
326             var taxes =  self.pos.get('taxes');
327             var taxtotal = 0;
328             _.each(taxes_ids, function(el) {
329                 var tax = _.detect(taxes, function(t) {return t.id === el;});
330                 if (tax.price_include) {
331                     var tmp;
332                     if (tax.type === "percent") {
333                         tmp =  base - (base / (1 + tax.amount));
334                     } else if (tax.type === "fixed") {
335                         tmp = tax.amount * self.get('quantity');
336                     } else {
337                         throw "This type of tax is not supported by the point of sale: " + tax.type;
338                     }
339                     taxtotal += tmp;
340                     totalNoTax -= tmp;
341                 } else {
342                     var tmp;
343                     if (tax.type === "percent") {
344                         tmp = tax.amount * base;
345                     } else if (tax.type === "fixed") {
346                         tmp = tax.amount * self.get('quantity');
347                     } else {
348                         throw "This type of tax is not supported by the point of sale: " + tax.type;
349                     }
350                     taxtotal += tmp;
351                     totalTax += tmp;
352                 }
353             });
354             return {
355                 "priceWithTax": totalTax,
356                 "priceWithoutTax": totalNoTax,
357                 "tax": taxtotal,
358             };
359         },
360         exportAsJSON: function() {
361             return {
362                 qty: this.get('quantity'),
363                 price_unit: this.get('list_price'),
364                 discount: this.get('discount'),
365                 product_id: this.get('id')
366             };
367         },
368     });
369
370     module.OrderlineCollection = Backbone.Collection.extend({
371         model: module.Orderline,
372     });
373
374     // Every PaymentLine has all the attributes of the corresponding CashRegister.
375     module.Paymentline = Backbone.Model.extend({
376         defaults: { 
377             amount: 0,
378         },
379         initialize: function(attributes) {
380             Backbone.Model.prototype.initialize.apply(this, arguments);
381         },
382         getAmount: function(){
383             return this.get('amount');
384         },
385         exportAsJSON: function(){
386             return {
387                 name: instance.web.datetime_to_str(new Date()),
388                 statement_id: this.get('id'),
389                 account_id: (this.get('account_id'))[0],
390                 journal_id: (this.get('journal_id'))[0],
391                 amount: this.getAmount()
392             };
393         },
394     });
395
396     module.PaymentlineCollection = Backbone.Collection.extend({
397         model: module.Paymentline,
398     });
399     
400     module.Order = Backbone.Model.extend({
401         defaults:{
402             validated: false,
403             step: 'products',
404         },
405         initialize: function(attributes){
406             Backbone.Model.prototype.initialize.apply(this, arguments);
407             this.set({
408                 creationDate:   new Date(),
409                 orderLines:     new module.OrderlineCollection(),
410                 paymentLines:   new module.PaymentlineCollection(),
411                 name:           "Order " + this.generateUniqueId(),
412             });
413             this.pos =     attributes.pos; //TODO put that in set and remember to use 'get' to read it ... 
414             this.bind('change:validated', this.validatedChanged);
415             return this;
416         },
417         events: {
418             'change:validated': 'validatedChanged'
419         },
420         validatedChanged: function() {
421             if (this.get("validated") && !this.previous("validated")) {
422                 this.pos.screen_selector.set_current_screen('receipt'); 
423                 //this.set({'screen': 'receipt'});
424             }
425         },
426         generateUniqueId: function() {
427             return new Date().getTime();
428         },
429         addProduct: function(product) {
430             var existing;
431             existing = (this.get('orderLines')).get(product.id);
432             if (existing != null) {
433                 if(existing.get('weighted')){
434                     existing.incrementWeight(product.attributes.weight);
435                 }else{
436                     existing.incrementQuantity();
437                 }
438             } else {
439                 var attr = product.toJSON();
440                 attr.pos = this.pos;
441                 var line = new module.Orderline(attr);
442                 this.get('orderLines').add(line);
443                 line.bind('killme', function() {
444                     this.get('orderLines').remove(line);
445                 }, this);
446             }
447         },
448         addPaymentLine: function(cashRegister) {
449             var newPaymentline;
450             newPaymentline = new module.Paymentline(cashRegister);
451             /* TODO: Should be 0 for cash-like accounts */
452             newPaymentline.set({
453                 amount: this.getDueLeft()
454             });
455             return (this.get('paymentLines')).add(newPaymentline);
456         },
457         getName: function() {
458             return this.get('name');
459         },
460         getTotal: function() {
461             return (this.get('orderLines')).reduce((function(sum, orderLine) {
462                 return sum + orderLine.getPriceWithTax();
463             }), 0);
464         },
465         getTotalTaxExcluded: function() {
466             return (this.get('orderLines')).reduce((function(sum, orderLine) {
467                 return sum + orderLine.getPriceWithoutTax();
468             }), 0);
469         },
470         getTax: function() {
471             return (this.get('orderLines')).reduce((function(sum, orderLine) {
472                 return sum + orderLine.getTax();
473             }), 0);
474         },
475         getPaidTotal: function() {
476             return (this.get('paymentLines')).reduce((function(sum, paymentLine) {
477                 return sum + paymentLine.getAmount();
478             }), 0);
479         },
480         getChange: function() {
481             return this.getPaidTotal() - this.getTotal();
482         },
483         getDueLeft: function() {
484             return this.getTotal() - this.getPaidTotal();
485         },
486         exportAsJSON: function() {
487             var orderLines, paymentLines;
488             orderLines = [];
489             (this.get('orderLines')).each(_.bind( function(item) {
490                 return orderLines.push([0, 0, item.exportAsJSON()]);
491             }, this));
492             paymentLines = [];
493             (this.get('paymentLines')).each(_.bind( function(item) {
494                 return paymentLines.push([0, 0, item.exportAsJSON()]);
495             }, this));
496             return {
497                 name: this.getName(),
498                 amount_paid: this.getPaidTotal(),
499                 amount_total: this.getTotal(),
500                 amount_tax: this.getTax(),
501                 amount_return: this.getChange(),
502                 lines: orderLines,
503                 statement_ids: paymentLines
504             };
505         },
506     });
507
508     module.OrderCollection = Backbone.Collection.extend({
509         model: module.Order,
510     });
511
512     /*
513      The numpad handles both the choice of the property currently being modified
514      (quantity, price or discount) and the edition of the corresponding numeric value.
515      */
516     module.NumpadState = Backbone.Model.extend({
517         defaults: {
518             buffer: "0",
519             mode: "quantity"
520         },
521         appendNewChar: function(newChar) {
522             var oldBuffer;
523             oldBuffer = this.get('buffer');
524             if (oldBuffer === '0') {
525                 this.set({
526                     buffer: newChar
527                 });
528             } else if (oldBuffer === '-0') {
529                 this.set({
530                     buffer: "-" + newChar
531                 });
532             } else {
533                 this.set({
534                     buffer: (this.get('buffer')) + newChar
535                 });
536             }
537             this.updateTarget();
538         },
539         deleteLastChar: function() {
540             var tempNewBuffer;
541             tempNewBuffer = (this.get('buffer')).slice(0, -1) || "0";
542             if (isNaN(tempNewBuffer)) {
543                 tempNewBuffer = "0";
544             }
545             this.set({
546                 buffer: tempNewBuffer
547             });
548             this.updateTarget();
549         },
550         switchSign: function() {
551             var oldBuffer;
552             oldBuffer = this.get('buffer');
553             this.set({
554                 buffer: oldBuffer[0] === '-' ? oldBuffer.substr(1) : "-" + oldBuffer
555             });
556             this.updateTarget();
557         },
558         changeMode: function(newMode) {
559             this.set({
560                 buffer: "0",
561                 mode: newMode
562             });
563         },
564         reset: function() {
565             this.set({
566                 buffer: "0",
567                 mode: "quantity"
568             });
569         },
570         updateTarget: function() {
571             var bufferContent, params;
572             bufferContent = this.get('buffer');
573             if (bufferContent && !isNaN(bufferContent)) {
574                 this.trigger('setValue', parseFloat(bufferContent));
575             }
576         },
577     });
578 }