[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:
Robin Heinz
2021-09-03 15:45:02 +00:00
parent 6799161c52
commit d9dca8feb7
10 changed files with 323 additions and 22 deletions
+55
View File
@@ -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
+11 -2
View File
@@ -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';
@@ -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';
+119 -14
View File
@@ -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">
@@ -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">