From e725d5615ab3d7a52d29698d12a8b023bc8631ff Mon Sep 17 00:00:00 2001 From: "Pedram (PEBR)" Date: Tue, 12 Sep 2023 15:09:48 +0000 Subject: [PATCH] [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) Signed-off-by: Pedram Bi Ria (pebr) --- addons/point_of_sale/models/pos_config.py | 10 ++++- addons/point_of_sale/models/pos_session.py | 27 +++++++++++- .../static/src/app/store/pos_store.js | 28 ++++++++++++ .../static/tests/tours/ProductScreen.tour.js | 19 ++++++++ addons/point_of_sale/tests/test_frontend.py | 44 +++++++++++++++++++ 5 files changed, 126 insertions(+), 2 deletions(-) diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py index 7aac215f2dc..eac5fb205a6 100644 --- a/addons/point_of_sale/models/pos_config.py +++ b/addons/point_of_sale/models/pos_config.py @@ -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 diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index 97ae72583e1..7490a8b60fd 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -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' diff --git a/addons/point_of_sale/static/src/app/store/pos_store.js b/addons/point_of_sale/static/src/app/store/pos_store.js index b72d03d072f..e8cae91ff73 100644 --- a/addons/point_of_sale/static/src/app/store/pos_store.js +++ b/addons/point_of_sale/static/src/app/store/pos_store.js @@ -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() { diff --git a/addons/point_of_sale/static/tests/tours/ProductScreen.tour.js b/addons/point_of_sale/static/tests/tours/ProductScreen.tour.js index 44f2d488f65..6849289a4b6 100644 --- a/addons/point_of_sale/static/tests/tours/ProductScreen.tour.js +++ b/addons/point_of_sale/static/tests/tours/ProductScreen.tour.js @@ -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(), +}); diff --git a/addons/point_of_sale/tests/test_frontend.py b/addons/point_of_sale/tests/test_frontend.py index ae71811854c..103220dc83c 100644 --- a/addons/point_of_sale/tests/test_frontend.py +++ b/addons/point_of_sale/tests/test_frontend.py @@ -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'