function openerp_pos_models(instance, module){ //module is instance.point_of_sale
var QWeb = instance.web.qweb;
+ var _t = instance.web._t;
var round_di = instance.web.round_decimals;
var round_pr = instance.web.round_precision
Backbone.Model.prototype.initialize.call(this, attributes);
var self = this;
this.session = session;
- this.ready = $.Deferred(); // used to notify the GUI that the PosModel has loaded all resources
this.flush_mutex = new $.Mutex(); // used to make sure the orders are sent to the server once at time
+ this.pos_widget = attributes.pos_widget;
- this.barcode_reader = new module.BarcodeReader({'pos': this}); // used to read barcodes
- this.proxy = new module.ProxyDevice(); // used to communicate to the hardware devices via a local proxy
- this.db = new module.PosLS(); // a database used to store the products and categories
- this.db.clear('products','categories');
+ this.proxy = new module.ProxyDevice(this); // used to communicate to the hardware devices via a local proxy
+ this.barcode_reader = new module.BarcodeReader({'pos': this, proxy:this.proxy}); // used to read barcodes
+ this.proxy_queue = new module.JobQueue(); // used to prevent parallels communications to the proxy
+ this.db = new module.PosDB(); // a local database used to search trough products and categories & store pending orders
this.debug = jQuery.deparam(jQuery.param.querystring()).debug !== undefined; //debug mode
+
+ // Business data; loaded from the server at launch
+ this.accounting_precision = 2; //TODO
+ this.company_logo = null;
+ this.company_logo_base64 = '';
+ this.currency = null;
+ this.shop = null;
+ this.company = null;
+ this.user = null;
+ this.users = [];
+ this.partners = [];
+ this.cashier = null;
+ this.cashregisters = [];
+ this.bankstatements = [];
+ this.taxes = [];
+ this.pos_session = null;
+ this.config = null;
+ this.units = [];
+ this.units_by_id = {};
+ this.pricelist = null;
+ window.posmodel = this;
- // default attributes values. If null, it will be loaded below.
+ // these dynamic attributes can be watched for change by other models or widgets
this.set({
- 'nbr_pending_operations': 0,
-
- 'currency': {symbol: '$', position: 'after'},
- 'shop': null,
- 'company': null,
- 'user': null, // the user that loaded the pos
- 'user_list': null, // list of all users
- 'partner_list': null, // list of all partners with an ean
- 'cashier': null, // the logged cashier, if different from user
-
+ 'synch': { state:'connected', pending:0 },
'orders': new module.OrderCollection(),
- //this is the product list as seen by the product list widgets, it will change based on the category filters
- 'products': new module.ProductCollection(),
- 'cashRegisters': null,
-
- 'bank_statements': null,
- 'taxes': null,
- 'pos_session': null,
- 'pos_config': null,
- 'units': null,
- 'units_by_id': null,
- 'pricelist': null,
-
'selectedOrder': null,
});
- this.get('orders').bind('remove', function(){ self.on_removed_order(); });
+ this.bind('change:synch',function(pos,synch){
+ clearTimeout(self.synch_timeout);
+ self.synch_timeout = setTimeout(function(){
+ if(synch.state !== 'disconnected' && synch.pending > 0){
+ self.set('synch',{state:'disconnected', pending:synch.pending});
+ }
+ },3000);
+ });
+
+ this.get('orders').bind('remove', function(order,_unused_,options){
+ self.on_removed_order(order,options.index,options.reason);
+ });
// We fetch the backend data on the server asynchronously. this is done only when the pos user interface is launched,
// Any change on this data made on the server is thus not reflected on the point of sale until it is relaunched.
// when all the data has loaded, we compute some stuff, and declare the Pos ready to be used.
- $.when(this.load_server_data())
- .done(function(){
- //self.log_loaded_data(); //Uncomment if you want to log the data to the console for easier debugging
- self.ready.resolve();
- }).fail(function(){
- //we failed to load some backend data, or the backend was badly configured.
- //the error messages will be displayed in PosWidget
- self.ready.reject();
+ this.ready = this.load_server_data()
+ .then(function(){
+ if(self.config.use_proxy){
+ return self.connect_to_proxy();
+ }
+ });
+
+ },
+
+ // releases ressources holds by the model at the end of life of the posmodel
+ destroy: function(){
+ // FIXME, should wait for flushing, return a deferred to indicate successfull destruction
+ // this.flush();
+ this.proxy.close();
+ this.barcode_reader.disconnect();
+ this.barcode_reader.disconnect_from_proxy();
+ },
+ connect_to_proxy: function(){
+ var self = this;
+ var done = new $.Deferred();
+ this.barcode_reader.disconnect_from_proxy();
+ this.pos_widget.loading_message(_t('Connecting to the PosBox'),0);
+ this.pos_widget.loading_skip(function(){
+ self.proxy.stop_searching();
+ });
+ this.proxy.autoconnect({
+ force_ip: self.config.proxy_ip || undefined,
+ progress: function(prog){
+ self.pos_widget.loading_progress(prog);
+ },
+ }).then(function(){
+ if(self.config.iface_scan_via_proxy){
+ self.barcode_reader.connect_to_proxy();
+ }
+ }).always(function(){
+ done.resolve();
});
+ return done;
},
// helper function to load data from the server
fetch: function(model, fields, domain, ctx){
+ this._load_progress = (this._load_progress || 0) + 0.05;
+ this.pos_widget.loading_message(_t('Loading')+' '+model,this._load_progress);
return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all()
},
// loads all the needed data on the sever. returns a deferred indicating when all the data has loaded.
var loaded = self.fetch('res.users',['name','company_id'],[['id','=',this.session.uid]])
.then(function(users){
- self.set('user',users[0]);
+ self.user = users[0];
return self.fetch('res.company',
[
'phone',
'partner_id',
],
- [['id','=',users[0].company_id[0]]]);
+ [['id','=',users[0].company_id[0]]],
+ {show_address_only: true});
}).then(function(companies){
- self.set('company',companies[0]);
-
- return self.fetch('res.partner',['contact_address'],[['id','=',companies[0].partner_id[0]]]);
- }).then(function(company_partners){
- self.get('company').contact_address = company_partners[0].contact_address;
+ self.company = companies[0];
return self.fetch('product.uom', null, null);
}).then(function(units){
- self.set('units',units);
+ self.units = units;
var units_by_id = {};
for(var i = 0, len = units.length; i < len; i++){
units_by_id[units[i].id] = units[i];
}
- self.set('units_by_id',units_by_id);
-
- return self.fetch('product.packaging', null, null);
- }).then(function(packagings){
- self.set('product.packaging',packagings);
+ self.units_by_id = units_by_id;
return self.fetch('res.users', ['name','ean13'], [['ean13', '!=', false]]);
}).then(function(users){
- self.set('user_list',users);
+ self.users = users;
return self.fetch('res.partner', ['name','ean13'], [['ean13', '!=', false]]);
}).then(function(partners){
- self.set('partner_list',partners);
+ self.partners = partners;
- return self.fetch('account.tax', ['amount', 'price_include', 'type']);
+ return self.fetch('account.tax', ['name','amount', 'price_include', 'type']);
}).then(function(taxes){
- self.set('taxes', taxes);
+ self.taxes = taxes;
return self.fetch(
'pos.session',
['id', 'journal_ids','name','user_id','config_id','start_at','stop_at'],
[['state', '=', 'opened'], ['user_id', '=', self.session.uid]]
);
- }).then(function(sessions){
- self.set('pos_session', sessions[0]);
+ }).then(function(pos_sessions){
+ self.pos_session = pos_sessions[0];
return self.fetch(
'pos.config',
- ['name','journal_ids','shop_id','journal_id',
+ ['name','journal_ids','warehouse_id','journal_id','pricelist_id',
'iface_self_checkout', 'iface_led', 'iface_cashdrawer',
- 'iface_payment_terminal', 'iface_electronic_scale', 'iface_barscan', 'iface_vkeyboard',
- 'iface_print_via_proxy','iface_cashdrawer','state','sequence_id','session_ids'],
- [['id','=', self.get('pos_session').config_id[0]]]
+ 'iface_payment_terminal', 'iface_electronic_scale', 'iface_barscan',
+ 'iface_vkeyboard','iface_print_via_proxy','iface_scan_via_proxy',
+ 'iface_cashdrawer','iface_invoicing','iface_big_scrollbars',
+ 'receipt_header','receipt_footer','proxy_ip',
+ 'state','sequence_id','session_ids'],
+ [['id','=', self.pos_session.config_id[0]]]
);
}).then(function(configs){
- var pos_config = configs[0];
- self.set('pos_config', pos_config);
- self.iface_electronic_scale = !!pos_config.iface_electronic_scale;
- self.iface_print_via_proxy = !!pos_config.iface_print_via_proxy;
- self.iface_vkeyboard = !!pos_config.iface_vkeyboard;
- self.iface_self_checkout = !!pos_config.iface_self_checkout;
- self.iface_cashdrawer = !!pos_config.iface_cashdrawer;
-
- return self.fetch('sale.shop',[],[['id','=',pos_config.shop_id[0]]]);
+ self.config = configs[0];
+ self.config.use_proxy = self.config.iface_payment_terminal ||
+ self.config.iface_electronic_scale ||
+ self.config.iface_print_via_proxy ||
+ self.config.iface_scan_via_proxy ||
+ self.config.iface_cashdrawer;
+
+ return self.fetch('stock.warehouse',[],[['id','=', self.config.warehouse_id[0]]]);
}).then(function(shops){
- self.set('shop',shops[0]);
+ self.shop = shops[0];
- return self.fetch('product.pricelist',['currency_id'],[['id','=',self.get('shop').pricelist_id[0]]]);
+ return self.fetch('product.pricelist',['currency_id'],[['id','=',self.config.pricelist_id[0]]]);
}).then(function(pricelists){
- self.set('pricelist',pricelists[0]);
+ self.pricelist = pricelists[0];
- return self.fetch('res.currency',['symbol','position','rounding','accuracy'],[['id','=',self.get('pricelist').currency_id[0]]]);
+ return self.fetch('res.currency',['symbol','position','rounding','accuracy'],[['id','=',self.pricelist.currency_id[0]]]);
}).then(function(currencies){
- self.set('currency',currencies[0]);
+ self.currency = currencies[0];
+ /*
+ return (new instance.web.Model('decimal.precision')).call('get_precision',[['Account']]);
+ }).then(function(precision){
+ self.accounting_precision = precision;
+ console.log("PRECISION",precision);
+*/
return self.fetch('product.packaging',['ean','product_id']);
}).then(function(packagings){
self.db.add_packagings(packagings);
- return self.fetch('pos.category', ['id','name','parent_id','child_id','image'])
+ return self.fetch('product.public.category', ['id','name','parent_id','child_id','image'])
}).then(function(categories){
self.db.add_categories(categories);
return self.fetch(
'product.product',
- ['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13', 'default_code', 'variants',
+ ['name', 'list_price','price','public_categ_id', 'taxes_id', 'ean13', 'default_code', 'variants',
'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description'],
[['sale_ok','=',true],['available_in_pos','=',true]],
- {pricelist: self.get('shop').pricelist_id[0]} // context for price
+ {pricelist: self.pricelist.id} // context for price
);
}).then(function(products){
self.db.add_products(products);
return self.fetch(
'account.bank.statement',
['account_id','currency','journal_id','state','name','user_id','pos_session_id'],
- [['state','=','open'],['pos_session_id', '=', self.get('pos_session').id]]
+ [['state','=','open'],['pos_session_id', '=', self.pos_session.id]]
);
- }).then(function(bank_statements){
- var journals = new Array();
- _.each(bank_statements,function(statement) {
+ }).then(function(bankstatements){
+ var journals = [];
+ _.each(bankstatements,function(statement) {
journals.push(statement.journal_id[0])
});
- self.set('bank_statements', bank_statements);
+ self.bankstatements = bankstatements;
return self.fetch('account.journal', undefined, [['id','in', journals]]);
}).then(function(journals){
- self.set('journals',journals);
+ self.journals = journals;
// associate the bank statements with their journals.
- var bank_statements = self.get('bank_statements');
- for(var i = 0, ilen = bank_statements.length; i < ilen; i++){
+ var bankstatements = self.bankstatements
+ for(var i = 0, ilen = bankstatements.length; i < ilen; i++){
for(var j = 0, jlen = journals.length; j < jlen; j++){
- if(bank_statements[i].journal_id[0] === journals[j].id){
- bank_statements[i].journal = journals[j];
- bank_statements[i].self_checkout_payment_method = journals[j].self_checkout_payment_method;
+ if(bankstatements[i].journal_id[0] === journals[j].id){
+ bankstatements[i].journal = journals[j];
+ bankstatements[i].self_checkout_payment_method = journals[j].self_checkout_payment_method;
}
}
}
- self.set({'cashRegisters' : new module.CashRegisterCollection(self.get('bank_statements'))});
+ self.cashregisters = bankstatements;
+
+ // Load the company Logo
+
+ self.company_logo = new Image();
+ var logo_loaded = new $.Deferred();
+ self.company_logo.onload = function(){
+ var img = self.company_logo;
+ var ratio = 1;
+ var targetwidth = 300;
+ var maxheight = 150;
+ if( img.width !== targetwidth ){
+ ratio = targetwidth / img.width;
+ }
+ if( img.height * ratio > maxheight ){
+ ratio = maxheight / img.height;
+ }
+ var width = Math.floor(img.width * ratio);
+ var height = Math.floor(img.height * ratio);
+ var c = document.createElement('canvas');
+ c.width = width;
+ c.height = height
+ var ctx = c.getContext('2d');
+ ctx.drawImage(self.company_logo,0,0, width, height);
+
+ self.company_logo_base64 = c.toDataURL();
+ logo_loaded.resolve();
+ };
+ self.company_logo.onerror = function(){
+ logo_loaded.reject();
+ };
+ self.company_logo.crossOrigin = "anonymous";
+ self.company_logo.src = '/web/binary/company_logo'+'?_'+Math.random();
+
+ return logo_loaded;
});
-
+
return loaded;
},
- // logs the usefull posmodel data to the console for debug purposes
- log_loaded_data: function(){
- console.log('PosModel data has been loaded:');
- console.log('PosModel: units:',this.get('units'));
- console.log('PosModel: bank_statements:',this.get('bank_statements'));
- console.log('PosModel: journals:',this.get('journals'));
- console.log('PosModel: taxes:',this.get('taxes'));
- console.log('PosModel: pos_session:',this.get('pos_session'));
- console.log('PosModel: pos_config:',this.get('pos_config'));
- console.log('PosModel: cashRegisters:',this.get('cashRegisters'));
- console.log('PosModel: shop:',this.get('shop'));
- console.log('PosModel: company:',this.get('company'));
- console.log('PosModel: currency:',this.get('currency'));
- console.log('PosModel: user_list:',this.get('user_list'));
- console.log('PosModel: user:',this.get('user'));
- console.log('PosModel.session:',this.session);
- console.log('PosModel end of data log.');
- },
-
// this is called when an order is removed from the order collection. It ensures that there is always an existing
// order and a valid selected order
- on_removed_order: function(removed_order){
- if( this.get('orders').isEmpty()){
- this.add_new_order();
+ on_removed_order: function(removed_order,index,reason){
+ if(reason === 'abandon' && this.get('orders').size() > 0){
+ // when we intentionally remove an unfinished order, and there is another existing one
+ this.set({'selectedOrder' : this.get('orders').at(index) || this.get('orders').last()});
}else{
- this.set({ selectedOrder: this.get('orders').last() });
+ // when the order was automatically removed after completion,
+ // or when we intentionally delete the only concurrent order
+ this.add_new_order();
}
},
- // saves the order locally and try to send it to the backend. 'record' is a bizzarely defined JSON version of the Order
- push_order: function(record) {
- this.db.add_order(record);
- this.flush();
- },
-
//creates a new empty order and sets it as the current order
add_new_order: function(){
var order = new module.Order({pos:this});
this.set('selectedOrder', order);
},
+ //removes the current order
+ delete_current_order: function(){
+ this.get('selectedOrder').destroy({'reason':'abandon'});
+ },
+
+ // saves the order locally and try to send it to the backend.
+ // it returns a deferred that succeeds after having tried to send the order and all the other pending orders.
+ push_order: function(order) {
+ var self = this;
+ this.proxy.log('push_order',order.export_as_JSON());
+ var order_id = this.db.add_order(order.export_as_JSON());
+ var pushed = new $.Deferred();
+
+ this.set('synch',{state:'connecting', pending:self.db.get_orders().length});
+
+ this.flush_mutex.exec(function(){
+ var flushed = self._flush_all_orders();
+
+ flushed.always(function(){
+ pushed.resolve();
+ });
+
+ return flushed;
+ });
+ return pushed;
+ },
+
+ // saves the order locally and try to send it to the backend and make an invoice
+ // returns a deferred that succeeds when the order has been posted and successfully generated
+ // an invoice. This method can fail in various ways:
+ // error-no-client: the order must have an associated partner_id. You can retry to make an invoice once
+ // this error is solved
+ // error-transfer: there was a connection error during the transfer. You can retry to make the invoice once
+ // the network connection is up
+
+ push_and_invoice_order: function(order){
+ var self = this;
+ var invoiced = new $.Deferred();
+
+ if(!order.get_client()){
+ invoiced.reject('error-no-client');
+ return invoiced;
+ }
+
+ var order_id = this.db.add_order(order.export_as_JSON());
+
+ this.set('synch',{state:'connecting', pending:self.db.get_orders().length});
+
+ this.flush_mutex.exec(function(){
+ var done = new $.Deferred(); // holds the mutex
+
+ // send the order to the server
+ // we have a 30 seconds timeout on this push.
+ // FIXME: if the server takes more than 30 seconds to accept the order,
+ // the client will believe it wasn't successfully sent, and very bad
+ // things will happen as a duplicate will be sent next time
+ // so we must make sure the server detects and ignores duplicated orders
+
+ var transfer = self._flush_order(order_id, {timeout:30000, to_invoice:true});
+
+ transfer.fail(function(){
+ invoiced.reject('error-transfer');
+ done.reject();
+ });
+
+ // on success, get the order id generated by the server
+ transfer.pipe(function(order_server_id){
+ // generate the pdf and download it
+ self.pos_widget.do_action('point_of_sale.pos_invoice_report',{additional_context:{
+ active_ids:order_server_id,
+ }});
+ invoiced.resolve();
+ done.resolve();
+ });
+
+ return done;
+
+ });
+
+ return invoiced;
+ },
+
// attemps to send all pending orders ( stored in the pos_db ) to the server,
// and remove the successfully sent ones from the db once
// it has been confirmed that they have been sent correctly.
flush: function() {
- //TODO make the mutex work
- //this makes sure only one _int_flush is called at the same time
- /*
- return this.flush_mutex.exec(_.bind(function() {
- return this._flush(0);
- }, this));
- */
- this._flush(0);
+ var self = this;
+ var flushed = new $.Deferred();
+
+ this.flush_mutex.exec(function(){
+ var done = new $.Deferred();
+
+ self._flush_all_orders()
+ .done( function(){ flushed.resolve();})
+ .fail( function(){ flushed.reject(); })
+ .always(function(){ done.resolve(); });
+
+ return done;
+ });
+
+ return flushed;
+ },
+
+ // attempts to send the locally stored order of id 'order_id'
+ // the sending is asynchronous and can take some time to decide if it is successful or not
+ // it is therefore important to only call this method from inside a mutex
+ // this method returns a deferred indicating wether the sending was successful or not
+ // there is a timeout parameter which is set to 2 seconds by default.
+ _flush_order: function( order_id, options) {
+ return this._flush_all_orders([this.db.get_order(order_id)], options);
},
- // attempts to send an order of index 'index' in the list of order to send. The index
- // is used to skip orders that failed. do not call this method outside the mutex provided
- // by flush()
- _flush: function(index){
+
+ // attempts to send all the locally stored orders. As with _flush_order, it should only be
+ // called from within a mutex.
+ // this method returns a deferred that always succeeds when all orders have been tried to be sent,
+ // even if none of them could actually be sent.
+ _flush_all_orders: function ( order, options) {
var self = this;
- var orders = this.db.get_orders();
- self.set('nbr_pending_operations',orders.length);
+ orders = order || self.db.get_orders()
+ self.set('synch', {
+ state: 'connecting',
+ pending: self.get('synch').pending
+ });
+ return self._save_to_server( orders, options).done(function () {
+ var pending = orders.length;
+ self.set('synch', {
+ state: pending ? 'connecting' : 'connected',
+ pending: pending
+ });
+ });
+ },
- var order = orders[index];
- if(!order){
- return;
+ // send an array of orders to the server
+ // available options:
+ // - timeout: timeout for the rpc call in ms
+ _save_to_server: function (orders, options) {
+ if (!orders || !orders.length) {
+ var result = $.Deferred();
+ result.resolve();
+ return result;
}
- //try to push an order to the server
- // shadow : true is to prevent a spinner to appear in case of timeout
- (new instance.web.Model('pos.order')).call('create_from_ui',[[order]],undefined,{ shadow:true })
- .fail(function(unused, event){
- //don't show error popup if it fails
- event.preventDefault();
- console.error('Failed to send order:',order);
- self._flush(index+1);
- })
- .done(function(){
- //remove from db if success
+
+ options = options || {};
+
+ var self = this;
+ var timeout = typeof options.timeout === 'number' ? options.timeout : 7500 * orders.length;
+
+ // we try to send the order. shadow prevents a spinner if it takes too long. (unless we are sending an invoice,
+ // then we want to notify the user that we are waiting on something )
+ var posOrderModel = new instance.web.Model('pos.order');
+ return posOrderModel.call('create_from_ui',
+ [_.map(orders, function (order) {
+ order.to_invoice = options.to_invoice || false;
+ return order;
+ })],
+ undefined,
+ {
+ shadow: !options.to_invoice,
+ timeout: timeout
+ }
+ ).then(function () {
+ _.each(orders, function (order) {
self.db.remove_order(order.id);
- self._flush(index);
});
+ }).fail(function (unused, event){
+ // prevent an error popup creation by the rpc failure
+ // we want the failure to be silent as we send the orders in the background
+ event.preventDefault();
+ console.error('Failed to send orders:', orders);
+ });
},
- scan_product: function(parsed_ean){
+ scan_product: function(parsed_code){
var self = this;
- var product = this.db.get_product_by_ean13(parsed_ean.base_ean);
var selectedOrder = this.get('selectedOrder');
+ if(parsed_code.encoding === 'ean13'){
+ var product = this.db.get_product_by_ean13(parsed_code.base_code);
+ }else if(parsed_code.encoding === 'reference'){
+ var product = this.db.get_product_by_reference(parsed_code.code);
+ }
if(!product){
return false;
}
- if(parsed_ean.type === 'price'){
- selectedOrder.addProduct(new module.Product(product), {price:parsed_ean.value});
- }else if(parsed_ean.type === 'weight'){
- selectedOrder.addProduct(new module.Product(product), {quantity:parsed_ean.value, merge:false});
+ if(parsed_code.type === 'price'){
+ selectedOrder.addProduct(product, {price:parsed_code.value});
+ }else if(parsed_code.type === 'weight'){
+ selectedOrder.addProduct(product, {quantity:parsed_code.value, merge:false});
}else{
- selectedOrder.addProduct(new module.Product(product));
+ selectedOrder.addProduct(product);
}
return true;
},
});
- module.CashRegister = Backbone.Model.extend({
- });
-
- module.CashRegisterCollection = Backbone.Collection.extend({
- model: module.CashRegister,
- });
-
- module.Product = Backbone.Model.extend({
- get_image_url: function(){
- return instance.session.url('/web/binary/image', {model: 'product.product', field: 'image', id: this.get('id')});
- },
- });
-
- module.ProductCollection = Backbone.Collection.extend({
- model: module.Product,
- });
-
// An orderline represent one element of the content of a client's shopping cart.
// An orderline contains a product, its quantity, its price, discount. etc.
// An Order contains zero or more Orderlines.
this.pos = options.pos;
this.order = options.order;
this.product = options.product;
- this.price = options.product.get('price');
+ this.price = options.product.price;
this.quantity = 1;
this.quantityStr = '1';
this.discount = 0;
var disc = Math.min(Math.max(parseFloat(discount) || 0, 0),100);
this.discount = disc;
this.discountStr = '' + disc;
- this.trigger('change');
+ this.trigger('change',this);
},
// returns the discount [0,100]%
get_discount: function(){
this.quantityStr = '' + this.quantity;
}
}
- this.trigger('change');
+ this.trigger('change',this);
},
// return the quantity of product
get_quantity: function(){
},
// return the unit of measure of the product
get_unit: function(){
- var unit_id = (this.product.get('uos_id') || this.product.get('uom_id'));
+ var unit_id = (this.product.uos_id || this.product.uom_id);
if(!unit_id){
return undefined;
}
if(!this.pos){
return undefined;
}
- return this.pos.get('units_by_id')[unit_id];
+ return this.pos.units_by_id[unit_id];
},
// return the product of this orderline
get_product: function(){
// selects or deselects this orderline
set_selected: function(selected){
this.selected = selected;
- this.trigger('change');
+ this.trigger('change',this);
},
// returns true if this orderline is selected
is_selected: function(){
// when we add an new orderline we want to merge it with the last line to see reduce the number of items
// in the orderline. This returns true if it makes sense to merge the two
can_be_merged_with: function(orderline){
- if( this.get_product().get('id') !== orderline.get_product().get('id')){ //only orderline of the same product can be merged
+ if( this.get_product().id !== orderline.get_product().id){ //only orderline of the same product can be merged
return false;
}else if(this.get_product_type() !== orderline.get_product_type()){
return false;
qty: this.get_quantity(),
price_unit: this.get_unit_price(),
discount: this.get_discount(),
- product_id: this.get_product().get('id'),
+ product_id: this.get_product().id,
};
},
//used to create a json of the ticket, to be sent to the printer
unit_name: this.get_unit().name,
price: this.get_unit_price(),
discount: this.get_discount(),
- product_name: this.get_product().get('name'),
+ product_name: this.get_product().name,
price_display : this.get_display_price(),
price_with_tax : this.get_price_with_tax(),
price_without_tax: this.get_price_without_tax(),
tax: this.get_tax(),
- product_description: this.get_product().get('description'),
- product_description_sale: this.get_product().get('description_sale'),
+ product_description: this.get_product().description,
+ product_description_sale: this.get_product().description_sale,
};
},
// changes the base price of the product for this orderline
set_unit_price: function(price){
this.price = round_di(parseFloat(price) || 0, 2);
- this.trigger('change');
+ this.trigger('change',this);
},
get_unit_price: function(){
- var rounding = this.pos.get('currency').rounding;
+ var rounding = this.pos.currency.rounding;
return round_pr(this.price,rounding);
},
get_display_price: function(){
- var rounding = this.pos.get('currency').rounding;
+ var rounding = this.pos.currency.rounding;
return round_pr(round_pr(this.get_unit_price() * this.get_quantity(),rounding) * (1- this.get_discount()/100.0),rounding);
},
get_price_without_tax: function(){
get_tax: function(){
return this.get_all_prices().tax;
},
+ get_tax_details: function(){
+ return this.get_all_prices().taxDetails;
+ },
get_all_prices: function(){
var self = this;
- var currency_rounding = this.pos.get('currency').rounding;
+ var currency_rounding = this.pos.currency.rounding;
var base = round_pr(round_pr(this.get_quantity() * this.get_unit_price(), currency_rounding) * (1.0 - (this.get_discount() / 100.0)), currency_rounding);
var totalTax = base;
var totalNoTax = base;
- var product_list = this.pos.get('product_list');
var product = this.get_product();
- var taxes_ids = product.get('taxes_id');;
- var taxes = self.pos.get('taxes');
+ var taxes_ids = product.taxes_id;
+ var taxes = self.pos.taxes;
var taxtotal = 0;
+ var taxdetail = {};
_.each(taxes_ids, function(el) {
var tax = _.detect(taxes, function(t) {return t.id === el;});
if (tax.price_include) {
tmp = round_pr(tmp,currency_rounding);
taxtotal += tmp;
totalNoTax -= tmp;
+ taxdetail[tax.id] = tmp;
} else {
var tmp;
if (tax.type === "percent") {
tmp = round_pr(tmp,currency_rounding);
taxtotal += tmp;
totalTax += tmp;
+ taxdetail[tax.id] = tmp;
}
});
return {
"priceWithTax": totalTax,
"priceWithoutTax": totalNoTax,
"tax": taxtotal,
+ "taxDetails": taxdetail,
};
},
});
model: module.Orderline,
});
- // Every PaymentLine contains a cashregister and an amount of money.
+ // Every Paymentline contains a cashregister and an amount of money.
module.Paymentline = Backbone.Model.extend({
initialize: function(attributes, options) {
this.amount = 0;
- this.cashregister = options.cashRegister;
+ this.cashregister = options.cashregister;
+ this.name = this.cashregister.journal_id[1];
+ this.selected = false;
},
//sets the amount of money on this payment line
set_amount: function(value){
- this.amount = parseFloat(value) || 0;
- this.trigger('change');
+ this.amount = round_di(parseFloat(value) || 0, 2);
+ this.trigger('change:amount',this);
},
// returns the amount of money on this paymentline
get_amount: function(){
return this.amount;
},
- // returns the associated cashRegister
- get_cashregister: function(){
- return this.cashregister;
+ set_selected: function(selected){
+ if(this.selected !== selected){
+ this.selected = selected;
+ this.trigger('change:selected',this);
+ }
},
+ // returns the associated cashregister
//exports as JSON for server communication
export_as_JSON: function(){
return {
name: instance.web.datetime_to_str(new Date()),
- statement_id: this.cashregister.get('id'),
- account_id: (this.cashregister.get('account_id'))[0],
- journal_id: (this.cashregister.get('journal_id'))[0],
+ statement_id: this.cashregister.id,
+ account_id: this.cashregister.account_id[0],
+ journal_id: this.cashregister.journal_id[0],
amount: this.get_amount()
};
},
export_for_printing: function(){
return {
amount: this.get_amount(),
- journal: this.cashregister.get('journal_id')[1],
+ journal: this.cashregister.journal_id[1],
};
},
});
// An order more or less represents the content of a client's shopping cart (the OrderLines)
- // plus the associated payment information (the PaymentLines)
+ // plus the associated payment information (the Paymentlines)
// there is always an active ('selected') order in the Pos, a new one is created
// automaticaly once an order is completed and sent to the server.
module.Order = Backbone.Model.extend({
initialize: function(attributes){
Backbone.Model.prototype.initialize.apply(this, arguments);
+ this.uid = this.generateUniqueId();
this.set({
creationDate: new Date(),
orderLines: new module.OrderlineCollection(),
paymentLines: new module.PaymentlineCollection(),
- name: "Order " + this.generateUniqueId(),
+ name: "Order " + this.uid,
client: null,
});
- this.pos = attributes.pos;
- this.selected_orderline = undefined;
+ this.pos = attributes.pos;
+ this.selected_orderline = undefined;
+ this.selected_paymentline = undefined;
this.screen_data = {}; // see ScreenSelector
this.receipt_type = 'receipt'; // 'receipt' || 'invoice'
return this;
},
addProduct: function(product, options){
options = options || {};
- var attr = product.toJSON();
+ var attr = JSON.parse(JSON.stringify(product));
attr.pos = this.pos;
attr.order = this;
var line = new module.Orderline({}, {pos: this.pos, order: this, product: product});
getLastOrderline: function(){
return this.get('orderLines').at(this.get('orderLines').length -1);
},
- addPaymentLine: function(cashRegister) {
+ addPaymentline: function(cashregister) {
var paymentLines = this.get('paymentLines');
- var newPaymentline = new module.Paymentline({},{cashRegister:cashRegister});
- if(cashRegister.get('journal').type !== 'cash'){
- newPaymentline.set_amount( this.getDueLeft() );
+ var newPaymentline = new module.Paymentline({},{cashregister:cashregister});
+ if(cashregister.journal.type !== 'cash'){
+ newPaymentline.set_amount( Math.max(this.getDueLeft(),0) );
}
paymentLines.add(newPaymentline);
+ this.selectPaymentline(newPaymentline);
+
+ },
+ removePaymentline: function(line){
+ if(this.selected_paymentline === line){
+ this.selectPaymentline(undefined);
+ }
+ this.get('paymentLines').remove(line);
},
getName: function() {
return this.get('name');
return sum + orderLine.get_tax();
}), 0);
},
+ getTaxDetails: function(){
+ var details = {};
+ var fulldetails = [];
+ var taxes_by_id = {};
+
+ for(var i = 0; i < this.pos.taxes.length; i++){
+ taxes_by_id[this.pos.taxes[i].id] = this.pos.taxes[i];
+ }
+
+ this.get('orderLines').each(function(line){
+ var ldetails = line.get_tax_details();
+ for(var id in ldetails){
+ if(ldetails.hasOwnProperty(id)){
+ details[id] = (details[id] || 0) + ldetails[id];
+ }
+ }
+ });
+
+ for(var id in details){
+ if(details.hasOwnProperty(id)){
+ fulldetails.push({amount: details[id], tax: taxes_by_id[id]});
+ }
+ }
+
+ return fulldetails;
+ },
getPaidTotal: function() {
return (this.get('paymentLines')).reduce((function(sum, paymentLine) {
return sum + paymentLine.get_amount();
paymentlines.push(paymentline.export_for_printing());
});
var client = this.get('client');
- var cashier = this.pos.get('cashier') || this.pos.get('user');
- var company = this.pos.get('company');
- var shop = this.pos.get('shop');
+ var cashier = this.pos.cashier || this.pos.user;
+ var company = this.pos.company;
+ var shop = this.pos.shop;
var date = new Date();
return {
total_tax: this.getTax(),
total_paid: this.getPaidTotal(),
total_discount: this.getDiscountTotal(),
+ tax_details: this.getTaxDetails(),
change: this.getChange(),
name : this.getName(),
client: client ? client.name : null ,
invoice_id: null, //TODO
cashier: cashier ? cashier.name : null,
+ header: this.pos.config.receipt_header || '',
+ footer: this.pos.config.receipt_footer || '',
+ precision: {
+ price: 2,
+ money: 2,
+ quantity: 3,
+ },
date: {
year: date.getFullYear(),
month: date.getMonth(),
date: date.getDate(), // day of the month
day: date.getDay(), // day of the week
hour: date.getHours(),
- minute: date.getMinutes()
+ minute: date.getMinutes() ,
+ isostring: date.toISOString(),
},
company:{
email: company.email,
website: company.website,
company_registry: company.company_registry,
- contact_address: company.contact_address,
+ contact_address: company.partner_id[1],
vat: company.vat,
name: company.name,
phone: company.phone,
+ logo: this.pos.company_logo_base64,
},
shop:{
name: shop.name,
},
- currency: this.pos.get('currency'),
+ currency: this.pos.currency,
};
},
- exportAsJSON: function() {
+ export_as_JSON: function() {
var orderLines, paymentLines;
orderLines = [];
(this.get('orderLines')).each(_.bind( function(item) {
amount_return: this.getChange(),
lines: orderLines,
statement_ids: paymentLines,
- pos_session_id: this.pos.get('pos_session').id,
- partner_id: this.get('client') ? this.get('client').id : undefined,
- user_id: this.pos.get('cashier') ? this.pos.get('cashier').id : this.pos.get('user').id,
+ pos_session_id: this.pos.pos_session.id,
+ partner_id: this.get_client() ? this.get_client().id : false,
+ user_id: this.pos.cashier ? this.pos.cashier.id : this.pos.user.id,
+ uid: this.uid,
};
},
getSelectedLine: function(){
this.selected_orderline = undefined;
}
},
+ deselectLine: function(){
+ if(this.selected_orderline){
+ this.selected_orderline.set_selected(false);
+ this.selected_orderline = undefined;
+ }
+ },
+ selectPaymentline: function(line){
+ if(line !== this.selected_paymentline){
+ if(this.selected_paymentline){
+ this.selected_paymentline.set_selected(false);
+ }
+ this.selected_paymentline = line;
+ if(this.selected_paymentline){
+ this.selected_paymentline.set_selected(true);
+ }
+ this.trigger('change:selected_paymentline',this.selected_paymentline);
+ }
+ },
});
module.OrderCollection = Backbone.Collection.extend({