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 @@ +