[IMP] point_of_sale: less confusing synchronisation indicator
[odoo/odoo.git] / addons / point_of_sale / static / src / js / models.js
1 function openerp_pos_models(instance, module){ //module is instance.point_of_sale
2     var QWeb = instance.web.qweb;
3         var _t = instance.web._t;
4
5     var round_di = instance.web.round_decimals;
6     var round_pr = instance.web.round_precision
7     
8     // The PosModel contains the Point Of Sale's representation of the backend.
9     // Since the PoS must work in standalone ( Without connection to the server ) 
10     // it must contains a representation of the server's PoS backend. 
11     // (taxes, product list, configuration options, etc.)  this representation
12     // is fetched and stored by the PosModel at the initialisation. 
13     // this is done asynchronously, a ready deferred alows the GUI to wait interactively 
14     // for the loading to be completed 
15     // There is a single instance of the PosModel for each Front-End instance, it is usually called
16     // 'pos' and is available to all widgets extending PosWidget.
17
18     module.PosModel = Backbone.Model.extend({
19         initialize: function(session, attributes) {
20             Backbone.Model.prototype.initialize.call(this, attributes);
21             var  self = this;
22             this.session = session;                 
23             this.flush_mutex = new $.Mutex();                   // used to make sure the orders are sent to the server once at time
24             this.pos_widget = attributes.pos_widget;
25
26             this.proxy = new module.ProxyDevice();              // used to communicate to the hardware devices via a local proxy
27             this.barcode_reader = new module.BarcodeReader({'pos': this, proxy:this.proxy});  // used to read barcodes
28             this.barcode_reader.connect_to_proxy();
29             this.proxy_queue = new module.JobQueue();           // used to prevent parallels communications to the proxy
30             this.db = new module.PosDB();                       // a local database used to search trough products and categories & store pending orders
31             this.debug = jQuery.deparam(jQuery.param.querystring()).debug !== undefined;    //debug mode 
32             
33             // Business data; loaded from the server at launch
34             this.accounting_precision = 2; //TODO
35             this.company_logo = null;
36             this.company_logo_base64 = '';
37             this.currency = null;
38             this.shop = null;
39             this.company = null;
40             this.user = null;
41             this.users = [];
42             this.partners = [];
43             this.cashier = null;
44             this.cashregisters = [];
45             this.bankstatements = [];
46             this.taxes = [];
47             this.pos_session = null;
48             this.config = null;
49             this.units = [];
50             this.units_by_id = {};
51             this.pricelist = null;
52             window.posmodel = this;
53
54             // these dynamic attributes can be watched for change by other models or widgets
55             this.set({
56                 'synch':            { state:'connected', pending:0 }, 
57                 'orders':           new module.OrderCollection(),
58                 'selectedOrder':    null,
59             });
60
61             this.bind('change:synch',function(pos,synch){
62                 clearTimeout(self.synch_timeout);
63                 self.synch_timeout = setTimeout(function(){
64                     if(synch.state !== 'disconnected' && synch.pending > 0){
65                         self.set('synch',{state:'disconnected', pending:synch.pending});
66                     }
67                 },3000);
68             });
69
70             this.get('orders').bind('remove', function(order,_unused_,options){ 
71                 self.on_removed_order(order,options.index,options.reason); 
72             });
73             
74             // We fetch the backend data on the server asynchronously. this is done only when the pos user interface is launched,
75             // Any change on this data made on the server is thus not reflected on the point of sale until it is relaunched. 
76             // when all the data has loaded, we compute some stuff, and declare the Pos ready to be used. 
77             this.ready = this.load_server_data()
78                 .then(function(){
79                     return self.connect_to_posbox();
80                 });
81             
82         },
83
84         // releases ressources holds by the model at the end of life of the posmodel
85         destroy: function(){
86             // FIXME, should wait for flushing, return a deferred to indicate successfull destruction
87             // this.flush();
88             this.proxy.close();
89             this.barcode_reader.disconnect();
90         },
91
92         connect_to_posbox: function(){
93             var self = this;
94             this.pos_widget.loading_message(_t('Connecting to the PosBox'),0);
95             
96             this.pos_widget.loading_skip(function(){
97                     self.proxy.stop_searching();
98                 });
99
100             return this.proxy.find_proxy({
101                     force_ip: self.config.proxy_ip || undefined,
102                     progress: function(prog){ 
103                         self.pos_widget.loading_progress(prog);
104                     },
105                 }).then(function(proxies){
106                     if(proxies.length > 0){
107                         self.proxy.connect(proxies[0]);
108                     }
109                 });
110         },
111
112         // helper function to load data from the server
113         fetch: function(model, fields, domain, ctx){
114             this._load_progress = (this._load_progress || 0) + 0.05; 
115             this.pos_widget.loading_message(_t('Loading')+' '+model,this._load_progress);
116             return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all()
117         },
118         // loads all the needed data on the sever. returns a deferred indicating when all the data has loaded. 
119         load_server_data: function(){
120             var self = this;
121
122             var loaded = self.fetch('res.users',['name','company_id'],[['id','=',this.session.uid]]) 
123                 .then(function(users){
124                     self.user = users[0];
125
126                     return self.fetch('res.company',
127                     [
128                         'currency_id',
129                         'email',
130                         'website',
131                         'company_registry',
132                         'vat',
133                         'name',
134                         'phone',
135                         'partner_id',
136                     ],
137                     [['id','=',users[0].company_id[0]]]);
138                 }).then(function(companies){
139                     self.company = companies[0];
140
141                     return self.fetch('res.partner',['contact_address'],[['id','=',companies[0].partner_id[0]]]);
142                 }).then(function(company_partners){
143                     self.company.contact_address = company_partners[0].contact_address;
144
145                     return self.fetch('product.uom', null, null);
146                 }).then(function(units){
147                     self.units = units;
148                     var units_by_id = {};
149                     for(var i = 0, len = units.length; i < len; i++){
150                         units_by_id[units[i].id] = units[i];
151                     }
152                     self.units_by_id = units_by_id;
153                     
154                     return self.fetch('res.users', ['name','ean13'], [['ean13', '!=', false]]);
155                 }).then(function(users){
156                     self.users = users;
157
158                     return self.fetch('res.partner', ['name','ean13'], [['ean13', '!=', false]]);
159                 }).then(function(partners){
160                     self.partners = partners;
161
162                     return self.fetch('account.tax', ['name','amount', 'price_include', 'type']);
163                 }).then(function(taxes){
164                     self.taxes = taxes;
165
166                     return self.fetch(
167                         'pos.session', 
168                         ['id', 'journal_ids','name','user_id','config_id','start_at','stop_at'],
169                         [['state', '=', 'opened'], ['user_id', '=', self.session.uid]]
170                     );
171                 }).then(function(pos_sessions){
172                     self.pos_session = pos_sessions[0];
173
174                     return self.fetch(
175                         'pos.config',
176                         ['name','journal_ids','warehouse_id','journal_id','pricelist_id',
177                          'iface_self_checkout', 'iface_led', 'iface_cashdrawer',
178                          'iface_payment_terminal', 'iface_electronic_scale', 'iface_barscan', 'iface_vkeyboard',
179                          'iface_print_via_proxy','iface_cashdrawer','iface_invoicing','iface_big_scrollbars',
180                          'receipt_header','receipt_footer','proxy_ip',
181                          'state','sequence_id','session_ids'],
182                         [['id','=', self.pos_session.config_id[0]]]
183                     );
184                 }).then(function(configs){
185                     self.config = configs[0];
186
187                     return self.fetch('stock.warehouse',[],[['id','=', self.config.warehouse_id[0]]]);
188                 }).then(function(shops){
189                     self.shop = shops[0];
190
191                     return self.fetch('product.pricelist',['currency_id'],[['id','=',self.config.pricelist_id[0]]]);
192                 }).then(function(pricelists){
193                     self.pricelist = pricelists[0];
194
195                     return self.fetch('res.currency',['symbol','position','rounding','accuracy'],[['id','=',self.pricelist.currency_id[0]]]);
196                 }).then(function(currencies){
197                     self.currency = currencies[0];
198
199                     /*
200                     return (new instance.web.Model('decimal.precision')).call('get_precision',[['Account']]);
201                 }).then(function(precision){
202                     self.accounting_precision = precision;
203                     console.log("PRECISION",precision);
204 */
205                     return self.fetch('product.packaging',['ean','product_id']);
206                 }).then(function(packagings){
207                     self.db.add_packagings(packagings);
208
209                     return self.fetch('pos.category', ['id','name','parent_id','child_id','image'])
210                 }).then(function(categories){
211                     self.db.add_categories(categories);
212
213                     return self.fetch(
214                         'product.product', 
215                         ['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13', 'default_code',
216                          'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description'],
217                         [['sale_ok','=',true],['available_in_pos','=',true]],
218                         {pricelist: self.pricelist.id} // context for price
219                     );
220                 }).then(function(products){
221                     self.db.add_products(products);
222
223                     return self.fetch(
224                         'account.bank.statement',
225                         ['account_id','currency','journal_id','state','name','user_id','pos_session_id'],
226                         [['state','=','open'],['pos_session_id', '=', self.pos_session.id]]
227                     );
228                 }).then(function(bankstatements){
229                     var journals = [];
230                     _.each(bankstatements,function(statement) {
231                         journals.push(statement.journal_id[0])
232                     });
233                     self.bankstatements = bankstatements;
234                     return self.fetch('account.journal', undefined, [['id','in', journals]]);
235                 }).then(function(journals){
236                     self.journals = journals; 
237
238                     // associate the bank statements with their journals. 
239                     var bankstatements = self.bankstatements
240                     for(var i = 0, ilen = bankstatements.length; i < ilen; i++){
241                         for(var j = 0, jlen = journals.length; j < jlen; j++){
242                             if(bankstatements[i].journal_id[0] === journals[j].id){
243                                 bankstatements[i].journal = journals[j];
244                                 bankstatements[i].self_checkout_payment_method = journals[j].self_checkout_payment_method;
245                             }
246                         }
247                     }
248                     self.cashregisters = bankstatements;
249
250                     // Load the company Logo
251
252                     self.company_logo = new Image();
253                     self.company_logo.crossOrigin = 'anonymous';
254                     var  logo_loaded = new $.Deferred();
255                     self.company_logo.onload = function(){
256                         var c = document.createElement('canvas');
257                             c.width  = self.company_logo.width;
258                             c.height = self.company_logo.height; 
259                         var ctx = c.getContext('2d');
260                             ctx.drawImage(self.company_logo,0,0);
261                         self.company_logo_base64 = c.toDataURL();
262                         logo_loaded.resolve();
263                     };
264                     self.company_logo.onerror = function(){
265                         logo_loaded.reject();
266                     };
267                     self.company_logo.src = window.location.origin + '/web/binary/company_logo';
268
269                     return logo_loaded;
270                 });
271         
272             return loaded;
273         },
274
275         // this is called when an order is removed from the order collection. It ensures that there is always an existing
276         // order and a valid selected order
277         on_removed_order: function(removed_order,index,reason){
278             if(reason === 'abandon' && this.get('orders').size() > 0){
279                 // when we intentionally remove an unfinished order, and there is another existing one
280                 this.set({'selectedOrder' : this.get('orders').at(index) || this.get('orders').last()});
281             }else{
282                 // when the order was automatically removed after completion, 
283                 // or when we intentionally delete the only concurrent order
284                 this.add_new_order();
285             }
286         },
287
288         //creates a new empty order and sets it as the current order
289         add_new_order: function(){
290             var order = new module.Order({pos:this});
291             this.get('orders').add(order);
292             this.set('selectedOrder', order);
293         },
294
295         //removes the current order
296         delete_current_order: function(){
297             this.get('selectedOrder').destroy({'reason':'abandon'});
298         },
299
300         // saves the order locally and try to send it to the backend. 
301         // it returns a deferred that succeeds after having tried to send the order and all the other pending orders.
302         push_order: function(order) {
303             var self = this;
304             this.proxy.log('push_order',order.export_as_JSON());
305             var order_id = this.db.add_order(order.export_as_JSON());
306             var pushed = new $.Deferred();
307
308             this.set('synch',{state:'connecting', pending:self.db.get_orders().length});
309
310             this.flush_mutex.exec(function(){
311                 var flushed = self._flush_all_orders();
312
313                 flushed.always(function(){
314                     pushed.resolve();
315                 });
316
317                 return flushed;
318             });
319             return pushed;
320         },
321
322         // saves the order locally and try to send it to the backend and make an invoice
323         // returns a deferred that succeeds when the order has been posted and successfully generated
324         // an invoice. This method can fail in various ways:
325         // error-no-client: the order must have an associated partner_id. You can retry to make an invoice once
326         //     this error is solved
327         // error-transfer: there was a connection error during the transfer. You can retry to make the invoice once
328         //     the network connection is up 
329
330         push_and_invoice_order: function(order){
331             var self = this;
332             var invoiced = new $.Deferred(); 
333
334             if(!order.get_client()){
335                 invoiced.reject('error-no-client');
336                 return invoiced;
337             }
338
339             var order_id = this.db.add_order(order.export_as_JSON());
340
341             this.set('synch',{state:'connecting', pending:self.db.get_orders().length});
342
343             this.flush_mutex.exec(function(){
344                 var done = new $.Deferred(); // holds the mutex
345
346                 // send the order to the server
347                 // we have a 30 seconds timeout on this push.
348                 // FIXME: if the server takes more than 30 seconds to accept the order,
349                 // the client will believe it wasn't successfully sent, and very bad
350                 // things will happen as a duplicate will be sent next time
351                 // so we must make sure the server detects and ignores duplicated orders
352
353                 var transfer = self._flush_order(order_id, {timeout:30000, to_invoice:true});
354                 
355                 transfer.fail(function(){
356                     invoiced.reject('error-transfer');
357                     done.reject();
358                 });
359
360                 // on success, get the order id generated by the server
361                 transfer.pipe(function(order_server_id){    
362                     // generate the pdf and download it
363                     self.pos_widget.do_action('point_of_sale.pos_invoice_report',{additional_context:{ 
364                         active_ids:order_server_id,
365                     }});
366                     invoiced.resolve();
367                     done.resolve();
368                 });
369
370                 return done;
371
372             });
373
374             return invoiced;
375         },
376
377         // attemps to send all pending orders ( stored in the pos_db ) to the server,
378         // and remove the successfully sent ones from the db once
379         // it has been confirmed that they have been sent correctly.
380         flush: function() {
381             var self = this;
382             var flushed = new $.Deferred();
383
384             this.flush_mutex.exec(function(){
385                 var done = new $.Deferred();
386
387                 self._flush_all_orders()
388                     .done(  function(){ flushed.resolve();})
389                     .fail(  function(){ flushed.reject(); })
390                     .always(function(){ done.resolve();   });
391
392                 return done;
393             });
394
395             return flushed;
396         },
397
398         // attempts to send the locally stored order of id 'order_id'
399         // the sending is asynchronous and can take some time to decide if it is successful or not
400         // it is therefore important to only call this method from inside a mutex
401         // this method returns a deferred indicating wether the sending was successful or not
402         // there is a timeout parameter which is set to 2 seconds by default. 
403         _flush_order: function(order_id, options){
404             var self   = this;
405             options = options || {};
406             timeout = typeof options.timeout === 'number' ? options.timeout : 7500;
407
408             this.set('synch',{state:'connecting', pending: this.get('synch').pending});
409
410             var order  = this.db.get_order(order_id);
411             order.to_invoice = options.to_invoice || false;
412
413             if(!order){
414                 // flushing a non existing order always fails
415                 return (new $.Deferred()).reject();
416             }
417
418             // we try to send the order. shadow prevents a spinner if it takes too long. (unless we are sending an invoice,
419             // then we want to notify the user that we are waiting on something )
420             var rpc = (new instance.web.Model('pos.order')).call('create_from_ui',[[order]],undefined,{shadow: !options.to_invoice, timeout:timeout});
421
422             rpc.fail(function(unused,event){
423                 // prevent an error popup creation by the rpc failure
424                 // we want the failure to be silent as we send the orders in the background
425                 event.preventDefault();
426                 console.error('Failed to send order:',order);
427             });
428
429             rpc.done(function(){
430                 self.db.remove_order(order_id);
431                 var pending = self.db.get_orders().length;
432                 self.set('synch',{state: pending ? 'connecting' : 'connected', pending:pending});
433             });
434
435             return rpc;
436         },
437         
438         // attempts to send all the locally stored orders. As with _flush_order, it should only be
439         // called from within a mutex. 
440         // this method returns a deferred that always succeeds when all orders have been tried to be sent,
441         // even if none of them could actually be sent. 
442         _flush_all_orders: function(){
443             var self = this;
444             var orders = this.db.get_orders();
445             var tried_all = new $.Deferred();
446
447             function rec_flush(index){
448                 if(index < orders.length){
449                     self._flush_order(orders[index].id).always(function(){ 
450                         rec_flush(index+1); 
451                     })
452                 }else{
453                     tried_all.resolve();
454                 }
455             }
456             rec_flush(0);
457
458             return tried_all;
459         },
460
461         scan_product: function(parsed_code){
462             var self = this;
463             var selectedOrder = this.get('selectedOrder');
464             if(parsed_code.encoding === 'ean13'){
465                 var product = this.db.get_product_by_ean13(parsed_code.base_code);
466             }else if(parsed_code.encoding === 'reference'){
467                 var product = this.db.get_product_by_reference(parsed_code.code);
468             }
469
470             if(!product){
471                 return false;
472             }
473
474             if(parsed_code.type === 'price'){
475                 selectedOrder.addProduct(product, {price:parsed_code.value});
476             }else if(parsed_code.type === 'weight'){
477                 selectedOrder.addProduct(product, {quantity:parsed_code.value, merge:false});
478             }else{
479                 selectedOrder.addProduct(product);
480             }
481             return true;
482         },
483     });
484
485     // An orderline represent one element of the content of a client's shopping cart.
486     // An orderline contains a product, its quantity, its price, discount. etc. 
487     // An Order contains zero or more Orderlines.
488     module.Orderline = Backbone.Model.extend({
489         initialize: function(attr,options){
490             this.pos = options.pos;
491             this.order = options.order;
492             this.product = options.product;
493             this.price   = options.product.price;
494             this.quantity = 1;
495             this.quantityStr = '1';
496             this.discount = 0;
497             this.discountStr = '0';
498             this.type = 'unit';
499             this.selected = false;
500         },
501         // sets a discount [0,100]%
502         set_discount: function(discount){
503             var disc = Math.min(Math.max(parseFloat(discount) || 0, 0),100);
504             this.discount = disc;
505             this.discountStr = '' + disc;
506             this.trigger('change',this);
507         },
508         // returns the discount [0,100]%
509         get_discount: function(){
510             return this.discount;
511         },
512         get_discount_str: function(){
513             return this.discountStr;
514         },
515         get_product_type: function(){
516             return this.type;
517         },
518         // sets the quantity of the product. The quantity will be rounded according to the 
519         // product's unity of measure properties. Quantities greater than zero will not get 
520         // rounded to zero
521         set_quantity: function(quantity){
522             if(quantity === 'remove'){
523                 this.order.removeOrderline(this);
524                 return;
525             }else{
526                 var quant = Math.max(parseFloat(quantity) || 0, 0);
527                 var unit = this.get_unit();
528                 if(unit){
529                     this.quantity    = Math.max(unit.rounding, round_pr(quant, unit.rounding));
530                     this.quantityStr = this.quantity.toFixed(Math.max(0,Math.ceil(Math.log(1.0 / unit.rounding) / Math.log(10))));
531                 }else{
532                     this.quantity    = quant;
533                     this.quantityStr = '' + this.quantity;
534                 }
535             }
536             this.trigger('change',this);
537         },
538         // return the quantity of product
539         get_quantity: function(){
540             return this.quantity;
541         },
542         get_quantity_str: function(){
543             return this.quantityStr;
544         },
545         get_quantity_str_with_unit: function(){
546             var unit = this.get_unit();
547             if(unit && unit.name !== 'Unit(s)'){
548                 return this.quantityStr + ' ' + unit.name;
549             }else{
550                 return this.quantityStr;
551             }
552         },
553         // return the unit of measure of the product
554         get_unit: function(){
555             var unit_id = (this.product.uos_id || this.product.uom_id);
556             if(!unit_id){
557                 return undefined;
558             }
559             unit_id = unit_id[0];
560             if(!this.pos){
561                 return undefined;
562             }
563             return this.pos.units_by_id[unit_id];
564         },
565         // return the product of this orderline
566         get_product: function(){
567             return this.product;
568         },
569         // selects or deselects this orderline
570         set_selected: function(selected){
571             this.selected = selected;
572             this.trigger('change',this);
573         },
574         // returns true if this orderline is selected
575         is_selected: function(){
576             return this.selected;
577         },
578         // when we add an new orderline we want to merge it with the last line to see reduce the number of items
579         // in the orderline. This returns true if it makes sense to merge the two
580         can_be_merged_with: function(orderline){
581             if( this.get_product().id !== orderline.get_product().id){    //only orderline of the same product can be merged
582                 return false;
583             }else if(this.get_product_type() !== orderline.get_product_type()){
584                 return false;
585             }else if(this.get_discount() > 0){             // we don't merge discounted orderlines
586                 return false;
587             }else if(this.price !== orderline.price){
588                 return false;
589             }else{ 
590                 return true;
591             }
592         },
593         merge: function(orderline){
594             this.set_quantity(this.get_quantity() + orderline.get_quantity());
595         },
596         export_as_JSON: function() {
597             return {
598                 qty: this.get_quantity(),
599                 price_unit: this.get_unit_price(),
600                 discount: this.get_discount(),
601                 product_id: this.get_product().id,
602             };
603         },
604         //used to create a json of the ticket, to be sent to the printer
605         export_for_printing: function(){
606             return {
607                 quantity:           this.get_quantity(),
608                 unit_name:          this.get_unit().name,
609                 price:              this.get_unit_price(),
610                 discount:           this.get_discount(),
611                 product_name:       this.get_product().name,
612                 price_display :     this.get_display_price(),
613                 price_with_tax :    this.get_price_with_tax(),
614                 price_without_tax:  this.get_price_without_tax(),
615                 tax:                this.get_tax(),
616                 product_description:      this.get_product().description,
617                 product_description_sale: this.get_product().description_sale,
618             };
619         },
620         // changes the base price of the product for this orderline
621         set_unit_price: function(price){
622             this.price = round_di(parseFloat(price) || 0, 2);
623             this.trigger('change',this);
624         },
625         get_unit_price: function(){
626             var rounding = this.pos.currency.rounding;
627             return round_pr(this.price,rounding);
628         },
629         get_display_price: function(){
630             var rounding = this.pos.currency.rounding;
631             return  round_pr(round_pr(this.get_unit_price() * this.get_quantity(),rounding) * (1- this.get_discount()/100.0),rounding);
632         },
633         get_price_without_tax: function(){
634             return this.get_all_prices().priceWithoutTax;
635         },
636         get_price_with_tax: function(){
637             return this.get_all_prices().priceWithTax;
638         },
639         get_tax: function(){
640             return this.get_all_prices().tax;
641         },
642         get_tax_details: function(){
643             return this.get_all_prices().taxDetails;
644         },
645         get_all_prices: function(){
646             var self = this;
647             var currency_rounding = this.pos.currency.rounding;
648             var base = round_pr(this.get_quantity() * this.get_unit_price() * (1.0 - (this.get_discount() / 100.0)), currency_rounding);
649             var totalTax = base;
650             var totalNoTax = base;
651             
652             var product =  this.get_product(); 
653             var taxes_ids = product.taxes_id;
654             var taxes =  self.pos.taxes;
655             var taxtotal = 0;
656             var taxdetail = {};
657             _.each(taxes_ids, function(el) {
658                 var tax = _.detect(taxes, function(t) {return t.id === el;});
659                 if (tax.price_include) {
660                     var tmp;
661                     if (tax.type === "percent") {
662                         tmp =  base - round_pr(base / (1 + tax.amount),currency_rounding); 
663                     } else if (tax.type === "fixed") {
664                         tmp = round_pr(tax.amount * self.get_quantity(),currency_rounding);
665                     } else {
666                         throw "This type of tax is not supported by the point of sale: " + tax.type;
667                     }
668                     tmp = round_pr(tmp,currency_rounding);
669                     taxtotal += tmp;
670                     totalNoTax -= tmp;
671                     taxdetail[tax.id] = tmp;
672                 } else {
673                     var tmp;
674                     if (tax.type === "percent") {
675                         tmp = tax.amount * base;
676                     } else if (tax.type === "fixed") {
677                         tmp = tax.amount * self.get_quantity();
678                     } else {
679                         throw "This type of tax is not supported by the point of sale: " + tax.type;
680                     }
681                     tmp = round_pr(tmp,currency_rounding);
682                     taxtotal += tmp;
683                     totalTax += tmp;
684                     taxdetail[tax.id] = tmp;
685                 }
686             });
687             return {
688                 "priceWithTax": totalTax,
689                 "priceWithoutTax": totalNoTax,
690                 "tax": taxtotal,
691                 "taxDetails": taxdetail,
692             };
693         },
694     });
695
696     module.OrderlineCollection = Backbone.Collection.extend({
697         model: module.Orderline,
698     });
699
700     // Every Paymentline contains a cashregister and an amount of money.
701     module.Paymentline = Backbone.Model.extend({
702         initialize: function(attributes, options) {
703             this.amount = 0;
704             this.cashregister = options.cashregister;
705             this.name = this.cashregister.journal_id[1];
706             this.selected = false;
707         },
708         //sets the amount of money on this payment line
709         set_amount: function(value){
710             this.amount = round_di(parseFloat(value) || 0, 2);
711             this.trigger('change:amount',this);
712         },
713         // returns the amount of money on this paymentline
714         get_amount: function(){
715             return this.amount;
716         },
717         set_selected: function(selected){
718             if(this.selected !== selected){
719                 this.selected = selected;
720                 this.trigger('change:selected',this);
721             }
722         },
723         // returns the associated cashregister
724         //exports as JSON for server communication
725         export_as_JSON: function(){
726             return {
727                 name: instance.web.datetime_to_str(new Date()),
728                 statement_id: this.cashregister.id,
729                 account_id: this.cashregister.account_id[0],
730                 journal_id: this.cashregister.journal_id[0],
731                 amount: this.get_amount()
732             };
733         },
734         //exports as JSON for receipt printing
735         export_for_printing: function(){
736             return {
737                 amount: this.get_amount(),
738                 journal: this.cashregister.journal_id[1],
739             };
740         },
741     });
742
743     module.PaymentlineCollection = Backbone.Collection.extend({
744         model: module.Paymentline,
745     });
746     
747
748     // An order more or less represents the content of a client's shopping cart (the OrderLines) 
749     // plus the associated payment information (the Paymentlines) 
750     // there is always an active ('selected') order in the Pos, a new one is created
751     // automaticaly once an order is completed and sent to the server.
752     module.Order = Backbone.Model.extend({
753         initialize: function(attributes){
754             Backbone.Model.prototype.initialize.apply(this, arguments);
755             this.uid =     this.generateUniqueId();
756             this.set({
757                 creationDate:   new Date(),
758                 orderLines:     new module.OrderlineCollection(),
759                 paymentLines:   new module.PaymentlineCollection(),
760                 name:           "Order " + this.uid,
761                 client:         null,
762             });
763             this.pos = attributes.pos; 
764             this.selected_orderline   = undefined;
765             this.selected_paymentline = undefined;
766             this.screen_data = {};  // see ScreenSelector
767             this.receipt_type = 'receipt';  // 'receipt' || 'invoice'
768             return this;
769         },
770         generateUniqueId: function() {
771             return new Date().getTime();
772         },
773         addProduct: function(product, options){
774             options = options || {};
775             var attr = JSON.parse(JSON.stringify(product));
776             attr.pos = this.pos;
777             attr.order = this;
778             var line = new module.Orderline({}, {pos: this.pos, order: this, product: product});
779
780             if(options.quantity !== undefined){
781                 line.set_quantity(options.quantity);
782             }
783             if(options.price !== undefined){
784                 line.set_unit_price(options.price);
785             }
786
787             var last_orderline = this.getLastOrderline();
788             if( last_orderline && last_orderline.can_be_merged_with(line) && options.merge !== false){
789                 last_orderline.merge(line);
790             }else{
791                 this.get('orderLines').add(line);
792             }
793             this.selectLine(this.getLastOrderline());
794         },
795         removeOrderline: function( line ){
796             this.get('orderLines').remove(line);
797             this.selectLine(this.getLastOrderline());
798         },
799         getLastOrderline: function(){
800             return this.get('orderLines').at(this.get('orderLines').length -1);
801         },
802         addPaymentline: function(cashregister) {
803             var paymentLines = this.get('paymentLines');
804             var newPaymentline = new module.Paymentline({},{cashregister:cashregister});
805             if(cashregister.journal.type !== 'cash'){
806                 newPaymentline.set_amount( Math.max(this.getDueLeft(),0) );
807             }
808             paymentLines.add(newPaymentline);
809             this.selectPaymentline(newPaymentline);
810
811         },
812         removePaymentline: function(line){
813             if(this.selected_paymentline === line){
814                 this.selectPaymentline(undefined);
815             }
816             this.get('paymentLines').remove(line);
817         },
818         getName: function() {
819             return this.get('name');
820         },
821         getSubtotal : function(){
822             return (this.get('orderLines')).reduce((function(sum, orderLine){
823                 return sum + orderLine.get_display_price();
824             }), 0);
825         },
826         getTotalTaxIncluded: function() {
827             return (this.get('orderLines')).reduce((function(sum, orderLine) {
828                 return sum + orderLine.get_price_with_tax();
829             }), 0);
830         },
831         getDiscountTotal: function() {
832             return (this.get('orderLines')).reduce((function(sum, orderLine) {
833                 return sum + (orderLine.get_unit_price() * (orderLine.get_discount()/100) * orderLine.get_quantity());
834             }), 0);
835         },
836         getTotalTaxExcluded: function() {
837             return (this.get('orderLines')).reduce((function(sum, orderLine) {
838                 return sum + orderLine.get_price_without_tax();
839             }), 0);
840         },
841         getTax: function() {
842             return (this.get('orderLines')).reduce((function(sum, orderLine) {
843                 return sum + orderLine.get_tax();
844             }), 0);
845         },
846         getTaxDetails: function(){
847             var details = {};
848             var fulldetails = [];
849             var taxes_by_id = {};
850             
851             for(var i = 0; i < this.pos.taxes.length; i++){
852                 taxes_by_id[this.pos.taxes[i].id] = this.pos.taxes[i];
853             }
854
855             this.get('orderLines').each(function(line){
856                 var ldetails = line.get_tax_details();
857                 for(var id in ldetails){
858                     if(ldetails.hasOwnProperty(id)){
859                         details[id] = (details[id] || 0) + ldetails[id];
860                     }
861                 }
862             });
863             
864             for(var id in details){
865                 if(details.hasOwnProperty(id)){
866                     fulldetails.push({amount: details[id], tax: taxes_by_id[id]});
867                 }
868             }
869
870             return fulldetails;
871         },
872         getPaidTotal: function() {
873             return (this.get('paymentLines')).reduce((function(sum, paymentLine) {
874                 return sum + paymentLine.get_amount();
875             }), 0);
876         },
877         getChange: function() {
878             return this.getPaidTotal() - this.getTotalTaxIncluded();
879         },
880         getDueLeft: function() {
881             return this.getTotalTaxIncluded() - this.getPaidTotal();
882         },
883         // sets the type of receipt 'receipt'(default) or 'invoice'
884         set_receipt_type: function(type){
885             this.receipt_type = type;
886         },
887         get_receipt_type: function(){
888             return this.receipt_type;
889         },
890         // the client related to the current order.
891         set_client: function(client){
892             this.set('client',client);
893         },
894         get_client: function(){
895             return this.get('client');
896         },
897         get_client_name: function(){
898             var client = this.get('client');
899             return client ? client.name : "";
900         },
901         // the order also stores the screen status, as the PoS supports
902         // different active screens per order. This method is used to
903         // store the screen status.
904         set_screen_data: function(key,value){
905             if(arguments.length === 2){
906                 this.screen_data[key] = value;
907             }else if(arguments.length === 1){
908                 for(key in arguments[0]){
909                     this.screen_data[key] = arguments[0][key];
910                 }
911             }
912         },
913         //see set_screen_data
914         get_screen_data: function(key){
915             return this.screen_data[key];
916         },
917         // exports a JSON for receipt printing
918         export_for_printing: function(){
919             var orderlines = [];
920             this.get('orderLines').each(function(orderline){
921                 orderlines.push(orderline.export_for_printing());
922             });
923
924             var paymentlines = [];
925             this.get('paymentLines').each(function(paymentline){
926                 paymentlines.push(paymentline.export_for_printing());
927             });
928             var client  = this.get('client');
929             var cashier = this.pos.cashier || this.pos.user;
930             var company = this.pos.company;
931             var shop    = this.pos.shop;
932             var date = new Date();
933
934             return {
935                 orderlines: orderlines,
936                 paymentlines: paymentlines,
937                 subtotal: this.getSubtotal(),
938                 total_with_tax: this.getTotalTaxIncluded(),
939                 total_without_tax: this.getTotalTaxExcluded(),
940                 total_tax: this.getTax(),
941                 total_paid: this.getPaidTotal(),
942                 total_discount: this.getDiscountTotal(),
943                 tax_details: this.getTaxDetails(),
944                 change: this.getChange(),
945                 name : this.getName(),
946                 client: client ? client.name : null ,
947                 invoice_id: null,   //TODO
948                 cashier: cashier ? cashier.name : null,
949                 header: this.pos.config.receipt_header || '',
950                 footer: this.pos.config.receipt_footer || '',
951                 precision: {
952                     price: 2,
953                     money: 2,
954                     quantity: 3,
955                 },
956                 date: { 
957                     year: date.getFullYear(), 
958                     month: date.getMonth(), 
959                     date: date.getDate(),       // day of the month 
960                     day: date.getDay(),         // day of the week 
961                     hour: date.getHours(), 
962                     minute: date.getMinutes() ,
963                     isostring: date.toISOString(),
964                 }, 
965                 company:{
966                     email: company.email,
967                     website: company.website,
968                     company_registry: company.company_registry,
969                     contact_address: company.contact_address, 
970                     vat: company.vat,
971                     name: company.name,
972                     phone: company.phone,
973                     logo:  this.pos.company_logo_base64,
974                 },
975                 shop:{
976                     name: shop.name,
977                 },
978                 currency: this.pos.currency,
979             };
980         },
981         export_as_JSON: function() {
982             var orderLines, paymentLines;
983             orderLines = [];
984             (this.get('orderLines')).each(_.bind( function(item) {
985                 return orderLines.push([0, 0, item.export_as_JSON()]);
986             }, this));
987             paymentLines = [];
988             (this.get('paymentLines')).each(_.bind( function(item) {
989                 return paymentLines.push([0, 0, item.export_as_JSON()]);
990             }, this));
991             return {
992                 name: this.getName(),
993                 amount_paid: this.getPaidTotal(),
994                 amount_total: this.getTotalTaxIncluded(),
995                 amount_tax: this.getTax(),
996                 amount_return: this.getChange(),
997                 lines: orderLines,
998                 statement_ids: paymentLines,
999                 pos_session_id: this.pos.pos_session.id,
1000                 partner_id: this.get_client() ? this.get_client().id : false,
1001                 user_id: this.pos.cashier ? this.pos.cashier.id : this.pos.user.id,
1002                 uid: this.uid,
1003             };
1004         },
1005         getSelectedLine: function(){
1006             return this.selected_orderline;
1007         },
1008         selectLine: function(line){
1009             if(line){
1010                 if(line !== this.selected_orderline){
1011                     if(this.selected_orderline){
1012                         this.selected_orderline.set_selected(false);
1013                     }
1014                     this.selected_orderline = line;
1015                     this.selected_orderline.set_selected(true);
1016                 }
1017             }else{
1018                 this.selected_orderline = undefined;
1019             }
1020         },
1021         deselectLine: function(){
1022             if(this.selected_orderline){
1023                 this.selected_orderline.set_selected(false);
1024                 this.selected_orderline = undefined;
1025             }
1026         },
1027         selectPaymentline: function(line){
1028             if(line !== this.selected_paymentline){
1029                 if(this.selected_paymentline){
1030                     this.selected_paymentline.set_selected(false);
1031                 }
1032                 this.selected_paymentline = line;
1033                 if(this.selected_paymentline){
1034                     this.selected_paymentline.set_selected(true);
1035                 }
1036                 this.trigger('change:selected_paymentline',this.selected_paymentline);
1037             }
1038         },
1039     });
1040
1041     module.OrderCollection = Backbone.Collection.extend({
1042         model: module.Order,
1043     });
1044
1045     /*
1046      The numpad handles both the choice of the property currently being modified
1047      (quantity, price or discount) and the edition of the corresponding numeric value.
1048      */
1049     module.NumpadState = Backbone.Model.extend({
1050         defaults: {
1051             buffer: "0",
1052             mode: "quantity"
1053         },
1054         appendNewChar: function(newChar) {
1055             var oldBuffer;
1056             oldBuffer = this.get('buffer');
1057             if (oldBuffer === '0') {
1058                 this.set({
1059                     buffer: newChar
1060                 });
1061             } else if (oldBuffer === '-0') {
1062                 this.set({
1063                     buffer: "-" + newChar
1064                 });
1065             } else {
1066                 this.set({
1067                     buffer: (this.get('buffer')) + newChar
1068                 });
1069             }
1070             this.trigger('set_value',this.get('buffer'));
1071         },
1072         deleteLastChar: function() {
1073             if(this.get('buffer') === ""){
1074                 if(this.get('mode') === 'quantity'){
1075                     this.trigger('set_value','remove');
1076                 }else{
1077                     this.trigger('set_value',this.get('buffer'));
1078                 }
1079             }else{
1080                 var newBuffer = this.get('buffer').slice(0,-1) || "";
1081                 this.set({ buffer: newBuffer });
1082                 this.trigger('set_value',this.get('buffer'));
1083             }
1084         },
1085         switchSign: function() {
1086             var oldBuffer;
1087             oldBuffer = this.get('buffer');
1088             this.set({
1089                 buffer: oldBuffer[0] === '-' ? oldBuffer.substr(1) : "-" + oldBuffer
1090             });
1091             this.trigger('set_value',this.get('buffer'));
1092         },
1093         changeMode: function(newMode) {
1094             this.set({
1095                 buffer: "0",
1096                 mode: newMode
1097             });
1098         },
1099         reset: function() {
1100             this.set({
1101                 buffer: "0",
1102                 mode: "quantity"
1103             });
1104         },
1105         resetValue: function(){
1106             this.set({buffer:'0'});
1107         },
1108     });
1109 }