[IMP] point_of_sale: Limit product and partner load
In order to improve the perf at the opening of the point of sale, this commit allows to only load a limited quantity of products and partners in the POS. This also include a new option that allows to load them in the background if they're not all loaded. Part-of: odoo/odoo#72108
This commit is contained in:
@@ -229,6 +229,18 @@ class PosConfig(models.Model):
|
||||
string='Shipping Policy', required=True, default='direct',
|
||||
help="If you deliver all products at once, the delivery order will be scheduled based on the greatest "
|
||||
"product lead time. Otherwise, it will be based on the shortest.")
|
||||
limited_products_loading = fields.Boolean('Limited Product Loading',
|
||||
help="we load all starred products (favorite), all services, recent inventory movements of products, and the most recently updated products.\n"
|
||||
"When the session is open, we keep on loading all remaining products in the background.\n"
|
||||
"In the meantime, you can click on the 'database icon' in the searchbar to load products from database.")
|
||||
limited_products_amount = fields.Integer(default=20000)
|
||||
product_load_background = fields.Boolean()
|
||||
limited_partners_loading = fields.Boolean('Limited Partners Loading',
|
||||
help="By default, 100 partners are loaded.\n"
|
||||
"When the session is open, we keep on loading all remaining partners in the background.\n"
|
||||
"In the meantime, you can use the 'Load Customers' button to load partners from database.")
|
||||
limited_partners_amount = fields.Integer(default=100)
|
||||
partner_load_background = fields.Boolean()
|
||||
|
||||
@api.depends('use_pricelist', 'available_pricelist_ids')
|
||||
def _compute_allowed_pricelist_ids(self):
|
||||
@@ -722,3 +734,46 @@ class PosConfig(models.Model):
|
||||
pos_config.write({'invoice_journal_id': invoice_journal_id.id})
|
||||
else:
|
||||
pos_config.write({'module_account': False})
|
||||
|
||||
def get_limited_products_loading(self):
|
||||
self.env.cr.execute("""
|
||||
WITH pm AS
|
||||
(
|
||||
SELECT product_id,
|
||||
Max(write_date) date
|
||||
FROM stock_quant
|
||||
GROUP BY product_id)
|
||||
SELECT p.id
|
||||
FROM product_product p
|
||||
LEFT JOIN product_template t
|
||||
ON (
|
||||
product_tmpl_id=t.id)
|
||||
LEFT JOIN pm
|
||||
ON (
|
||||
p.id=pm.product_id)
|
||||
WHERE p.active
|
||||
AND t.available_in_pos
|
||||
ORDER BY t.priority DESC,
|
||||
t.type DESC,
|
||||
COALESCE(pm.date,p.write_date) DESC limit %s
|
||||
""", [str(self.limited_products_amount)])
|
||||
return self.env.cr.fetchall()
|
||||
|
||||
def get_limited_partners_loading(self):
|
||||
self.env.cr.execute("""
|
||||
WITH pm AS
|
||||
(
|
||||
SELECT partner_id,
|
||||
Count(partner_id) order_count
|
||||
FROM pos_order
|
||||
GROUP BY partner_id)
|
||||
SELECT id
|
||||
FROM res_partner AS partner
|
||||
LEFT JOIN pm
|
||||
ON (
|
||||
partner.id = pm.partner_id)
|
||||
ORDER BY COALESCE(pm.order_count, 0) DESC,
|
||||
NAME limit %s;
|
||||
""", [str(self.limited_partners_amount)])
|
||||
result = self.env.cr.fetchall()
|
||||
return result
|
||||
|
||||
@@ -3454,7 +3454,7 @@ td {
|
||||
.search-bar-portal .search-box .icon {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
margin-left: 12px;
|
||||
margin-left: 4px;
|
||||
z-index: 1;
|
||||
color: #4f5b66;
|
||||
}
|
||||
@@ -3462,7 +3462,16 @@ td {
|
||||
.search-bar-portal .search-box .clear-icon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin-right: 12px;
|
||||
margin-right: 24px;
|
||||
z-index: 1;
|
||||
color: #4f5b66;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-bar-portal .search-box .database-icon {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
margin-left: 22px;
|
||||
z-index: 1;
|
||||
color: #4f5b66;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -47,7 +47,6 @@ odoo.define('point_of_sale.ClientListScreen', function(require) {
|
||||
};
|
||||
this.updateClientList = debounce(this.updateClientList, 70);
|
||||
}
|
||||
|
||||
// Lifecycle hooks
|
||||
back() {
|
||||
if(this.state.detailIsShown) {
|
||||
@@ -69,11 +68,13 @@ odoo.define('point_of_sale.ClientListScreen', function(require) {
|
||||
}
|
||||
|
||||
get clients() {
|
||||
let res;
|
||||
if (this.state.query && this.state.query.trim() !== '') {
|
||||
return this.env.pos.db.search_partner(this.state.query.trim());
|
||||
res = this.env.pos.db.search_partner(this.state.query.trim());
|
||||
} else {
|
||||
return this.env.pos.db.get_partners_sorted(1000);
|
||||
res = this.env.pos.db.get_partners_sorted(1000);
|
||||
}
|
||||
return res.sort(function (a, b) { return a.name.localeCompare(b.name) });
|
||||
}
|
||||
get isNextButtonVisible() {
|
||||
return this.state.selectedClient ? true : false;
|
||||
@@ -96,7 +97,8 @@ odoo.define('point_of_sale.ClientListScreen', function(require) {
|
||||
|
||||
// We declare this event handler as a debounce function in
|
||||
// order to lower its trigger rate.
|
||||
updateClientList(event) {
|
||||
async updateClientList(event) {
|
||||
var newClientList = await this.getNewClient();
|
||||
this.state.query = event.target.value;
|
||||
const clients = this.clients;
|
||||
if (event.code === 'Enter' && clients.length === 1) {
|
||||
@@ -173,6 +175,37 @@ odoo.define('point_of_sale.ClientListScreen', function(require) {
|
||||
cancelEdit() {
|
||||
this.deactivateEditMode();
|
||||
}
|
||||
async searchClient() {
|
||||
let result = await this.getNewClient();
|
||||
this.env.pos.db.add_partners(result);
|
||||
if(!result.length) {
|
||||
await this.showPopup('ErrorPopup', {
|
||||
title: this.env._t(''),
|
||||
body: this.env._t('No customer found'),
|
||||
});
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
async getNewClient() {
|
||||
var domain = [];
|
||||
if(this.state.query) {
|
||||
domain = [["name", "ilike", this.state.query + "%"]];
|
||||
}
|
||||
var fields = _.find(this.env.pos.models, function(model){ return model.label === 'load_partners'; }).fields;
|
||||
var result = await this.rpc({
|
||||
model: 'res.partner',
|
||||
method: 'search_read',
|
||||
args: [domain, fields],
|
||||
kwargs: {
|
||||
limit: 10,
|
||||
},
|
||||
},{
|
||||
timeout: 3000,
|
||||
shadow: true,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
ClientListScreen.template = 'ClientListScreen';
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ odoo.define('point_of_sale.ProductsWidget', function(require) {
|
||||
useListener('update-search', this._updateSearch);
|
||||
useListener('try-add-product', this._tryAddProduct);
|
||||
useListener('clear-search', this._clearSearch);
|
||||
useListener('update-product-list', this._updateProductList);
|
||||
this.state = useState({ searchWord: '' });
|
||||
}
|
||||
mounted() {
|
||||
@@ -32,14 +33,16 @@ odoo.define('point_of_sale.ProductsWidget', function(require) {
|
||||
return this.state.searchWord.trim();
|
||||
}
|
||||
get productsToDisplay() {
|
||||
let list = [];
|
||||
if (this.searchWord !== '') {
|
||||
return this.env.pos.db.search_product_in_category(
|
||||
list = this.env.pos.db.search_product_in_category(
|
||||
this.selectedCategoryId,
|
||||
this.searchWord
|
||||
);
|
||||
} else {
|
||||
return this.env.pos.db.get_product_by_category(this.selectedCategoryId);
|
||||
list = this.env.pos.db.get_product_by_category(this.selectedCategoryId);
|
||||
}
|
||||
return list.sort(function (a, b) { return a.display_name.localeCompare(b.display_name) });
|
||||
}
|
||||
get subcategories() {
|
||||
return this.env.pos.db
|
||||
@@ -79,6 +82,10 @@ odoo.define('point_of_sale.ProductsWidget', function(require) {
|
||||
_clearSearch() {
|
||||
this.state.searchWord = '';
|
||||
}
|
||||
_updateProductList(event) {
|
||||
this.render();
|
||||
this.trigger('switch-category', 0);
|
||||
}
|
||||
}
|
||||
ProductsWidget.template = 'ProductsWidget';
|
||||
|
||||
|
||||
+34
@@ -3,6 +3,8 @@ odoo.define('point_of_sale.ProductsWidgetControlPanel', function(require) {
|
||||
|
||||
const { useRef } = owl.hooks;
|
||||
const { debounce } = owl.utils;
|
||||
const { identifyError } = require('point_of_sale.utils');
|
||||
const { ConnectionLostError, ConnectionAbortedError } = require('@web/core/network/rpc_service');
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { posbus } = require('point_of_sale.utils');
|
||||
@@ -37,6 +39,38 @@ odoo.define('point_of_sale.ProductsWidgetControlPanel', function(require) {
|
||||
this.trigger('switch-category', 0);
|
||||
this.trigger('update-search', productName);
|
||||
}
|
||||
async loadProductFromDB() {
|
||||
if(!this.searchWordInput.el.value)
|
||||
return;
|
||||
|
||||
try {
|
||||
let ProductIds = await this.rpc({
|
||||
model: 'product.product',
|
||||
method: 'search',
|
||||
args: [[['name', 'ilike', this.searchWordInput.el.value + "%"]]],
|
||||
context: this.env.session.user_context,
|
||||
});
|
||||
if(!ProductIds.length) {
|
||||
this.showPopup('ErrorPopup', {
|
||||
title: this.env._t(''),
|
||||
body: this.env._t("No product found"),
|
||||
});
|
||||
} else {
|
||||
await this.env.pos._addProducts(ProductIds);
|
||||
}
|
||||
this.trigger('update-product-list');
|
||||
} catch (error) {
|
||||
const identifiedError = identifyError(error)
|
||||
if (identifiedError instanceof ConnectionLostError || identifiedError instanceof ConnectionAbortedError) {
|
||||
return this.showPopup('OfflineErrorPopup', {
|
||||
title: this.env._t('Network Error'),
|
||||
body: this.env._t("Product is not loaded. Tried loading the product from the server but there is a network error."),
|
||||
});
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ProductsWidgetControlPanel.template = 'ProductsWidgetControlPanel';
|
||||
|
||||
|
||||
@@ -147,6 +147,13 @@ exports.PosModel = Backbone.Model.extend({
|
||||
|
||||
return this.connect_to_proxy();
|
||||
}
|
||||
if(this.config.limited_products_loading) {
|
||||
await this.env.pos._addProducts(await this.loadProductsFromOffset(0));
|
||||
if(this.config.product_load_background)
|
||||
this.loadProductsBackground();
|
||||
}
|
||||
if(this.config.partner_load_background )
|
||||
this.loadPartnersBackground();
|
||||
return Promise.resolve();
|
||||
},
|
||||
// releases ressources holds by the model at the end of life of the posmodel
|
||||
@@ -245,16 +252,6 @@ exports.PosModel = Backbone.Model.extend({
|
||||
loaded: function(self,unit){
|
||||
self.uom_unit_id = unit[0].res_id;
|
||||
}
|
||||
},{
|
||||
model: 'res.partner',
|
||||
label: 'load_partners',
|
||||
fields: ['name','street','city','state_id','country_id','vat','lang',
|
||||
'phone','zip','mobile','email','barcode','write_date',
|
||||
'property_account_position_id','property_product_pricelist'],
|
||||
loaded: function(self,partners){
|
||||
self.partners = partners;
|
||||
self.db.add_partners(partners);
|
||||
},
|
||||
},{
|
||||
model: 'res.country.state',
|
||||
fields: ['name', 'country_id'],
|
||||
@@ -347,6 +344,27 @@ exports.PosModel = Backbone.Model.extend({
|
||||
self.pos_session.sequence_number = Math.max(self.pos_session.sequence_number, orders[i].data.sequence_number+1);
|
||||
}
|
||||
},
|
||||
},{
|
||||
model: 'res.partner',
|
||||
label: 'load_partners',
|
||||
fields: ['name','street','city','state_id','country_id','vat','lang',
|
||||
'phone','zip','mobile','email','barcode','write_date',
|
||||
'property_account_position_id','property_product_pricelist'],
|
||||
domain: async function(self){
|
||||
if(self.config.limited_partners_loading) {
|
||||
const result = await self.rpc({
|
||||
model: 'pos.config',
|
||||
method: 'get_limited_partners_loading',
|
||||
args: [self.config.id],
|
||||
});
|
||||
return [['id','in', result.map(elem => elem[0])]];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
loaded: function(self,partners){
|
||||
self.partners = partners;
|
||||
self.db.add_partners(partners);
|
||||
},
|
||||
},{
|
||||
model: 'stock.picking.type',
|
||||
fields: ['use_create_lots', 'use_existing_lots'],
|
||||
@@ -454,6 +472,8 @@ exports.PosModel = Backbone.Model.extend({
|
||||
},
|
||||
},{
|
||||
model: 'product.product',
|
||||
label: 'load_products',
|
||||
condition: function (self) { return !self.config.limited_products_loading; },
|
||||
fields: ['display_name', 'lst_price', 'standard_price', 'categ_id', 'pos_categ_id', 'taxes_id',
|
||||
'barcode', 'default_code', 'to_weight', 'uom_id', 'description_sale', 'description',
|
||||
'product_tmpl_id','tracking', 'write_date', 'available_in_pos', 'attribute_line_ids'],
|
||||
@@ -468,6 +488,7 @@ exports.PosModel = Backbone.Model.extend({
|
||||
domain.unshift(['id', '=', self.config.tip_product_id[0]]);
|
||||
domain.unshift('|');
|
||||
}
|
||||
|
||||
return domain;
|
||||
},
|
||||
context: function(self){ return { display_default_code: false }; },
|
||||
@@ -663,7 +684,7 @@ exports.PosModel = Backbone.Model.extend({
|
||||
var tmp = {}; // this is used to share a temporary state between models loaders
|
||||
|
||||
var loaded = new Promise(function (resolve, reject) {
|
||||
function load_model(index) {
|
||||
async function load_model(index) {
|
||||
if (index >= self.models.length) {
|
||||
resolve();
|
||||
} else {
|
||||
@@ -677,13 +698,13 @@ exports.PosModel = Backbone.Model.extend({
|
||||
}
|
||||
|
||||
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 domain = typeof model.domain === 'function' ? await 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;
|
||||
|
||||
if( model.model ){
|
||||
if(model.model ){
|
||||
var params = {
|
||||
model: model.model,
|
||||
context: _.extend(context, self.session.user_context || {}),
|
||||
@@ -845,7 +866,91 @@ exports.PosModel = Backbone.Model.extend({
|
||||
this.get('orders').add(orders);
|
||||
}
|
||||
},
|
||||
|
||||
async _loadMissingProducts(orders) {
|
||||
const missingProductIds = new Set([]);
|
||||
for (const order of orders) {
|
||||
for (const line of order.lines) {
|
||||
const productId = line[2].product_id;
|
||||
if (missingProductIds.has(productId)) continue;
|
||||
if (!this.db.get_product_by_id(productId)) {
|
||||
missingProductIds.add(productId);
|
||||
}
|
||||
}
|
||||
}
|
||||
const productModel = _.find(this.models, function(model){return model.model === 'product.product';});
|
||||
const fields = productModel.fields;
|
||||
const products = await this.rpc({
|
||||
model: 'product.product',
|
||||
method: 'read',
|
||||
args: [[...missingProductIds], fields],
|
||||
context: Object.assign(this.session.user_context, { display_default_code: false }),
|
||||
});
|
||||
productModel.loaded(this, products);
|
||||
},
|
||||
async _loadMissingPartners(orders) {
|
||||
const missingPartnerIds = new Set([]);
|
||||
for (const order of orders) {
|
||||
const partnerId = order.partner_id;
|
||||
if(missingPartnerIds.has(partnerId)) continue;
|
||||
if (partnerId && !this.db.get_partner_by_id(partnerId)) {
|
||||
missingPartnerIds.add(partnerId);
|
||||
}
|
||||
}
|
||||
const partnerModel = _.find(this.models, function(model){return model.model === 'res.partner';});
|
||||
const fields = partnerModel.fields;
|
||||
if(missingPartnerIds) {
|
||||
const partners = await this.rpc({
|
||||
model: 'res.partner',
|
||||
method: 'read',
|
||||
args: [[...missingPartnerIds], fields],
|
||||
context: Object.assign(this.session.user_context, { display_default_code: false }),
|
||||
});
|
||||
partnerModel.loaded(this, partners);
|
||||
}
|
||||
},
|
||||
loadProductsFromOffset: async function(offset) {
|
||||
let ProductIds = [];
|
||||
let product_model = _.find(this.models, (model) => model.model === 'product.product');
|
||||
ProductIds = await this.rpc({
|
||||
model: 'product.product',
|
||||
method: 'search',
|
||||
args: [product_model.domain(this)],
|
||||
kwargs: {
|
||||
offset: this.env.pos.config.limited_products_amount * offset,
|
||||
limit: this.env.pos.config.limited_products_amount,
|
||||
},
|
||||
context: this.env.session.user_context,
|
||||
});
|
||||
return ProductIds;
|
||||
},
|
||||
loadProductsBackground: async function() {
|
||||
let offset = 0
|
||||
let ProductIds = [];
|
||||
do {
|
||||
ProductIds = await this.loadProductsFromOffset(offset)
|
||||
this.env.pos._addProducts(ProductIds);
|
||||
offset += 1;
|
||||
} while(ProductIds.length);
|
||||
},
|
||||
loadPartnersBackground: async function() {
|
||||
let i = 1;
|
||||
let PartnerIds = [];
|
||||
var fields = _.find(this.env.pos.models, function(model){ return model.label === 'load_partners'; }).fields;
|
||||
do {
|
||||
PartnerIds = await this.rpc({
|
||||
model: 'res.partner',
|
||||
method: 'search_read',
|
||||
args: [[], fields],
|
||||
kwargs: {
|
||||
limit: this.env.pos.config.limited_partners_amount,
|
||||
offset: this.env.pos.config.limited_partners_amount * i
|
||||
},
|
||||
context: this.env.session.user_context,
|
||||
});
|
||||
this.env.pos.db.add_partners(PartnerIds);
|
||||
i += 1;
|
||||
} while(PartnerIds.length);
|
||||
},
|
||||
set_start_order: function(){
|
||||
var orders = this.get('orders').models;
|
||||
|
||||
|
||||
@@ -44,6 +44,16 @@
|
||||
<input placeholder="Search Customers" size="1" t-on-keyup="updateClientList" />
|
||||
<span class="search-clear-client"></span>
|
||||
</div>
|
||||
<div class="button back" t-on-click="searchClient()" t-if="!state.detailIsShown">
|
||||
<span class="database-icon">
|
||||
<i class="fa fa-database"/>
|
||||
</span>
|
||||
<t t-if="!env.isMobile">
|
||||
<span class="load-customer-search">
|
||||
Load Customers
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<section class="full-content">
|
||||
<div class="client-window">
|
||||
|
||||
+5
@@ -39,10 +39,15 @@
|
||||
<Portal target="'.pos .search-bar-portal'">
|
||||
<div class="search-box">
|
||||
<span class="icon"><i class="fa fa-search"></i></span>
|
||||
|
||||
<span t-on-click="clearSearch" class="clear-icon">
|
||||
<i class="fa fa-times" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="database-icon">
|
||||
<i class="fa fa-database" t-on-click="loadProductFromDB"/>
|
||||
</span>
|
||||
<input t-ref="search-word-input" type="text" placeholder="Search Products..." t-on-keyup="updateSearch" />
|
||||
|
||||
</div>
|
||||
</Portal>
|
||||
</div>
|
||||
|
||||
@@ -168,6 +168,27 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="limited_partners_loading"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="limited_partners_loading" string="Limited Partners Loading"/>
|
||||
<div class="text-muted">
|
||||
Only load a limited number of customers at the opening of the PoS.
|
||||
</div>
|
||||
<div class="content-group mt16" attrs="{'invisible' : [('limited_partners_loading', '=', False)]}">
|
||||
<div groups="base.group_no_one">
|
||||
<label for="limited_partners_amount" string="Number of Partners Loaded"/>
|
||||
<field name="limited_partners_amount" class="oe_inline"/>
|
||||
</div>
|
||||
<div>
|
||||
<field name="partner_load_background" class="oe_inline"/>
|
||||
<label for="partner_load_background" string="Load all remaining partners in the background" class="oe_inline"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Connected Devices</h2>
|
||||
<div class="row mt16 o_settings_container" id="posbox_reference">
|
||||
@@ -566,6 +587,28 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="limited_products_loading"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="limited_products_loading" string="Limited Products Loading"/>
|
||||
<div class="text-muted">
|
||||
Only load most common products at the opening of the PoS.
|
||||
</div>
|
||||
<div class="content-group mt16" attrs="{'invisible' : [('limited_products_loading', '=', False)]}">
|
||||
<div groups="base.group_no_one">
|
||||
<label for="limited_products_amount" string="Number of Products Loaded"/>
|
||||
<field name="limited_products_amount" class="oe_inline"/>
|
||||
</div>
|
||||
<div>
|
||||
<field name="product_load_background" class="oe_inline"/>
|
||||
<label for="product_load_background" string="Load all remaining products in the background" class="oe_inline"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<h2>Accounting</h2>
|
||||
<div class="row mt16 o_settings_container" id="accounting_section">
|
||||
|
||||
Reference in New Issue
Block a user