2012-05-14 14:03:40 +00:00
|
|
|
function openerp_pos_models(instance, module){ //module is instance.point_of_sale
|
2012-04-24 16:25:46 +00:00
|
|
|
var QWeb = instance.web.qweb;
|
|
|
|
|
|
|
|
module.LocalStorageDAO = instance.web.Class.extend({
|
|
|
|
add_operation: function(operation) {
|
|
|
|
var self = this;
|
|
|
|
return $.async_when().pipe(function() {
|
|
|
|
var tmp = self._get('oe_pos_operations', []);
|
|
|
|
var last_id = self._get('oe_pos_operations_sequence', 1);
|
|
|
|
tmp.push({'id': last_id, 'data': operation});
|
|
|
|
self._set('oe_pos_operations', tmp);
|
|
|
|
self._set('oe_pos_operations_sequence', last_id + 1);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
remove_operation: function(id) {
|
|
|
|
var self = this;
|
|
|
|
return $.async_when().pipe(function() {
|
|
|
|
var tmp = self._get('oe_pos_operations', []);
|
|
|
|
tmp = _.filter(tmp, function(el) {
|
|
|
|
return el.id !== id;
|
|
|
|
});
|
|
|
|
self._set('oe_pos_operations', tmp);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
get_operations: function() {
|
|
|
|
var self = this;
|
|
|
|
return $.async_when().pipe(function() {
|
|
|
|
return self._get('oe_pos_operations', []);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
_get: function(key, default_) {
|
|
|
|
var txt = localStorage[key];
|
|
|
|
if (! txt)
|
|
|
|
return default_;
|
|
|
|
return JSON.parse(txt);
|
|
|
|
},
|
|
|
|
_set: function(key, value) {
|
|
|
|
localStorage[key] = JSON.stringify(value);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
var fetch = function(osvModel, fields, domain){
|
|
|
|
var dataSetSearch = new instance.web.DataSetSearch(null, osvModel, {}, domain);
|
|
|
|
return dataSetSearch.read_slice(fields, 0);
|
|
|
|
};
|
|
|
|
|
2012-05-21 14:09:01 +00:00
|
|
|
// The PosModel contains the Point Of Sale's representation of the backend.
|
|
|
|
// Since the PoS must work in standalone ( Without connection to the server )
|
|
|
|
// it must contains a representation of the server's PoS backend.
|
|
|
|
// (taxes, product list, configuration options, etc.) this representation
|
|
|
|
// is fetched and stored by the PosModel at the initialisation.
|
|
|
|
// this is done asynchronously, a ready deferred alows the GUI to wait interactively
|
|
|
|
// for the loading to be completed
|
|
|
|
// There is a single instance of the PosModel for each Front-End instance, it is usually called
|
|
|
|
// 'pos' and is available to almost all widgets.
|
|
|
|
|
2012-04-24 16:25:46 +00:00
|
|
|
module.PosModel = Backbone.Model.extend({
|
|
|
|
initialize: function(session, attributes) {
|
|
|
|
Backbone.Model.prototype.initialize.call(this, attributes);
|
|
|
|
var self = this;
|
2012-05-21 14:09:01 +00:00
|
|
|
this.dao = new module.LocalStorageDAO(); // used to store the order's data on the Hard Drive
|
|
|
|
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.build_tree = _.bind(this.build_tree, this); // ???
|
|
|
|
this.session = session;
|
2012-04-24 16:25:46 +00:00
|
|
|
this.categories = {};
|
2012-05-29 13:31:41 +00:00
|
|
|
this.root_category = null;
|
2012-05-21 14:09:01 +00:00
|
|
|
this.barcode_reader = new module.BarcodeReader({'pos': this}); // used to read barcodes
|
2012-05-23 12:47:54 +00:00
|
|
|
this.proxy = new module.ProxyDevice(); // used to communicate to the hardware devices via a local proxy
|
2012-05-21 14:09:01 +00:00
|
|
|
|
2012-05-29 13:31:41 +00:00
|
|
|
// pos settings
|
|
|
|
this.use_scale = false;
|
|
|
|
this.use_proxy_printer = false;
|
|
|
|
this.use_virtual_keyboard = false;
|
|
|
|
this.use_websql = false;
|
|
|
|
this.use_barcode_scanner = false;
|
|
|
|
|
2012-05-21 14:09:01 +00:00
|
|
|
// default attributes values. If null, it will be loaded below.
|
2012-04-24 16:25:46 +00:00
|
|
|
this.set({
|
2012-05-21 14:09:01 +00:00
|
|
|
'nbr_pending_operations': 0,
|
|
|
|
|
|
|
|
'currency': {symbol: '$', position: 'after'},
|
|
|
|
'shop': null,
|
|
|
|
'company': null,
|
|
|
|
'user': null,
|
|
|
|
|
|
|
|
'orders': new module.OrderCollection(),
|
|
|
|
'products': new module.ProductCollection(),
|
|
|
|
'cashRegisters': null,
|
|
|
|
|
|
|
|
'product_list': null,
|
|
|
|
'bank_statements': null,
|
|
|
|
'taxes': null,
|
|
|
|
'pos_session': null,
|
|
|
|
'pos_config': null,
|
|
|
|
'categories': null,
|
|
|
|
|
|
|
|
'selectedOrder': undefined,
|
2012-04-24 16:25:46 +00:00
|
|
|
});
|
2012-05-21 14:09:01 +00:00
|
|
|
|
|
|
|
this.get('orders').bind('remove', _.bind( this.on_removed_order, this ) );
|
2012-04-24 16:25:46 +00:00
|
|
|
|
2012-05-21 14:09:01 +00:00
|
|
|
// We fetch the backend data on the server asynchronously
|
|
|
|
|
2012-05-24 15:33:15 +00:00
|
|
|
var cat_def = fetch('pos.category', ['id','name', 'parent_id', 'child_id', 'to_weight'])
|
2012-04-24 16:25:46 +00:00
|
|
|
.pipe(function(result){
|
|
|
|
return self.set({'categories': result});
|
|
|
|
});
|
|
|
|
|
|
|
|
var prod_def = fetch(
|
|
|
|
'product.product',
|
2012-05-08 15:49:42 +00:00
|
|
|
['name', 'list_price', 'pos_categ_id', 'taxes_id','product_image_small', 'ean13'],
|
2012-05-05 16:12:52 +00:00
|
|
|
[['pos_categ_id','!=', false]]
|
2012-04-24 16:25:46 +00:00
|
|
|
).then(function(result){
|
2012-05-21 14:09:01 +00:00
|
|
|
self.set({'product_list': result});
|
2012-04-24 16:25:46 +00:00
|
|
|
});
|
|
|
|
|
2012-05-24 15:33:15 +00:00
|
|
|
// associate the products with their categories
|
|
|
|
var prod_process_def = $.when(cat_def, prod_def)
|
|
|
|
.pipe(function(){
|
|
|
|
var product_list = self.get('product_list');
|
|
|
|
var categories = self.get('categories');
|
|
|
|
var cat_by_id = {};
|
|
|
|
for(var i = 0; i < categories.length; i++){
|
|
|
|
cat_by_id[categories[i].id] = categories[i];
|
|
|
|
}
|
|
|
|
//set the parent in the category
|
|
|
|
for(var i = 0; i < categories.length; i++){
|
|
|
|
categories[i].parent_category = cat_by_id[categories[i].parent_id[0]];
|
|
|
|
}
|
|
|
|
for(var i = 0; i < product_list.length; i++){
|
|
|
|
product_list[i].pos_category = cat_by_id[product_list[i].pos_categ_id[0]];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2012-05-21 14:09:01 +00:00
|
|
|
var tax_def = fetch('account.tax', ['amount','price_include','type'])
|
|
|
|
.then(function(result){
|
|
|
|
self.set({'taxes': result});
|
|
|
|
});
|
|
|
|
|
|
|
|
var session_def = fetch( // loading the PoS Session.
|
2012-05-08 15:28:05 +00:00
|
|
|
'pos.session',
|
2012-05-22 12:53:32 +00:00
|
|
|
['id', 'journal_ids','name','user_id','config_id','start_at','stop_at'],
|
2012-05-08 15:28:05 +00:00
|
|
|
[['state', '=', 'opened'], ['user_id', '=', this.session.uid]]
|
2012-05-21 14:09:01 +00:00
|
|
|
).pipe(function(result) {
|
|
|
|
|
2012-05-22 12:53:32 +00:00
|
|
|
// some data are associated with the pos session, like the pos config and bank statements.
|
2012-05-21 14:09:01 +00:00
|
|
|
// we must have a valid session before we can read those.
|
|
|
|
|
2012-05-22 12:53:32 +00:00
|
|
|
var session_data_def = new $.Deferred();
|
2012-05-21 14:09:01 +00:00
|
|
|
|
2012-05-08 16:42:26 +00:00
|
|
|
if( result.length !== 0 ) {
|
2012-05-16 13:01:42 +00:00
|
|
|
var pos_session = result[0];
|
|
|
|
|
|
|
|
self.set({'pos_session': pos_session});
|
|
|
|
|
2012-05-22 12:53:32 +00:00
|
|
|
var pos_config_def = fetch(
|
2012-05-16 13:01:42 +00:00
|
|
|
'pos.config',
|
|
|
|
['name','journal_ids','shop_id','journal_id',
|
|
|
|
'iface_self_checkout', 'iface_websql', 'iface_led', 'iface_cashdrawer',
|
|
|
|
'iface_payment_terminal', 'iface_electronic_scale', 'iface_barscan', 'iface_vkeyboard',
|
|
|
|
'iface_print_via_proxy','state','sequence_id','session_ids'],
|
|
|
|
[['id','=', pos_session.config_id[0]]]
|
|
|
|
).then(function(result){
|
2012-05-21 14:09:01 +00:00
|
|
|
self.set({'pos_config': result[0]});
|
2012-05-29 13:31:41 +00:00
|
|
|
this.use_scale = result[0].iface_electronic_scale || false;
|
|
|
|
this.use_proxy_printer = result[0].iface_print_via_proxy || false;
|
|
|
|
this.use_virtual_keyboard = result[0].iface_vkeyboard || false;
|
|
|
|
this.use_websql = result[0].iface_websql || false;
|
|
|
|
this.use_barcode_scanner = result[0].iface_barscan || false;
|
|
|
|
this.use_selfcheckout = result[0].iface_self_checkout || false;
|
2012-05-16 13:01:42 +00:00
|
|
|
});
|
2012-05-22 12:53:32 +00:00
|
|
|
|
|
|
|
var bank_def = fetch(
|
|
|
|
'account.bank.statement',
|
2012-05-29 14:00:08 +00:00
|
|
|
['account_id','currency','journal_id','state','name','user_id','pos_session_id'],
|
|
|
|
[['state','=','open'],['pos_session_id', '=', pos_session.id]]
|
2012-05-22 12:53:32 +00:00
|
|
|
).then(function(result){
|
|
|
|
self.set({'bank_statements':result});
|
|
|
|
});
|
|
|
|
|
2012-05-24 10:22:57 +00:00
|
|
|
var journal_def = fetch(
|
|
|
|
'account.journal',
|
|
|
|
undefined,
|
|
|
|
[['user_id','=',pos_session.user_id[0]]]
|
|
|
|
).then(function(result){
|
|
|
|
self.set({'journals':result});
|
|
|
|
});
|
|
|
|
|
|
|
|
// associate the bank statements with their journals.
|
|
|
|
var bank_process_def = $.when(bank_def, journal_def)
|
|
|
|
.then(function(){
|
|
|
|
var bank_statements = self.get('bank_statements');
|
|
|
|
var journals = self.get('journals');
|
|
|
|
for(var i = 0, ilen = bank_statements.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];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
session_data_def = $.when(pos_config_def,bank_def,journal_def,bank_process_def);
|
2012-05-22 12:53:32 +00:00
|
|
|
|
2012-05-16 13:01:42 +00:00
|
|
|
}else{
|
2012-05-22 12:53:32 +00:00
|
|
|
session_data_def.reject();
|
2012-05-08 16:42:26 +00:00
|
|
|
}
|
2012-05-22 12:53:32 +00:00
|
|
|
return session_data_def;
|
2012-04-24 16:25:46 +00:00
|
|
|
});
|
|
|
|
|
2012-05-21 14:09:01 +00:00
|
|
|
// when all the data has loaded, we compute some stuff, and declare the Pos ready to be used.
|
2012-05-24 15:33:15 +00:00
|
|
|
$.when(cat_def, prod_def, session_def, tax_def, prod_process_def, this.get_app_data(), this.flush())
|
2012-05-21 14:09:01 +00:00
|
|
|
.then(function(){
|
|
|
|
self.build_tree();
|
2012-05-29 13:31:41 +00:00
|
|
|
self.build_categories();
|
2012-05-21 09:14:42 +00:00
|
|
|
self.set({'cashRegisters' : new module.CashRegisterCollection(self.get('bank_statements'))});
|
|
|
|
console.log('cashRegisters:',self.get('cashRegisters'));
|
2012-04-24 17:20:47 +00:00
|
|
|
self.ready.resolve();
|
2012-05-21 14:09:01 +00:00
|
|
|
self.log_loaded_data();
|
2012-05-25 12:15:39 +00:00
|
|
|
},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();
|
2012-04-24 17:20:47 +00:00
|
|
|
});
|
2012-05-21 14:09:01 +00:00
|
|
|
},
|
2012-04-24 17:20:47 +00:00
|
|
|
|
2012-05-21 14:09:01 +00:00
|
|
|
// 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: categories:',this.get('categories'));
|
|
|
|
console.log('PosModel: product_list:',this.get('product_list'));
|
|
|
|
console.log('PosModel: bank_statements:',this.get('bank_statements'));
|
2012-05-24 10:22:57 +00:00
|
|
|
console.log('PosModel: journals:',this.get('journals'));
|
2012-05-21 14:09:01 +00:00
|
|
|
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.session:',this.session);
|
|
|
|
console.log('PosModel.categories:',this.categories);
|
|
|
|
console.log('PosModel end of data log.');
|
2012-04-24 17:20:47 +00:00
|
|
|
|
2012-04-24 16:25:46 +00:00
|
|
|
},
|
2012-05-21 14:09:01 +00:00
|
|
|
|
|
|
|
// 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_and_select_order(new module.Order({ pos: this }));
|
|
|
|
}
|
|
|
|
if( this.get('selectedOrder') === removed_order){
|
|
|
|
this.set({ selectedOrder: this.get('orders').last() });
|
|
|
|
}
|
|
|
|
},
|
2012-04-24 16:25:46 +00:00
|
|
|
|
2012-05-21 14:09:01 +00:00
|
|
|
// load some data from the server, used in initialize
|
2012-04-24 16:25:46 +00:00
|
|
|
get_app_data: function() {
|
|
|
|
var self = this;
|
|
|
|
return $.when(new instance.web.Model("sale.shop").get_func("search_read")([]).pipe(function(result) {
|
|
|
|
self.set({'shop': result[0]});
|
|
|
|
var company_id = result[0]['company_id'][0];
|
|
|
|
return new instance.web.Model("res.company").get_func("read")(company_id, ['currency_id', 'name', 'phone']).pipe(function(result) {
|
|
|
|
self.set({'company': result});
|
|
|
|
var currency_id = result['currency_id'][0]
|
|
|
|
return new instance.web.Model("res.currency").get_func("read")([currency_id],
|
|
|
|
['symbol', 'position']).pipe(function(result) {
|
|
|
|
self.set({'currency': result[0]});
|
|
|
|
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}), new instance.web.Model("res.users").get_func("read")(this.session.uid, ['name']).pipe(function(result) {
|
|
|
|
self.set({'user': result});
|
|
|
|
}));
|
|
|
|
},
|
2012-05-21 14:09:01 +00:00
|
|
|
|
2012-04-24 16:25:46 +00:00
|
|
|
push_order: function(record) {
|
|
|
|
var self = this;
|
2012-05-23 16:02:55 +00:00
|
|
|
console.log('push_order',record);
|
2012-04-24 16:25:46 +00:00
|
|
|
return this.dao.add_operation(record).pipe(function(){
|
|
|
|
return self.flush();
|
|
|
|
});
|
|
|
|
},
|
2012-05-21 14:09:01 +00:00
|
|
|
|
|
|
|
add_and_select_order: function(newOrder) {
|
2012-04-24 17:20:47 +00:00
|
|
|
(this.get('orders')).add(newOrder);
|
|
|
|
return this.set({
|
|
|
|
selectedOrder: newOrder
|
|
|
|
});
|
|
|
|
},
|
2012-05-21 14:09:01 +00:00
|
|
|
|
|
|
|
// attemps to send all pending orders ( stored in the DAO ) to the server.
|
|
|
|
// it will do it one by one, and remove the successfully sent ones from the DAO once
|
|
|
|
// it has been confirmed that they have been received.
|
2012-04-24 16:25:46 +00:00
|
|
|
flush: function() {
|
2012-05-21 14:09:01 +00:00
|
|
|
//this makes sure only one _int_flush is called at the same time
|
2012-05-23 16:02:55 +00:00
|
|
|
console.log('flush operations');
|
2012-04-24 16:25:46 +00:00
|
|
|
return this.flush_mutex.exec(_.bind(function() {
|
|
|
|
return this._int_flush();
|
|
|
|
}, this));
|
|
|
|
},
|
|
|
|
_int_flush : function() {
|
|
|
|
var self = this;
|
2012-05-04 15:27:20 +00:00
|
|
|
|
2012-04-24 16:25:46 +00:00
|
|
|
this.dao.get_operations().pipe(function(operations) {
|
2012-05-24 10:22:57 +00:00
|
|
|
// operations are really Orders that are converted to json.
|
|
|
|
// they are saved to disk and then we attempt to send them to the backend so that they can
|
|
|
|
// be applied.
|
|
|
|
// since the network is not reliable we potentially have many 'pending operations' that have not been sent.
|
|
|
|
|
2012-04-24 16:25:46 +00:00
|
|
|
self.set( {'nbr_pending_operations':operations.length} );
|
|
|
|
if(operations.length === 0){
|
|
|
|
return $.when();
|
|
|
|
}
|
2012-04-25 14:55:55 +00:00
|
|
|
var op = operations[0];
|
2012-04-24 16:25:46 +00:00
|
|
|
|
|
|
|
// we prevent the default error handler and assume errors
|
|
|
|
// are a normal use case, except we stop the current iteration
|
|
|
|
|
2012-05-23 16:02:55 +00:00
|
|
|
return (new instance.web.Model('pos.order')).get_func('create_from_ui')([op])
|
2012-04-24 16:25:46 +00:00
|
|
|
.fail(function(unused, event){
|
2012-05-24 10:22:57 +00:00
|
|
|
// wtf ask niv
|
2012-04-24 16:25:46 +00:00
|
|
|
event.preventDefault();
|
|
|
|
})
|
|
|
|
.pipe(function(){
|
2012-05-24 10:22:57 +00:00
|
|
|
// success: remove the successfully sent operation, and try to send the next one
|
2012-04-25 14:55:55 +00:00
|
|
|
self.dao.remove_operation(operations[0].id).pipe(function(){
|
2012-04-24 16:25:46 +00:00
|
|
|
return self._int_flush();
|
|
|
|
});
|
|
|
|
}, function(){
|
2012-05-24 10:22:57 +00:00
|
|
|
// in case of error we just sit there and do nothing. wtf ask niv
|
2012-04-24 16:25:46 +00:00
|
|
|
return $.when();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
2012-05-29 13:31:41 +00:00
|
|
|
build_categories : function(){
|
2012-05-25 12:15:39 +00:00
|
|
|
var categories = this.get('categories');
|
2012-05-29 13:31:41 +00:00
|
|
|
var products = this.get('product_list');
|
|
|
|
|
|
|
|
//append the content of array2 into array1
|
|
|
|
function append(array1, array2){
|
|
|
|
for(var i = 0, len = array2.length; i < len; i++){
|
|
|
|
array1.push(array2[i]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function appendSet(set1, set2){
|
|
|
|
for(key in set2){
|
|
|
|
set1[key] = set2[key];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var categories_by_id = {};
|
|
|
|
for(var i = 0; i < categories.length; i++){
|
|
|
|
categories_by_id[categories[i].id] = categories[i];
|
|
|
|
}
|
|
|
|
|
|
|
|
var root_category = {
|
|
|
|
name : 'Root',
|
|
|
|
id : 0,
|
|
|
|
parent : null,
|
|
|
|
childrens : [],
|
|
|
|
};
|
|
|
|
|
|
|
|
// add parent and childrens field to categories, find root_categories
|
|
|
|
for(var i = 0; i < categories.length; i++){
|
|
|
|
var cat = categories[i];
|
|
|
|
|
|
|
|
cat.parent = categories_by_id[cat.parent_id[0]];
|
|
|
|
if(!cat.parent){
|
|
|
|
root_category.childrens.push(cat);
|
|
|
|
cat.parent = root_category;
|
|
|
|
}
|
|
|
|
|
|
|
|
cat.childrens = [];
|
|
|
|
for(var j = 0; j < cat.child_id.length; j++){
|
|
|
|
cat.childrens.push(categories_by_id[ cat.child_id[j] ]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
categories.push(root_category);
|
|
|
|
|
|
|
|
// set some default fields for next steps
|
|
|
|
for(var i = 0; i < categories.length; i++){
|
|
|
|
var cat = categories[i];
|
|
|
|
|
|
|
|
cat.product_list = []; //list of all products in the category
|
|
|
|
cat.product_set = {}; // [product.id] === true if product is in category
|
|
|
|
cat.weightable_product_list = [];
|
|
|
|
cat.weightable_product_set = {};
|
|
|
|
cat.regular_product_list = []; //not weightable
|
|
|
|
cat.regular_product_set = {};
|
|
|
|
cat.progeny = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
this.root_category = root_category;
|
|
|
|
|
|
|
|
console.log('categories:',categories);
|
|
|
|
|
|
|
|
//we add the products to the categories.
|
|
|
|
for(var i = 0, len = products.length; i < len; i++){
|
|
|
|
var product = products[i];
|
|
|
|
var cat = categories_by_id[product.pos_categ_id[0]];
|
|
|
|
if(cat){
|
|
|
|
cat.product_list.push(product);
|
|
|
|
cat.product_set[product.id] = true;
|
|
|
|
if(product.weightable){
|
|
|
|
cat.weightable_product_list.push(product);
|
|
|
|
cat.weightable_product_set[product.id] = true;
|
|
|
|
}else{
|
|
|
|
cat.regular_product_list.push(product);
|
|
|
|
cat.regular_product_set[product.id] = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// add ancestor field to categories, contains the list of parents of parents, from root to parent
|
|
|
|
function make_ancestors(cat, ancestors){
|
|
|
|
cat.ancestors = ancestors.slice(0);
|
|
|
|
ancestors.push(cat);
|
|
|
|
|
|
|
|
for(var i = 0; i < cat.childrens.length; i++){
|
|
|
|
make_ancestors(cat.childrens[i], ancestors.slice(0));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// add progeny field to categories, contains all subcategories of a category
|
|
|
|
function make_progeny(cat){
|
|
|
|
for(var i = 0; i < cat.childrens.length; i++){
|
|
|
|
make_progeny(cat.childrens[i]);
|
|
|
|
cat.progeny.push(cat.childrens[i]);
|
|
|
|
append(cat.progeny,cat.childrens[i].progeny);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//add the products of the subcategories to the parent categories
|
|
|
|
function make_products(cat){
|
|
|
|
for(var i = 0; i < cat.childrens.length; i++){
|
|
|
|
make_products(cat.childrens[i]);
|
|
|
|
|
|
|
|
append(cat.product_list, cat.childrens[i].product_list);
|
|
|
|
append(cat.weightable_product_list, cat.childrens[i].weightable_product_list);
|
|
|
|
append(cat.regular_product_list, cat.childrens[i].regular_product_list);
|
|
|
|
|
|
|
|
appendSet(cat.product_set, cat.childrens[i].product_set);
|
|
|
|
appendSet(cat.weightable_product_set, cat.childrens[i].weightable_product_set);
|
|
|
|
appendSet(cat.regular_product_set, cat.childrens[i].regular_product_set);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
make_ancestors(root_category,[]);
|
|
|
|
make_progeny(root_category);
|
|
|
|
make_products(root_category);
|
|
|
|
},
|
|
|
|
|
2012-04-24 16:25:46 +00:00
|
|
|
build_tree: function() {
|
|
|
|
var c, id, _i, _len, _ref, _ref2;
|
|
|
|
_ref = this.get('categories');
|
|
|
|
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
|
|
|
c = _ref[_i];
|
|
|
|
this.categories[c.id] = {
|
|
|
|
id: c.id,
|
|
|
|
name: c.name,
|
|
|
|
children: c.child_id,
|
|
|
|
parent: c.parent_id[0],
|
|
|
|
ancestors: [c.id],
|
|
|
|
subtree: [c.id]
|
|
|
|
};
|
|
|
|
}
|
|
|
|
_ref2 = this.categories;
|
|
|
|
for (id in _ref2) {
|
|
|
|
c = _ref2[id];
|
|
|
|
this.current_category = c;
|
|
|
|
this.build_ancestors(c.parent);
|
|
|
|
this.build_subtree(c);
|
|
|
|
}
|
|
|
|
this.categories[0] = {
|
|
|
|
ancestors: [],
|
|
|
|
children: (function() {
|
|
|
|
var _j, _len2, _ref3, _results;
|
|
|
|
_ref3 = this.get('categories');
|
|
|
|
_results = [];
|
|
|
|
for (_j = 0, _len2 = _ref3.length; _j < _len2; _j++) {
|
|
|
|
c = _ref3[_j];
|
|
|
|
if (!(c.parent_id[0] != null)) {
|
|
|
|
_results.push(c.id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return _results;
|
|
|
|
}).call(this),
|
|
|
|
subtree: (function() {
|
|
|
|
var _j, _len2, _ref3, _results;
|
|
|
|
_ref3 = this.get('categories');
|
|
|
|
_results = [];
|
|
|
|
for (_j = 0, _len2 = _ref3.length; _j < _len2; _j++) {
|
|
|
|
c = _ref3[_j];
|
|
|
|
_results.push(c.id);
|
|
|
|
}
|
|
|
|
return _results;
|
|
|
|
}).call(this)
|
|
|
|
};
|
|
|
|
},
|
|
|
|
build_ancestors: function(parent) {
|
|
|
|
if (parent != null) {
|
|
|
|
this.current_category.ancestors.unshift(parent);
|
|
|
|
return this.build_ancestors(this.categories[parent].parent);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
build_subtree: function(category) {
|
|
|
|
var c, _i, _len, _ref, _results;
|
|
|
|
_ref = category.children;
|
|
|
|
_results = [];
|
|
|
|
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
|
|
|
c = _ref[_i];
|
|
|
|
this.current_category.subtree.push(c);
|
|
|
|
_results.push(this.build_subtree(this.categories[c]));
|
|
|
|
}
|
|
|
|
return _results;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2012-05-16 13:01:42 +00:00
|
|
|
module.CashRegister = Backbone.Model.extend({
|
|
|
|
});
|
|
|
|
|
|
|
|
module.CashRegisterCollection = Backbone.Collection.extend({
|
|
|
|
model: module.CashRegister,
|
|
|
|
});
|
|
|
|
|
2012-04-24 16:25:46 +00:00
|
|
|
module.Product = Backbone.Model.extend({
|
|
|
|
});
|
|
|
|
|
|
|
|
module.ProductCollection = Backbone.Collection.extend({
|
|
|
|
model: module.Product,
|
|
|
|
});
|
|
|
|
|
2012-05-21 14:09:01 +00:00
|
|
|
// 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.
|
|
|
|
// Currently there is a limitation in that there can only be once orderline by type
|
|
|
|
// of product, but this will be subject to changes TODO
|
|
|
|
//
|
|
|
|
// An Order contains zero or more Orderlines.
|
2012-04-24 16:25:46 +00:00
|
|
|
module.Orderline = Backbone.Model.extend({
|
|
|
|
defaults: {
|
|
|
|
quantity: 1,
|
|
|
|
list_price: 0,
|
2012-05-06 21:34:39 +00:00
|
|
|
discount: 0,
|
|
|
|
weighted: false,
|
2012-05-22 14:59:56 +00:00
|
|
|
product_type: 'unit',
|
2012-04-24 16:25:46 +00:00
|
|
|
},
|
|
|
|
initialize: function(attributes) {
|
2012-04-24 17:20:47 +00:00
|
|
|
this.pos = attributes.pos;
|
2012-04-24 16:25:46 +00:00
|
|
|
Backbone.Model.prototype.initialize.apply(this, arguments);
|
2012-05-06 21:34:39 +00:00
|
|
|
|
|
|
|
if(attributes.weight){
|
|
|
|
this.setWeight(attributes.weight);
|
|
|
|
this.set({weighted: true});
|
2012-05-22 14:59:56 +00:00
|
|
|
this.set({product_type: 'weight'});
|
2012-05-06 21:34:39 +00:00
|
|
|
}
|
|
|
|
|
2012-04-24 16:25:46 +00:00
|
|
|
this.bind('change:quantity', function(unused, qty) {
|
|
|
|
if (qty == 0)
|
|
|
|
this.trigger('killme');
|
|
|
|
}, this);
|
|
|
|
},
|
2012-05-22 14:59:56 +00:00
|
|
|
|
|
|
|
// 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('id') !== orderline.get('id')){ //only orderline of the same product can be merged
|
|
|
|
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 false;
|
|
|
|
}else if(this.get('product_type') === 'unit'){
|
|
|
|
return true;
|
|
|
|
}else if(this.get('product_type') === 'weight'){
|
|
|
|
return true;
|
|
|
|
}else if(this.get('product_type') === 'price'){
|
|
|
|
return this.get('list_price') === orderline.get('list_price');
|
|
|
|
}else{
|
|
|
|
console.error('point_of_sale/pos_models.js/Orderline.can_be_merged_with() : unknown product type:',this.get('product_type'));
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
// Modifies this orderline so that it also contains the contents of another orderline.
|
|
|
|
// the two orderlines must be mergable ('can_be_merged_with()' === true)
|
|
|
|
merge: function(orderline){
|
|
|
|
this.set({quantity : this.get('quantity') + orderline.get('quantity') });
|
|
|
|
},
|
2012-05-06 21:34:39 +00:00
|
|
|
setWeight: function(weight){
|
|
|
|
return this.set({
|
|
|
|
quantity: weight,
|
|
|
|
});
|
|
|
|
},
|
2012-04-24 16:25:46 +00:00
|
|
|
incrementQuantity: function() {
|
|
|
|
return this.set({
|
|
|
|
quantity: (this.get('quantity')) + 1
|
|
|
|
});
|
|
|
|
},
|
2012-05-06 21:34:39 +00:00
|
|
|
incrementWeight: function(weight){
|
|
|
|
return this.set({
|
|
|
|
quantity: (this.get('quantity')) + weight,
|
|
|
|
});
|
|
|
|
},
|
2012-05-11 16:02:23 +00:00
|
|
|
set_discount: function(discount){
|
|
|
|
this.set({'discount': discount});
|
|
|
|
},
|
2012-04-24 16:25:46 +00:00
|
|
|
getPriceWithoutTax: function() {
|
|
|
|
return this.getAllPrices().priceWithoutTax;
|
|
|
|
},
|
|
|
|
getPriceWithTax: function() {
|
|
|
|
return this.getAllPrices().priceWithTax;
|
|
|
|
},
|
|
|
|
getTax: function() {
|
|
|
|
return this.getAllPrices().tax;
|
|
|
|
},
|
|
|
|
getAllPrices: function() {
|
|
|
|
var self = this;
|
|
|
|
var base = (this.get('quantity')) * (this.get('list_price')) * (1 - (this.get('discount')) / 100);
|
|
|
|
var totalTax = base;
|
|
|
|
var totalNoTax = base;
|
|
|
|
|
2012-04-24 17:20:47 +00:00
|
|
|
var product_list = self.pos.get('product_list');
|
2012-04-24 16:25:46 +00:00
|
|
|
var product = _.detect(product_list, function(el) {return el.id === self.get('id');});
|
|
|
|
var taxes_ids = product.taxes_id;
|
2012-04-24 17:20:47 +00:00
|
|
|
var taxes = self.pos.get('taxes');
|
2012-04-24 16:25:46 +00:00
|
|
|
var taxtotal = 0;
|
|
|
|
_.each(taxes_ids, function(el) {
|
|
|
|
var tax = _.detect(taxes, function(t) {return t.id === el;});
|
|
|
|
if (tax.price_include) {
|
|
|
|
var tmp;
|
|
|
|
if (tax.type === "percent") {
|
|
|
|
tmp = base - (base / (1 + tax.amount));
|
|
|
|
} else if (tax.type === "fixed") {
|
|
|
|
tmp = tax.amount * self.get('quantity');
|
|
|
|
} else {
|
|
|
|
throw "This type of tax is not supported by the point of sale: " + tax.type;
|
|
|
|
}
|
|
|
|
taxtotal += tmp;
|
|
|
|
totalNoTax -= tmp;
|
|
|
|
} else {
|
|
|
|
var tmp;
|
|
|
|
if (tax.type === "percent") {
|
|
|
|
tmp = tax.amount * base;
|
|
|
|
} else if (tax.type === "fixed") {
|
|
|
|
tmp = tax.amount * self.get('quantity');
|
|
|
|
} else {
|
|
|
|
throw "This type of tax is not supported by the point of sale: " + tax.type;
|
|
|
|
}
|
|
|
|
taxtotal += tmp;
|
|
|
|
totalTax += tmp;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return {
|
|
|
|
"priceWithTax": totalTax,
|
|
|
|
"priceWithoutTax": totalNoTax,
|
|
|
|
"tax": taxtotal,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
exportAsJSON: function() {
|
|
|
|
return {
|
|
|
|
qty: this.get('quantity'),
|
|
|
|
price_unit: this.get('list_price'),
|
|
|
|
discount: this.get('discount'),
|
|
|
|
product_id: this.get('id')
|
|
|
|
};
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
module.OrderlineCollection = Backbone.Collection.extend({
|
|
|
|
model: module.Orderline,
|
|
|
|
});
|
|
|
|
|
|
|
|
// Every PaymentLine has all the attributes of the corresponding CashRegister.
|
|
|
|
module.Paymentline = Backbone.Model.extend({
|
|
|
|
defaults: {
|
|
|
|
amount: 0,
|
|
|
|
},
|
|
|
|
initialize: function(attributes) {
|
|
|
|
Backbone.Model.prototype.initialize.apply(this, arguments);
|
|
|
|
},
|
|
|
|
getAmount: function(){
|
2012-05-21 09:14:42 +00:00
|
|
|
return this.get('amount');
|
2012-04-24 16:25:46 +00:00
|
|
|
},
|
|
|
|
exportAsJSON: function(){
|
|
|
|
return {
|
|
|
|
name: instance.web.datetime_to_str(new Date()),
|
|
|
|
statement_id: this.get('id'),
|
|
|
|
account_id: (this.get('account_id'))[0],
|
|
|
|
journal_id: (this.get('journal_id'))[0],
|
|
|
|
amount: this.getAmount()
|
|
|
|
};
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
module.PaymentlineCollection = Backbone.Collection.extend({
|
|
|
|
model: module.Paymentline,
|
|
|
|
});
|
|
|
|
|
2012-05-21 14:09:01 +00:00
|
|
|
// 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.
|
|
|
|
|
2012-04-24 16:25:46 +00:00
|
|
|
module.Order = Backbone.Model.extend({
|
|
|
|
initialize: function(attributes){
|
|
|
|
Backbone.Model.prototype.initialize.apply(this, arguments);
|
|
|
|
this.set({
|
|
|
|
creationDate: new Date(),
|
|
|
|
orderLines: new module.OrderlineCollection(),
|
|
|
|
paymentLines: new module.PaymentlineCollection(),
|
|
|
|
name: "Order " + this.generateUniqueId(),
|
|
|
|
});
|
2012-04-24 17:20:47 +00:00
|
|
|
this.pos = attributes.pos; //TODO put that in set and remember to use 'get' to read it ...
|
2012-05-21 16:57:21 +00:00
|
|
|
this.pos_widget = attributes.pos_widget; //FIXME we shouldn't depend on pos_widget in the models
|
2012-05-11 16:02:23 +00:00
|
|
|
this.last_orderline = undefined;
|
2012-04-24 16:25:46 +00:00
|
|
|
return this;
|
|
|
|
},
|
|
|
|
generateUniqueId: function() {
|
|
|
|
return new Date().getTime();
|
|
|
|
},
|
2012-05-22 14:59:56 +00:00
|
|
|
addProduct: function(product){
|
|
|
|
var attr = product.toJSON();
|
|
|
|
attr.pos = this.pos;
|
|
|
|
var line = new module.Orderline(attr);
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
if( this.last_orderline && this.last_orderline.can_be_merged_with(line) ){
|
|
|
|
this.last_orderline.merge(line);
|
|
|
|
}else{
|
|
|
|
this.get('orderLines').add(line);
|
|
|
|
line.bind('killme', function() {
|
|
|
|
this.get('orderLines').remove(line);
|
|
|
|
}, this);
|
|
|
|
this.last_orderline = line;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
addProductOld: function(product) {
|
2012-04-24 16:25:46 +00:00
|
|
|
var existing;
|
2012-05-22 14:59:56 +00:00
|
|
|
|
2012-04-24 16:25:46 +00:00
|
|
|
existing = (this.get('orderLines')).get(product.id);
|
|
|
|
if (existing != null) {
|
2012-05-11 16:02:23 +00:00
|
|
|
this.last_orderline = existing;
|
2012-05-06 21:34:39 +00:00
|
|
|
if(existing.get('weighted')){
|
|
|
|
existing.incrementWeight(product.attributes.weight);
|
|
|
|
}else{
|
|
|
|
existing.incrementQuantity();
|
|
|
|
}
|
2012-04-24 16:25:46 +00:00
|
|
|
} else {
|
|
|
|
var attr = product.toJSON();
|
2012-04-24 17:20:47 +00:00
|
|
|
attr.pos = this.pos;
|
2012-04-24 16:25:46 +00:00
|
|
|
var line = new module.Orderline(attr);
|
2012-05-22 14:59:56 +00:00
|
|
|
console.log('new Orderline:',line,attr);
|
2012-05-11 16:02:23 +00:00
|
|
|
this.last_orderline = line;
|
2012-04-24 16:25:46 +00:00
|
|
|
this.get('orderLines').add(line);
|
|
|
|
line.bind('killme', function() {
|
|
|
|
this.get('orderLines').remove(line);
|
|
|
|
}, this);
|
|
|
|
}
|
|
|
|
},
|
2012-05-21 09:14:42 +00:00
|
|
|
addPaymentLine: function(cashRegister) {
|
2012-04-24 16:25:46 +00:00
|
|
|
var newPaymentline;
|
2012-05-21 09:14:42 +00:00
|
|
|
newPaymentline = new module.Paymentline(cashRegister);
|
2012-04-24 16:25:46 +00:00
|
|
|
/* TODO: Should be 0 for cash-like accounts */
|
|
|
|
newPaymentline.set({
|
|
|
|
amount: this.getDueLeft()
|
|
|
|
});
|
|
|
|
return (this.get('paymentLines')).add(newPaymentline);
|
|
|
|
},
|
|
|
|
getName: function() {
|
|
|
|
return this.get('name');
|
|
|
|
},
|
|
|
|
getTotal: function() {
|
|
|
|
return (this.get('orderLines')).reduce((function(sum, orderLine) {
|
|
|
|
return sum + orderLine.getPriceWithTax();
|
|
|
|
}), 0);
|
|
|
|
},
|
|
|
|
getTotalTaxExcluded: function() {
|
|
|
|
return (this.get('orderLines')).reduce((function(sum, orderLine) {
|
|
|
|
return sum + orderLine.getPriceWithoutTax();
|
|
|
|
}), 0);
|
|
|
|
},
|
|
|
|
getTax: function() {
|
|
|
|
return (this.get('orderLines')).reduce((function(sum, orderLine) {
|
|
|
|
return sum + orderLine.getTax();
|
|
|
|
}), 0);
|
|
|
|
},
|
|
|
|
getPaidTotal: function() {
|
|
|
|
return (this.get('paymentLines')).reduce((function(sum, paymentLine) {
|
|
|
|
return sum + paymentLine.getAmount();
|
|
|
|
}), 0);
|
|
|
|
},
|
|
|
|
getChange: function() {
|
|
|
|
return this.getPaidTotal() - this.getTotal();
|
|
|
|
},
|
|
|
|
getDueLeft: function() {
|
|
|
|
return this.getTotal() - this.getPaidTotal();
|
|
|
|
},
|
|
|
|
exportAsJSON: function() {
|
|
|
|
var orderLines, paymentLines;
|
|
|
|
orderLines = [];
|
|
|
|
(this.get('orderLines')).each(_.bind( function(item) {
|
|
|
|
return orderLines.push([0, 0, item.exportAsJSON()]);
|
|
|
|
}, this));
|
|
|
|
paymentLines = [];
|
|
|
|
(this.get('paymentLines')).each(_.bind( function(item) {
|
|
|
|
return paymentLines.push([0, 0, item.exportAsJSON()]);
|
|
|
|
}, this));
|
|
|
|
return {
|
|
|
|
name: this.getName(),
|
|
|
|
amount_paid: this.getPaidTotal(),
|
|
|
|
amount_total: this.getTotal(),
|
|
|
|
amount_tax: this.getTax(),
|
|
|
|
amount_return: this.getChange(),
|
|
|
|
lines: orderLines,
|
|
|
|
statement_ids: paymentLines
|
|
|
|
};
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
module.OrderCollection = Backbone.Collection.extend({
|
|
|
|
model: module.Order,
|
|
|
|
});
|
|
|
|
|
|
|
|
/*
|
|
|
|
The numpad handles both the choice of the property currently being modified
|
|
|
|
(quantity, price or discount) and the edition of the corresponding numeric value.
|
|
|
|
*/
|
|
|
|
module.NumpadState = Backbone.Model.extend({
|
|
|
|
defaults: {
|
|
|
|
buffer: "0",
|
|
|
|
mode: "quantity"
|
|
|
|
},
|
|
|
|
appendNewChar: function(newChar) {
|
|
|
|
var oldBuffer;
|
|
|
|
oldBuffer = this.get('buffer');
|
|
|
|
if (oldBuffer === '0') {
|
|
|
|
this.set({
|
|
|
|
buffer: newChar
|
|
|
|
});
|
|
|
|
} else if (oldBuffer === '-0') {
|
|
|
|
this.set({
|
|
|
|
buffer: "-" + newChar
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.set({
|
|
|
|
buffer: (this.get('buffer')) + newChar
|
|
|
|
});
|
|
|
|
}
|
|
|
|
this.updateTarget();
|
|
|
|
},
|
|
|
|
deleteLastChar: function() {
|
|
|
|
var tempNewBuffer;
|
|
|
|
tempNewBuffer = (this.get('buffer')).slice(0, -1) || "0";
|
|
|
|
if (isNaN(tempNewBuffer)) {
|
|
|
|
tempNewBuffer = "0";
|
|
|
|
}
|
|
|
|
this.set({
|
|
|
|
buffer: tempNewBuffer
|
|
|
|
});
|
|
|
|
this.updateTarget();
|
|
|
|
},
|
|
|
|
switchSign: function() {
|
|
|
|
var oldBuffer;
|
|
|
|
oldBuffer = this.get('buffer');
|
|
|
|
this.set({
|
|
|
|
buffer: oldBuffer[0] === '-' ? oldBuffer.substr(1) : "-" + oldBuffer
|
|
|
|
});
|
|
|
|
this.updateTarget();
|
|
|
|
},
|
|
|
|
changeMode: function(newMode) {
|
|
|
|
this.set({
|
|
|
|
buffer: "0",
|
|
|
|
mode: newMode
|
|
|
|
});
|
|
|
|
},
|
|
|
|
reset: function() {
|
|
|
|
this.set({
|
|
|
|
buffer: "0",
|
|
|
|
mode: "quantity"
|
|
|
|
});
|
|
|
|
},
|
|
|
|
updateTarget: function() {
|
|
|
|
var bufferContent, params;
|
|
|
|
bufferContent = this.get('buffer');
|
|
|
|
if (bufferContent && !isNaN(bufferContent)) {
|
2012-05-11 16:02:23 +00:00
|
|
|
this.trigger('set_value', parseFloat(bufferContent));
|
2012-04-24 16:25:46 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|