diff --git a/base_name_search_improved/__manifest__.py b/base_name_search_improved/__manifest__.py index 9653b979b..d3893ad67 100644 --- a/base_name_search_improved/__manifest__.py +++ b/base_name_search_improved/__manifest__.py @@ -1,19 +1,15 @@ # © 2016 Daniel Reis # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { - 'name': 'Improved Name Search', - 'summary': 'Friendlier search when typing in relation fields', - 'version': '12.0.1.1.0', - 'category': 'Uncategorized', - 'website': 'https://odoo-community.org/', - 'author': 'Daniel Reis, Odoo Community Association (OCA), ADHOC SA', - 'license': 'AGPL-3', - 'data': [ - 'views/ir_model_views.xml', - ], - 'depends': [ - 'base', - ], - 'installable': True, + "name": "Improved Name Search", + "summary": "Friendlier search when typing in relation fields", + "version": "12.0.1.1.0", + "category": "Uncategorized", + "website": "https://odoo-community.org/", + "author": "Daniel Reis, Odoo Community Association (OCA), ADHOC SA", + "license": "AGPL-3", + "data": ["views/ir_model_views.xml",], + "depends": ["base",], + "installable": True, "uninstall_hook": "uninstall_hook", } diff --git a/base_name_search_improved/hooks.py b/base_name_search_improved/hooks.py index 295f4f11e..511185688 100644 --- a/base_name_search_improved/hooks.py +++ b/base_name_search_improved/hooks.py @@ -1,6 +1,7 @@ -from odoo import api, models import logging +from odoo import api, models + _logger = logging.getLogger(__name__) diff --git a/base_name_search_improved/models/ir_model.py b/base_name_search_improved/models/ir_model.py index 103a2a3be..ad2bf9199 100644 --- a/base_name_search_improved/models/ir_model.py +++ b/base_name_search_improved/models/ir_model.py @@ -1,32 +1,34 @@ # © 2016 Daniel Reis # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import models, fields, api, tools, _ -from lxml import etree -from ast import literal_eval -from odoo.exceptions import ValidationError import logging +from ast import literal_eval + +from lxml import etree + +from odoo import _, api, fields, models, tools +from odoo.exceptions import ValidationError + _logger = logging.getLogger(__name__) # Extended name search is only used on some operators -ALLOWED_OPS = set(['ilike', 'like']) +ALLOWED_OPS = {"ilike", "like"} @tools.ormcache(skiparg=0) def _get_rec_names(self): "List of fields to search into" - model = self.env['ir.model'].search( - [('model', '=', str(self._name))]) + model = self.env["ir.model"].search([("model", "=", str(self._name))]) rec_name = [self._rec_name] or [] - other_names = model.name_search_ids.mapped('name') + other_names = model.name_search_ids.mapped("name") return rec_name + other_names @tools.ormcache(skiparg=0) def _get_add_smart_search(self): "Add Smart Search on search views" - model = self.env['ir.model'].search([('model', '=', str(self._name))]) + model = self.env["ir.model"].search([("model", "=", str(self._name))]) # Run only if module is installed - if hasattr(model, 'add_smart_search'): + if hasattr(model, "add_smart_search"): return model.add_smart_search return False @@ -34,8 +36,11 @@ def _get_add_smart_search(self): @tools.ormcache(skiparg=0) def _get_name_search_domain(self): "Add Smart Search on search views" - name_search_domain = self.env['ir.model'].search( - [('model', '=', str(self._name))]).name_search_domain + name_search_domain = ( + self.env["ir.model"] + .search([("model", "=", str(self._name))]) + .name_search_domain + ) if name_search_domain: return literal_eval(name_search_domain) return [] @@ -44,11 +49,12 @@ def _get_name_search_domain(self): def _extend_name_results(self, domain, results, limit): result_count = len(results) if result_count < limit: - domain += [('id', 'not in', [x[0] for x in results])] + domain += [("id", "not in", [x[0] for x in results])] recs = self.search(domain, limit=limit - result_count) results.extend(recs.name_get()) return results + # TODO move all this to register_hook @@ -60,11 +66,17 @@ def _add_magic_fields(self): res = _add_magic_fields_original(self) if ( - 'base_name_search_improved' in self.env.registry._init_modules and - 'smart_search' not in self._fields): - self._add_field('smart_search', fields.Char( - automatic=True, compute='_compute_smart_search', - search='_search_smart_search')) + "base_name_search_improved" in self.env.registry._init_modules + and "smart_search" not in self._fields + ): + self._add_field( + "smart_search", + fields.Char( + automatic=True, + compute="_compute_smart_search", + search="_search_smart_search", + ), + ) return res @@ -72,53 +84,44 @@ models.BaseModel._add_magic_fields = _add_magic_fields class IrModel(models.Model): - _inherit = 'ir.model' + _inherit = "ir.model" - add_smart_search = fields.Boolean( - help="Add Smart Search on search views", - ) - name_search_ids = fields.Many2many( - 'ir.model.fields', - string='Name Search Fields') - name_search_domain = fields.Char( - ) + add_smart_search = fields.Boolean(help="Add Smart Search on search views",) + name_search_ids = fields.Many2many("ir.model.fields", string="Name Search Fields") + name_search_domain = fields.Char() - @api.constrains( - 'name_search_ids', 'name_search_domain', 'add_smart_search') + @api.constrains("name_search_ids", "name_search_domain", "add_smart_search") def update_search_wo_restart(self): self.clear_caches() - @api.constrains('name_search_domain') + @api.constrains("name_search_domain") def check_name_search_domain(self): - for rec in self.filtered('name_search_domain'): + for rec in self.filtered("name_search_domain"): name_search_domain = False try: name_search_domain = literal_eval(rec.name_search_domain) except Exception as error: - raise ValidationError(_( - "Couldn't eval Name Search Domain (%s)") % error) + raise ValidationError( + _("Couldn't eval Name Search Domain (%s)") % error + ) if not isinstance(name_search_domain, list): - raise ValidationError(_( - 'Name Search Domain must be a list of tuples')) + raise ValidationError(_("Name Search Domain must be a list of tuples")) @api.model_cr def _register_hook(self): - def make_name_search(): - @api.model - def name_search(self, name='', args=None, - operator='ilike', limit=100): + def name_search(self, name="", args=None, operator="ilike", limit=100): limit = limit or 0 - enabled = self.env.context.get('name_search_extended', True) + enabled = self.env.context.get("name_search_extended", True) superself = self.sudo() if enabled: # we add domain args = args or [] + _get_name_search_domain(superself) # Perform standard name search res = name_search.origin( - self, name=name, args=args, operator=operator, - limit=limit) + self, name=name, args=args, operator=operator, limit=limit + ) # Perform extended name search # Note: Empty name causes error on # Customer->More->Portal Access Management @@ -130,13 +133,14 @@ class IrModel(models.Model): for rec_name in all_names[1:]: domain = [(rec_name, operator, name)] res = _extend_name_results( - self, base_domain + domain, res, limit) + 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(' ', '%'))] + domain = [(rec_name, operator, name.replace(" ", "%"))] res = _extend_name_results( - self, base_domain + domain, res, limit) + 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 @@ -147,39 +151,44 @@ class IrModel(models.Model): word_domain = [] for rec_name in all_names: word_domain = ( - word_domain and ['|'] + word_domain or - word_domain + word_domain and ["|"] + word_domain or word_domain ) + [(rec_name, operator, word)] - domain = ( - domain and ['&'] + domain or domain - ) + word_domain + domain = (domain and ["&"] + domain or domain) + word_domain res = _extend_name_results( - self, base_domain + domain, res, limit) + self, base_domain + domain, res, limit + ) return res + return name_search def patch_fields_view_get(): @api.model def fields_view_get( - self, view_id=None, view_type='form', toolbar=False, - submenu=False): + self, view_id=None, view_type="form", toolbar=False, submenu=False + ): res = fields_view_get.origin( - self, view_id=view_id, view_type=view_type, - toolbar=toolbar, submenu=submenu) - if view_type == 'search' and _get_add_smart_search(self): - eview = etree.fromstring(res['arch']) + self, + view_id=view_id, + view_type=view_type, + toolbar=toolbar, + submenu=submenu, + ) + if view_type == "search" and _get_add_smart_search(self): + eview = etree.fromstring(res["arch"]) placeholders = eview.xpath("//search/field") if placeholders: placeholder = placeholders[0] else: placeholder = eview.xpath("//search")[0] placeholder.addnext( - etree.Element('field', {'name': 'smart_search'})) + etree.Element("field", {"name": "smart_search"}) + ) eview.remove(placeholder) - res['arch'] = etree.tostring(eview) - res['fields'].update(self.fields_get(['smart_search'])) + res["arch"] = etree.tostring(eview) + res["fields"].update(self.fields_get(["smart_search"])) return res + return fields_view_get @api.multi @@ -197,7 +206,7 @@ class IrModel(models.Model): primero los que mejor coinciden, en este caso eso no es necesario Igualmente seguro se puede mejorar y unificar bastante código """ - enabled = self.env.context.get('name_search_extended', True) + enabled = self.env.context.get("name_search_extended", True) name = value if name and enabled and operator in ALLOWED_OPS: superself = self.sudo() @@ -207,29 +216,25 @@ class IrModel(models.Model): word_domain = [] for rec_name in all_names: word_domain = ( - word_domain and ['|'] + word_domain or - word_domain + word_domain and ["|"] + word_domain or word_domain ) + [(rec_name, operator, word)] - domain = ( - domain and ['&'] + domain or domain - ) + word_domain + domain = (domain and ["&"] + domain or domain) + word_domain return domain return [] # add methods of computed fields - if not hasattr(models.BaseModel, '_compute_smart_search'): + if not hasattr(models.BaseModel, "_compute_smart_search"): models.BaseModel._compute_smart_search = _compute_smart_search - if not hasattr(models.BaseModel, '_search_smart_search'): + if not hasattr(models.BaseModel, "_search_smart_search"): models.BaseModel._search_smart_search = _search_smart_search - _logger.info('Patching fields_view_get on BaseModel') - models.BaseModel._patch_method( - 'fields_view_get', patch_fields_view_get()) + _logger.info("Patching fields_view_get on BaseModel") + models.BaseModel._patch_method("fields_view_get", patch_fields_view_get()) for model in self.sudo().search(self.ids or []): Model = self.env.get(model.model) if Model is not None: - Model._patch_method('name_search', make_name_search()) + Model._patch_method("name_search", make_name_search()) return super(IrModel, self)._register_hook() diff --git a/base_name_search_improved/tests/test_name_search.py b/base_name_search_improved/tests/test_name_search.py index 1d17bae3a..09d6d46b9 100644 --- a/base_name_search_improved/tests/test_name_search.py +++ b/base_name_search_improved/tests/test_name_search.py @@ -7,62 +7,64 @@ from odoo.tests.common import TransactionCase, at_install, post_install @at_install(False) @post_install(True) class NameSearchCase(TransactionCase): - def setUp(self): super(NameSearchCase, self).setUp() - phone_field = self.env.ref('base.field_res_partner_phone') - model_partner = self.env.ref('base.model_res_partner') + phone_field = self.env.ref("base.field_res_partner_phone") + model_partner = self.env.ref("base.model_res_partner") model_partner.name_search_ids = phone_field model_partner.add_smart_search = True # this use does not make muche sense but with base module we dont have # much models to use for tests model_partner.name_search_domain = "[('parent_id', '=', False)]" - self.Partner = self.env['res.partner'] + self.Partner = self.env["res.partner"] self.partner1 = self.Partner.create( - {'name': 'Luigi Verconti', - 'customer': True, - 'phone': '+351 555 777 333'}) + {"name": "Luigi Verconti", "customer": True, "phone": "+351 555 777 333"} + ) self.partner2 = self.Partner.create( - {'name': 'Ken Shabby', - 'customer': True, - 'phone': '+351 555 333 777'}) + {"name": "Ken Shabby", "customer": True, "phone": "+351 555 333 777"} + ) self.partner3 = self.Partner.create( - {'name': 'Johann Gambolputty of Ulm', - 'supplier': True, - 'phone': '+351 777 333 555'}) + { + "name": "Johann Gambolputty of Ulm", + "supplier": True, + "phone": "+351 777 333 555", + } + ) def test_RelevanceOrderedResults(self): """Return results ordered by relevance""" - res = self.Partner.name_search('555 777') + res = self.Partner.name_search("555 777") self.assertEqual( - res[0][0], self.partner1.id, - 'Match full string honoring spaces') + res[0][0], self.partner1.id, "Match full string honoring spaces" + ) self.assertEqual( - res[1][0], self.partner2.id, - 'Match words honoring order of appearance') + 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') + 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', args=[('supplier', '=', True)]) + res = self.Partner.name_search("+351", args=[("supplier", "=", True)]) gambulputty = self.partner3.id self.assertEqual(len(res), 1) self.assertEqual(res[0][0], gambulputty) diff --git a/base_name_search_improved/views/ir_model_views.xml b/base_name_search_improved/views/ir_model_views.xml index 71d9c71e9..d70ba9aab 100644 --- a/base_name_search_improved/views/ir_model_views.xml +++ b/base_name_search_improved/views/ir_model_views.xml @@ -1,25 +1,23 @@ - + - - Add Name Searchable to Models ir.model - + - - - + + + - view.model.form ir.model @@ -27,37 +25,51 @@
-
-
- - - + + + - + - - - - + + + + @@ -66,52 +78,70 @@
- view.model.tree ir.model - - - - - + + + + + - view.model.search ir.model - - - - - + + + + + - Custom Searches ir.model form tree,form - - + + )]" + /> {'search_default_extra_search': 1} [('transient', '=', False)] - - - + />