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'