-function openerp_pos_models(instance, module){ //module is instance.point_of_sale
+openerp.point_of_sale.load_models = function load_models(instance, module){ //module is instance.point_of_sale
+ "use strict";
+
var QWeb = instance.web.qweb;
var _t = instance.web._t;
+ var barcode_parser_module = instance.barcodes;
var round_di = instance.web.round_decimals;
var round_pr = instance.web.round_precision
this.pos_widget = attributes.pos_widget;
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.barcode_reader = new module.BarcodeReader({'pos': this, proxy:this.proxy});
+
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
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;
+ this.order_sequence = 1;
window.posmodel = this;
// these dynamic attributes can be watched for change by other models or widgets
if(self.config.use_proxy){
return self.connect_to_proxy();
}
- });
-
+ }); // used to read barcodes);
},
// releases ressources holds by the model at the end of life of the posmodel
return done;
},
- // helper function to load data from the server
+ // helper function to load data from the server. Obsolete use the models loader below.
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()
},
+
+ // Server side model loaders. This is the list of the models that need to be loaded from
+ // the server. The models are loaded one by one by this list's order. The 'loaded' callback
+ // is used to store the data in the appropriate place once it has been loaded. This callback
+ // can return a deferred that will pause the loading of the next module.
+ // a shared temporary dictionary is available for loaders to communicate private variables
+ // used during loading such as object ids, etc.
+ models: [
+ {
+ model: 'res.users',
+ fields: ['name','company_id'],
+ ids: function(self){ return [self.session.uid]; },
+ loaded: function(self,users){ self.user = users[0]; },
+ },{
+ model: 'res.company',
+ fields: [ 'currency_id', 'email', 'website', 'company_registry', 'vat', 'name', 'phone', 'partner_id' , 'country_id'],
+ ids: function(self){ return [self.user.company_id[0]] },
+ loaded: function(self,companies){ self.company = companies[0]; },
+ },{
+ model: 'decimal.precision',
+ fields: ['name','digits'],
+ loaded: function(self,dps){
+ self.dp = {};
+ for (var i = 0; i < dps.length; i++) {
+ self.dp[dps[i].name] = dps[i].digits;
+ }
+ },
+ },{
+ model: 'product.uom',
+ fields: [],
+ domain: null,
+ loaded: function(self,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];
+ units[i].groupable = ( units[i].category_id[0] === 1 );
+ units[i].is_unit = ( units[i].id === 1 );
+ }
+ self.units_by_id = units_by_id;
+ }
+ },{
+ model: 'res.partner',
+ fields: ['name','street','city','state_id','country_id','vat','phone','zip','mobile','email','barcode','write_date'],
+ domain: [['customer','=',true]],
+ loaded: function(self,partners){
+ self.partners = partners;
+ self.db.add_partners(partners);
+ },
+ },{
+ model: 'res.country',
+ fields: ['name'],
+ loaded: function(self,countries){
+ self.countries = countries;
+ self.company.country = null;
+ for (var i = 0; i < countries.length; i++) {
+ if (countries[i].id === self.company.country_id[0]){
+ self.company.country = countries[i];
+ }
+ }
+ },
+ },{
+ model: 'account.tax',
+ fields: ['name','amount', 'price_include', 'type'],
+ domain: null,
+ loaded: function(self,taxes){
+ self.taxes = taxes;
+ self.taxes_by_id = {};
+
+ for (var i = 0; i < taxes.length; i++) {
+ self.taxes_by_id[taxes[i].id] = taxes[i];
+ }
+ },
+ },{
+ model: 'pos.session',
+ fields: ['id', 'journal_ids','name','user_id','config_id','start_at','stop_at','sequence_number','login_number'],
+ domain: function(self){ return [['state','=','opened'],['user_id','=',self.session.uid]]; },
+ loaded: function(self,pos_sessions){
+ self.pos_session = pos_sessions[0];
+ },
+ },{
+ model: 'pos.config',
+ fields: [],
+ domain: function(self){ return [['id','=', self.pos_session.config_id[0]]]; },
+ loaded: function(self,configs){
+ 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;
+
+ if (self.config.company_id[0] !== self.user.company_id[0]) {
+ throw new Error(_t("Error: The Point of Sale User must belong to the same company as the Point of Sale. You are probably trying to load the point of sale as an administrator in a multi-company setup, with the administrator account set to the wrong company."));
+ }
+
+ self.db.set_uuid(self.config.uuid);
+
+ var orders = self.db.get_orders();
+ for (var i = 0; i < orders.length; i++) {
+ self.pos_session.sequence_number = Math.max(self.pos_session.sequence_number, orders[i].data.sequence_number+1);
+ }
+ },
+ },{
+ model: 'res.users',
+ fields: ['name','pos_security_pin','groups_id','barcode'],
+ domain: function(self){ return [['company_id','=',self.user.company_id[0]],'|', ['groups_id','=', self.config.group_pos_manager_id[0]],['groups_id','=', self.config.group_pos_user_id[0]]]; },
+ loaded: function(self,users){
+ // we attribute a role to the user, 'cashier' or 'manager', depending
+ // on the group the user belongs.
+ var pos_users = [];
+ for (var i = 0; i < users.length; i++) {
+ var user = users[i];
+ for (var j = 0; j < user.groups_id.length; j++) {
+ var group_id = user.groups_id[j];
+ if (group_id === self.config.group_pos_manager_id[0]) {
+ user.role = 'manager';
+ break;
+ } else if (group_id === self.config.group_pos_user_id[0]) {
+ user.role = 'cashier';
+ }
+ }
+ if (user.role) {
+ pos_users.push(user);
+ }
+ // replace the current user with its updated version
+ if (user.id === self.user.id) {
+ self.user = user;
+ }
+ }
+ self.users = pos_users;
+ },
+ },{
+ model: 'stock.location',
+ fields: [],
+ ids: function(self){ return [self.config.stock_location_id[0]]; },
+ loaded: function(self, locations){ self.shop = locations[0]; },
+ },{
+ model: 'product.pricelist',
+ fields: ['currency_id'],
+ ids: function(self){ return [self.config.pricelist_id[0]]; },
+ loaded: function(self, pricelists){ self.pricelist = pricelists[0]; },
+ },{
+ model: 'res.currency',
+ fields: ['symbol','position','rounding','accuracy'],
+ ids: function(self){ return [self.pricelist.currency_id[0]]; },
+ loaded: function(self, currencies){
+ self.currency = currencies[0];
+ if (self.currency.rounding > 0) {
+ self.currency.decimals = Math.ceil(Math.log(1.0 / self.currency.rounding) / Math.log(10));
+ } else {
+ self.currency.decimals = 0;
+ }
+
+ },
+ },{
+ model: 'product.packaging',
+ fields: ['barcode','product_tmpl_id'],
+ domain: null,
+ loaded: function(self, packagings){
+ self.db.add_packagings(packagings);
+ },
+ },{
+ model: 'pos.category',
+ fields: ['id','name','parent_id','child_id','image'],
+ domain: null,
+ loaded: function(self, categories){
+ self.db.add_categories(categories);
+ },
+ },{
+ model: 'product.product',
+ fields: ['display_name', 'list_price','price','pos_categ_id', 'taxes_id', 'barcode', 'default_code',
+ 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description',
+ 'product_tmpl_id'],
+ order: ['sequence','name'],
+ domain: [['sale_ok','=',true],['available_in_pos','=',true]],
+ context: function(self){ return { pricelist: self.pricelist.id, display_default_code: false }; },
+ loaded: function(self, products){
+ self.db.add_products(products);
+ },
+ },{
+ model: 'account.bank.statement',
+ fields: ['account_id','currency','journal_id','state','name','user_id','pos_session_id'],
+ domain: function(self){ return [['state', '=', 'open'],['pos_session_id', '=', self.pos_session.id]]; },
+ loaded: function(self, cashregisters, tmp){
+ self.cashregisters = cashregisters;
+
+ tmp.journals = [];
+ _.each(cashregisters,function(statement){
+ tmp.journals.push(statement.journal_id[0]);
+ });
+ },
+ },{
+ model: 'account.journal',
+ fields: [],
+ domain: function(self,tmp){ return [['id','in',tmp.journals]]; },
+ loaded: function(self, journals){
+ self.journals = journals;
+
+ // associate the bank statements with their journals.
+ var cashregisters = self.cashregisters;
+ for(var i = 0, ilen = cashregisters.length; i < ilen; i++){
+ for(var j = 0, jlen = journals.length; j < jlen; j++){
+ if(cashregisters[i].journal_id[0] === journals[j].id){
+ cashregisters[i].journal = journals[j];
+ }
+ }
+ }
+
+ self.cashregisters_by_id = {};
+ for (var i = 0; i < self.cashregisters.length; i++) {
+ self.cashregisters_by_id[self.cashregisters[i].id] = self.cashregisters[i];
+ }
+
+ self.cashregisters = self.cashregisters.sort(function(a,b){
+ return a.journal.sequence - b.journal.sequence;
+ });
+
+ },
+ }, {
+ label: 'fonts',
+ loaded: function(self){
+ var fonts_loaded = new $.Deferred();
+ // Waiting for fonts to be loaded to prevent receipt printing
+ // from printing empty receipt while loading Inconsolata
+ // ( The font used for the receipt )
+ waitForWebfonts(['Lato','Inconsolata'], function(){
+ fonts_loaded.resolve();
+ });
+ // The JS used to detect font loading is not 100% robust, so
+ // do not wait more than 5sec
+ setTimeout(function(){
+ fonts_loaded.resolve();
+ },5000);
+
+ return fonts_loaded;
+ },
+ },{
+ label: 'pictures',
+ loaded: function(self){
+ 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;
+ },
+ }, {
+ label: 'barcodes',
+ loaded: function(self) {
+ var barcode_parser = new barcode_parser_module.BarcodeParser({'nomenclature_id': self.config.barcode_nomenclature_id});
+ self.barcode_reader.set_barcode_parser(barcode_parser);
+ return barcode_parser.is_loaded();
+ },
+ }
+ ],
+
// loads all the needed data on the sever. returns a deferred indicating when all the data has loaded.
load_server_data: function(){
var self = this;
+ var loaded = new $.Deferred();
+ var progress = 0;
+ var progress_step = 1.0 / self.models.length;
+ var tmp = {}; // this is used to share a temporary state between models loaders
+
+ function load_model(index){
+ if(index >= self.models.length){
+ loaded.resolve();
+ }else{
+ var model = self.models[index];
+ self.pos_widget.loading_message(_t('Loading')+' '+(model.label || model.model || ''), progress);
- var loaded = self.fetch('res.users',['name','company_id'],[['id','=',this.session.uid]])
- .then(function(users){
- self.user = users[0];
-
- return self.fetch('res.company',
- [
- 'currency_id',
- 'email',
- 'website',
- 'company_registry',
- 'vat',
- 'name',
- 'phone',
- 'partner_id',
- ],
- [['id','=',users[0].company_id[0]]],
- {show_address_only: true});
- }).then(function(companies){
- self.company = companies[0];
-
- return self.fetch('product.uom', null, null);
- }).then(function(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];
+ var cond = typeof model.condition === 'function' ? model.condition(self,tmp) : true;
+ if (!cond) {
+ load_model(index+1);
+ return;
}
- self.units_by_id = units_by_id;
+
+ var fields = typeof model.fields === 'function' ? model.fields(self,tmp) : model.fields;
+ var domain = typeof model.domain === 'function' ? model.domain(self,tmp) : model.domain;
+ var context = typeof model.context === 'function' ? model.context(self,tmp) : model.context;
+ var ids = typeof model.ids === 'function' ? model.ids(self,tmp) : model.ids;
+ var order = typeof model.order === 'function' ? model.order(self,tmp): model.order;
+ progress += progress_step;
- return self.fetch('res.users', ['name','ean13'], [['ean13', '!=', false]]);
- }).then(function(users){
- self.users = users;
-
- return self.fetch('res.partner', ['name','ean13'], [['ean13', '!=', false]]);
- }).then(function(partners){
- self.partners = partners;
-
- return self.fetch('account.tax', ['name','amount', 'price_include', 'type']);
- }).then(function(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(pos_sessions){
- self.pos_session = pos_sessions[0];
-
- return self.fetch(
- 'pos.config',
- ['name','journal_ids','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_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){
- 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('product.pricelist',['currency_id'],[['id','=',self.config.pricelist_id[0]]]);
- }).then(function(pricelists){
- self.pricelist = pricelists[0];
-
- return self.fetch('res.currency',['symbol','position','rounding','accuracy'],[['id','=',self.pricelist.currency_id[0]]]);
- }).then(function(currencies){
- 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'])
- }).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',
- 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description'],
- [['sale_ok','=',true],['available_in_pos','=',true]],
- {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.pos_session.id]]
- );
- }).then(function(bankstatements){
- var journals = [];
- _.each(bankstatements,function(statement) {
- journals.push(statement.journal_id[0])
- });
- self.bankstatements = bankstatements;
- return self.fetch('account.journal', undefined, [['id','in', journals]]);
- }).then(function(journals){
- self.journals = journals;
-
- // associate the bank statements with their journals.
- 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(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.cashregisters = bankstatements;
-
- // Load the company Logo
-
- self.company_logo = new Image();
- self.company_logo.crossOrigin = 'anonymous';
- 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( model.model ){
+ if (model.ids) {
+ var records = new instance.web.Model(model.model).call('read',[ids,fields],context);
+ } else {
+ var records = new instance.web.Model(model.model).query(fields).filter(domain).order_by(order).context(context).all()
}
- if( img.height * ratio > maxheight ){
- ratio = maxheight / img.height;
+ records.then(function(result){
+ try{ // catching exceptions in model.loaded(...)
+ $.when(model.loaded(self,result,tmp))
+ .then(function(){ load_model(index + 1); },
+ function(err){ loaded.reject(err); });
+ }catch(err){
+ console.error(err.stack);
+ loaded.reject(err);
+ }
+ },function(err){
+ loaded.reject(err);
+ });
+ }else if( model.loaded ){
+ try{ // catching exceptions in model.loaded(...)
+ $.when(model.loaded(self,tmp))
+ .then( function(){ load_model(index +1); },
+ function(err){ loaded.reject(err); });
+ }catch(err){
+ loaded.reject(err);
}
- 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();
- window.logo64 = self.company_logo_base64;
- logo_loaded.resolve();
- };
- self.company_logo.onerror = function(){
- logo_loaded.reject();
- };
- self.company_logo.src = window.location.origin + '/web/binary/company_logo';
-
- return logo_loaded;
- });
-
+ }else{
+ load_model(index + 1);
+ }
+ }
+ }
+
+ try{
+ load_model(0);
+ }catch(err){
+ loaded.reject(err);
+ }
+
return loaded;
},
+ // reload the list of partner, returns as a deferred that resolves if there were
+ // updated partners, and fails if not
+ load_new_partners: function(){
+ var self = this;
+ var def = new $.Deferred();
+ var fields = _.find(this.models,function(model){ return model.model === 'res.partner'; }).fields;
+ new instance.web.Model('res.partner')
+ .query(fields)
+ .filter([['write_date','>',this.db.get_partner_write_date()]])
+ .all({'timeout':3000, 'shadow': true})
+ .then(function(partners){
+ if (self.db.add_partners(partners)) { // check if the partners we got were real updates
+ def.resolve();
+ } else {
+ def.reject();
+ }
+ }, function(err,event){ event.preventDefault(); def.reject(); });
+ return def;
+ },
+
// 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,index,reason){
- if(reason === 'abandon' && this.get('orders').size() > 0){
+ var order_list = this.get_order_list();
+ if( (reason === 'abandon' || removed_order.temporary) && order_list.length > 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()});
+ this.set_order(order_list[index] || order_list[order_list.length -1]);
}else{
// when the order was automatically removed after completion,
// or when we intentionally delete the only concurrent order
}
},
+ // returns the user who is currently the cashier for this point of sale
+ get_cashier: function(){
+ return this.cashier || this.user;
+ },
+ // changes the current cashier
+ set_cashier: function(user){
+ this.cashier = user;
+ },
//creates a new empty order and sets it as the current order
add_new_order: function(){
- var order = new module.Order({pos:this});
+ var order = new module.Order({},{pos:this});
this.get('orders').add(order);
this.set('selectedOrder', order);
+ return order;
+ },
+ // load the locally saved unpaid orders for this session.
+ load_orders: function(){
+ var jsons = this.db.get_unpaid_orders();
+ var orders = [];
+ var not_loaded_count = 0;
+
+ for (var i = 0; i < jsons.length; i++) {
+ var json = jsons[i];
+ if (json.pos_session_id === this.pos_session.id) {
+ orders.push(new module.Order({},{
+ pos: this,
+ json: json,
+ }));
+ } else {
+ not_loaded_count += 1;
+ }
+ }
+
+ if (not_loaded_count) {
+ console.info('There are '+not_loaded_count+' locally saved unpaid orders belonging to another session');
+ }
+
+ orders = orders.sort(function(a,b){
+ return a.sequence_number - b.sequence_number;
+ });
+
+ if (orders.length) {
+ this.get('orders').add(orders);
+ }
+ },
+
+ set_start_order: function(){
+ var orders = this.get('orders').models;
+
+ if (orders.length && !this.get('selectedOrder')) {
+ this.set('selectedOrder',orders[0]);
+ } else {
+ this.add_new_order();
+ }
+ },
+
+ // return the current order
+ get_order: function(){
+ return this.get('selectedOrder');
+ },
+
+ // change the current order
+ set_order: function(order){
+ this.set({ selectedOrder: order });
+ },
+
+ // return the list of unpaid orders
+ get_order_list: function(){
+ return this.get('orders').models;
},
//removes the current order
delete_current_order: function(){
- this.get('selectedOrder').destroy({'reason':'abandon'});
+ var order = this.get_order();
+ if (order) {
+ order.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});
+ if(order){
+ this.proxy.log('push_order',order.export_as_JSON());
+ this.db.add_order(order.export_as_JSON());
+ }
+
+ var pushed = new $.Deferred();
this.flush_mutex.exec(function(){
- var flushed = self._flush_all_orders();
+ var flushed = self._flush_orders(self.db.get_orders());
- flushed.always(function(){
+ flushed.always(function(ids){
pushed.resolve();
});
-
- return flushed;
});
return pushed;
},
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
// 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});
+ var transfer = self._flush_orders([self.db.get_order(order_id)], {timeout:30000, to_invoice:true});
transfer.fail(function(){
invoiced.reject('error-transfer');
// 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 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() {
+ // wrapper around the _save_to_server that updates the synch status widget
+ _flush_orders: function(orders, options) {
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(); });
+ this.set('synch',{ state: 'connecting', pending: orders.length});
- 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 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 () {
- var self = this;
- self.set('synch', {
- state: 'connecting',
- pending: self.get('synch').pending
- });
- return self._save_to_server(self.db.get_orders()).done(function () {
+ return self._save_to_server(orders, options).done(function (server_ids) {
var pending = self.db.get_orders().length;
+
self.set('synch', {
state: pending ? 'connecting' : 'connected',
pending: pending
});
+
+ return server_ids;
});
},
// send an array of orders to the server
// available options:
// - timeout: timeout for the rpc call in ms
+ // returns a deferred that resolves with the list of
+ // server generated ids for the sent orders
_save_to_server: function (orders, options) {
if (!orders || !orders.length) {
var result = $.Deferred();
- result.resolve();
+ result.resolve([]);
return result;
}
shadow: !options.to_invoice,
timeout: timeout
}
- ).then(function () {
+ ).then(function (server_ids) {
_.each(orders, function (order) {
self.db.remove_order(order.id);
});
- }).fail(function (unused, event){
+ return server_ids;
+ }).fail(function (error, event){
+ if(error.code === 200 ){ // Business Logic Error, not a connection problem
+ //if warning do not need to display traceback!!
+ if (error.data.exception_type == 'warning') {
+ delete error.data.debug;
+ }
+ self.pos_widget.screen_selector.show_popup('error-traceback',{
+ message: error.data.message,
+ comment: error.data.debug
+ });
+ }
// 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();
scan_product: function(parsed_code){
var self = this;
- 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);
- }
+ var selectedOrder = this.get_order();
+ var product = this.db.get_product_by_barcode(parsed_code.base_code);
if(!product){
return false;
}
if(parsed_code.type === 'price'){
- selectedOrder.addProduct(product, {price:parsed_code.value});
+ selectedOrder.add_product(product, {price:parsed_code.value});
}else if(parsed_code.type === 'weight'){
- selectedOrder.addProduct(product, {quantity:parsed_code.value, merge:false});
+ selectedOrder.add_product(product, {quantity:parsed_code.value, merge:false});
+ }else if(parsed_code.type === 'discount'){
+ selectedOrder.add_product(product, {discount:parsed_code.value, merge:false});
}else{
- selectedOrder.addProduct(product);
+ selectedOrder.add_product(product);
}
return true;
},
});
+ var orderline_id = 1;
+
// 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.
module.Orderline = Backbone.Model.extend({
initialize: function(attr,options){
- this.pos = options.pos;
+ this.pos = options.pos;
this.order = options.order;
+ if (options.json) {
+ this.init_from_JSON(options.json);
+ return;
+ }
this.product = options.product;
this.price = options.product.price;
this.quantity = 1;
this.discountStr = '0';
this.type = 'unit';
this.selected = false;
+ this.id = orderline_id++;
+ },
+ init_from_JSON: function(json) {
+ this.product = this.pos.db.get_product_by_id(json.product_id);
+ if (!this.product) {
+ console.error('ERROR: attempting to recover product not available in the point of sale');
+ }
+ this.price = json.price_unit;
+ this.set_discount(json.discount);
+ this.set_quantity(json.qty);
+ },
+ clone: function(){
+ var orderline = new module.Orderline({},{
+ pos: this.pos,
+ order: null,
+ product: this.product,
+ price: this.price,
+ });
+ orderline.quantity = this.quantity;
+ orderline.quantityStr = this.quantityStr;
+ orderline.discount = this.discount;
+ orderline.type = this.type;
+ orderline.selected = false;
+ return orderline;
},
// sets a discount [0,100]%
set_discount: function(discount){
// rounded to zero
set_quantity: function(quantity){
if(quantity === 'remove'){
- this.order.removeOrderline(this);
+ this.order.remove_orderline(this);
return;
}else{
var quant = parseFloat(quantity) || 0;
var unit = this.get_unit();
if(unit){
- this.quantity = round_pr(quant, unit.rounding);
- this.quantityStr = this.quantity.toFixed(Math.ceil(Math.log(1.0 / unit.rounding) / Math.log(10)));
+ if (unit.rounding) {
+ this.quantity = round_pr(quant, unit.rounding);
+ this.quantityStr = this.quantity.toFixed(Math.ceil(Math.log(1.0 / unit.rounding) / Math.log(10)));
+ } else {
+ this.quantity = round_pr(quant, 1);
+ this.quantityStr = this.quantity.toFixed(0);
+ }
}else{
this.quantity = quant;
this.quantityStr = '' + this.quantity;
},
get_quantity_str_with_unit: function(){
var unit = this.get_unit();
- if(unit && unit.name !== 'Unit(s)'){
+ if(unit && !unit.is_unit){
return this.quantityStr + ' ' + unit.name;
}else{
return this.quantityStr;
},
// return the unit of measure of the product
get_unit: function(){
- var unit_id = (this.product.uos_id || this.product.uom_id);
+ var unit_id = this.product.uom_id;
if(!unit_id){
return undefined;
}
can_be_merged_with: function(orderline){
if( this.get_product().id !== orderline.get_product().id){ //only orderline of the same product can be merged
return false;
+ }else if(!this.get_unit() || !this.get_unit().groupable){
+ return false;
}else if(this.get_product_type() !== orderline.get_product_type()){
return false;
}else if(this.get_discount() > 0){ // we don't merge discounted orderlines
return {
quantity: this.get_quantity(),
unit_name: this.get_unit().name,
- price: this.get_unit_price(),
+ price: this.get_unit_display_price(),
discount: this.get_discount(),
- product_name: this.get_product().name,
+ product_name: this.get_product().display_name,
price_display : this.get_display_price(),
price_with_tax : this.get_price_with_tax(),
price_without_tax: this.get_price_without_tax(),
},
// changes the base price of the product for this orderline
set_unit_price: function(price){
- this.price = round_di(parseFloat(price) || 0, 2);
+ this.price = round_di(parseFloat(price) || 0, this.pos.dp['Product Price']);
this.trigger('change',this);
},
get_unit_price: function(){
- var rounding = this.pos.currency.rounding;
- return round_pr(this.price,rounding);
+ return this.price;
+ },
+ get_unit_display_price: function(){
+ if (this.pos.config.iface_tax_included) {
+ var quantity = this.quantity;
+ this.quantity = 1.0;
+ var price = this.get_all_prices().priceWithTax;
+ this.quantity = quantity;
+ return price;
+ } else {
+ return this.get_unit_price();
+ }
},
- get_display_price: function(){
+ get_base_price: function(){
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_display_price: function(){
+ return this.get_base_price();
+ if (this.pos.config.iface_tax_included) {
+ return this.get_all_prices().priceWithTax;
+ } else {
+ return this.get_base_price();
+ }
+ },
get_price_without_tax: function(){
return this.get_all_prices().priceWithoutTax;
},
get_tax_details: function(){
return this.get_all_prices().taxDetails;
},
+ get_taxes: function(){
+ var taxes_ids = this.get_product().taxes_id;
+ var taxes = [];
+ for (var i = 0; i < taxes_ids.length; i++) {
+ taxes.push(this.pos.taxes_by_id[taxes_ids[i]]);
+ }
+ return taxes;
+ },
get_all_prices: function(){
var self = this;
var currency_rounding = this.pos.currency.rounding;
- var base = round_pr(this.get_quantity() * this.get_unit_price() * (1.0 - (this.get_discount() / 100.0)), currency_rounding);
+ var base = this.get_base_price();
var totalTax = base;
var totalNoTax = base;
// Every Paymentline contains a cashregister and an amount of money.
module.Paymentline = Backbone.Model.extend({
initialize: function(attributes, options) {
+ this.pos = options.pos;
this.amount = 0;
+ this.selected = false;
+ if (options.json) {
+ this.init_from_JSON(options.json);
+ return;
+ }
this.cashregister = options.cashregister;
this.name = this.cashregister.journal_id[1];
- this.selected = false;
+ },
+ init_from_JSON: function(json){
+ this.amount = json.amount;
+ this.cashregister = this.pos.cashregisters_by_id[json.statement_id];
+ this.name = this.cashregister.journal_id[1];
},
//sets the amount of money on this payment line
set_amount: function(value){
- this.amount = round_di(parseFloat(value) || 0, 2);
- this.trigger('change:amount',this);
+ this.amount = round_di(parseFloat(value) || 0, this.pos.currency.decimals);
+ this.trigger('change',this);
},
// returns the amount of money on this paymentline
get_amount: function(){
return this.amount;
},
+ get_amount_str: function(){
+ return this.amount.toFixed(this.pos.currency.decimals);
+ },
set_selected: function(selected){
if(this.selected !== selected){
this.selected = selected;
- this.trigger('change:selected',this);
+ this.trigger('change',this);
}
},
// returns the associated cashregister
module.PaymentlineCollection = Backbone.Collection.extend({
model: module.Paymentline,
});
-
// An order more or less represents the content of a client's shopping cart (the OrderLines)
// 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){
+ initialize: function(attributes,options){
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.uid,
- client: null,
- });
- this.pos = attributes.pos;
+ options = options || {};
+
+ this.init_locked = true;
+ this.pos = options.pos;
this.selected_orderline = undefined;
this.selected_paymentline = undefined;
- this.screen_data = {}; // see ScreenSelector
- this.receipt_type = 'receipt'; // 'receipt' || 'invoice'
+ this.screen_data = {}; // see ScreenSelector
+ this.temporary = options.temporary || false;
+ this.creation_date = new Date();
+ this.to_invoice = false;
+ this.orderlines = new module.OrderlineCollection();
+ this.paymentlines = new module.PaymentlineCollection();
+ this.pos_session_id = this.pos.pos_session.id;
+
+ this.set({ client: null });
+
+ if (options.json) {
+ this.init_from_JSON(options.json);
+ } else {
+ this.sequence_number = this.pos.pos_session.sequence_number++;
+ this.uid = this.generate_unique_id();
+ this.name = _t("Order ") + this.uid;
+ }
+
+ this.on('change', function(){ this.save_to_db("order:change"); }, this);
+ this.orderlines.on('change', function(){ this.save_to_db("orderline:change"); }, this);
+ this.orderlines.on('add', function(){ this.save_to_db("orderline:add"); }, this);
+ this.orderlines.on('remove', function(){ this.save_to_db("orderline:remove"); }, this);
+ this.paymentlines.on('change', function(){ this.save_to_db("paymentline:change"); }, this);
+ this.paymentlines.on('add', function(){ this.save_to_db("paymentline:add"); }, this);
+ this.paymentlines.on('remove', function(){ this.save_to_db("paymentline:rem"); }, this);
+
+ this.init_locked = false;
+ this.save_to_db();
+
return this;
},
- generateUniqueId: function() {
- return new Date().getTime();
+ save_to_db: function(){
+ if (!this.init_locked) {
+ this.pos.db.save_unpaid_order(this);
+ }
+ },
+ init_from_JSON: function(json) {
+ this.sequence_number = json.sequence_number;
+ this.pos.pos_session.sequence_number = Math.max(this.sequence_number+1,this.pos.pos_session.sequence_number);
+ this.session_id = json.pos_session_id;
+ this.uid = json.uid;
+ this.name = _t("Order ") + this.uid;
+ if (json.partner_id) {
+ var client = this.pos.db.get_partner_by_id(json.partner_id);
+ if (!client) {
+ console.error('ERROR: trying to load a parner not available in the pos');
+ }
+ } else {
+ var client = null;
+ }
+ this.set_client(client);
+
+ this.temporary = false; // FIXME
+ this.to_invoice = false; // FIXME
+
+ var orderlines = json.lines;
+ for (var i = 0; i < orderlines.length; i++) {
+ var orderline = orderlines[i][2];
+ this.add_orderline(new module.Orderline({}, {pos: this.pos, order: this, json: orderline}));
+ }
+
+ var paymentlines = json.statement_ids;
+ for (var i = 0; i < paymentlines.length; i++) {
+ var paymentline = paymentlines[i][2];
+ var newpaymentline = new module.Paymentline({},{pos: this.pos, json: paymentline});
+ this.paymentlines.add(newpaymentline);
+
+ if (i === paymentlines.length - 1) {
+ this.select_paymentline(newpaymentline);
+ }
+ }
},
- addProduct: function(product, options){
+ export_as_JSON: function() {
+ var orderLines, paymentLines;
+ orderLines = [];
+ this.orderlines.each(_.bind( function(item) {
+ return orderLines.push([0, 0, item.export_as_JSON()]);
+ }, this));
+ paymentLines = [];
+ this.paymentlines.each(_.bind( function(item) {
+ return paymentLines.push([0, 0, item.export_as_JSON()]);
+ }, this));
+ return {
+ name: this.get_name(),
+ amount_paid: this.get_total_paid(),
+ amount_total: this.get_total_with_tax(),
+ amount_tax: this.get_total_tax(),
+ amount_return: this.get_change(),
+ lines: orderLines,
+ statement_ids: paymentLines,
+ pos_session_id: this.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,
+ sequence_number: this.sequence_number,
+ };
+ },
+ export_for_printing: function(){
+ var orderlines = [];
+ var self = this;
+
+ this.orderlines.each(function(orderline){
+ orderlines.push(orderline.export_for_printing());
+ });
+
+ var paymentlines = [];
+ this.paymentlines.each(function(paymentline){
+ paymentlines.push(paymentline.export_for_printing());
+ });
+ var client = this.get('client');
+ var cashier = this.pos.cashier || this.pos.user;
+ var company = this.pos.company;
+ var shop = this.pos.shop;
+ var date = new Date();
+
+ function is_xml(subreceipt){
+ return subreceipt ? (subreceipt.split('\n')[0].indexOf('<!DOCTYPE QWEB') >= 0) : false;
+ }
+
+ function render_xml(subreceipt){
+ if (!is_xml(subreceipt)) {
+ return subreceipt;
+ } else {
+ subreceipt = subreceipt.split('\n').slice(1).join('\n');
+ var qweb = new QWeb2.Engine();
+ qweb.debug = instance.session.debug;
+ qweb.default_dict = _.clone(QWeb.default_dict);
+ qweb.add_template('<templates><t t-name="subreceipt">'+subreceipt+'</t></templates>');
+
+ return qweb.render('subreceipt',{'pos':self.pos,'widget':self.pos.pos_widget,'order':self, 'receipt': receipt}) ;
+ }
+ }
+
+ var receipt = {
+ orderlines: orderlines,
+ paymentlines: paymentlines,
+ subtotal: this.get_subtotal(),
+ total_with_tax: this.get_total_with_tax(),
+ total_without_tax: this.get_total_without_tax(),
+ total_tax: this.get_total_tax(),
+ total_paid: this.get_total_paid(),
+ total_discount: this.get_total_discount(),
+ tax_details: this.get_tax_details(),
+ change: this.get_change(),
+ name : this.get_name(),
+ 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() ,
+ isostring: date.toISOString(),
+ localestring: date.toLocaleString(),
+ },
+ company:{
+ email: company.email,
+ website: company.website,
+ company_registry: company.company_registry,
+ 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.currency,
+ };
+
+ if (is_xml(this.pos.config.receipt_header)){
+ receipt.header_xml = render_xml(this.pos.config.receipt_header);
+ }
+
+ if (is_xml(this.pos.config.receipt_footer)){
+ receipt.footer_xml = render_xml(this.pos.config.receipt_footer);
+ }
+
+ return receipt;
+ },
+ is_empty: function(){
+ return this.orderlines.models.length === 0;
+ },
+ generate_unique_id: function() {
+ // Generates a public identification number for the order.
+ // The generated number must be unique and sequential. They are made 12 digit long
+ // to fit into EAN-13 barcodes, should it be needed
+
+ function zero_pad(num,size){
+ var s = ""+num;
+ while (s.length < size) {
+ s = "0" + s;
+ }
+ return s;
+ }
+ return zero_pad(this.pos.pos_session.id,5) +'-'+
+ zero_pad(this.pos.pos_session.login_number,3) +'-'+
+ zero_pad(this.sequence_number,4);
+ },
+ get_name: function() {
+ return this.name;
+ },
+ /* ---- Order Lines --- */
+ add_orderline: function(line){
+ if(line.order){
+ line.order.remove_orderline(line);
+ }
+ line.order = this;
+ this.orderlines.add(line);
+ this.select_orderline(this.get_last_orderline());
+ },
+ get_orderline: function(id){
+ var orderlines = this.orderlines.models;
+ for(var i = 0; i < orderlines.length; i++){
+ if(orderlines[i].id === id){
+ return orderlines[i];
+ }
+ }
+ return null;
+ },
+ get_orderlines: function(){
+ return this.orderlines.models;
+ },
+ get_last_orderline: function(){
+ return this.orderlines.at(this.orderlines.length -1);
+ },
+ remove_orderline: function( line ){
+ this.orderlines.remove(line);
+ this.select_orderline(this.get_last_orderline());
+ },
+ add_product: function(product, options){
options = options || {};
var attr = JSON.parse(JSON.stringify(product));
attr.pos = this.pos;
if(options.price !== undefined){
line.set_unit_price(options.price);
}
+ if(options.discount !== undefined){
+ line.set_discount(options.discount);
+ }
+
+ if(options.extras !== undefined){
+ for (var prop in options.extras) {
+ line[prop] = options.extras[prop];
+ }
+ }
- var last_orderline = this.getLastOrderline();
+ var last_orderline = this.get_last_orderline();
if( last_orderline && last_orderline.can_be_merged_with(line) && options.merge !== false){
last_orderline.merge(line);
}else{
- this.get('orderLines').add(line);
+ this.orderlines.add(line);
}
- this.selectLine(this.getLastOrderline());
+ this.select_orderline(this.get_last_orderline());
},
- removeOrderline: function( line ){
- this.get('orderLines').remove(line);
- this.selectLine(this.getLastOrderline());
+ get_selected_orderline: function(){
+ return this.selected_orderline;
},
- getLastOrderline: function(){
- return this.get('orderLines').at(this.get('orderLines').length -1);
+ select_orderline: function(line){
+ if(line){
+ if(line !== this.selected_orderline){
+ if(this.selected_orderline){
+ this.selected_orderline.set_selected(false);
+ }
+ this.selected_orderline = line;
+ this.selected_orderline.set_selected(true);
+ }
+ }else{
+ this.selected_orderline = undefined;
+ }
},
- addPaymentline: function(cashregister) {
- var paymentLines = this.get('paymentLines');
- var newPaymentline = new module.Paymentline({},{cashregister:cashregister});
- if(cashregister.journal.type !== 'cash'){
- newPaymentline.set_amount( Math.max(this.getDueLeft(),0) );
+ deselect_orderline: function(){
+ if(this.selected_orderline){
+ this.selected_orderline.set_selected(false);
+ this.selected_orderline = undefined;
+ }
+ },
+ /* ---- Payment Lines --- */
+ add_paymentline: function(cashregister) {
+ var newPaymentline = new module.Paymentline({},{cashregister:cashregister, pos: this.pos});
+ if(cashregister.journal.type !== 'cash' || this.pos.config.iface_precompute_cash){
+ newPaymentline.set_amount( Math.max(this.get_due(),0) );
}
- paymentLines.add(newPaymentline);
- this.selectPaymentline(newPaymentline);
+ this.paymentlines.add(newPaymentline);
+ this.select_paymentline(newPaymentline);
},
- removePaymentline: function(line){
+ get_paymentlines: function(){
+ return this.paymentlines.models;
+ },
+ remove_paymentline: function(line){
if(this.selected_paymentline === line){
- this.selectPaymentline(undefined);
+ this.select_paymentline(undefined);
+ }
+ this.paymentlines.remove(line);
+ },
+ clean_empty_paymentlines: function() {
+ var lines = this.paymentlines.models;
+ var empty = [];
+ for ( var i = 0; i < lines.length; i++) {
+ if (!lines[i].get_amount()) {
+ empty.push(lines[i]);
+ }
+ }
+ for ( var i = 0; i < empty.length; i++) {
+ this.remove_paymentline(empty[i]);
}
- this.get('paymentLines').remove(line);
},
- getName: function() {
- return this.get('name');
+ select_paymentline: 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);
+ }
},
- getSubtotal : function(){
- return (this.get('orderLines')).reduce((function(sum, orderLine){
+ /* ---- Payment Status --- */
+ get_subtotal : function(){
+ return this.orderlines.reduce((function(sum, orderLine){
return sum + orderLine.get_display_price();
}), 0);
},
- getTotalTaxIncluded: function() {
- return (this.get('orderLines')).reduce((function(sum, orderLine) {
+ get_total_with_tax: function() {
+ return this.orderlines.reduce((function(sum, orderLine) {
return sum + orderLine.get_price_with_tax();
}), 0);
},
- getDiscountTotal: function() {
- return (this.get('orderLines')).reduce((function(sum, orderLine) {
- return sum + (orderLine.get_unit_price() * (orderLine.get_discount()/100) * orderLine.get_quantity());
+ get_total_without_tax: function() {
+ return this.orderlines.reduce((function(sum, orderLine) {
+ return sum + orderLine.get_price_without_tax();
}), 0);
},
- getTotalTaxExcluded: function() {
- return (this.get('orderLines')).reduce((function(sum, orderLine) {
- return sum + orderLine.get_price_without_tax();
+ get_total_discount: function() {
+ return this.orderlines.reduce((function(sum, orderLine) {
+ return sum + (orderLine.get_unit_price() * (orderLine.get_discount()/100) * orderLine.get_quantity());
}), 0);
},
- getTax: function() {
- return (this.get('orderLines')).reduce((function(sum, orderLine) {
+ get_total_tax: function() {
+ return this.orderlines.reduce((function(sum, orderLine) {
return sum + orderLine.get_tax();
}), 0);
},
- getTaxDetails: function(){
+ get_total_paid: function() {
+ return this.paymentlines.reduce((function(sum, paymentLine) {
+ return sum + paymentLine.get_amount();
+ }), 0);
+ },
+ get_tax_details: function(){
var details = {};
var fulldetails = [];
var taxes_by_id = {};
taxes_by_id[this.pos.taxes[i].id] = this.pos.taxes[i];
}
- this.get('orderLines').each(function(line){
+ this.orderlines.each(function(line){
var ldetails = line.get_tax_details();
for(var id in ldetails){
if(ldetails.hasOwnProperty(id)){
for(var id in details){
if(details.hasOwnProperty(id)){
- fulldetails.push({amount: details[id], tax: taxes_by_id[id]});
+ fulldetails.push({amount: details[id], tax: taxes_by_id[id], name: taxes_by_id[id].name});
}
}
return fulldetails;
},
- getPaidTotal: function() {
- return (this.get('paymentLines')).reduce((function(sum, paymentLine) {
- return sum + paymentLine.get_amount();
- }), 0);
+ // Returns a total only for the orderlines with products belonging to the category
+ get_total_for_category_with_tax: function(categ_id){
+ var total = 0;
+ var self = this;
+
+ if (categ_id instanceof Array) {
+ for (var i = 0; i < categ_id.length; i++) {
+ total += this.get_total_for_category_with_tax(categ_id[i]);
+ }
+ return total;
+ }
+
+ this.orderlines.each(function(line){
+ if ( self.pos.db.category_contains(categ_id,line.product.id) ) {
+ total += line.get_price_with_tax();
+ }
+ });
+
+ return total;
},
- getChange: function() {
- return this.getPaidTotal() - this.getTotalTaxIncluded();
+ get_total_for_taxes: function(tax_id){
+ var total = 0;
+ var self = this;
+
+ if (!(tax_id instanceof Array)) {
+ tax_id = [tax_id];
+ }
+
+ var tax_set = {};
+
+ for (var i = 0; i < tax_id.length; i++) {
+ tax_set[tax_id[i]] = true;
+ }
+
+ this.orderlines.each(function(line){
+ var taxes_ids = line.get_product().taxes_id;
+ for (var i = 0; i < taxes_ids.length; i++) {
+ if (tax_set[taxes_ids[i]]) {
+ total += line.get_price_with_tax();
+ return;
+ }
+ }
+ });
+
+ return total;
},
- getDueLeft: function() {
- return this.getTotalTaxIncluded() - this.getPaidTotal();
+ get_change: function(paymentline) {
+ if (!paymentline) {
+ var change = this.get_total_paid() - this.get_total_with_tax();
+ } else {
+ var change = -this.get_total_with_tax();
+ var lines = this.paymentlines.models;
+ for (var i = 0; i < lines.length; i++) {
+ change += lines[i].get_amount();
+ if (lines[i] === paymentline) {
+ break;
+ }
+ }
+ }
+ return round_pr(Math.max(0,change), this.pos.currency.rounding);
},
- // sets the type of receipt 'receipt'(default) or 'invoice'
- set_receipt_type: function(type){
- this.receipt_type = type;
+ get_due: function(paymentline) {
+ if (!paymentline) {
+ var due = this.get_total_with_tax() - this.get_total_paid();
+ } else {
+ var due = this.get_total_with_tax();
+ var lines = this.paymentlines.models;
+ for (var i = 0; i < lines.length; i++) {
+ if (lines[i] === paymentline) {
+ break;
+ } else {
+ due -= lines[i].get_amount();
+ }
+ }
+ }
+ return round_pr(Math.max(0,due), this.pos.currency.rounding);
+ },
+ is_paid: function(){
+ return this.get_due() === 0;
+ },
+ is_paid_with_cash: function(){
+ return !!this.paymentlines.find( function(pl){
+ return pl.cashregister.journal.type === 'cash';
+ });
},
- get_receipt_type: function(){
- return this.receipt_type;
+ finalize: function(){
+ this.destroy();
},
+ destroy: function(args){
+ Backbone.Model.prototype.destroy.apply(this,arguments);
+ this.pos.db.remove_unpaid_order(this);
+ },
+ /* ---- Invoice --- */
+ set_to_invoice: function(to_invoice) {
+ this.to_invoice = to_invoice;
+ },
+ is_to_invoice: function(){
+ return this.to_invoice;
+ },
+ /* ---- Client / Customer --- */
// the client related to the current order.
set_client: function(client){
this.set('client',client);
var client = this.get('client');
return client ? client.name : "";
},
+ /* ---- Screen Status --- */
// the order also stores the screen status, as the PoS supports
// different active screens per order. This method is used to
// store the screen status.
if(arguments.length === 2){
this.screen_data[key] = value;
}else if(arguments.length === 1){
- for(key in arguments[0]){
+ for(var key in arguments[0]){
this.screen_data[key] = arguments[0][key];
}
}
get_screen_data: function(key){
return this.screen_data[key];
},
- // exports a JSON for receipt printing
- export_for_printing: function(){
- var orderlines = [];
- this.get('orderLines').each(function(orderline){
- orderlines.push(orderline.export_for_printing());
- });
-
- var paymentlines = [];
- this.get('paymentLines').each(function(paymentline){
- paymentlines.push(paymentline.export_for_printing());
- });
- var client = this.get('client');
- var cashier = this.pos.cashier || this.pos.user;
- var company = this.pos.company;
- var date = new Date();
-
- return {
- orderlines: orderlines,
- paymentlines: paymentlines,
- subtotal: this.getSubtotal(),
- total_with_tax: this.getTotalTaxIncluded(),
- total_without_tax: this.getTotalTaxExcluded(),
- 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() ,
- isostring: date.toISOString(),
- localestring: date.toLocaleString(),
- },
- company:{
- email: company.email,
- website: company.website,
- company_registry: company.company_registry,
- contact_address: company.partner_id[1],
- vat: company.vat,
- name: company.name,
- phone: company.phone,
- logo: this.pos.company_logo_base64,
- },
- currency: this.pos.currency,
- };
- },
- export_as_JSON: function() {
- var orderLines, paymentLines;
- orderLines = [];
- (this.get('orderLines')).each(_.bind( function(item) {
- return orderLines.push([0, 0, item.export_as_JSON()]);
- }, this));
- paymentLines = [];
- (this.get('paymentLines')).each(_.bind( function(item) {
- return paymentLines.push([0, 0, item.export_as_JSON()]);
- }, this));
- return {
- name: this.getName(),
- amount_paid: this.getPaidTotal(),
- amount_total: this.getTotalTaxIncluded(),
- amount_tax: this.getTax(),
- amount_return: this.getChange(),
- lines: orderLines,
- statement_ids: paymentLines,
- 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(){
- return this.selected_orderline;
- },
- selectLine: function(line){
- if(line){
- if(line !== this.selected_orderline){
- if(this.selected_orderline){
- this.selected_orderline.set_selected(false);
- }
- this.selected_orderline = line;
- this.selected_orderline.set_selected(true);
- }
- }else{
- 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({
}
},
switchSign: function() {
- console.log('switchsing');
var oldBuffer;
oldBuffer = this.get('buffer');
this.set({