From e8a41762ad599e08535cdfef9bcb433343b7d285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 22 Oct 2021 16:47:46 +0200 Subject: [PATCH] [IMP] mail: link 'mail.alias_domain' to aliases PURPOSE Allow alias domains to be multiple, notably to be used in a multi company environment where each company has its own alias domain. SPECIFICATIONS Add 'mail.alias.domain' information on alias model. Aliases do not use global configuration parameters anymore. Instead they are linked to an alias domain e.g. 'sales' linked to 'mycompany.com' alias domain: 'sales@mycompany.com'. It is now considered as a different alias compared to 'sales@mycompany.in' which has the same alias_name but a different alias domain. Constraints and checks are added, as * uniqueness of aliases is now checked inside a given domain; * an alias name must not clash with its domain bounce or catchall; * a combination of (alias_name, alias_domain_id) must be unique; Crm multi-company environment is also updated to match the new alias domain behavior. Task-36879 (Mail: Support Multi Domains Aliases) Part-of: odoo/odoo#76734 --- addons/crm/tests/common.py | 12 ++ .../crm/tests/test_crm_lead_notification.py | 26 ++- .../hr_expense/models/res_config_settings.py | 1 + .../models/hr_recruitment_source.py | 1 + addons/mail/models/mail_alias.py | 191 +++++++++-------- addons/mail/models/mail_alias_domain.py | 17 +- .../mail/models/mail_alias_mixin_optional.py | 68 +++++- addons/mail/models/models.py | 49 ++++- addons/mail/views/mail_alias_views.xml | 10 +- addons/test_mail/tests/test_mail_alias.py | 199 ++++++++++++++---- 10 files changed, 426 insertions(+), 148 deletions(-) diff --git a/addons/crm/tests/common.py b/addons/crm/tests/common.py index 468f8cd46cc..053c8aaca71 100644 --- a/addons/crm/tests/common.py +++ b/addons/crm/tests/common.py @@ -265,6 +265,18 @@ class TestCrmCommon(TestSalesCommon, MailCase): 'email': 'company.2@test.example.com', 'name': 'New Test Company', }) + cls.alias_bounce_c2 = 'bounce.c2' + cls.alias_catchall_c2 = 'catchall.c2' + cls.alias_default_from_c2 = 'notifications.c2' + cls.alias_domain_c2_name = 'test.mycompany2.com' + cls.mail_alias_domain_c2 = cls.env['mail.alias.domain'].create({ + 'bounce_alias': cls.alias_bounce_c2, + 'catchall_alias': cls.alias_catchall_c2, + 'company_ids': [(4, cls.company_2.id)], + 'default_from': cls.alias_default_from_c2, + 'name': cls.alias_domain_c2_name, + 'sequence': 2, + }) cls.user_sales_manager_mc = mail_new_test_user( cls.env, diff --git a/addons/crm/tests/test_crm_lead_notification.py b/addons/crm/tests/test_crm_lead_notification.py index 046271094c3..1ecc805ad86 100644 --- a/addons/crm/tests/test_crm_lead_notification.py +++ b/addons/crm/tests/test_crm_lead_notification.py @@ -14,6 +14,8 @@ class NewLeadNotification(TestCrmCommon): def setUpClass(cls): """ Activate some langs to test lang propagation in various mail flows """ super(NewLeadNotification, cls).setUpClass() + cls._activate_multi_company() + cls.test_email = '"Test Email" ' model_lang = cls.env['res.lang'].sudo().with_context(active_test=False) @@ -221,14 +223,14 @@ class NewLeadNotification(TestCrmCommon): @mute_logger('odoo.addons.mail.models.mail_thread') def test_new_lead_from_email_multicompany(self): company0 = self.env.company - company1 = self.env['res.company'].create({'name': 'new_company'}) + company1 = self.company_2 self.env.user.write({ 'company_ids': [(4, company0.id, False), (4, company1.id, False)], }) - crm_team_model = self.env['ir.model'].search([('model', '=', 'crm.team')]) - crm_lead_model = self.env['ir.model'].search([('model', '=', 'crm.lead')]) + crm_team_model_id = self.env['ir.model']._get_id('crm.team') + crm_lead_model_id = self.env['ir.model']._get_id('crm.lead') crm_team0 = self.env['crm.team'].create({ 'name': 'crm team 0', @@ -240,16 +242,18 @@ class NewLeadNotification(TestCrmCommon): }) mail_alias0 = self.env['mail.alias'].create({ + 'alias_domain_id': company0.alias_domain_id.id, 'alias_name': 'sale_team_0', - 'alias_model_id': crm_lead_model.id, - 'alias_parent_model_id': crm_team_model.id, + 'alias_model_id': crm_lead_model_id, + 'alias_parent_model_id': crm_team_model_id, 'alias_parent_thread_id': crm_team0.id, 'alias_defaults': "{'type': 'opportunity', 'team_id': %s}" % crm_team0.id, }) mail_alias1 = self.env['mail.alias'].create({ + 'alias_domain_id': company1.alias_domain_id.id, 'alias_name': 'sale_team_1', - 'alias_model_id': crm_lead_model.id, - 'alias_parent_model_id': crm_team_model.id, + 'alias_model_id': crm_lead_model_id, + 'alias_parent_model_id': crm_team_model_id, 'alias_parent_thread_id': crm_team1.id, 'alias_defaults': "{'type': 'opportunity', 'team_id': %s}" % crm_team1.id, }) @@ -257,12 +261,12 @@ class NewLeadNotification(TestCrmCommon): crm_team0.write({'alias_id': mail_alias0.id}) crm_team1.write({'alias_id': mail_alias1.id}) - new_message0 = """MIME-Version: 1.0 + new_message0 = f"""MIME-Version: 1.0 Date: Thu, 27 Dec 2018 16:27:45 +0100 Message-ID: Subject: sale team 0 in company 0 From: A client -To: sale_team_0@test.mycompany.com +To: {mail_alias0.display_name} Content-Type: multipart/alternative; boundary="000000000000a47519057e029630" --000000000000a47519057e029630 @@ -278,12 +282,12 @@ Content-Transfer-Encoding: quoted-printable --000000000000a47519057e029630-- """ - new_message1 = """MIME-Version: 1.0 + new_message1 = f"""MIME-Version: 1.0 Date: Thu, 27 Dec 2018 16:27:45 +0100 Message-ID: Subject: sale team 1 in company 1 From: B client -To: sale_team_1@test.mycompany.com +To: {mail_alias1.display_name} Content-Type: multipart/alternative; boundary="000000000000a47519057e029630" --000000000000a47519057e029630 diff --git a/addons/hr_expense/models/res_config_settings.py b/addons/hr_expense/models/res_config_settings.py index c82e228920f..852e0d81601 100644 --- a/addons/hr_expense/models/res_config_settings.py +++ b/addons/hr_expense/models/res_config_settings.py @@ -40,6 +40,7 @@ class ResConfigSettings(models.TransientModel): # create data again alias = self.env['mail.alias'].sudo().create({ 'alias_contact': 'employees', + 'alias_domain_id': self.env.company.alias_domain_id.id, 'alias_model_id': self.env['ir.model']._get_id('hr.expense'), 'alias_name': self.hr_expense_alias_prefix, }) diff --git a/addons/hr_recruitment/models/hr_recruitment_source.py b/addons/hr_recruitment/models/hr_recruitment_source.py index 2b8bd6ea3c2..8a3e765422e 100644 --- a/addons/hr_recruitment/models/hr_recruitment_source.py +++ b/addons/hr_recruitment/models/hr_recruitment_source.py @@ -30,6 +30,7 @@ class RecruitmentSource(models.Model): 'medium_id': medium.id, 'source_id': source.source_id.id, }, + 'alias_domain_id': source.job_id.company_id.alias_domain_id.id or self.env.company.alias_domain_id.id, 'alias_model_id': self.env['ir.model']._get_id('hr.applicant'), 'alias_name': f"{source.job_id.alias_name or source.job_id.name}+{source.name}", 'alias_parent_thread_id': source.job_id.id, diff --git a/addons/mail/models/mail_alias.py b/addons/mail/models/mail_alias.py index c06f45919aa..40f39d05240 100644 --- a/addons/mail/models/mail_alias.py +++ b/addons/mail/models/mail_alias.py @@ -3,11 +3,12 @@ import ast import re - +from collections import defaultdict from markupsafe import Markup from odoo import _, api, fields, models from odoo.exceptions import ValidationError, UserError +from odoo.osv import expression from odoo.tools import is_html_empty, remove_accents # see rfc5322 section 3.2.3 @@ -37,7 +38,10 @@ 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_domain = fields.Char('Alias domain', compute='_compute_alias_domain') + alias_domain_id = fields.Many2one( + 'mail.alias.domain', string='Alias Domain', ondelete='restrict', + default=lambda self: self.env.company.alias_domain_id) + alias_domain = fields.Char('Alias domain name', related='alias_domain_id.name') # target: create / update alias_model_id = fields.Many2one('ir.model', 'Aliased Model', required=True, ondelete="cascade", help="The model (Odoo Document Kind) to which this alias " @@ -86,9 +90,14 @@ class Alias(models.Model): ], compute='_compute_alias_status', store=True, help='Alias status assessed on the last message received.') - _sql_constraints = [ - ('alias_unique', 'UNIQUE(alias_name)', 'Unfortunately this email alias is already used, please choose a unique one') - ] + def init(self): + """Make sure there aren't multiple records for the same name and alias + domain. Not in _sql_constraint because COALESCE is not supported for + PostgreSQL constraint. """ + self.env.cr.execute(""" + CREATE UNIQUE INDEX IF NOT EXISTS mail_alias_name_domain_unique + ON mail_alias (alias_name, COALESCE(alias_domain_id, 0)) + """) @api.constrains('alias_name') def _check_alias_is_ascii(self): @@ -115,6 +124,19 @@ class Alias(models.Model): _('Invalid expression, it must be a literal python dictionary definition e.g. "{\'field\': \'value\'}"') ) from e + @api.constrains('alias_name', 'alias_domain_id') + def _check_alias_domain_clash(self): + """ Within a given alias domain, aliases should not conflict with bounce + or catchall email addresses, as emails should be unique for the gateway. """ + failing = self.filtered(lambda alias: alias.alias_name and alias.alias_name in [ + alias.alias_domain_id.bounce_alias, alias.alias_domain_id.catchall_alias + ]) + if failing: + raise ValidationError( + _('Aliases %(alias_names)s is already used as bounce or catchall address. Please choose another alias.', + alias_names=', '.join(failing.mapped('display_name'))) + ) + @api.depends('alias_domain', 'alias_name') def _compute_display_name(self): """ Return the mail alias display alias_name, including the catchall @@ -128,10 +150,6 @@ class Alias(models.Model): else: record.display_name = _("Inactive Alias") - @api.depends('alias_name') - def _compute_alias_domain(self): - self.alias_domain = self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain") - @api.depends('alias_contact', 'alias_defaults', 'alias_model_id') def _compute_alias_status(self): """Reset alias_status to "not_tested" when fields, that can be the source of an error, are modified.""" @@ -139,33 +157,48 @@ class Alias(models.Model): @api.model_create_multi def create(self, vals_list): - """ Creates email.alias records according to the values provided in - ``vals`` with 1 alteration: + """ Creates mail.alias records according to the values provided in + ``vals`` but sanitize 'alias_name' by replacing certain unsafe + characters; set default alias domain if not given. - * ``alias_name`` value may be cleaned by replacing certain unsafe - characters; - - :raise UserError: if given alias_name is already assigned or there are - duplicates in given vals_list; + :raise UserError: if given (alias_name, alias_domain_id) already exists + or if there are duplicates in given vals_list; """ - alias_names = [self._sanitize_alias_name(vals.get('alias_name')) for vals in vals_list] - self._check_unique(alias_names) - for vals, alias_name in zip(vals_list, alias_names): - vals['alias_name'] = alias_name + alias_names, alias_domains = [], [] + for vals in vals_list: + vals['alias_name'] = self._sanitize_alias_name(vals.get('alias_name')) + alias_names.append(vals['alias_name']) + vals['alias_domain_id'] = vals.get('alias_domain_id', self.env.company.alias_domain_id.id) + alias_domains.append(self.env['mail.alias.domain'].browse(vals['alias_domain_id'])) + + self._check_unique(alias_names, alias_domains) return super().create(vals_list) def write(self, vals): - """ Raise UserError with a meaningfull message instead of letting the - unicity constraint give its error. """ + """ Raise UserError with a meaningful message instead of letting the + uniqueness constraint raise an SQL error. To check uniqueness we have + to rebuild pairs of names / domains to validate, taking into account + that a void alias_domain_id is acceptable (but also raises for + uniqueness). + """ + alias_names, alias_domains = [], [] if 'alias_name' in vals: vals['alias_name'] = self._sanitize_alias_name(vals['alias_name']) if vals.get('alias_name') and self.ids: - self._check_unique([vals['alias_name']]) - if len(self) > 1: - raise UserError( - _('Email alias %(alias_name)s cannot be used on %(count)d records at the same time. Please update records one by one.', - alias_name=vals['alias_name'], count=len(self)) - ) + alias_names = [vals['alias_name']] * len(self) + elif 'alias_name' not in vals and 'alias_domain_id' in vals: + # avoid checking when writing the same value + if [vals['alias_domain_id']] != self.alias_domain_id.ids: + alias_names = self.filtered('alias_name').mapped('alias_name') + + if alias_names: + tocheck_records = self if vals.get('alias_name') else self.filtered('alias_name') + if 'alias_domain_id' in vals: + alias_domains = [self.env['mail.alias.domain'].browse(vals['alias_domain_id'])] * len(tocheck_records) + else: + alias_domains = [record.alias_domain_id for record in tocheck_records] + self._check_unique(alias_names, alias_domains) + return super().write(vals) def _clean_and_check_mail_catchall_allowed_domains(self, value): @@ -180,70 +213,64 @@ class Alias(models.Model): ) return ",".join(value) - def _check_unique(self, sanitized_names, skip_icp_keys=None): + def _check_unique(self, alias_names, alias_domains): """ Check unicity constraint won't be raised, otherwise raise a UserError with a complete error message. Also check unicity against alias config parameters. - :param list sanitized_names: a list of names (considered as sanitized + :param list alias_names: a list of names (considered as sanitized and ready to be sent to DB); + :param list alias_domains: list of alias_domain records under which + the check is performed, as uniqueness is performed for given pair + (name, alias_domain); """ - valid_names = list(filter(None, sanitized_names)) + if len(alias_names) != len(alias_domains): + msg = (f"Invalid call to '_check_unique': names and domains should make coherent lists, " + f"received {', '.join(alias_names)} and {', '.join(alias_domains.mapped('name'))}") + raise ValueError(msg) - # list itself should be unique obviously - seen = set() - dupes = [name for name in valid_names if name in seen or seen.add(name)] - if dupes: - raise UserError( - _('Email aliases %(alias_name)s cannot be used on several records at the same time. Please update records one by one.', - alias_name=', '.join(dupes)) - ) - - alias_domain = self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain") - icp_to_check = dict( - (icp_key, self.env["ir.config_parameter"].sudo().get_param(icp_key)) - for icp_key in {'mail.bounce.alias', 'mail.catchall.alias'} - if icp_key not in (skip_icp_keys or ()) - ) - icp_label_by_key = { - 'mail.bounce.alias': _('bounce'), - 'mail.catchall.alias': _('catchall'), - } - - # matches catchall or bounce alias - for sanitized_name in valid_names: - for icp_key, icp_value in icp_to_check.items(): - if icp_value and sanitized_name == icp_value: - matching_alias_name = f'{sanitized_name}@{alias_domain}' if alias_domain else sanitized_name - raise UserError( - _('The e-mail alias %(matching_alias_name)s is already used as %(alias_duplicate)s alias. Please choose another alias.', - matching_alias_name=matching_alias_name, - alias_duplicate=icp_label_by_key[icp_key]) - ) + # reorder per alias domain, keep only not void alias names (void domain also checks uniqueness) + domain_to_names = defaultdict(list) + for alias_name, alias_domain in zip(alias_names, alias_domains): + if alias_name and alias_name in domain_to_names[alias_domain]: + raise UserError( + _('Email aliases %(alias_name)s cannot be used on several records at the same time. Please update records one by one.', + alias_name=alias_name) + ) + if alias_name: + domain_to_names[alias_domain].append(alias_name) # matches existing alias - matching_alias = self.env['mail.alias'] - if valid_names: - domain = [('alias_name', 'in', valid_names)] - if self: - domain += [('id', 'not in', self.ids)] - matching_alias = self.search(domain, limit=1) - if not matching_alias: + domain = expression.OR([ + ['&', ('alias_name', 'in', alias_names), ('alias_domain_id', '=', alias_domain.id)] + for alias_domain, alias_names in domain_to_names.items() + ]) + if domain and self: + domain = expression.AND([domain, [('id', 'not in', self.ids)]]) + existing = self.search(domain, limit=1) if domain else self.env['mail.alias'] + if not existing: return - if matching_alias.alias_parent_model_id and matching_alias.alias_parent_thread_id: - # If parent model and parent thread ID both are set, display document name also in the warning - document_name = self.env[matching_alias.alias_parent_model_id.model].sudo().browse(matching_alias.alias_parent_thread_id).display_name - raise UserError( - _('The e-mail alias %(matching_alias_name)s is already used by the %(document_name)s %(model_name)s. Choose another alias or change it on the other document.', - matching_alias_name=matching_alias.display_name, - document_name=document_name, - model_name=matching_alias.alias_parent_model_id.name) - ) - raise UserError( - _('The e-mail alias %(matching_alias_name)s is already linked with %(alias_model_name)s. Choose another alias or change it on the linked model.', - matching_alias_name=matching_alias.display_name, - alias_model_name=matching_alias.alias_model_id.name) - ) + if existing.alias_parent_model_id and existing.alias_parent_thread_id: + parent_name = self.env[existing.alias_parent_model_id.model].sudo().browse(existing.alias_parent_thread_id).display_name + msg_begin = _( + 'Alias %(matching_name)s (%(current_id)s) is already linked with %(alias_model_name)s (%(matching_id)s) and used by the %(parent_name)s %(parent_model_name)s.', + alias_model_name=existing.alias_model_id.name, + current_id=self.ids if self else _('your alias'), + matching_id=existing.id, + matching_name=existing.display_name, + parent_name=parent_name, + parent_model_name=existing.alias_parent_model_id.name + ) + else: + msg_begin = _( + 'Alias %(matching_name)s (%(current_id)s) is already linked with %(alias_model_name)s (%(matching_id)s).', + alias_model_name=existing.alias_model_id.name, + current_id=self.ids if self else _('new'), + matching_id=existing.id, + matching_name=existing.display_name, + ) + msg_end = _('Choose another value or change it on the other document.') + raise UserError(f'{msg_begin} {msg_end}') @api.model def _sanitize_alias_name(self, name): diff --git a/addons/mail/models/mail_alias_domain.py b/addons/mail/models/mail_alias_domain.py index b5ab29bff91..995976bd926 100644 --- a/addons/mail/models/mail_alias_domain.py +++ b/addons/mail/models/mail_alias_domain.py @@ -80,9 +80,15 @@ class AliasDomain(models.Model): catchall=tocheck.catchall_email) ) - existing = self.env['mail.alias'].search( - [('alias_name', 'in', list(set(names)))], - limit=1, + # search on left-part only to speedup, then filter on right part + potential_aliases = self.env['mail.alias'].search([ + ('alias_name', 'in', list(set(names))), + ('alias_domain_id', '!=', False) + ]) + existing = next( + (alias for alias in potential_aliases + if alias.display_name in (self.mapped('bounce_email') + self.mapped('catchall_email'))), + self.env['mail.alias'] ) if existing: document_name = False @@ -126,11 +132,14 @@ class AliasDomain(models.Model): alias_domains = super().create(vals_list) - # alias domain init: populate companies at first creation + # alias domain init: populate companies and aliases at first creation if alias_domains and self.search_count([]) == len(alias_domains): self.env['res.company'].search( [('alias_domain_id', '=', False)] ).alias_domain_id = alias_domains[0].id + self.env['mail.alias'].sudo().search( + [('alias_domain_id', '=', False)] + ).alias_domain_id = alias_domains[0].id return alias_domains diff --git a/addons/mail/models/mail_alias_mixin_optional.py b/addons/mail/models/mail_alias_mixin_optional.py index 95b9edf5e19..271e4eb3291 100644 --- a/addons/mail/models/mail_alias_mixin_optional.py +++ b/addons/mail/models/mail_alias_mixin_optional.py @@ -20,14 +20,13 @@ class AliasMixinOptional(models.AbstractModel): alias_id = fields.Many2one('mail.alias', string='Alias', ondelete="restrict", required=False) alias_name = fields.Char(related='alias_id.alias_name', readonly=False) - alias_domain = fields.Char('Alias domain', compute='_compute_alias_domain') + alias_domain_id = fields.Many2one( + 'mail.alias.domain', string='Alias Domain', + 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') - @api.depends('alias_name') - def _compute_alias_domain(self): - self.alias_domain = self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain") - @api.depends('alias_domain', 'alias_name') def _compute_alias_email(self): """ Alias email can be used in views, as it is Falsy when having no domain @@ -45,15 +44,29 @@ class AliasMixinOptional(models.AbstractModel): def create(self, vals_list): """ Create aliases using sudo if an alias is required, notably if its name is given. """ + # prefetch company information, used for alias domain + company_fname = self._mail_get_company_field() + if company_fname: + company_id_default = self.default_get([company_fname]).get(company_fname) or self.env.company.id + company_prefetch_ids = {vals[company_fname] for vals in vals_list if vals.get(company_fname)} + company_prefetch_ids.add(company_id_default) + else: + company_id_default = self.env.company.id + company_prefetch_ids = {company_id_default} + # prepare all alias values alias_vals_list, record_vals_list = [], [] for vals in vals_list: if vals.get('alias_name'): vals['alias_name'] = self.env['mail.alias']._sanitize_alias_name(vals['alias_name']) if self._require_new_alias(vals): + company_id = vals.get(company_fname) or company_id_default + company = self.env['res.company'].with_prefetch(company_prefetch_ids).browse(company_id) alias_vals, record_vals = self._alias_filter_fields(vals) # generate record-agnostic base alias values - alias_vals.update(self.env[self._name]._alias_get_creation_values()) + alias_vals.update(self.env[self._name].with_context( + default_alias_domain_id=company.alias_domain_id.id, + )._alias_get_creation_values()) alias_vals_list.append(alias_vals) record_vals_list.append(record_vals) @@ -75,15 +88,20 @@ class AliasMixinOptional(models.AbstractModel): records = super().create(valid_vals_list) - for record in records.filtered('alias_id'): - record.alias_id.sudo().write(record._alias_get_creation_values()) + # update alias values with values coming from record, post-create to have + # access to all its values (notably its ID) + records_walias = records.filtered('alias_id') + for record in records_walias: + alias_values = record._alias_get_creation_values() + record.alias_id.sudo().write(alias_values) return records def write(self, vals): """ Split writable fields of mail.alias and other fields alias fields will - write with sudo and the other normally. If alias does not exist and we - try to set a name, create the alias automatically. """ + write with sudo and the other normally. Also handle alias_domain_id + update. If alias does not exist and we try to set a name, create the + alias automatically. """ # create missing aliases if vals.get('alias_name'): alias_create_values = [ @@ -101,6 +119,14 @@ class AliasMixinOptional(models.AbstractModel): alias_vals, record_vals = self._alias_filter_fields(vals, filters=self.ALIAS_WRITEABLE_FIELDS) if record_vals: super().write(record_vals) + + # synchronize alias domain if company environment changed + company_fname = self._mail_get_company_field() + if company_fname in vals: + alias_domain_values = self.filtered('alias_id')._alias_get_alias_domain_id() + for record, alias_domain_id in alias_domain_values.items(): + record.sudo().alias_domain_id = alias_domain_id.id + if alias_vals and (record_vals or self.check_access_rights('write', raise_exception=False)): self.mapped('alias_id').sudo().write(alias_vals) @@ -131,14 +157,34 @@ class AliasMixinOptional(models.AbstractModel): # MIXIN TOOL OVERRIDE METHODS # -------------------------------------------------- + def _alias_get_alias_domain_id(self): + """ Return alias domain value to synchronize with owner's company. + Implementing it with a compute is complicated, as its 'alias_domain_id' + is a field on 'mail.alias' model, coming from 'alias_id' field and due + to current implementation of the mixin, notably the create / write + overrides, compute is not called in all cases. We therefore use a tool + method to call in the mixin. """ + alias_domain_values = {} + record_companies = self._mail_get_companies() + for record in self: + record_company = record_companies[record.id] + alias_domain_values[record] = ( + record_company.alias_domain_id + or record.alias_domain_id or self.env.company.alias_domain_id + ) + return alias_domain_values + def _alias_get_creation_values(self): """ Return values to create an alias, or to write on the alias after its creation. """ - return { + values = { 'alias_parent_thread_id': self.id if self.id else False, 'alias_parent_model_id': self.env['ir.model']._get_id(self._name), } + if self.env.context.get('default_alias_domain_id'): + values['alias_domain_id'] = self.env.context['default_alias_domain_id'] + return values def _alias_filter_fields(self, values, filters=False): """ Split the vals dict into two dictionnary of vals, one for alias diff --git a/addons/mail/models/models.py b/addons/mail/models/models.py index fe6864fa3a6..3b7d39645ad 100644 --- a/addons/mail/models/models.py +++ b/addons/mail/models/models.py @@ -22,6 +22,51 @@ class BaseModel(models.AbstractModel): # FIELDS HELPERS # ------------------------------------------------------------ + def _mail_get_alias_domains(self, default_company=False): + """ Return alias domain linked to each record in self. It is based + on the company (record's company, environment company) and fallback + on the first found alias domain if configuration is not correct. + + :param default_company: default company in case records + have no company (or no company field); defaults to env.company; + + :return: for each record ID in self, found + """ + record_companies = self._mail_get_companies(default=(default_company or self.env.company)) + + # prepare default alias domain, fetch only if necessary + default_domain = (default_company or self.env.company).alias_domain_id + all_companies = self.env['res.company'].browse({comp.id for comp in record_companies.values()}) + # early optimization: search only if necessary + if not default_domain and any(not comp.alias_domain_id for comp in all_companies): + default_domain = self.env['mail.alias.domain'].search([], limit=1) + + return { + record.id: ( + record_companies[record.id].alias_domain_id or default_domain + ) + for record in self + } + + @api.model + def _mail_get_company_field(self): + return 'company_id' if 'company_id' in self else False + + def _mail_get_companies(self, default=False): + """ Return company linked to each record in self. + + :param default: default value if no company field is found + or if it holds a void value. Defaults to a void recordset; + + :return: for each record ID in self, found + """ + default_company = default or self.env['res.company'] + company_fname = self._mail_get_company_field() + return { + record.id: (record[company_fname] or default_company) if company_fname else default_company + for record in self + } + @api.model def _mail_get_partner_fields(self, introspect_fields=False): """ This method returns the fields to use to find the contact to link @@ -33,7 +78,7 @@ class BaseModel(models.AbstractModel): res.partner model. This is used notably when partners are mandatory like in voip; - :return list: list of valid field names that can be used to retrieve + :return: list of valid field names that can be used to retrieve a partner (customer) on the record; """ partner_fnames = [fname for fname in ('partner_id', 'partner_ids') if fname in self] @@ -49,7 +94,7 @@ class BaseModel(models.AbstractModel): :param bool introspect_fields: see '_mail_get_partner_fields'; - :return dict: for each record ID, a res.partner recordsets being default + :return: for each record ID, a res.partner recordsets being default customers to contact; """ partner_fields = self._mail_get_partner_fields(introspect_fields=introspect_fields) diff --git a/addons/mail/views/mail_alias_views.xml b/addons/mail/views/mail_alias_views.xml index d7572778253..e3cb6ab6deb 100644 --- a/addons/mail/views/mail_alias_views.xml +++ b/addons/mail/views/mail_alias_views.xml @@ -27,7 +27,11 @@
-

@

+

+ @ + +

+ @@ -82,6 +87,7 @@ + @@ -92,6 +98,8 @@ +