diff --git a/base_name_search_improved/README.rst b/base_name_search_improved/README.rst index 3dee5936f43..ab0b207f148 100644 --- a/base_name_search_improved/README.rst +++ b/base_name_search_improved/README.rst @@ -17,13 +17,13 @@ Improved Name Search :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github - :target: https://github.com/OCA/server-tools/tree/17.0/base_name_search_improved + :target: https://github.com/OCA/server-tools/tree/18.0/base_name_search_improved :alt: OCA/server-tools .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/server-tools-17-0/server-tools-17-0-base_name_search_improved + :target: https://translation.odoo-community.org/projects/server-tools-18-0/server-tools-18-0-base_name_search_improved :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=17.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=18.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -115,7 +115,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -155,6 +155,6 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. -This module is part of the `OCA/server-tools `_ project on GitHub. +This module is part of the `OCA/server-tools `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/base_name_search_improved/__manifest__.py b/base_name_search_improved/__manifest__.py index d2574acbb19..c94a4ceaf5b 100644 --- a/base_name_search_improved/__manifest__.py +++ b/base_name_search_improved/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Improved Name Search", "summary": "Friendlier search when typing in relation fields", - "version": "17.0.1.0.0", + "version": "18.0.1.0.0", "category": "Uncategorized", "website": "https://github.com/OCA/server-tools", "author": "Daniel Reis, Odoo Community Association (OCA), ADHOC SA", diff --git a/base_name_search_improved/models/ir_model.py b/base_name_search_improved/models/ir_model.py index 4bf4d8a3971..582bdfbc95c 100644 --- a/base_name_search_improved/models/ir_model.py +++ b/base_name_search_improved/models/ir_model.py @@ -3,12 +3,12 @@ import logging from ast import literal_eval -from collections import defaultdict from lxml import etree -from odoo import _, api, fields, models, tools +from odoo import api, fields, models, tools from odoo.exceptions import ValidationError +from odoo.osv import expression _logger = logging.getLogger(__name__) # Extended name search is only used on some operators @@ -68,58 +68,6 @@ def _extend_name_results(self, domain, results, limit): return results -def patch_name_search(): - @api.model - def _name_search( - self, name="", domain=None, operator="ilike", limit=100, order=None - ): - # Perform standard name search - res = _name_search.origin( - self, - name=name, - domain=domain, - limit=limit, - order=order, - ) - if name and _get_use_smart_name_search(self.sudo()) and operator in ALLOWED_OPS: - # _name_search.origin is a query, we need to convert it to a list - res = self.browse(res).ids - limit = limit or 0 - - # we add domain - args = domain or [] + _get_name_search_domain(self.sudo()) - - # Support a list of fields to search on - all_names = _get_rec_names(self.sudo()) - base_domain = args or [] - # Try regular search on each additional search field - for rec_name in all_names[1:]: - domain = [(rec_name, operator, name)] - res = _extend_name_results(self, base_domain + domain, res, limit) - # Try ordered word search on each of the search fields - for rec_name in all_names: - domain = [(rec_name, operator, name.replace(" ", "%"))] - res = _extend_name_results(self, base_domain + domain, res, limit) - # Try unordered word search on each of the search fields - # we only perform this search if we have at least one - # separator character - # also, if have raise the limit we skeep this iteration - if " " in name and len(res) < limit: - domain = [] - for word in name.split(): - word_domain = [] - for rec_name in all_names: - word_domain = ( - word_domain and ["|"] + word_domain or word_domain - ) + [(rec_name, operator, word)] - domain = (domain and ["&"] + domain or domain) + word_domain - res = _extend_name_results(self, base_domain + domain, res, limit) - - return res - - return _name_search - - class Base(models.AbstractModel): _inherit = "base" @@ -133,28 +81,79 @@ def _compute_smart_search(self): @api.model def _search_smart_search(self, operator, value): - """ - For now this method does not call - self._name_search(name, operator=operator) since it is not as - performant if unlimited records are called which is what name - search should return. That is why it is reimplemented here - again. In addition, name_search has a logic which first tries - to return best match, which in this case is not necessary. - Surely, it can be improved and a lot of code can be unified. - """ - name = value - if name and operator in ALLOWED_OPS: + if value and operator in ALLOWED_OPS: + matching_records = self.with_context( + force_smart_name_search=True + ).name_search(name=value, operator=operator, limit=0) + if matching_records: + record_ids = [record[0] for record in matching_records] + return [("id", "in", record_ids)] + return [] + + @api.model + def _search_display_name(self, operator, value): + domain = super()._search_display_name(operator, value) + if self.env.context.get( + "force_smart_name_search", False + ) or _get_use_smart_name_search(self.sudo()): all_names = _get_rec_names(self.sudo()) - domain = _get_name_search_domain(self.sudo()) - for word in name.split(): + additional_domain = _get_name_search_domain(self.sudo()) + + for word in value.split(): word_domain = [] for rec_name in all_names: word_domain = ( word_domain and ["|"] + word_domain or word_domain ) + [(rec_name, operator, word)] - domain = (domain and ["&"] + domain or domain) + word_domain - return domain - return [] + additional_domain = ( + additional_domain and ["&"] + additional_domain or additional_domain + ) + word_domain + + return expression.OR([additional_domain, domain]) + + return domain + + @api.model + def name_search(self, name="", args=None, operator="ilike", limit=100): + if not name or not ( + self.env.context.get("force_smart_name_search", False) + or _get_use_smart_name_search(self.sudo()) + ): + return super().name_search(name, args, operator, limit) + + all_names = _get_rec_names(self.sudo()) + base_domain = args or [] + limit = limit or 0 + results = [] + + for rec_name in all_names: + domain = expression.AND([base_domain, [(rec_name, operator, name)]]) + results = _extend_name_results(self, domain, results, limit) + + for rec_name in all_names: + domain = expression.AND( + [base_domain, [(rec_name, operator, name.replace(" ", "%"))]] + ) + results = _extend_name_results(self, domain, results, limit) + + if " " in name: + unordered_domain = [] + for word in name.split(): + word_domain = expression.OR( + [[(rec_name, operator, word)] for rec_name in all_names] + ) + unordered_domain = ( + expression.AND([unordered_domain, word_domain]) + if unordered_domain + else word_domain + ) + results = _extend_name_results( + self, expression.AND([base_domain, unordered_domain]), results, limit + ) + + results = results[:limit] + records = self.browse(results) + return [(record.id, record.display_name) for record in records] @api.model def _get_view(self, view_id=None, view_type="form", **options): @@ -224,38 +223,9 @@ def check_name_search_domain(self): RecursionError, ) as e: raise ValidationError( - _("Couldn't eval Name Search Domain (%s)") % e + self.env._("Couldn't eval Name Search Domain (%s)") % e ) from e if not isinstance(name_search_domain, list): - raise ValidationError(_("Name Search Domain must be a list of tuples")) - - def _register_hook(self): - """Apply monkey patches. - - Patch `fields_view_get` on the base model, and a freshly generated copy - of `_name_search` on each concrete model. - - We want to skip abstract models because patching those may mess up the - inheritance. An example of this is `ir.model` which is assigned the - studio mixin using `inherit = ['studio.mixin', 'ir.model']`. - If the mixin itself is patched, and the method is overridden once again - (in, say, enterprise's `documents_spreadsheet`), the super() method - called in that override is the patched version of `studio.mixin` rather - than the override of `ir.model` in the base module, which is now skipped - entirely. - """ - _logger.info("Patching BaseModel for Smart Search") - - patched_models = defaultdict(set) - - def patch(model, name, method): - if model not in patched_models[name]: - ModelClass = type(model) - method.origin = getattr(ModelClass, name) - setattr(ModelClass, name, method) - - for model in self.sudo().search(self.ids or []): - Model = self.env.get(model.model) - if Model is not None and not Model._abstract: - patch(Model, "_name_search", patch_name_search()) - return super()._register_hook() + raise ValidationError( + self.env._("Name Search Domain must be a list of tuples") + ) diff --git a/base_name_search_improved/static/description/index.html b/base_name_search_improved/static/description/index.html index 5e41a927a1c..761b6c21f93 100644 --- a/base_name_search_improved/static/description/index.html +++ b/base_name_search_improved/static/description/index.html @@ -369,7 +369,7 @@

Improved Name Search

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:fe0fce7aeb356dfbf982cb648994712ec1907ee6ab73a221eee4cda4039e7a33 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Beta License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runboat

Extends the name search feature to use additional, more relaxed matching methods, and to allow searching into configurable additional record fields.

@@ -450,7 +450,7 @@

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -feedback.

+feedback.

Do not contact contributors directly about support or help with technical issues.

@@ -486,7 +486,7 @@

Maintainers

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

-

This module is part of the OCA/server-tools project on GitHub.

+

This module is part of the OCA/server-tools project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

diff --git a/base_name_search_improved/tests/test_name_search.py b/base_name_search_improved/tests/test_name_search.py index 2a6f9df3be2..fcdd44e0f96 100644 --- a/base_name_search_improved/tests/test_name_search.py +++ b/base_name_search_improved/tests/test_name_search.py @@ -41,38 +41,40 @@ def setUpClass(cls): def test_RelevanceOrderedResults(self): """Return results ordered by relevance""" - res = self.Partner._name_search("555 777") - self.assertEqual(res[0], self.partner1.id, "Match full string honoring spaces") + res = self.Partner.name_search("555 777") self.assertEqual( - res[1], self.partner2.id, "Match words honoring order of appearance" + res[0][0], self.partner1.id, "Match full string honoring spaces" ) self.assertEqual( - res[2], + res[1][0], self.partner2.id, "Match words honoring order of appearance" + ) + self.assertEqual( + res[2][0], self.partner3.id, "Match all words, regardless of order of appearance", ) def test_NameSearchMustMatchAllWords(self): """Must Match All Words""" - res = self.Partner._name_search("ulm aaa 555 777") + res = self.Partner.name_search("ulm aaa 555 777") self.assertFalse(res) def test_NameSearchDifferentFields(self): """Must Match All Words""" - res = self.Partner._name_search("ulm 555 777") + res = self.Partner.name_search("ulm 555 777") self.assertEqual(len(res), 1) def test_NameSearchDomain(self): """Must not return a partner with parent""" - res = self.Partner._name_search("Edward Foster") + res = self.Partner.name_search("Edward Foster") self.assertFalse(res) def test_MustHonorDomain(self): """Must also honor a provided Domain""" - res = self.Partner._name_search("+351", domain=[("vat", "=", "3333")]) + res = self.Partner.name_search("+351", args=[("vat", "=", "3333")]) gambulputty = self.partner3.id self.assertEqual(len(res), 1) - self.assertEqual(res[0], gambulputty) + self.assertEqual(res[0][0], gambulputty) def test_SmartSearchWarning(self): """Must check the funtional work of _compute_smart_search_warning""" diff --git a/base_name_search_improved/views/ir_model_views.xml b/base_name_search_improved/views/ir_model_views.xml index 93d91ed6374..31a5c81692a 100644 --- a/base_name_search_improved/views/ir_model_views.xml +++ b/base_name_search_improved/views/ir_model_views.xml @@ -50,7 +50,6 @@

- - + - + @@ -93,11 +92,10 @@ - view.model.tree + view.model.list ir.model - - + - + @@ -143,12 +141,12 @@ Smart Searches ir.model - tree,form + list,form