diff --git a/addons/mail/models/mail_alias.py b/addons/mail/models/mail_alias.py index 91288442b06..590b86a7dd0 100644 --- a/addons/mail/models/mail_alias.py +++ b/addons/mail/models/mail_alias.py @@ -38,6 +38,8 @@ class Alias(models.Model): alias_name = fields.Char( 'Alias Name', copy=False, help="The name of the email alias, e.g. 'jobs' if you want to catch emails for ") + alias_full_name = fields.Char('Alias Email', compute='_compute_alias_full_name', store=True) + display_name = fields.Char(string='Display Name', compute='_compute_display_name') alias_domain_id = fields.Many2one( 'mail.alias.domain', string='Alias Domain', ondelete='restrict', default=lambda self: self.env.company.alias_domain_id) @@ -210,6 +212,18 @@ class Alias(models.Model): alias_names=', '.join(failing.mapped('display_name'))) ) + @api.depends('alias_domain_id.name', 'alias_name') + def _compute_alias_full_name(self): + """ A bit like display_name, but without the 'inactive alias' UI display. + Moreover it is stored, allowing to search on it. """ + for record in self: + if record.alias_domain_id and record.alias_name: + record.alias_full_name = f"{record.alias_name}@{record.alias_domain_id.name}" + elif record.alias_name: + record.alias_full_name = record.alias_name + else: + record.alias_full_name = False + @api.depends('alias_domain', 'alias_name') def _compute_display_name(self): """ Return the mail alias display alias_name, including the catchall diff --git a/addons/mail/models/mail_alias_mixin_optional.py b/addons/mail/models/mail_alias_mixin_optional.py index 9d18e05c613..fd8e383dd0e 100644 --- a/addons/mail/models/mail_alias_mixin_optional.py +++ b/addons/mail/models/mail_alias_mixin_optional.py @@ -25,7 +25,7 @@ class AliasMixinOptional(models.AbstractModel): related='alias_id.alias_domain_id', readonly=False) alias_domain = fields.Char('Alias Domain Name', related='alias_id.alias_domain') alias_defaults = fields.Text(related='alias_id.alias_defaults') - alias_email = fields.Char('Email Alias', compute='_compute_alias_email') + alias_email = fields.Char('Email Alias', compute='_compute_alias_email', search='_search_alias_email') @api.depends('alias_domain', 'alias_name') def _compute_alias_email(self): @@ -36,6 +36,9 @@ class AliasMixinOptional(models.AbstractModel): for record in self.filtered(lambda rec: rec.alias_name and rec.alias_domain): record.alias_email = f"{record.alias_name}@{record.alias_domain}" + def _search_alias_email(self, operator, operand): + return [('alias_id.alias_full_name', operator, operand)] + # -------------------------------------------------- # CRUD # -------------------------------------------------- diff --git a/addons/test_mail/tests/test_mail_alias.py b/addons/test_mail/tests/test_mail_alias.py index 7a43359aca7..13116ff19fb 100644 --- a/addons/test_mail/tests/test_mail_alias.py +++ b/addons/test_mail/tests/test_mail_alias.py @@ -348,6 +348,69 @@ class TestMailAlias(TestMailAliasCommon): for alias in aliases: self.assertEqual(alias.alias_name, False) + @users('admin') + def test_search(self): + """ Test search on aliases, notably searching on display_name which should + be split on searching on alias_name and alias_domain_id. """ + # ensure existing aliases to ease future asserts + existing = self.env['mail.alias'].search([('alias_domain_id', '!=', False)]) + self.assertEqual(existing.alias_domain_id, self.mail_alias_domain) + existing.write({'alias_name': False}) # don't be annoyed by existing aliases + + mail_alias_domain = self.mail_alias_domain.with_env(self.env) + mail_alias_domain_c2 = self.mail_alias_domain_c2.with_env(self.env) + self.assertEqual(mail_alias_domain.name, 'test.mycompany.com') + self.assertEqual(mail_alias_domain_c2.name, 'test.mycompany2.com') + + aliases = self.env['mail.alias'].create([ + { + 'alias_model_id': self.env['ir.model']._get('mail.test.container.mc').id, + 'alias_name': f'test.search.{idx}', + 'alias_domain_id': domain.id, + } + for idx in range(5) + for domain in (mail_alias_domain + mail_alias_domain_c2) + ]) + aliases_d1 = aliases.filtered(lambda a: a.alias_domain_id == mail_alias_domain) + aliases_d2 = aliases.filtered(lambda a: a.alias_domain_id == mail_alias_domain_c2) + + # search on alias_name: classic search + self.assertEqual( + self.env['mail.alias'].search([('alias_name', 'ilike', 'test.search')]), + aliases + ) + + # search on alias_fullname: search on aggregated of {name}@{domain} + for search_term, expected, msg in [ + ('mycompany', aliases, + 'Match all aliases on both domains as "mycompany" is contained in those two'), + (mail_alias_domain.name, aliases_d1, + 'Exact match on domain 1: should find all aliases in that domain'), + (mail_alias_domain_c2.name, aliases_d2, + 'Exact match on domain 2: should find all aliases in that domain'), + ('search.0@test.mycompany', aliases.filtered(lambda a: a.alias_name == 'test.search.0'), + 'Match in both domains'), + ('search.0@test.mycompany.com', aliases.filtered(lambda a: a.alias_name == 'test.search.0' and a.alias_domain_id == mail_alias_domain), + 'Match only in domain 1'), + ('search@test.mycompany.com', self.env['mail.alias'], + 'Does not match even as ilike'), + ]: + with self.subTest(search_term=search_term): + self.assertEqual( + self.env['mail.alias'].search([('alias_full_name', 'ilike', search_term)]), + expected, msg + ) + + # search using IN operator + for search_term, expected, msg in [ + (['mycompany'], self.env['mail.alias'], 'mycompany is too vague: does not match a left- and right- part (!= ilike)'), + ([mail_alias_domain.name], self.env['mail.alias'], 'Match only right-part of aliases emails'), + ]: + with self.subTest(search_term=search_term): + self.assertEqual(self.env['mail.alias'].search([('alias_full_name', 'in', search_term)]), + expected, msg + ) + @users('admin') def test_alias_setup(self): """ Test various constraints / configuration of alias model""" @@ -679,6 +742,32 @@ class TestMailAliasMixin(TestMailAliasCommon): with self.assertRaises(exceptions.ValidationError): record.write({'alias_defaults': "{'custom_field': brokendict"}) + @users('erp_manager') + def test_alias_mixin_alias_email(self): + """ Test 'alias_email' mixin field computation and search capability """ + Model = self.env['mail.test.container.mc'] + records = Model.create([ + { + 'alias_name': f'alias.email.{idx}', # will be present in all companies + 'company_id': company.id, + 'name': f'Test {company.id} {idx}', + } + for company in (self.company_admin, self.company_2) + for idx in range(5) + ]) + self.assertEqual( + Model.search([('alias_email', 'ilike', 'alias.email')]), records, + 'Search: partial search: any domain, matching all left parts') + self.assertEqual( + Model.search([('alias_email', 'ilike', 'alias.email.0')]), records[0] + records[5], + 'Search: partial search: any domain, matching some left parts') + self.assertEqual( + Model.search([('alias_email', '=', self.mail_alias_domain.name)]), Model, + 'Search: partial search: does not match any complete email') + self.assertEqual( + Model.search([('alias_email', '=', f'alias.email.1@{self.mail_alias_domain.name}')]), records[1], + 'Search: both part search: search on name + domain') + @users('employee') @mute_logger('odoo.addons.base.models.ir_model') def test_alias_mixin_alias_id_management(self):