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