diff --git a/addons/account/wizard/setup_wizards.py b/addons/account/wizard/setup_wizards.py index 52ddf221ef0..90c870782ca 100644 --- a/addons/account/wizard/setup_wizards.py +++ b/addons/account/wizard/setup_wizards.py @@ -79,8 +79,11 @@ class SetupBarBankConfigWizard(models.TransientModel): num_journals_without_account = fields.Integer(default=lambda self: self._number_unlinked_journal()) def _number_unlinked_journal(self): - return self.env['account.journal'].search([('type', '=', 'bank'), ('bank_account_id', '=', False), - ('id', '!=', self.default_linked_journal_id())], count=True) + return self.env['account.journal'].search_count([ + ('type', '=', 'bank'), + ('bank_account_id', '=', False), + ('id', '!=', self.default_linked_journal_id()), + ]) @api.onchange('acc_number') def _onchange_acc_number(self): diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 22fb969c361..7bdaa7e4958 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -774,7 +774,7 @@ class Lead(models.Model): return result @api.model - def search(self, domain, offset=0, limit=None, order=None, count=False): + def search(self, domain, offset=0, limit=None, order=None): """ Override to support ordering on my_activity_date_deadline. Ordering through web client calls search_read with an order parameter set. @@ -804,8 +804,9 @@ class Lead(models.Model): All other search and search_read are left untouched by this override to avoid side effects. Search_count is not affected by this override. """ - if count or not order or 'my_activity_date_deadline' not in order: - return super(Lead, self).search(domain, offset=offset, limit=limit, order=order, count=count) + if not order or 'my_activity_date_deadline' not in order: + return super().search(domain, offset, limit, order) + order_items = [order_item.strip().lower() for order_item in (order or self._order).split(',')] # Perform a read_group on my activities to get a mapping lead_id / deadline @@ -825,7 +826,7 @@ class Lead(models.Model): # Search leads linked to those activities and order them. See docstring # of this method for more details. - search_res = super(Lead, self).search(my_lead_domain, offset=0, limit=None, order=my_lead_order, count=count) + search_res = super().search(my_lead_domain, order=my_lead_order) my_lead_ids_ordered = sorted(search_res.ids, key=lambda lead_id: my_lead_mapping[lead_id], reverse=not activity_asc) # keep only requested window (offset + limit, or offset+) my_lead_ids_keep = my_lead_ids_ordered[offset:(offset + limit)] if limit else my_lead_ids_ordered[offset:] @@ -847,9 +848,9 @@ class Lead(models.Model): lead_offset = 0 lead_order = ', '.join(item for item in order_items if 'my_activity_date_deadline' not in item) - other_lead_res = super(Lead, self).search( + other_lead_res = super().search( expression.AND([[('id', 'not in', my_lead_ids_skip)], domain]), - offset=lead_offset, limit=lead_limit, order=lead_order, count=count + lead_offset, lead_limit, lead_order, ) return self.browse(my_lead_ids_keep) + other_lead_res diff --git a/addons/hr/models/hr_employee.py b/addons/hr/models/hr_employee.py index 81c807f801a..0e2dfcaf601 100644 --- a/addons/hr/models/hr_employee.py +++ b/addons/hr/models/hr_employee.py @@ -236,7 +236,7 @@ class HrEmployeePrivate(models.Model): return self.env['hr.employee.public'].get_view(view_id, view_type, **options) @api.model - def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): + def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None): """ We override the _search because it is the method that checks the access rights This is correct to override the _search. That way we enforce the fact that calling @@ -246,14 +246,14 @@ class HrEmployeePrivate(models.Model): employees exactly match the ids of the related hr.employee. """ if self.check_access_rights('read', raise_exception=False): - return super(HrEmployeePrivate, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) + return super()._search(domain, offset, limit, order, access_rights_uid) try: - ids = self.env['hr.employee.public']._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) + ids = self.env['hr.employee.public']._search(domain, offset, limit, order, access_rights_uid) except ValueError: raise AccessError(_('You do not have access to this document.')) - if not count and isinstance(ids, Query): + if isinstance(ids, Query): # the result is expected from this table, so we should link tables - ids = super(HrEmployeePrivate, self.sudo())._search([('id', 'in', ids)]) + ids = super(HrEmployeePrivate, self.sudo())._search([('id', 'in', ids)], order=order) return ids def get_formview_id(self, access_uid=None): diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py index 32b31f335fa..08446c13dce 100644 --- a/addons/hr_holidays/models/hr_leave_type.py +++ b/addons/hr_holidays/models/hr_leave_type.py @@ -553,7 +553,7 @@ class HolidaysType(models.Model): return res @api.model - def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): + def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None): """ Override _search to order the results, according to some employee. The order is the following @@ -566,12 +566,13 @@ class HolidaysType(models.Model): to the method. """ employee_id = self._get_contextual_employee_id() - post_sort = (not count and not order and employee_id) - leave_ids = super(HolidaysType, self)._search(args, offset=offset, limit=(None if post_sort else limit), order=order, count=count, access_rights_uid=access_rights_uid) - leaves = self.browse(leave_ids) - if post_sort: - return leaves.sorted(key=self._model_sorting_key, reverse=True).ids[:limit or None] - return leave_ids + if not order and employee_id: + # retrieve all leaves, sort them, then apply offset and limit + leaves = self.browse(super()._search(domain, access_rights_uid=access_rights_uid)) + leaves = leaves.sorted(key=self._model_sorting_key, reverse=True) + leaves = leaves[offset:(offset + limit) if limit else None] + return leaves.ids + return super()._search(domain, offset, limit, order, access_rights_uid) def action_see_days_allocated(self): self.ensure_one() diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index 824a0a1179b..4b73235f465 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -372,7 +372,7 @@ class MailActivity(models.Model): return super(MailActivity, self).unlink() @api.model - def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): + def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None): """ Override that adds specific access rights of mail.activity, to remove ids uid could not see according to our custom rules. Please refer to _filter_access_rules_remaining for more details about those rules. @@ -381,16 +381,10 @@ class MailActivity(models.Model): # Rules do not apply to administrator if self.env.is_superuser(): - return super(MailActivity, self)._search( - args, offset=offset, limit=limit, order=order, - count=count, access_rights_uid=access_rights_uid) - # Perform a super with count as False, to have the ids, not a counter - ids = super(MailActivity, self)._search( - args, offset=offset, limit=limit, order=order, - count=False, access_rights_uid=access_rights_uid) - if not ids and count: - return 0 - elif not ids: + return super()._search(domain, offset, limit, order, access_rights_uid) + + ids = super()._search(domain, offset, limit, order, access_rights_uid) + if not ids: return ids # check read access rights before checking the actual rules on the given ids @@ -426,12 +420,9 @@ class MailActivity(models.Model): activity['id'] for activity in activities_to_check if activity['res_model'] == doc_model and activity['res_id'] in valid_doc_ids) - if count: - return len(allowed_ids) - else: - # re-construct a list based on ids, because 'allowed_ids' does not keep the original order - id_list = [id for id in ids if id in allowed_ids] - return id_list + # re-construct a list based on ids, because 'allowed_ids' does not keep the original order + id_list = [id for id in ids if id in allowed_ids] + return id_list @api.model def _read_group_raw(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): @@ -447,7 +438,7 @@ class MailActivity(models.Model): # Rules do not apply to administrator if not self.env.is_superuser(): - allowed_ids = self._search(domain, count=False) + allowed_ids = self._search(domain) if allowed_ids: domain = expression.AND([domain, [('id', 'in', allowed_ids)]]) else: diff --git a/addons/mail/models/mail_blacklist.py b/addons/mail/models/mail_blacklist.py index a4fcd1e9cf4..28acd4e947b 100644 --- a/addons/mail/models/mail_blacklist.py +++ b/addons/mail/models/mail_blacklist.py @@ -51,23 +51,18 @@ class MailBlackList(models.Model): values['email'] = tools.email_normalize(values['email']) return super(MailBlackList, self).write(values) - def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): + def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None): """ Override _search in order to grep search on email field and make it lower-case and sanitized """ - if args: - new_args = [] - for arg in args: - if isinstance(arg, (list, tuple)) and arg[0] == 'email' and isinstance(arg[2], str): - normalized = tools.email_normalize(arg[2]) - if normalized: - new_args.append([arg[0], arg[1], normalized]) - else: - new_args.append(arg) - else: - new_args.append(arg) - else: - new_args = args - return super(MailBlackList, self)._search(new_args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) + def normalize(arg): + if isinstance(arg, (list, tuple)) and arg[0] == 'email' and isinstance(arg[2], str): + normalized = tools.email_normalize(arg[2]) + if normalized: + return (arg[0], arg[1], normalized) + return arg + + domain = [normalize(item) for item in domain] + return super()._search(domain, offset, limit, order, access_rights_uid) def _add(self, email, message=None): normalized = tools.email_normalize(email) diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index e584d1ee3b3..6e8c4c9e6a4 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -256,7 +256,7 @@ class Message(models.Model): self._cr.execute("""CREATE INDEX IF NOT EXISTS mail_message_model_res_id_id_idx ON mail_message (model, res_id, id)""") @api.model - def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): + def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None): """ Override that adds specific access rights of mail.message, to remove ids uid could not see according to our custom rules. Please refer to check_access_rule for more details about those rules. @@ -273,19 +273,13 @@ class Message(models.Model): """ # Rules do not apply to administrator if self.env.is_superuser(): - return super(Message, self)._search( - args, offset=offset, limit=limit, order=order, - count=count, access_rights_uid=access_rights_uid) + return super()._search(domain, offset, limit, order, access_rights_uid) # Non-employee see only messages with a subtype and not internal if not self.env['res.users'].has_group('base.group_user'): - args = expression.AND([self._get_search_domain_share(), args]) + domain = expression.AND([self._get_search_domain_share(), domain]) # Perform a super with count as False, to have the ids, not a counter - ids = super(Message, self)._search( - args, offset=offset, limit=limit, order=order, - count=False, access_rights_uid=access_rights_uid) - if not ids and count: - return 0 - elif not ids: + ids = super()._search(domain, offset, limit, order, access_rights_uid) + if not ids: return ids pid = self.env.user.partner_id.id @@ -319,12 +313,9 @@ class Message(models.Model): final_ids = author_ids | partner_ids | allowed_ids - if count: - return len(final_ids) - else: - # re-construct a list based on ids, because set did not keep the original order - id_list = [id for id in ids if id in final_ids] - return id_list + # re-construct a list based on ids, because set did not keep the original order + id_list = [id for id in ids if id in final_ids] + return id_list @api.model def _find_allowed_model_wise(self, doc_model, doc_dict): diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py index 70f48a58838..2ff22019fdf 100644 --- a/addons/mrp/models/mrp_bom.py +++ b/addons/mrp/models/mrp_bom.py @@ -219,7 +219,7 @@ class MrpBom(models.Model): def check_kit_has_not_orderpoint(self): product_ids = [pid for bom in self.filtered(lambda bom: bom.type == "phantom") for pid in (bom.product_id.ids or bom.product_tmpl_id.product_variant_ids.ids)] - if self.env['stock.warehouse.orderpoint'].search([('product_id', 'in', product_ids)], count=True): + if self.env['stock.warehouse.orderpoint'].search_count([('product_id', 'in', product_ids)], limit=1): raise ValidationError(_("You can not create a kit-type bill of materials for products that have at least one reordering rule.")) @api.ondelete(at_uninstall=False) diff --git a/addons/mrp/models/stock_warehouse.py b/addons/mrp/models/stock_warehouse.py index 55e878dde2c..9ece67f904c 100644 --- a/addons/mrp/models/stock_warehouse.py +++ b/addons/mrp/models/stock_warehouse.py @@ -303,9 +303,13 @@ class Orderpoint(models.Model): @api.constrains('product_id') def check_product_is_not_kit(self): - if self.env['mrp.bom'].search(['|', ('product_id', 'in', self.product_id.ids), - '&', ('product_id', '=', False), ('product_tmpl_id', 'in', self.product_id.product_tmpl_id.ids), - ('type', '=', 'phantom')], count=True): + domain = [ + '|', ('product_id', 'in', self.product_id.ids), + '&', ('product_id', '=', False), + ('product_tmpl_id', 'in', self.product_id.product_tmpl_id.ids), + ('type', '=', 'phantom'), + ] + if self.env['mrp.bom'].search_count(domain, limit=1): raise ValidationError(_("A product with a kit-type bill of materials can not have a reordering rule.")) def _get_orderpoint_products(self): diff --git a/addons/phone_validation/models/phone_blacklist.py b/addons/phone_validation/models/phone_blacklist.py index f940a9fb6b9..c8deaf74926 100644 --- a/addons/phone_validation/models/phone_blacklist.py +++ b/addons/phone_validation/models/phone_blacklist.py @@ -60,23 +60,18 @@ class PhoneBlackList(models.Model): values['number'] = sanitized return super(PhoneBlackList, self).write(values) - def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): + def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None): """ Override _search in order to grep search on sanitized number field """ - if args: - new_args = [] - for arg in args: - if isinstance(arg, (list, tuple)) and arg[0] == 'number' and isinstance(arg[2], str): - number = arg[2] - sanitized = phone_validation.phone_sanitize_numbers_w_record([number], self.env.user)[number]['sanitized'] - if sanitized: - new_args.append([arg[0], arg[1], sanitized]) - else: - new_args.append(arg) - else: - new_args.append(arg) - else: - new_args = args - return super(PhoneBlackList, self)._search(new_args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) + def sanitize_number(arg): + if isinstance(arg, (list, tuple)) and arg[0] == 'number' and isinstance(arg[2], str): + number = arg[2] + sanitized = phone_validation.phone_sanitize_numbers_w_record([number], self.env.user)[number]['sanitized'] + if sanitized: + return (arg[0], arg[1], sanitized) + return arg + + domain = [sanitize_number(item) for item in domain] + return super()._search(domain, offset, limit, order, access_rights_uid) def add(self, number, message=None): sanitized = phone_validation.phone_sanitize_numbers_w_record([number], self.env.user)[number]['sanitized'] diff --git a/addons/product/models/product_product.py b/addons/product/models/product_product.py index 49b9cee8dc7..9f7c1f91108 100644 --- a/addons/product/models/product_product.py +++ b/addons/product/models/product_product.py @@ -408,11 +408,11 @@ class ProductProduct(models.Model): return template.product_variant_id or template._create_first_product_variant() @api.model - def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): + def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None): # TDE FIXME: strange if self._context.get('search_default_categ_id'): - args.append((('categ_id', 'child_of', self._context['search_default_categ_id']))) - return super(ProductProduct, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) + domain.append((('categ_id', 'child_of', self._context['search_default_categ_id']))) + return super()._search(domain, offset, limit, order, access_rights_uid) @api.depends_context('display_default_code', 'seller_id') def _compute_display_name(self): diff --git a/addons/project/models/project.py b/addons/project/models/project.py index adab59489f3..d42fa7306a7 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -672,7 +672,7 @@ class Project(models.Model): return result @api.model - def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): + def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None): new_order, item_index, desc = [], -1, False for index, order_item in enumerate((order or self._order).split(',')): order_item_list = order_item.strip().lower().split(' ') @@ -681,7 +681,7 @@ class Project(models.Model): desc = order_item_list[-1] == 'desc' else: new_order.append(order_item) - query = super()._search(args, offset, limit, ', '.join(new_order), count, access_rights_uid) + query = super()._search(domain, offset, limit, ', '.join(new_order), access_rights_uid) if item_index != -1: query_order_list = query.order.split(',') query_order_list.insert(item_index, f""" @@ -1890,10 +1890,10 @@ class Task(models.Model): return super(Task, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) @api.model - def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): - fields_list = {term[0] for term in args if isinstance(term, (tuple, list)) and term not in [expression.TRUE_LEAF, expression.FALSE_LEAF]} + def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None): + fields_list = {term[0] for term in domain if isinstance(term, (tuple, list)) and term not in [expression.TRUE_LEAF, expression.FALSE_LEAF]} self._ensure_fields_are_accessible(fields_list) - return super(Task, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) + return super()._search(domain, offset, limit, order, access_rights_uid) def mapped(self, func): # Note: This will protect the filtered method too diff --git a/addons/test_mail/tests/test_mail_activity.py b/addons/test_mail/tests/test_mail_activity.py index 91d59b93d3c..8cf68ca76ef 100644 --- a/addons/test_mail/tests/test_mail_activity.py +++ b/addons/test_mail/tests/test_mail_activity.py @@ -82,13 +82,13 @@ class TestActivityRights(TestActivityCommon): # can _search activities if access to the document self.env['mail.activity'].with_user(self.user_employee)._search( - [('id', '=', test_activity.id)], count=False) + [('id', '=', test_activity.id)]) # cannot _search activities if no access to the document with patch.object(MailTestActivity, 'check_access_rights', autospec=True, side_effect=_employee_crash): with self.assertRaises(exceptions.AccessError): searched_activity = self.env['mail.activity'].with_user(self.user_employee)._search( - [('id', '=', test_activity.id)], count=False) + [('id', '=', test_activity.id)]) # can read_group activities if access to the document read_group_result = self.env['mail.activity'].with_user(self.user_employee).read_group( diff --git a/odoo/addons/base/models/ir_attachment.py b/odoo/addons/base/models/ir_attachment.py index 92940260dc0..bfeaa03f8c1 100644 --- a/odoo/addons/base/models/ir_attachment.py +++ b/odoo/addons/base/models/ir_attachment.py @@ -528,23 +528,22 @@ class IrAttachment(models.Model): return super().read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) @api.model - def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): + def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None): # add res_field=False in domain if not present; the arg[0] trick below # works for domain items and '&'/'|'/'!' operators too discard_binary_fields_attachments = False - if not any(arg[0] in ('id', 'res_field') for arg in args): + if not any(arg[0] in ('id', 'res_field') for arg in domain): discard_binary_fields_attachments = True - args.insert(0, ('res_field', '=', False)) + domain = [('res_field', '=', False)] + domain - ids = super(IrAttachment, self)._search(args, offset=offset, limit=limit, order=order, - count=False, access_rights_uid=access_rights_uid) + ids = super()._search(domain, offset, limit, order, access_rights_uid) if self.env.is_superuser(): # rules do not apply for the superuser - return len(ids) if count else ids + return ids if not ids: - return 0 if count else [] + return [] # Work with a set, as list.remove() is prohibitive for large lists of documents # (takes 20+ seconds on a db with 100k docs during search_count()!) @@ -597,11 +596,12 @@ class IrAttachment(models.Model): # expected records to retrieve if len(orig_ids) == limit and len(result) < self._context.get('need', limit): need = self._context.get('need', limit) - len(result) - result.extend(self.with_context(need=need)._search(args, offset=offset + len(orig_ids), - limit=limit, order=order, count=False, - access_rights_uid=access_rights_uid)[:limit - len(result)]) + more_ids = self.with_context(need=need)._search( + domain, offset + len(orig_ids), limit, order, access_rights_uid, + ) + result.extend(more_ids[:limit - len(result)]) - return len(result) if count else list(result) + return list(result) def _read(self, fields): self.check('read') diff --git a/odoo/addons/base/models/ir_ui_menu.py b/odoo/addons/base/models/ir_ui_menu.py index 187a2d7e4d2..19b29fa4981 100644 --- a/odoo/addons/base/models/ir_ui_menu.py +++ b/odoo/addons/base/models/ir_ui_menu.py @@ -138,8 +138,8 @@ class IrUiMenu(models.Model): return self.filtered(lambda menu: menu.id in visible_ids) @api.model - def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): - menu_ids = super(IrUiMenu, self)._search(args, offset=0, limit=None, order=order, count=False, access_rights_uid=access_rights_uid) + def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None): + menu_ids = super()._search(domain, order=order, access_rights_uid=access_rights_uid) menus = self.browse(menu_ids) if menus: # menu filtering is done only on main menu tree, not other menu lists @@ -149,7 +149,7 @@ class IrUiMenu(models.Model): menus = menus[offset:] if limit: menus = menus[:limit] - return len(menus) if count else menus.ids + return menus.ids def name_get(self): return [(menu.id, menu._get_full_name()) for menu in self] diff --git a/odoo/addons/base/models/res_bank.py b/odoo/addons/base/models/res_bank.py index f8356590061..afb0dd48f12 100644 --- a/odoo/addons/base/models/res_bank.py +++ b/odoo/addons/base/models/res_bank.py @@ -113,19 +113,18 @@ class ResPartnerBank(models.Model): for acc in self] @api.model - def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): - pos = 0 - while pos < len(args): - # DLE P14 - if args[pos][0] == 'acc_number': - op = args[pos][1] - value = args[pos][2] + def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None): + def sanitize(arg): + if isinstance(arg, (tuple, list)) and arg[0] == 'acc_number': + value = arg[2] if not isinstance(value, str) and isinstance(value, Iterable): value = [sanitize_account_number(i) for i in value] else: value = sanitize_account_number(value) - if 'like' in op: + if 'like' in arg[1]: value = '%' + value + '%' - args[pos] = ('sanitized_acc_number', op, value) - pos += 1 - return super(ResPartnerBank, self)._search(args, offset, limit, order, count=count, access_rights_uid=access_rights_uid) + return ('sanitized_acc_number', arg[1], value) + return arg + + domain = [sanitize(item) for item in domain] + return super()._search(domain, offset, limit, order, access_rights_uid) diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py index f36acb3ba26..f40fa004271 100644 --- a/odoo/addons/base/models/res_partner.py +++ b/odoo/addons/base/models/res_partner.py @@ -886,15 +886,14 @@ class Partner(models.Model): return partner.name_get()[0] @api.model - def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): + def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None): """ Override search() to always show inactive children when searching via ``child_of`` operator. The ORM will always call search() with a simple domain of the form [('parent_id', 'in', [ids])]. """ # a special ``domain`` is set on the ``child_ids`` o2m to bypass this logic, as it uses similar domain expressions - if len(args) == 1 and len(args[0]) == 3 and args[0][:2] == ('parent_id','in') \ - and args[0][2] != [False]: + if len(domain) == 1 and len(domain[0]) == 3 and domain[0][:2] == ('parent_id', 'in') \ + and domain[0][2] != [False]: self = self.with_context(active_test=False) - return super(Partner, self)._search(args, offset=offset, limit=limit, order=order, - count=count, access_rights_uid=access_rights_uid) + return super()._search(domain, offset, limit, order, access_rights_uid) @api.model @api.returns('self', lambda value: value.id) diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py index 6883d1efd92..47304cec5a2 100644 --- a/odoo/addons/base/models/res_users.py +++ b/odoo/addons/base/models/res_users.py @@ -188,14 +188,14 @@ class Groups(models.Model): return where @api.model - def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): + def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None): # add explicit ordering if search is sorted on full_name if order and order.startswith('full_name'): - groups = super(Groups, self).search(args) + groups = super().search(domain) groups = groups.sorted('full_name', reverse=order.endswith('DESC')) groups = groups[offset:offset+limit] if limit else groups[offset:] - return len(groups) if count else groups.ids - return super(Groups, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) + return groups.ids + return super()._search(domain, offset, limit, order, access_rights_uid) def copy(self, default=None): self.ensure_one() @@ -561,13 +561,12 @@ class Users(models.Model): return super(Users, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) @api.model - def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): - if not self.env.su and args: - domain_fields = {term[0] for term in args if isinstance(term, (tuple, list))} + def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None): + if not self.env.su and domain: + domain_fields = {term[0] for term in domain if isinstance(term, (tuple, list))} if domain_fields.intersection(USER_PRIVATE_FIELDS): raise AccessError(_('Invalid search criterion')) - return super(Users, self)._search(args, offset=offset, limit=limit, order=order, count=count, - access_rights_uid=access_rights_uid) + return super()._search(domain, offset, limit, order, access_rights_uid) @api.model_create_multi def create(self, vals_list): diff --git a/odoo/addons/base/tests/test_api.py b/odoo/addons/base/tests/test_api.py index e88d643181d..92fee961f34 100644 --- a/odoo/addons/base/tests/test_api.py +++ b/odoo/addons/base/tests/test_api.py @@ -70,10 +70,10 @@ class TestAPI(SavepointCaseWithUserDemo): @mute_logger('odoo.models') def test_04_query_count(self): - """ Test the search method with count=True. """ + """ Test the search_count method. """ self.cr.execute("SELECT COUNT(*) FROM res_partner WHERE active") count1 = self.cr.fetchone()[0] - count2 = self.env['res.partner'].search([], count=True) + count2 = self.env['res.partner'].search_count([]) self.assertIsInstance(count1, int) self.assertIsInstance(count2, int) self.assertEqual(count1, count2) diff --git a/odoo/addons/base/tests/test_search.py b/odoo/addons/base/tests/test_search.py index 1d9c140ce6c..6e004adc0ff 100644 --- a/odoo/addons/base/tests/test_search.py +++ b/odoo/addons/base/tests/test_search.py @@ -203,7 +203,4 @@ class test_search(TransactionCase): {'name': 'runbot'}, ]) self.assertEqual(len(partners) + count_partner_before, Partner.search_count([])) - self.assertEqual(len(partners) + count_partner_before, Partner.search([], count=True)) - self.assertEqual(3, Partner.search_count([], limit=3)) - self.assertEqual(3, Partner.search([], count=True, limit=3)) diff --git a/odoo/models.py b/odoo/models.py index 66dbb6bef98..0cca07bf1e7 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -1469,15 +1469,13 @@ class BaseModel(metaclass=MetaModel): list to match all records. :param limit: maximum number of record to count (upperbound) (default: all) """ - res = self.search(domain, limit=limit, count=True) - return res if isinstance(res, int) else len(res) + query = self._search(domain, limit=limit, order='id') + return len(query) @api.model - @api.returns('self', - upgrade=lambda self, value, domain, offset=0, limit=None, order=None, count=False: value if count else self.browse(value), - downgrade=lambda self, value, domain, offset=0, limit=None, order=None, count=False: value if count else value.ids) - def search(self, domain, offset=0, limit=None, order=None, count=False): - """ search(domain[, offset=0][, limit=None][, order=None][, count=False]) + @api.returns('self') + def search(self, domain, offset=0, limit=None, order=None): + """ search(domain[, offset=0][, limit=None][, order=None]) Searches for records based on the ``domain`` :ref:`search domain `. @@ -1487,12 +1485,11 @@ class BaseModel(metaclass=MetaModel): :param int offset: number of results to ignore (default: none) :param int limit: maximum number of records to return (default: all) :param str order: sort string - :param bool count: if True, only counts and returns the number of matching records (default: False) :returns: at most ``limit`` records matching the search criteria :raise AccessError: if user is not allowed to access requested information """ - res = self._search(domain, offset=offset, limit=limit, order=order, count=count) - return res if count else self.browse(res) + query = self._search(domain, offset=offset, limit=limit, order=order) + return self.browse(query) # # display_name, name_get, name_create, name_search @@ -4616,7 +4613,7 @@ class BaseModel(metaclass=MetaModel): self.env[model_name].flush_model(field_names) @api.model - def _search(self, domain, offset=0, limit=None, order=None, count=False, access_rights_uid=None): + def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None): """ Private implementation of search() method, allowing specifying the uid to use for the access right check. This is useful for example when filling in the selection list for a drop-down and avoiding access rights errors, @@ -4625,37 +4622,23 @@ class BaseModel(metaclass=MetaModel): :param access_rights_uid: optional user ID to use when checking access rights (not for ir.rules, this is only for ir.model.access) - :return: a list of record ids or an integer (if count is True) + :return: a collection of record ids (may be a ``Query`` object) """ model = self.with_user(access_rights_uid) if access_rights_uid else self model.check_access_rights('read') if expression.is_false(self, domain): # optimization: no need to query, as no record satisfies the domain - return 0 if count else [] + return () # the flush must be done before the _where_calc(), as the latter can do some selects self._flush_search(domain, order=order) query = self._where_calc(domain) self._apply_ir_rules(query, 'read') - query.limit = limit - - if count: - # Ignore order and offset when just counting, they don't make sense and could - # hurt performance - if limit: - # Special case to avoid counting every record in DB (which can be really slow). - # The result will be between 0 and limit. - query_str, params = query.select("") # generates a `SELECT FROM` (faster) - query_str = f"SELECT COUNT(*) FROM ({query_str}) t" - else: - query_str, params = query.select("COUNT(*)") - - self._cr.execute(query_str, params) - return self._cr.fetchone()[0] query.order = self._generate_order_by(order, query).replace('ORDER BY ', '') + query.limit = limit query.offset = offset return query diff --git a/odoo/tools/query.py b/odoo/tools/query.py index 39c30f27ff0..c3d27f5be53 100644 --- a/odoo/tools/query.py +++ b/odoo/tools/query.py @@ -223,6 +223,22 @@ class Query(object): return bool(self.get_result_ids()) def __len__(self): + if self._ids is None and not self.offset: + from_clause, where_clause, params = self.get_sql() + where_clause = where_clause or 'TRUE' + if self.limit: + # apply the limit, and wrap the query into a more efficient one + query_str = f''' + SELECT COUNT(*) FROM ( + SELECT FROM {from_clause} + WHERE {where_clause} + LIMIT {int(self.limit)} + ) t + ''' + else: + query_str = f'SELECT COUNT(*) FROM {from_clause} WHERE {where_clause}' + self._cr.execute(query_str, params) + return self._cr.fetchone()[0] return len(self.get_result_ids()) def __iter__(self):