[FIX] point_of_sale: fixed rounding issue for pos order when discount added(maintenan...
[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
4     var round_di = instance.web.round_decimals;
5     var round_pr = instance.web.round_precision
6     
7     // The PosModel contains the Point Of Sale's representation of the backend.
8     // Since the PoS must work in standalone ( Without connection to the server ) 
9     // it must contains a representation of the server's PoS backend. 
10     // (taxes, product list, configuration options, etc.)  this representation
11     // is fetched and stored by the PosModel at the initialisation. 
12     // this is done asynchronously, a ready deferred alows the GUI to wait interactively 
13     // for the loading to be completed 
14     // There is a single instance of the PosModel for each Front-End instance, it is usually called
15     // 'pos' and is available to all widgets extending PosWidget.
16
17     module.PosModel = Backbone.Model.extend({
18         initialize: function(session, attributes) {
19             Backbone.Model.prototype.initialize.call(this, attributes);
20             var  self = this;
21             this.session = session;                 
22             this.ready = $.Deferred();                          // used to notify the GUI that the PosModel has loaded all resources
23             this.flush_mutex = new $.Mutex();                   // used to make sure the orders are sent to the server once at time
24
25             this.barcode_reader = new module.BarcodeReader({'pos': this});  // used to read barcodes
26             this.proxy = new module.ProxyDevice();              // used to communicate to the hardware devices via a local proxy
27             this.db = new module.PosLS();                       // a database used to store the products and categories
28             this.db.clear('products','categories');
29             this.debug = jQuery.deparam(jQuery.param.querystring()).debug !== undefined;    //debug mode 
30
31             // default attributes values. If null, it will be loaded below.
32             this.set({
33                 'nbr_pending_operations': 0,    
34
35                 'currency':         {symbol: '$', position: 'after'},
36                 'shop':             null, 
37                 'company':          null,
38                 'user':             null,   // the user that loaded the pos
39                 'user_list':        null,   // list of all users
40                 'partner_list':     null,   // list of all partners with an ean
41                 'cashier':          null,   // the logged cashier, if different from user
42
43                 'orders':           new module.OrderCollection(),
44                 //this is the product list as seen by the product list widgets, it will change based on the category filters
45                 'products':         new module.ProductCollection(), 
46                 'cashRegisters':    null, 
47
48                 'bank_statements':  null,
49                 'taxes':            null,
50                 'pos_session':      null,
51                 'pos_config':       null,
52                 'units':            null,
53                 'units_by_id':      null,
54                 'pricelist':        null,
55
56                 'selectedOrder':    null,
57             });
58
59             this.get('orders').bind('remove', function(){ self.on_removed_order(); });
60             
61             // We fetch the backend data on the server asynchronously. this is done only when the pos user interface is launched,
62             // Any change on this data made on the server is thus not reflected on the point of sale until it is relaunched. 
63             // when all the data has loaded, we compute some stuff, and declare the Pos ready to be used. 
64             $.when(this.load_server_data())
65                 .done(function(){
66                     //self.log_loaded_data(); //Uncomment if you want to log the data to the console for easier debugging
67                     self.ready.resolve();
68                 }).fail(function(){
69                     //we failed to load some backend data, or the backend was badly configured.
70                     //the error messages will be displayed in PosWidget
71                     self.ready.reject();
72                 });
73         },
74
75         // helper function to load data from the server
76         fetch: function(model, fields, domain, ctx){
77             return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all()
78         },
79         // loads all the needed data on the sever. returns a deferred indicating when all the data has loaded. 
80         load_server_data: function(){
81             var self = this;
82
83             var loaded = self.fetch('res.users',['name','company_id'],[['id','=',this.session.uid]]) 
84                 .then(function(users){
85                     self.set('user',users[0]);
86
87                     return self.fetch('res.company',
88                     [
89                         'currency_id',
90                         'email',
91                         'website',
92                         'company_registry',
93                         'vat',
94                         'name',
95                         'phone',
96                         'partner_id',
97                     ],
98                     [['id','=',users[0].company_id[0]]]);
99                 }).then(function(companies){
100                     self.set('company',companies[0]);
101
102                     return self.fetch('res.partner',['contact_address'],[['id','=',companies[0].partner_id[0]]]);
103                 }).then(function(company_partners){
104                     self.get('company').contact_address = company_partners[0].contact_address;
105
106                     return self.fetch('product.uom', null, null);
107                 }).then(function(units){
108                     self.set('units',units);
109                     var units_by_id = {};
110                     for(var i = 0, len = units.length; i < len; i++){
111                         units_by_id[units[i].id] = units[i];
112                     }
113                     self.set('units_by_id',units_by_id);
114                     
115                     return self.fetch('product.packaging', null, null);
116                 }).then(function(packagings){
117                     self.set('product.packaging',packagings);
118                     
119                     return self.fetch('res.users', ['name','ean13'], [['ean13', '!=', false]]);
120                 }).then(function(users){
121                     self.set('user_list',users);
122
123                     return self.fetch('res.partner', ['name','ean13'], [['ean13', '!=', false]]);
124                 }).then(function(partners){
125                     self.set('partner_list',partners);
126
127                     return self.fetch('account.tax', ['amount', 'price_include', 'type']);
128                 }).then(function(taxes){
129                     self.set('taxes', taxes);
130
131                     return self.fetch(
132                         'pos.session', 
133                         ['id', 'journal_ids','name','user_id','config_id','start_at','stop_at'],
134                         [['state', '=', 'opened'], ['user_id', '=', self.session.uid]]
135                     );
136                 }).then(function(sessions){
137                     self.set('pos_session', sessions[0]);
138
139                     return self.fetch(
140                         'pos.config',
141                         ['name','journal_ids','shop_id','journal_id',
142                          'iface_self_checkout', 'iface_led', 'iface_cashdrawer',
143                          'iface_payment_terminal', 'iface_electronic_scale', 'iface_barscan', 'iface_vkeyboard',
144                          'iface_print_via_proxy','iface_cashdrawer','state','sequence_id','session_ids'],
145                         [['id','=', self.get('pos_session').config_id[0]]]
146                     );
147                 }).then(function(configs){
148                     var pos_config = configs[0];
149                     self.set('pos_config', pos_config);
150                     self.iface_electronic_scale    =  !!pos_config.iface_electronic_scale;  
151                     self.iface_print_via_proxy     =  !!pos_config.iface_print_via_proxy;
152                     self.iface_vkeyboard           =  !!pos_config.iface_vkeyboard; 
153                     self.iface_self_checkout       =  !!pos_config.iface_self_checkout;
154                     self.iface_cashdrawer          =  !!pos_config.iface_cashdrawer;
155
156                     return self.fetch('sale.shop',[],[['id','=',pos_config.shop_id[0]]]);
157                 }).then(function(shops){
158                     self.set('shop',shops[0]);
159
160                     return self.fetch('product.pricelist',['currency_id'],[['id','=',self.get('shop').pricelist_id[0]]]);
161                 }).then(function(pricelists){
162                     self.set('pricelist',pricelists[0]);
163
164                     return self.fetch('res.currency',['symbol','position','rounding','accuracy'],[['id','=',self.get('pricelist').currency_id[0]]]);
165                 }).then(function(currencies){
166                     self.set('currency',currencies[0]);
167
168                     return self.fetch('product.packaging',['ean','product_id']);
169                 }).then(function(packagings){
170                     self.db.add_packagings(packagings);
171
172                     return self.fetch('pos.category', ['id','name','parent_id','child_id','image'])
173                 }).then(function(categories){
174                     self.db.add_categories(categories);
175
176                     return self.fetch(
177                         'product.product', 
178                         ['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13', 'default_code', 'variants',
179                          'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description'],
180                         [['sale_ok','=',true],['available_in_pos','=',true]],
181                         {pricelist: self.get('shop').pricelist_id[0]} // context for price
182                     );
183                 }).then(function(products){
184                     self.db.add_products(products);
185
186                     return self.fetch(
187                         'account.bank.statement',
188                         ['account_id','currency','journal_id','state','name','user_id','pos_session_id'],
189                         [['state','=','open'],['pos_session_id', '=', self.get('pos_session').id]]
190                     );
191                 }).then(function(bank_statements){
192                     var journals = new Array();
193                     _.each(bank_statements,function(statement) {
194                         journals.push(statement.journal_id[0])
195                     });
196                     self.set('bank_statements', bank_statements);
197                     return self.fetch('account.journal', undefined, [['id','in', journals]]);
198                 }).then(function(journals){
199                     self.set('journals',journals);
200
201                     // associate the bank statements with their journals. 
202                     var bank_statements = self.get('bank_statements');
203                     for(var i = 0, ilen = bank_statements.length; i < ilen; i++){
204                         for(var j = 0, jlen = journals.length; j < jlen; j++){
205                             if(bank_statements[i].journal_id[0] === journals[j].id){
206                                 bank_statements[i].journal = journals[j];
207                                 bank_statements[i].self_checkout_payment_method = journals[j].self_checkout_payment_method;
208                             }
209                         }
210                     }
211                     self.set({'cashRegisters' : new module.CashRegisterCollection(self.get('bank_statements'))});
212                 });
213         
214             return loaded;
215         },
216
217         // logs the usefull posmodel data to the console for debug purposes
218         log_loaded_data: function(){
219             console.log('PosModel data has been loaded:');
220             console.log('PosModel: units:',this.get('units'));
221             console.log('PosModel: bank_statements:',this.get('bank_statements'));
222             console.log('PosModel: journals:',this.get('journals'));
223             console.log('PosModel: taxes:',this.get('taxes'));
224             console.log('PosModel: pos_session:',this.get('pos_session'));
225             console.log('PosModel: pos_config:',this.get('pos_config'));
226             console.log('PosModel: cashRegisters:',this.get('cashRegisters'));
227             console.log('PosModel: shop:',this.get('shop'));
228             console.log('PosModel: company:',this.get('company'));
229             console.log('PosModel: currency:',this.get('currency'));
230             console.log('PosModel: user_list:',this.get('user_list'));
231             console.log('PosModel: user:',this.get('user'));
232             console.log('PosModel.session:',this.session);
233             console.log('PosModel end of data log.');
234         },
235         
236         // this is called when an order is removed from the order collection. It ensures that there is always an existing
237         // order and a valid selected order
238         on_removed_order: function(removed_order){
239             if( this.get('orders').isEmpty()){
240                 this.add_new_order();
241             }else{
242                 this.set({ selectedOrder: this.get('orders').last() });
243             }
244         },
245
246         // saves the order locally and try to send it to the backend. 'record' is a bizzarely defined JSON version of the Order
247         push_order: function(record) {
248             this.db.add_order(record);
249             this.flush();
250         },
251
252         //creates a new empty order and sets it as the current order
253         add_new_order: function(){
254             var order = new module.Order({pos:this});
255             this.get('orders').add(order);
256             this.set('selectedOrder', order);
257         },
258
259         // attemps to send all pending orders ( stored in the pos_db ) to the server,
260         // and remove the successfully sent ones from the db once
261         // it has been confirmed that they have been sent correctly.
262         flush: function() {
263             //TODO make the mutex work 
264             //this makes sure only one _int_flush is called at the same time
265             /*
266             return this.flush_mutex.exec(_.bind(function() {
267                 return this._flush(0);
268             }, this));
269             */
270             this._flush(0);
271         },
272         // attempts to send an order of index 'index' in the list of order to send. The index
273         // is used to skip orders that failed. do not call this method outside the mutex provided
274         // by flush() 
275         _flush: function(index){
276             var self = this;
277             var orders = this.db.get_orders();
278             self.set('nbr_pending_operations',orders.length);
279
280             var order  = orders[index];
281             if(!order){
282                 return;
283             }
284             //try to push an order to the server
285             // shadow : true is to prevent a spinner to appear in case of timeout
286             (new instance.web.Model('pos.order')).call('create_from_ui',[[order]],undefined,{ shadow:true })
287                 .fail(function(unused, event){
288                     //don't show error popup if it fails 
289                     event.preventDefault();
290                     console.error('Failed to send order:',order);
291                     self._flush(index+1);
292                 })
293                 .done(function(){
294                     //remove from db if success
295                     self.db.remove_order(order.id);
296                     self._flush(index);
297                 });
298         },
299
300         scan_product: function(parsed_ean){
301             var self = this;
302             var product = this.db.get_product_by_ean13(parsed_ean.base_ean);
303             var selectedOrder = this.get('selectedOrder');
304
305             if(!product){
306                 return false;
307             }
308
309             if(parsed_ean.type === 'price'){
310                 selectedOrder.addProduct(new module.Product(product), {price:parsed_ean.value});
311             }else if(parsed_ean.type === 'weight'){
312                 selectedOrder.addProduct(new module.Product(product), {quantity:parsed_ean.value, merge:false});
313             }else{
314                 selectedOrder.addProduct(new module.Product(product));
315             }
316             return true;
317         },
318     });
319
320     module.CashRegister = Backbone.Model.extend({
321     });
322
323     module.CashRegisterCollection = Backbone.Collection.extend({
324         model: module.CashRegister,
325     });
326
327     module.Product = Backbone.Model.extend({
328         get_image_url: function(){
329             return instance.session.url('/web/binary/image', {model: 'product.product', field: 'image', id: this.get('id')});
330         },
331     });
332
333     module.ProductCollection = Backbone.Collection.extend({
334         model: module.Product,
335     });
336
337     // An orderline represent one element of the content of a client's shopping cart.
338     // An orderline contains a product, its quantity, its price, discount. etc. 
339     // An Order contains zero or more Orderlines.
340     module.Orderline = Backbone.Model.extend({
341         initialize: function(attr,options){
342             this.pos = options.pos;
343             this.order = options.order;
344             this.product = options.product;
345             this.price   = options.product.get('price');
346             this.quantity = 1;
347             this.quantityStr = '1';
348             this.discount = 0;
349             this.discountStr = '0';
350             this.type = 'unit';
351             this.selected = false;
352         },
353         // sets a discount [0,100]%
354         set_discount: function(discount){
355             var disc = Math.min(Math.max(parseFloat(discount) || 0, 0),100);
356             this.discount = disc;
357             this.discountStr = '' + disc;
358             this.trigger('change');
359         },
360         // returns the discount [0,100]%
361         get_discount: function(){
362             return this.discount;
363         },
364         get_discount_str: function(){
365             return this.discountStr;
366         },
367         get_product_type: function(){
368             return this.type;
369         },
370         // sets the quantity of the product. The quantity will be rounded according to the 
371         // product's unity of measure properties. Quantities greater than zero will not get 
372         // rounded to zero
373         set_quantity: function(quantity){
374             if(quantity === 'remove'){
375                 this.order.removeOrderline(this);
376                 return;
377             }else{
378                 var quant = Math.max(parseFloat(quantity) || 0, 0);
379                 var unit = this.get_unit();
380                 if(unit){
381                     this.quantity    = Math.max(unit.rounding, round_pr(quant, unit.rounding));
382                     this.quantityStr = this.quantity.toFixed(Math.max(0,Math.ceil(Math.log(1.0 / unit.rounding) / Math.log(10))));
383                 }else{
384                     this.quantity    = quant;
385                     this.quantityStr = '' + this.quantity;
386                 }
387             }
388             this.trigger('change');
389         },
390         // return the quantity of product
391         get_quantity: function(){
392             return this.quantity;
393         },
394         get_quantity_str: function(){
395             return this.quantityStr;
396         },
397         get_quantity_str_with_unit: function(){
398             var unit = this.get_unit();
399             if(unit && unit.name !== 'Unit(s)'){
400                 return this.quantityStr + ' ' + unit.name;
401             }else{
402                 return this.quantityStr;
403             }
404         },
405         // return the unit of measure of the product
406         get_unit: function(){
407             var unit_id = (this.product.get('uos_id') || this.product.get('uom_id'));
408             if(!unit_id){
409                 return undefined;
410             }
411             unit_id = unit_id[0];
412             if(!this.pos){
413                 return undefined;
414             }
415             return this.pos.get('units_by_id')[unit_id];
416         },
417         // return the product of this orderline
418         get_product: function(){
419             return this.product;
420         },
421         // selects or deselects this orderline
422         set_selected: function(selected){
423             this.selected = selected;
424             this.trigger('change');
425         },
426         // returns true if this orderline is selected
427         is_selected: function(){
428             return this.selected;
429         },
430         // when we add an new orderline we want to merge it with the last line to see reduce the number of items
431         // in the orderline. This returns true if it makes sense to merge the two
432         can_be_merged_with: function(orderline){
433             if( this.get_product().get('id') !== orderline.get_product().get('id')){    //only orderline of the same product can be merged
434                 return false;
435             }else if(this.get_product_type() !== orderline.get_product_type()){
436                 return false;
437             }else if(this.get_discount() > 0){             // we don't merge discounted orderlines
438                 return false;
439             }else if(this.price !== orderline.price){
440                 return false;
441             }else{ 
442                 return true;
443             }
444         },
445         merge: function(orderline){
446             this.set_quantity(this.get_quantity() + orderline.get_quantity());
447         },
448         export_as_JSON: function() {
449             return {
450                 qty: this.get_quantity(),
451                 price_unit: this.get_unit_price(),
452                 discount: this.get_discount(),
453                 product_id: this.get_product().get('id'),
454             };
455         },
456         //used to create a json of the ticket, to be sent to the printer
457         export_for_printing: function(){
458             return {
459                 quantity:           this.get_quantity(),
460                 unit_name:          this.get_unit().name,
461                 price:              this.get_unit_price(),
462                 discount:           this.get_discount(),
463                 product_name:       this.get_product().get('name'),
464                 price_display :     this.get_display_price(),
465                 price_with_tax :    this.get_price_with_tax(),
466                 price_without_tax:  this.get_price_without_tax(),
467                 tax:                this.get_tax(),
468                 product_description:      this.get_product().get('description'),
469                 product_description_sale: this.get_product().get('description_sale'),
470             };
471         },
472         // changes the base price of the product for this orderline
473         set_unit_price: function(price){
474             this.price = round_di(parseFloat(price) || 0, 2);
475             this.trigger('change');
476         },
477         get_unit_price: function(){
478             var rounding = this.pos.get('currency').rounding;
479             return round_pr(this.price,rounding);
480         },
481         get_display_price: function(){
482             var rounding = this.pos.get('currency').rounding;
483             return  round_pr(round_pr(this.get_unit_price() * this.get_quantity(),rounding) * (1- this.get_discount()/100.0),rounding);
484         },
485         get_price_without_tax: function(){
486             return this.get_all_prices().priceWithoutTax;
487         },
488         get_price_with_tax: function(){
489             return this.get_all_prices().priceWithTax;
490         },
491         get_tax: function(){
492             return this.get_all_prices().tax;
493         },
494         get_all_prices: function(){
495             var self = this;
496             var currency_rounding = this.pos.get('currency').rounding;
497             var base = round_pr(round_pr(this.get_quantity() * this.get_unit_price(), currency_rounding) * (1.0 - (this.get_discount() / 100.0)), currency_rounding);
498             var totalTax = base;
499             var totalNoTax = base;
500             
501             var product_list = this.pos.get('product_list');
502             var product =  this.get_product(); 
503             var taxes_ids = product.get('taxes_id');;
504             var taxes =  self.pos.get('taxes');
505             var taxtotal = 0;
506             _.each(taxes_ids, function(el) {
507                 var tax = _.detect(taxes, function(t) {return t.id === el;});
508                 if (tax.price_include) {
509                     var tmp;
510                     if (tax.type === "percent") {
511                         tmp =  base - round_pr(base / (1 + tax.amount),currency_rounding); 
512                     } else if (tax.type === "fixed") {
513                         tmp = round_pr(tax.amount * self.get_quantity(),currency_rounding);
514                     } else {
515                         throw "This type of tax is not supported by the point of sale: " + tax.type;
516                     }
517                     tmp = round_pr(tmp,currency_rounding);
518                     taxtotal += tmp;
519                     totalNoTax -= tmp;
520                 } else {
521                     var tmp;
522                     if (tax.type === "percent") {
523                         tmp = tax.amount * base;
524                     } else if (tax.type === "fixed") {
525                         tmp = tax.amount * self.get_quantity();
526                     } else {
527                         throw "This type of tax is not supported by the point of sale: " + tax.type;
528                     }
529                     tmp = round_pr(tmp,currency_rounding);
530                     taxtotal += tmp;
531                     totalTax += tmp;
532                 }
533             });
534             return {
535                 "priceWithTax": totalTax,
536                 "priceWithoutTax": totalNoTax,
537                 "tax": taxtotal,
538             };
539         },
540     });
541
542     module.OrderlineCollection = Backbone.Collection.extend({
543         model: module.Orderline,
544     });
545
546     // Every PaymentLine contains a cashregister and an amount of money.
547     module.Paymentline = Backbone.Model.extend({
548         initialize: function(attributes, options) {
549             this.amount = 0;
550             this.cashregister = options.cashRegister;
551         },
552         //sets the amount of money on this payment line
553         set_amount: function(value){
554             this.amount = parseFloat(value) || 0;
555             this.trigger('change');
556         },
557         // returns the amount of money on this paymentline
558         get_amount: function(){
559             return this.amount;
560         },
561         // returns the associated cashRegister
562         get_cashregister: function(){
563             return this.cashregister;
564         },
565         //exports as JSON for server communication
566         export_as_JSON: function(){
567             return {
568                 name: instance.web.datetime_to_str(new Date()),
569                 statement_id: this.cashregister.get('id'),
570                 account_id: (this.cashregister.get('account_id'))[0],
571                 journal_id: (this.cashregister.get('journal_id'))[0],
572                 amount: this.get_amount()
573             };
574         },
575         //exports as JSON for receipt printing
576         export_for_printing: function(){
577             return {
578                 amount: this.get_amount(),
579                 journal: this.cashregister.get('journal_id')[1],
580             };
581         },
582     });
583
584     module.PaymentlineCollection = Backbone.Collection.extend({
585         model: module.Paymentline,
586     });
587     
588
589     // An order more or less represents the content of a client's shopping cart (the OrderLines) 
590     // plus the associated payment information (the PaymentLines) 
591     // there is always an active ('selected') order in the Pos, a new one is created
592     // automaticaly once an order is completed and sent to the server.
593     module.Order = Backbone.Model.extend({
594         initialize: function(attributes){
595             Backbone.Model.prototype.initialize.apply(this, arguments);
596             this.set({
597                 creationDate:   new Date(),
598                 orderLines:     new module.OrderlineCollection(),
599                 paymentLines:   new module.PaymentlineCollection(),
600                 name:           "Order " + this.generateUniqueId(),
601                 client:         null,
602             });
603             this.pos =     attributes.pos; 
604             this.selected_orderline = undefined;
605             this.screen_data = {};  // see ScreenSelector
606             this.receipt_type = 'receipt';  // 'receipt' || 'invoice'
607             return this;
608         },
609         generateUniqueId: function() {
610             return new Date().getTime();
611         },
612         addProduct: function(product, options){
613             options = options || {};
614             var attr = product.toJSON();
615             attr.pos = this.pos;
616             attr.order = this;
617             var line = new module.Orderline({}, {pos: this.pos, order: this, product: product});
618
619             if(options.quantity !== undefined){
620                 line.set_quantity(options.quantity);
621             }
622             if(options.price !== undefined){
623                 line.set_unit_price(options.price);
624             }
625
626             var last_orderline = this.getLastOrderline();
627             if( last_orderline && last_orderline.can_be_merged_with(line) && options.merge !== false){
628                 last_orderline.merge(line);
629             }else{
630                 this.get('orderLines').add(line);
631             }
632             this.selectLine(this.getLastOrderline());
633         },
634         removeOrderline: function( line ){
635             this.get('orderLines').remove(line);
636             this.selectLine(this.getLastOrderline());
637         },
638         getLastOrderline: function(){
639             return this.get('orderLines').at(this.get('orderLines').length -1);
640         },
641         addPaymentLine: function(cashRegister) {
642             var paymentLines = this.get('paymentLines');
643             var newPaymentline = new module.Paymentline({},{cashRegister:cashRegister});
644             if(cashRegister.get('journal').type !== 'cash'){
645                 newPaymentline.set_amount( this.getDueLeft() );
646             }
647             paymentLines.add(newPaymentline);
648         },
649         getName: function() {
650             return this.get('name');
651         },
652         getSubtotal : function(){
653             return (this.get('orderLines')).reduce((function(sum, orderLine){
654                 return sum + orderLine.get_display_price();
655             }), 0);
656         },
657         getTotalTaxIncluded: function() {
658             return (this.get('orderLines')).reduce((function(sum, orderLine) {
659                 return sum + orderLine.get_price_with_tax();
660             }), 0);
661         },
662         getDiscountTotal: function() {
663             return (this.get('orderLines')).reduce((function(sum, orderLine) {
664                 return sum + (orderLine.get_unit_price() * (orderLine.get_discount()/100) * orderLine.get_quantity());
665             }), 0);
666         },
667         getTotalTaxExcluded: function() {
668             return (this.get('orderLines')).reduce((function(sum, orderLine) {
669                 return sum + orderLine.get_price_without_tax();
670             }), 0);
671         },
672         getTax: function() {
673             return (this.get('orderLines')).reduce((function(sum, orderLine) {
674                 return sum + orderLine.get_tax();
675             }), 0);
676         },
677         getPaidTotal: function() {
678             return (this.get('paymentLines')).reduce((function(sum, paymentLine) {
679                 return sum + paymentLine.get_amount();
680             }), 0);
681         },
682         getChange: function() {
683             return this.getPaidTotal() - this.getTotalTaxIncluded();
684         },
685         getDueLeft: function() {
686             return this.getTotalTaxIncluded() - this.getPaidTotal();
687         },
688         // sets the type of receipt 'receipt'(default) or 'invoice'
689         set_receipt_type: function(type){
690             this.receipt_type = type;
691         },
692         get_receipt_type: function(){
693             return this.receipt_type;
694         },
695         // the client related to the current order.
696         set_client: function(client){
697             this.set('client',client);
698         },
699         get_client: function(){
700             return this.get('client');
701         },
702         get_client_name: function(){
703             var client = this.get('client');
704             return client ? client.name : "";
705         },
706         // the order also stores the screen status, as the PoS supports
707         // different active screens per order. This method is used to
708         // store the screen status.
709         set_screen_data: function(key,value){
710             if(arguments.length === 2){
711                 this.screen_data[key] = value;
712             }else if(arguments.length === 1){
713                 for(key in arguments[0]){
714                     this.screen_data[key] = arguments[0][key];
715                 }
716             }
717         },
718         //see set_screen_data
719         get_screen_data: function(key){
720             return this.screen_data[key];
721         },
722         // exports a JSON for receipt printing
723         export_for_printing: function(){
724             var orderlines = [];
725             this.get('orderLines').each(function(orderline){
726                 orderlines.push(orderline.export_for_printing());
727             });
728
729             var paymentlines = [];
730             this.get('paymentLines').each(function(paymentline){
731                 paymentlines.push(paymentline.export_for_printing());
732             });
733             var client  = this.get('client');
734             var cashier = this.pos.get('cashier') || this.pos.get('user');
735             var company = this.pos.get('company');
736             var shop    = this.pos.get('shop');
737             var date = new Date();
738
739             return {
740                 orderlines: orderlines,
741                 paymentlines: paymentlines,
742                 subtotal: this.getSubtotal(),
743                 total_with_tax: this.getTotalTaxIncluded(),
744                 total_without_tax: this.getTotalTaxExcluded(),
745                 total_tax: this.getTax(),
746                 total_paid: this.getPaidTotal(),
747                 total_discount: this.getDiscountTotal(),
748                 change: this.getChange(),
749                 name : this.getName(),
750                 client: client ? client.name : null ,
751                 invoice_id: null,   //TODO
752                 cashier: cashier ? cashier.name : null,
753                 date: { 
754                     year: date.getFullYear(), 
755                     month: date.getMonth(), 
756                     date: date.getDate(),       // day of the month 
757                     day: date.getDay(),         // day of the week 
758                     hour: date.getHours(), 
759                     minute: date.getMinutes() 
760                 }, 
761                 company:{
762                     email: company.email,
763                     website: company.website,
764                     company_registry: company.company_registry,
765                     contact_address: company.contact_address, 
766                     vat: company.vat,
767                     name: company.name,
768                     phone: company.phone,
769                 },
770                 shop:{
771                     name: shop.name,
772                 },
773                 currency: this.pos.get('currency'),
774             };
775         },
776         exportAsJSON: function() {
777             var orderLines, paymentLines;
778             orderLines = [];
779             (this.get('orderLines')).each(_.bind( function(item) {
780                 return orderLines.push([0, 0, item.export_as_JSON()]);
781             }, this));
782             paymentLines = [];
783             (this.get('paymentLines')).each(_.bind( function(item) {
784                 return paymentLines.push([0, 0, item.export_as_JSON()]);
785             }, this));
786             return {
787                 name: this.getName(),
788                 amount_paid: this.getPaidTotal(),
789                 amount_total: this.getTotalTaxIncluded(),
790                 amount_tax: this.getTax(),
791                 amount_return: this.getChange(),
792                 lines: orderLines,
793                 statement_ids: paymentLines,
794                 pos_session_id: this.pos.get('pos_session').id,
795                 partner_id: this.get('client') ? this.get('client').id : undefined,
796                 user_id: this.pos.get('cashier') ? this.pos.get('cashier').id : this.pos.get('user').id,
797             };
798         },
799         getSelectedLine: function(){
800             return this.selected_orderline;
801         },
802         selectLine: function(line){
803             if(line){
804                 if(line !== this.selected_orderline){
805                     if(this.selected_orderline){
806                         this.selected_orderline.set_selected(false);
807                     }
808                     this.selected_orderline = line;
809                     this.selected_orderline.set_selected(true);
810                 }
811             }else{
812                 this.selected_orderline = undefined;
813             }
814         },
815     });
816
817     module.OrderCollection = Backbone.Collection.extend({
818         model: module.Order,
819     });
820
821     /*
822      The numpad handles both the choice of the property currently being modified
823      (quantity, price or discount) and the edition of the corresponding numeric value.
824      */
825     module.NumpadState = Backbone.Model.extend({
826         defaults: {
827             buffer: "0",
828             mode: "quantity"
829         },
830         appendNewChar: function(newChar) {
831             var oldBuffer;
832             oldBuffer = this.get('buffer');
833             if (oldBuffer === '0') {
834                 this.set({
835                     buffer: newChar
836                 });
837             } else if (oldBuffer === '-0') {
838                 this.set({
839                     buffer: "-" + newChar
840                 });
841             } else {
842                 this.set({
843                     buffer: (this.get('buffer')) + newChar
844                 });
845             }
846             this.trigger('set_value',this.get('buffer'));
847         },
848         deleteLastChar: function() {
849             if(this.get('buffer') === ""){
850                 if(this.get('mode') === 'quantity'){
851                     this.trigger('set_value','remove');
852                 }else{
853                     this.trigger('set_value',this.get('buffer'));
854                 }
855             }else{
856                 var newBuffer = this.get('buffer').slice(0,-1) || "";
857                 this.set({ buffer: newBuffer });
858                 this.trigger('set_value',this.get('buffer'));
859             }
860         },
861         switchSign: function() {
862             var oldBuffer;
863             oldBuffer = this.get('buffer');
864             this.set({
865                 buffer: oldBuffer[0] === '-' ? oldBuffer.substr(1) : "-" + oldBuffer
866             });
867             this.trigger('set_value',this.get('buffer'));
868         },
869         changeMode: function(newMode) {
870             this.set({
871                 buffer: "0",
872                 mode: newMode
873             });
874         },
875         reset: function() {
876             this.set({
877                 buffer: "0",
878                 mode: "quantity"
879             });
880         },
881         resetValue: function(){
882             this.set({buffer:'0'});
883         },
884     });
885 }