[IMP] point_of_sale: removed IndexeDB and asynchronous backend
bzr revid: fva@openerp.com-20120813160026-04w28p3pojzvdlkj
This commit is contained in:
@@ -1,284 +1,5 @@
|
||||
/**
|
||||
* WIP : AS OF THIS COMMIT, THE INDEXEDB BACKEND IS IN A NON WORKING STATE
|
||||
*
|
||||
* This file contains an IndexedDB (html5 database) backend for the Point Of Sale.
|
||||
* The IDB is used to store the list of products and especially their thumbnails, which may
|
||||
* not fit in the localStorage.
|
||||
* The IDB offers a big performance boost for products lookup, but not so much for search as
|
||||
* searching is not yet implemented in the IndexedDB API and must be performed manually in JS.
|
||||
*
|
||||
* this file also contains a localstorage implementation of the database to be used by browsers
|
||||
* that don't support the indexeddb api.
|
||||
*
|
||||
*/
|
||||
function openerp_pos_db(instance, module){
|
||||
|
||||
function importIndexedDB(){
|
||||
if('indexedDB' in window){
|
||||
return true;
|
||||
}else if('webkitIndexedDB' in window){
|
||||
window.indexedDB = window.webkitIndexedDB;
|
||||
window.IDBCursor = window.webkitIDBCursor;
|
||||
window.IDBDatabase = window.webkitIDBDatabase;
|
||||
window.IDBDatabaseError = window.webkitIDBDatabaseError;
|
||||
window.IDBDatabaseException = window.webkitIDBDatabaseException;
|
||||
window.IDBFactory = window.webkitIDBFactory;
|
||||
window.IDBIndex = window.webkitIDBIndex;
|
||||
window.IDBKeyRange = window.webkitIDBKeyRange;
|
||||
window.IDBObjectSrore = window.webkitIDBOjbectStore;
|
||||
window.IDBRequest = window.webkitIDBRequest;
|
||||
window.IDBTransaction = window.webkitIDBTransaction;
|
||||
}else if('mozIndexedDB' in window){
|
||||
window.indexedDB = window.mozIndexedDB;
|
||||
}else{
|
||||
throw new Error("DATABASE ERROR: IndexedDB not implemented. Please upgrade your web browser");
|
||||
}
|
||||
}
|
||||
importIndexedDB();
|
||||
|
||||
module.PosDB = instance.web.Class.extend({
|
||||
|
||||
version: 1,
|
||||
name: 'openerp_pos_db',
|
||||
limit: 50,
|
||||
|
||||
init: function(options){
|
||||
var self = this;
|
||||
options = options || {};
|
||||
|
||||
this.version = options.version || this.version;
|
||||
this.name = options.name || this.name;
|
||||
|
||||
var open_request = indexedDB.open(this.name, this.version);
|
||||
|
||||
open_request.onblocked = function(event){
|
||||
throw new Error("DATABASE ERROR: request blocked.");
|
||||
};
|
||||
|
||||
// new API (firefox)
|
||||
open_request.onupgradeneeded = function(event){
|
||||
console.log('using new API');
|
||||
var transaction = open_request.transaction;
|
||||
self._upgrade_db(event.oldVersion,transaction);
|
||||
};
|
||||
|
||||
open_request.onsuccess = function(event){
|
||||
console.log('db',self.db);
|
||||
self.db = open_request.result;
|
||||
var oldVersion = Number(self.db.version);
|
||||
|
||||
if(oldVersion !== self.version){
|
||||
// if we get to this point, it means that onupgradeneeded hasn't been called.
|
||||
// so we try the old api (webkit)
|
||||
if(!self.db.setVersion){
|
||||
throw new Error("DATABASE ERROR: database API is broken. Your web browser is probably out of date");
|
||||
}
|
||||
console.log('using old API');
|
||||
var version_request = self.db.setVersion(self.version);
|
||||
version_request.onsuccess = function(event){
|
||||
var transaction = version_request.result;
|
||||
self._upgrade_db(oldVersion, transaction);
|
||||
};
|
||||
}
|
||||
};
|
||||
},
|
||||
_upgrade_db: function(oldversion, transaction){
|
||||
console.log('upgrade_db:',oldversion);
|
||||
|
||||
this.db = transaction.db;
|
||||
var productStore = this.db.createObjectStore('products', {keyPath: 'id'});
|
||||
|
||||
productStore.createIndex('ean13', 'ean13', {unique:true});
|
||||
|
||||
productStore.createIndex('name', 'name', {unique:false});
|
||||
|
||||
productStore.createIndex('category','category', {unique:false});
|
||||
|
||||
var imageStore = this.db.createObjectStore('images', {keyPath: 'id'});
|
||||
|
||||
var categoryStore = this.db.createObjectStore('categories', {keypath:'name'});
|
||||
},
|
||||
|
||||
_add_data: function(store, data){
|
||||
var transaction = this.db.transaction([store], 'readwrite');
|
||||
var objectStore = transaction.objectStore(store);
|
||||
var request = objectStore.put(data);
|
||||
},
|
||||
/* adds a product to the database, replacing the original product if it is already present.
|
||||
* the product must be provided as a json as returned by the openerp backend
|
||||
*/
|
||||
add_product: function(product){
|
||||
if(product instanceof Array){
|
||||
for(var i = 0, len = product.length; i < len; i++){
|
||||
this.add_product(product[i]);
|
||||
}
|
||||
}else{
|
||||
if(product.product_image_small){
|
||||
var image = product.product_image_small;
|
||||
var product = _.clone(product);
|
||||
delete product['product_image_small'];
|
||||
this._add_data('images', { id: product.id, image:image});
|
||||
}
|
||||
this._add_data('products',product);
|
||||
}
|
||||
},
|
||||
|
||||
/* removes all the data in the database ! */
|
||||
clear: function(done_callback){
|
||||
var transaction = this.db.transaction(['products','images'],'readwrite');
|
||||
transaction.objectStore('products').clear();
|
||||
transaction.objectStore('images').clear();
|
||||
if(done_callback){
|
||||
transaction.oncomplete = function(){ done_callback(); };
|
||||
}
|
||||
},
|
||||
|
||||
get_product_count: function(result_callback){
|
||||
this.db.transaction('products').objectStore('products').count().onsuccess = function(event){
|
||||
result_callback(event.target.result);
|
||||
};
|
||||
},
|
||||
|
||||
get_image_count: function(result_callback){
|
||||
this.db.transaction('images').objectStore('images').count().onsuccess = function(event){
|
||||
result_callback(event.target.result);
|
||||
};
|
||||
},
|
||||
|
||||
/* fetches a product with an id of 'id', returns the product as the first parameter
|
||||
* of the result_callback function
|
||||
*/
|
||||
get_product_by_id: function(id, result_callback){
|
||||
var transaction = this.db.transaction('products');
|
||||
var objectStore = transaction.objectStore('products');
|
||||
var request = objectStore.get(id);
|
||||
request.onsuccess = function(event){
|
||||
if(result_callback){
|
||||
result_callback(event.target.result);
|
||||
}
|
||||
};
|
||||
},
|
||||
get_product_by_name: function(name, result_callback){
|
||||
var transaction = this.db.transaction('products');
|
||||
var objectStore = transaction.objectStore('products');
|
||||
var index = objectStore.index('name');
|
||||
index.get(name).onsuccess = function(event){
|
||||
result_callback(event.target.result);
|
||||
};
|
||||
},
|
||||
/* fetches the product with the ean13 matching the one provided. returns the product
|
||||
* as the first parameter of the result_callback function
|
||||
*/
|
||||
get_product_by_ean13: function(ean13, result_callback){
|
||||
var transaction = this.db.transaction('products');
|
||||
var objectStore = transaction.objectStore('products');
|
||||
var index = objectStore.index('ean13');
|
||||
index.get(ean13).onsuccess = function(event){
|
||||
result_callback(event.target.result);
|
||||
};
|
||||
},
|
||||
/* fetches all products belonging to a category, and returns the list of products
|
||||
* as the first paramter of the result_callback function
|
||||
*/
|
||||
get_product_by_category: function(category, result_callback){
|
||||
var transaction = this.db.transaction('products');
|
||||
var objectStore = transaction.objectStore('products');
|
||||
var index = objectStore.index('category');
|
||||
var list = [];
|
||||
index.openCursor(IDBKeyRange.only(category)).onsuccess = function(event){
|
||||
var cursor = event.target.result;
|
||||
if(cursor){
|
||||
list.push(cursor.value);
|
||||
cursor.continue();
|
||||
}else{
|
||||
result_callback(list);
|
||||
}
|
||||
};
|
||||
},
|
||||
/* Fetches a picture associated with a product.
|
||||
* product: This is a product json object as returned by the openerp backend.
|
||||
* result_callback: a callback that gets the image data as parameter.
|
||||
*/
|
||||
get_product_image: function(product, result_callback){
|
||||
var transaction = this.db.transaction('images');
|
||||
var objectStore = transaction.objectStore('images');
|
||||
var request = objectStore.get(product.id);
|
||||
request.onsuccess = function(event){
|
||||
result_callback(event.target.result.image);
|
||||
}
|
||||
},
|
||||
/* fields: a string or a list of string representing the list of the fields that
|
||||
* will be searched. the values of those field must be a string.
|
||||
* query: a string that will be matched in the content of one of the searched fields.
|
||||
* result_callback: a function that will be called with a list of all the products with
|
||||
* a field that match the query.
|
||||
*/
|
||||
search_product: function(fields,query,result_callback){
|
||||
var transaction = this.db.transaction('products');
|
||||
var objectStore = transaction.objectStore('products');
|
||||
var list = [];
|
||||
if(!(fields instanceof Array)){
|
||||
fields = [ fields ];
|
||||
}
|
||||
query = query.toLowerCase();
|
||||
objectStore.openCursor().onsuccess = function(event){
|
||||
var cursor = event.target.result;
|
||||
if(cursor){
|
||||
var obj = cursor.value;
|
||||
for(var i = 0, len = fields.length; i < len; i++){
|
||||
if(obj[fields[i]].toLowerCase().indexOf(query) != -1){
|
||||
list.push(obj);
|
||||
break;
|
||||
}
|
||||
}
|
||||
cursor.continue();
|
||||
}else{
|
||||
result_callback(list);
|
||||
}
|
||||
}
|
||||
},
|
||||
/* the callback function will be called with all products as parameter, by increasing id.
|
||||
* if the callback returns 'break' the iteration will stop
|
||||
*/
|
||||
for_all_products: function(callback){
|
||||
var transaction = this.db.transaction('products');
|
||||
var objectStore = transaction.objectStore('products');
|
||||
objectStore.openCursor().onsuccess = function(event){
|
||||
var cursor = event.target.result;
|
||||
if(cursor){
|
||||
var ret = callback(cursor.value);
|
||||
if(ret !== 'break'){
|
||||
cursor.continue();
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
/* the callback function will be called with all products as parameter by increasing id.
|
||||
* if the callback returns 'break', the iteration will stop
|
||||
* if the callback returns a product object, it will be inserted in the db, possibly
|
||||
* overwriting an existing product. The intended usage is for the callback to return a
|
||||
* modified version version of the product with the same id. Anything else and you're on your own.
|
||||
*/
|
||||
modify_all_products: function(callback){
|
||||
var transaction = this.db.transaction('products','readwrite');
|
||||
var objectStore = transaction.objectStore('products');
|
||||
objectStore.openCursor().onsuccess = function(event){
|
||||
var cursor = event.target.result;
|
||||
if(cursor){
|
||||
var ret = callback(cursor.value);
|
||||
if(ret === undefined || ret === null){
|
||||
cursor.continue();
|
||||
}else if(ret === 'break'){
|
||||
return;
|
||||
}else{
|
||||
objectStore.put(ret);
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
window.PosDB = module.PosDB;
|
||||
|
||||
/* PosLS is a LocalStorage based implementation of the point of sale database,
|
||||
it performs better for few products, but does not scale beyond 500 products.
|
||||
*/
|
||||
@@ -372,7 +93,7 @@ function openerp_pos_db(instance, module){
|
||||
make_ancestors(this.root_category_id, []);
|
||||
},
|
||||
/* this internal method returns from disc a dictionary associating ids to the products */
|
||||
_get_products: function(){
|
||||
_load_products: function(){
|
||||
if(this.products_cache){
|
||||
return this.products_cache;
|
||||
}
|
||||
@@ -384,12 +105,12 @@ function openerp_pos_db(instance, module){
|
||||
}
|
||||
},
|
||||
/* this internal method saves a dictionary associating ids to product to the disc */
|
||||
_set_products: function(products){
|
||||
_save_products: function(products){
|
||||
localStorage[this.products] = JSON.stringify(products);
|
||||
this.products_cache = products;
|
||||
},
|
||||
/* this internal method returns from disc a dictionary associating ids to the categories */
|
||||
_get_categories: function(){
|
||||
_load_categories: function(){
|
||||
if(this.categories_cache){
|
||||
return this.categories_cache;
|
||||
}
|
||||
@@ -401,13 +122,13 @@ function openerp_pos_db(instance, module){
|
||||
}
|
||||
},
|
||||
/* this internal method saves to disc an array associating ids to the categories */
|
||||
_set_categories: function(categories){
|
||||
_save_categories: function(categories){
|
||||
localStorage[this.categories] = JSON.stringify(categories);
|
||||
this.categories_cache = categories;
|
||||
},
|
||||
add_products: function(products){
|
||||
var stored_products = this._get_products();
|
||||
var stored_categories = this._get_categories();
|
||||
var stored_products = this._load_products();
|
||||
var stored_categories = this._load_categories();
|
||||
|
||||
if(!products instanceof Array){
|
||||
products = [products];
|
||||
@@ -429,16 +150,13 @@ function openerp_pos_db(instance, module){
|
||||
}
|
||||
stored_products[product.id] = product;
|
||||
}
|
||||
this._set_products(stored_products);
|
||||
this._set_categories(stored_categories);
|
||||
this._save_products(stored_products);
|
||||
this._save_categories(stored_categories);
|
||||
},
|
||||
/* removes all the data from the database. TODO : being able to selectively remove data */
|
||||
clear: function(done_callback){
|
||||
clear: function(){
|
||||
localStorage.removeItem(this.products);
|
||||
localStorage.removeItem(this.categories);
|
||||
if(done_callback){
|
||||
done_callback();
|
||||
}
|
||||
},
|
||||
/* this internal methods returns the count of properties in an object. */
|
||||
_count_props : function(obj){
|
||||
@@ -450,42 +168,27 @@ function openerp_pos_db(instance, module){
|
||||
}
|
||||
return count;
|
||||
},
|
||||
get_product_count: function(result_callback){
|
||||
result_callback(this._count_props(this._get_products()));
|
||||
get_product_by_id: function(id){
|
||||
return this._load_products()[id];
|
||||
},
|
||||
get_product_by_id: function(id, result_callback){
|
||||
var products = this._get_products();
|
||||
result_callback( products[id] );
|
||||
},
|
||||
get_product_by_name: function(name, result_callback){
|
||||
var products = this._get_products();
|
||||
for(var i in products){
|
||||
if(products[i] && products[i].name === name){
|
||||
result_callback(products[i]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
result_callback(undefined);
|
||||
},
|
||||
get_product_by_ean13: function(ean13, result_callback){
|
||||
var products = this._get_products();
|
||||
get_product_by_ean13: function(ean13){
|
||||
var products = this._load_products();
|
||||
for(var i in products){
|
||||
if( products[i] && products[i].ean13 === ean13){
|
||||
result_callback(products[i]);
|
||||
return;
|
||||
return products[i];
|
||||
}
|
||||
}
|
||||
result_callback(undefined);
|
||||
return undefined;
|
||||
},
|
||||
get_product_by_category: function(category_id, result_callback){
|
||||
var stored_categories = this._get_categories();
|
||||
var stored_products = this._get_products();
|
||||
get_product_by_category: function(category_id){
|
||||
var stored_categories = this._load_categories();
|
||||
var stored_products = this._load_products();
|
||||
var product_ids = stored_categories[category_id];
|
||||
var list = [];
|
||||
for(var i = 0, len = Math.min(product_ids.length,this.limit); i < len; i++){
|
||||
list.push(stored_products[product_ids[i]]);
|
||||
}
|
||||
result_callback(list);
|
||||
return list;
|
||||
},
|
||||
/* returns as a parameter of the result_callback function a list of products with :
|
||||
* - a category that is or is a child of category_id,
|
||||
@@ -493,10 +196,10 @@ function openerp_pos_db(instance, module){
|
||||
* If a search is started before the previous has returned, the previous search may be cancelled
|
||||
* (and the corresponding result_callback never called)
|
||||
*/
|
||||
search_product_in_category: function(category_id, fields, query, result_callback){
|
||||
search_product_in_category: function(category_id, fields, query){
|
||||
var self = this;
|
||||
var stored_categories = this._get_categories();
|
||||
var stored_products = this._get_products();
|
||||
var stored_categories = this._load_categories();
|
||||
var stored_products = this._load_products();
|
||||
var product_ids = stored_categories[category_id];
|
||||
var list = [];
|
||||
var count = 0;
|
||||
@@ -521,14 +224,14 @@ function openerp_pos_db(instance, module){
|
||||
}
|
||||
}
|
||||
}
|
||||
result_callback(list);
|
||||
return list;
|
||||
},
|
||||
// TODO move the order storage from the crappy DAO in pos_models.js
|
||||
add_order: function(order,done_callback){
|
||||
add_order: function(order){
|
||||
},
|
||||
remove_order: function(order_id, done_callback){
|
||||
remove_order: function(order_id){
|
||||
},
|
||||
get_orders: function(result_callback){
|
||||
get_orders: function(){
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
|
||||
self.db.add_categories(categories);
|
||||
return fetch(
|
||||
'product.product',
|
||||
['name', 'list_price','price','pos_categ_id', 'taxes_id','image', 'ean13', 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type'],
|
||||
['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13', 'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type'],
|
||||
[['pos_categ_id','!=', false]],
|
||||
{pricelist: self.get('shop').pricelist_id[0]} // context for price
|
||||
);
|
||||
@@ -367,23 +367,21 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
|
||||
|
||||
scan_product: function(parsed_ean){
|
||||
var self = this;
|
||||
var def = new $.Deferred();
|
||||
this.db.get_product_by_ean13(parsed_ean.base_ean, function(product){
|
||||
var selectedOrder = this.get('selectedOrder');
|
||||
if(!product){
|
||||
def.reject('product-not-found: '+parsed_ean.base_ean);
|
||||
return;
|
||||
}
|
||||
if(parsed_ean.type === 'price'){
|
||||
selectedOrder.addProduct(new module.Product(product), {price:parsed_ean.value});
|
||||
}else if(parsed_ean.type === 'weight'){
|
||||
selectedOrder.addProduct(new module.Product(product), {quantity:parsed_ean.value, merge:false});
|
||||
}else{
|
||||
selectedOrder.addProduct(new module.Product(product));
|
||||
}
|
||||
def.resolve();
|
||||
});
|
||||
return def;
|
||||
var product = this.db.get_product_by_ean13(parsed_ean.base_ean);
|
||||
var selectedOrder = this.get('selectedOrder');
|
||||
|
||||
if(!product){
|
||||
return false;
|
||||
}
|
||||
|
||||
if(parsed_ean.type === 'price'){
|
||||
selectedOrder.addProduct(new module.Product(product), {price:parsed_ean.value});
|
||||
}else if(parsed_ean.type === 'weight'){
|
||||
selectedOrder.addProduct(new module.Product(product), {quantity:parsed_ean.value, merge:false});
|
||||
}else{
|
||||
selectedOrder.addProduct(new module.Product(product));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -160,19 +160,17 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
|
||||
// there's an error.
|
||||
barcode_product_action: function(ean){
|
||||
var self = this;
|
||||
this.pos_widget.scan_product(ean)
|
||||
.done(function(){
|
||||
self.pos.proxy.scan_item_success(ean);
|
||||
if(self.barcode_product_screen){
|
||||
self.pos_widget.screen_selector.set_current_screen(self.barcode_product_screen);
|
||||
}
|
||||
})
|
||||
.fail(function(){
|
||||
self.pos.proxy.scan_item_error_unrecognized(ean);
|
||||
if(self.barcode_product_error_popup && self.pos_widget.screen_selector.get_user_mode() !== 'cashier'){
|
||||
self.pos_widget.screen_selector.show_popup(self.barcode_product_error_popup);
|
||||
}
|
||||
});
|
||||
if(pos_widget.scan_product(ean)){
|
||||
self.pos.proxy.scan_item_success(ean);
|
||||
if(self.barcode_product_screen){
|
||||
self.pos_widget.screen_selector.set_current_screen(self.barcode_product_screen);
|
||||
}
|
||||
}else{
|
||||
self.pos.proxy.scan_item_error_unrecognized(ean);
|
||||
if(self.barcode_product_error_popup && self.pos_widget.screen_selector.get_user_mode() !== 'cashier'){
|
||||
self.pos_widget.screen_selector.show_popup(self.barcode_product_error_popup);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// what happens when a cashier id barcode is scanned.
|
||||
|
||||
@@ -242,7 +242,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
this.renderElement();
|
||||
},
|
||||
get_image_url: function() {
|
||||
return 'data:image/gif;base64,'+this.model.get('image');
|
||||
return 'web/binary/image?session_id='+instance.connection.session_id+'&model=product.product&field=image&id='+this.model.get('id')+'&cache=1800';
|
||||
},
|
||||
renderElement: function() {
|
||||
this._super();
|
||||
@@ -471,25 +471,20 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
var self = this;
|
||||
|
||||
// find all products belonging to the current category
|
||||
this.pos.db.get_product_by_category(this.category.id, function(products){
|
||||
// product lists watch for reset events on 'products' to re-render.
|
||||
// FIXME that means all productlist widget re-render... even the hidden ones !
|
||||
self.pos.get('products').reset(products);
|
||||
});
|
||||
var products = this.pos.db.get_product_by_category(this.category.id);
|
||||
self.pos.get('products').reset(products);
|
||||
|
||||
// filter the products according to the search string
|
||||
this.$('.searchbox input').keyup(function(){
|
||||
query = $(this).val().toLowerCase();
|
||||
if(query){
|
||||
self.pos.db.search_product_in_category(self.category.id, ['name','ean13'], query, function(products){
|
||||
self.pos.get('products').reset(products);
|
||||
self.$('.search-clear').fadeIn();
|
||||
});
|
||||
var products = self.pos.db.search_product_in_category(self.category.id, ['name','ean13'], query);
|
||||
self.pos.get('products').reset(products);
|
||||
self.$('.search-clear').fadeIn();
|
||||
}else{
|
||||
self.pos.db.get_product_by_category(self.category.id, function(products){
|
||||
self.pos.get('products').reset(products);
|
||||
self.$('.search-clear').fadeOut();
|
||||
});
|
||||
var products = self.pos.db.get_product_by_category(self.category.id);
|
||||
self.pos.get('products').reset(products);
|
||||
self.$('.search-clear').fadeOut();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -497,11 +492,10 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
|
||||
|
||||
//reset the search when clicking on reset
|
||||
this.$('.search-clear').click(function(){
|
||||
self.pos.db.get_product_by_category(self.category.id, function(products){
|
||||
self.pos.get('products').reset(products);
|
||||
self.$('.searchbox input').val('').focus();
|
||||
self.$('.search-clear').fadeOut();
|
||||
});
|
||||
var products = self.pos.db.get_product_by_category(self.category.id);
|
||||
self.pos.get('products').reset(products);
|
||||
self.$('.searchbox input').val('').focus();
|
||||
self.$('.search-clear').fadeOut();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user