[IMP] point_of_sale: limit pricelist item loading based on products

Before this commit, the point of sale module would attempt to load all
pricelist items from the database, even if there were a high number
defined. This could cause memory issues.

Although the products loaded into the POS can be limited, all of the
pricelist items would still be loaded, even for not loaded products.

This commit improves the pricelist loading to only load pricelist
items related to the products loaded into the POS. When new products
are loaded, their associated pricelist items are added.

This optimization prevents unnecessary pricelist item loading and
reduces memory usage in cases with large pricelists.

opw-3374866

closes odoo/odoo#143265

X-original-commit: f39e8a125c64e4a225122d975500d78e5925f4bd
Signed-off-by: Joseph Caburnay (jcb) <jcb@odoo.com>
Signed-off-by: Pedram Bi Ria (pebr) <pebr@odoo.com>
This commit is contained in:
Pedram (PEBR)
2023-11-30 11:44:43 +00:00
parent 51518c0788
commit e725d5615a
5 changed files with 126 additions and 2 deletions
+9 -1
View File
@@ -752,7 +752,7 @@ class PosConfig(models.Model):
COALESCE(pm.date, product_product.write_date) DESC
LIMIT %s
"""
self.env.cr.execute(query, params + [20000])
self.env.cr.execute(query, params + [self.get_limited_product_count()])
product_ids = self.env.cr.fetchall()
products = self.env['product.product'].search([('id', 'in', product_ids)])
product_combo = products.filtered(lambda p: p['detailed_type'] == 'combo')
@@ -760,6 +760,14 @@ class PosConfig(models.Model):
products_available = products | product_in_combo
return products_available.read(fields)
def get_limited_product_count(self):
default_limit = 20000
config_param = self.env['ir.config_parameter'].sudo().get_param('point_of_sale.limited_product_count', default_limit)
try:
return int(config_param)
except (TypeError, ValueError, OverflowError):
return default_limit
def get_limited_partners_loading(self):
self.env.cr.execute("""
WITH pm AS
+26 -1
View File
@@ -1703,10 +1703,10 @@ class PosSession(models.Model):
'res.partner',
'stock.picking.type',
'res.users',
'product.product',
'product.pricelist',
'res.currency',
'pos.category',
'product.product',
'pos.combo',
'pos.combo.line',
'product.packaging',
@@ -1950,6 +1950,13 @@ class PosSession(models.Model):
'min_quantity',
]
def _product_pricelist_item_domain_by_product(self, product_tmpl_ids, product_ids, pricelists):
return [
('pricelist_id', 'in', [p['id'] for p in pricelists]),
'|', ('product_tmpl_id', '=', False), ('product_tmpl_id', 'in', product_tmpl_ids),
'|', ('product_id', '=', False), ('product_id', 'in', product_ids),
]
def _get_pos_ui_product_pricelist(self, params):
pricelists = self.env['product.pricelist'].search_read(**params['search_params'])
for pricelist in pricelists:
@@ -2187,6 +2194,14 @@ class PosSession(models.Model):
def _prepare_product_pricelists(self, pricelists):
pricelist_by_id = {pricelist['id']: pricelist for pricelist in pricelists}
pricelist_item_domain = [('pricelist_id', 'in', [p['id'] for p in pricelists])]
loaded_data = self._context.get('loaded_data')
if loaded_data:
pricelist_item_domain = self._product_pricelist_item_domain_by_product(
[p['product_tmpl_id'][0] for p in loaded_data['product.product']],
[p['id'] for p in loaded_data['product.product']],
pricelists)
for item in self.env['product.pricelist.item'].search_read(pricelist_item_domain, self._product_pricelist_item_fields()):
pricelist_by_id[item['pricelist_id'][0]]['items'].append(item)
@@ -2253,6 +2268,16 @@ class PosSession(models.Model):
def _get_closed_orders(self):
return self.order_ids.filtered(lambda o: o.state not in ['draft', 'cancel'])
def get_pos_ui_product_pricelist_item_by_product(self, product_tmpl_ids, product_ids):
pricelists = self.env['product.pricelist'].search_read(**self._loader_params_product_pricelist()['search_params'])
pricelist_item_domain = [
('pricelist_id', 'in', [p['id'] for p in pricelists]),
'|',
'&', ('product_id', '=', False), ('product_tmpl_id', 'in', product_tmpl_ids),
('product_id', 'in', product_ids)]
return self.env['product.pricelist.item'].search_read(pricelist_item_domain, self._product_pricelist_item_fields())
class ProcurementGroup(models.Model):
_inherit = 'procurement.group'
@@ -663,13 +663,40 @@ export class PosStore extends Reactive {
}
}
}
if(!missingProductIds.size) return;
const products = await this.orm.call(
"pos.session",
"get_pos_ui_product_product_by_params",
[odoo.pos_session_id, { domain: [["id", "in", [...missingProductIds]]] }]
);
await this._loadMissingPricelistItems(products);
this._loadProductProduct(products);
}
async _loadMissingPricelistItems(products) {
if(!products.length) return;
const product_tmpl_ids = products.map(product => product.product_tmpl_id[0]);
const product_ids = products.map(product => product.id);
const pricelistItems = await this.orm.call(
'pos.session',
'get_pos_ui_product_pricelist_item_by_product',
[odoo.pos_session_id, product_tmpl_ids, product_ids]
);
// Merge the loaded pricelist items with the existing pricelists
// Prioritizing the addition of newly loaded pricelist items to the start of the existing pricelists.
// This ensures that the order reflects the desired priority of items in the pricelistItems array.
// E.g. The order in the items should be: [product-pricelist-item, product-template-pricelist-item, category-pricelist-item, global-pricelist-item].
// for reference check order of the Product Pricelist Item model
for (const pricelist of this.pricelists) {
const itemIds = new Set(pricelist.items.map(item => item.id));
const _pricelistItems = pricelistItems.filter(item => {
return item.pricelist_id[0] === pricelist.id && !itemIds.has(item.id);
});
pricelist.items = [..._pricelistItems, ...pricelist.items];
}
}
// load the partners based on the ids
async _loadPartners(partnerIds) {
if (partnerIds.length > 0) {
@@ -1628,6 +1655,7 @@ export class PosStore extends Reactive {
odoo.pos_session_id,
{ domain: [["id", "in", ids]] },
]);
await this._loadMissingPricelistItems(product);
this._loadProductProduct(product);
}
isOpenOrderShareable() {
@@ -183,3 +183,22 @@ registry.category("web_tour.tours").add("ShowTaxExcludedTour", {
Chrome.endTour(),
].flat(),
});
registry.category("web_tour.tours").add("limitedProductPricelistLoading", {
test: true,
url: "/pos/ui",
steps: () =>
[
ProductScreen.confirmOpeningPopup(),
ProductScreen.scan_barcode("0100100"),
ProductScreen.selectedOrderlineHas('Test Product 1', '1.0', '80.0'),
ProductScreen.scan_barcode("0100200"),
ProductScreen.selectedOrderlineHas('Test Product 2', '1.0', '100.0'),
ProductScreen.scan_barcode("0100300"),
ProductScreen.selectedOrderlineHas('Test Product 3', '1.0', '50.0'),
Chrome.endTour(),
].flat(),
});
@@ -1047,6 +1047,50 @@ class TestUi(TestPointOfSaleHttpCommon):
self.main_pos_config.open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'ReceiptTrackingMethodTour', login="accountman")
def test_limited_product_pricelist_loading(self):
self.env['ir.config_parameter'].sudo().set_param('point_of_sale.limited_product_count', '1')
product_1 = self.env['product.product'].create({
'name': 'Test Product 1',
'list_price': 100,
'barcode': '0100100',
'taxes_id': False,
'available_in_pos': True,
})
product_2 = self.env['product.product'].create({
'name': 'Test Product 2',
'list_price': 200,
'barcode': '0100200',
'taxes_id': False,
'available_in_pos': True,
})
self.env['product.product'].create({
'name': 'Test Product 3',
'list_price': 300,
'barcode': '0100300',
'taxes_id': False,
'available_in_pos': True,
})
pricelist_item = self.env['product.pricelist.item'].create([{
'applied_on': '3_global',
'fixed_price': 50,
}, {
'applied_on': '1_product',
'product_tmpl_id': product_2.product_tmpl_id.id,
'fixed_price': 100,
}, {
'applied_on': '0_product_variant',
'product_id': product_1.id,
'fixed_price': 80,
}])
self.main_pos_config.pricelist_id.write({'item_ids': [(6, 0, pricelist_item.ids)]})
self.main_pos_config.open_ui()
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'limitedProductPricelistLoading', login="accountman")
# This class just runs the same tests as above but with mobile emulation
class MobileTestUi(TestUi):
browser_size = '375x667'