[REF] mail, various: cleanup alias usage

Cleanup alias usage and definition. Prepare code to ease future changes and
improvements. Notably

  * add a 'alias_email' computed field on the mixin allowing to have the
    complete alias email when set, and False in case it is inactive or linked
    to an inactive alias domain;
  * remove unnecessary alias_id field definition when just the help differs
    from the standard definition coming from the 'mail.alias.mixin';
  * use fields coming from 'inherits' instead of using alias_id and its sub-
    fields; notably use 'alias_display_name' and 'alias_email' fields;
  * remove useless custom code and management;
  * improve alias parameters support code in configuration parameters;

Task-3453343 (Mail: Cleanup Alias Usage)
Prepares Task-36879 (Mail: Support MultiCompany Aliases)

Part-of: odoo/odoo#130632
This commit is contained in:
Thibault Delavallée
2023-08-09 17:04:26 +02:00
parent 1af84e28ee
commit 638e0f658d
23 changed files with 63 additions and 84 deletions
+3 -3
View File
@@ -16,9 +16,9 @@
<div>
<t t-set="record" t-value="object.env['crm.team'].search([('alias_name', '!=', 'False')], limit=1)" />
<p class="tip_title">Tip: Convert incoming emails into opportunities</p>
<t t-if="record and record.alias_domain">
<p class="tip_content">Did you know emails sent to <t t-out="record.alias_id.display_name or ''"></t> generate opportunities in your pipeline?<br/>
<a t-attf-href="mailto:{{record.alias_id.display_name}}" target="_blank">Try sending an email</a> to your CRM. This email address is configurable by sales team members.</p>
<t t-if="record.alias_email">
<p class="tip_content">Did you know emails sent to <t t-out="record.alias_email"></t> generate opportunities in your pipeline?<br/>
<a t-attf-href="mailto:{{record.alias_email}}" target="_blank">Try sending an email</a> to your CRM. This email address is configurable by sales team members.</p>
</t>
<t t-else="">
<p class="tip_content">Did you know emails sent to a Sales Team alias generate opportunities in your pipeline?</p>
+2 -3
View File
@@ -1276,10 +1276,9 @@ class Lead(models.Model):
('alias_force_thread_id', '=', False)
], limit=1)
if alias_record and alias_record.alias_domain and alias_record.alias_name:
email = f'{alias_record.alias_name}@{alias_record.alias_domain}'
if alias_record.alias_domain and alias_record.alias_name:
sub_title = Markup(_('Use the <i>New</i> button, or send an email to %(email_link)s to test the email gateway.')) % {
'email_link': Markup("<b><a href='mailto:%s'>%s</a></b>") % (email, email),
'email_link': Markup("<b><a href='mailto:%s'>%s</a></b>") % (alias_record.display_name, alias_record.display_name),
}
return super().get_empty_list_help(
f'<p class="o_view_nocontent_smiling_face">{help_title}</p><p class="oe_view_nocontent_alias">{sub_title}</p>'
+1 -3
View File
@@ -24,9 +24,7 @@ class Team(models.Model):
use_leads = fields.Boolean('Leads', help="Check this box to filter and qualify incoming requests as leads before converting them into opportunities and assigning them to a salesperson.")
use_opportunities = fields.Boolean('Pipeline', default=True, help="Check this box to manage a presales process with opportunities.")
alias_id = fields.Many2one(
'mail.alias', string='Alias', ondelete="restrict", required=True,
help="The email address associated with this channel. New emails received will automatically create new leads assigned to the channel.")
alias_id = fields.Many2one(help="The email address associated with this channel. New emails received will automatically create new leads assigned to the channel.")
# assignment
assignment_enabled = fields.Boolean('Lead Assign', compute='_compute_assignment_enabled')
assignment_auto_enabled = fields.Boolean('Auto Assignment', compute='_compute_assignment_enabled')
+1 -1
View File
@@ -627,7 +627,7 @@ class TestCRMLead(TestCrmCommon):
new_lead = self.format_and_process(
INCOMING_EMAIL,
'unknown.sender@test.example.com',
'%s@%s' % (self.sales_team_1.alias_name, self.alias_domain),
self.sales_team_1.alias_email,
subject='Delivery cost inquiry',
target_model='crm.lead',
)
@@ -314,7 +314,7 @@ class TestCRMLeadMultiCompany(TestCrmCommon):
new_lead = self.format_and_process(
INCOMING_EMAIL,
customer_company.email,
'%s@%s' % (self.sales_team_1.alias_name, self.alias_domain),
self.sales_team_1.alias_email,
subject='Team having partner in company',
target_model='crm.lead',
)
+2 -2
View File
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="crm_action_helper" name="crm action helper">
<t t-if="team.alias_name and team.alias_domain">
<t t-if="team.alias_email">
<p class="o_view_nocontent_smiling_face">
Create an opportunity to start playing with your pipeline.
</p><p>Use the <i>New</i> button, or send an email to
<a t-attf-href="mailto:#{team.alias_id.display_name}"><t t-esc="team.alias_id.display_name"/></a>
<a t-attf-href="mailto:#{team.alias_email}"><t t-esc="team.alias_email"/></a>
to test the email gateway.</p>
</t>
<t t-else="">
+1 -2
View File
@@ -488,13 +488,12 @@ class HrExpense(models.Model):
use_mailgateway = self.env['ir.config_parameter'].sudo().get_param('hr_expense.use_mailgateway')
expense_alias = self.env.ref('hr_expense.mail_alias_expense') if use_mailgateway else False
if expense_alias and expense_alias.alias_domain and expense_alias.alias_name:
alias_email = f'{expense_alias.alias_name}@{expense_alias.alias_domain}'
# encode, but force %20 encoding for space instead of a + (URL / mailto difference)
params = werkzeug.urls.url_encode({'subject': _("Lunch with customer $12.32")}).replace('+', '%20')
return Markup(
"""<p>%(send_string)s <a href="mailto:%(alias_email)s?%(params)s">%(alias_email)s</a></p>"""
) % {
'alias_email': alias_email,
'alias_email': expense_alias.display_name,
'params': params,
'send_string': _("Or send your receipts at"),
}
+2 -2
View File
@@ -17,8 +17,8 @@
<p class="tip_content">
By setting an alias to a job position, emails sent to this address create applications automatically. You can even use multiple trackers to get statistics according to the source of the application: LinkedIn, Monster, Indeed, etc.
<t t-set="record" t-value="object.env['hr.job'].search([('alias_name', '!=', False)], limit=1)" />
<t t-if="record and record.alias_domain">
<a t-attf-href="mailto:{{record.alias_id.display_name}}" target="_blank" style="color: #875a7b; text-decoration: none;">Try sending an email</a>
<t t-if="record.alias_email">
<a t-attf-href="mailto:{{record.alias_email}}" target="_blank" style="color: #875a7b; text-decoration: none;">Try sending an email</a>
</t>
</p>
</div>
+5 -6
View File
@@ -336,11 +336,11 @@ class Applicant(models.Model):
def get_empty_list_help(self, help_message):
if 'active_id' in self.env.context and self.env.context.get('active_model') == 'hr.job':
alias_id = self.env['hr.job'].browse(self.env.context['active_id']).alias_id
hr_job = self.env['hr.job'].browse(self.env.context['active_id'])
elif self.env.context.get('default_job_id'):
alias_id = self.env['hr.job'].browse(self.env.context['default_job_id']).alias_id
hr_job = self.env['hr.job'].browse(self.env.context['default_job_id'])
else:
alias_id = False
hr_job = self.env['hr.job']
nocontent_body = Markup("""
<p class="o_view_nocontent_smiling_face">%(help_title)s</p>
@@ -350,11 +350,10 @@ class Applicant(models.Model):
'para_2': _("You can search into attachment's content, like resumes, with the searchbar."),
}
if alias_id and alias_id.alias_domain and alias_id.alias_name:
email = alias_id.display_name
if hr_job.alias_email:
nocontent_body += Markup('<p class="o_copy_paste_email oe_view_nocontent_alias">%(helper_email)s <a href="mailto:%(email)s">%(email)s</a></p>') % {
'helper_email': _("Create new applications by sending an email to"),
'email': email
'email': hr_job.alias_email,
}
return super().get_empty_list_help(nocontent_body)
+1 -3
View File
@@ -49,9 +49,7 @@ class Job(models.Model):
help="Person responsible of validating the employee's contracts.")
document_ids = fields.One2many('ir.attachment', compute='_compute_document_ids', string="Documents", readonly=True)
documents_count = fields.Integer(compute='_compute_document_ids', string="Document Count")
alias_id = fields.Many2one(
'mail.alias', "Alias", ondelete="restrict", required=True,
help="Email alias for this job position. New emails will automatically create new applicants for this job position.")
alias_id = fields.Many2one(help="Email alias for this job position. New emails will automatically create new applicants for this job position.")
color = fields.Integer("Color Index")
is_favorite = fields.Boolean(compute='_compute_is_favorite', inverse='_inverse_is_favorite')
favorite_user_ids = fields.Many2many('res.users', 'job_favorite_user_rel', 'job_id', 'user_id', default=_get_default_favorite_user_ids)
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo import fields, models
class RecruitmentSource(models.Model):
@@ -12,11 +12,12 @@ class RecruitmentSource(models.Model):
email = fields.Char(related='alias_id.display_name', string="Email", readonly=True)
has_domain = fields.Char(compute='_compute_has_domain')
job_id = fields.Many2one('hr.job', "Job", ondelete='cascade')
alias_id = fields.Many2one('mail.alias', "Alias ID")
alias_id = fields.Many2one('mail.alias', "Alias ID", ondelete='restrict')
medium_id = fields.Many2one('utm.medium', default=lambda self: self.env.ref('utm.utm_medium_website'))
def _compute_has_domain(self):
self.has_domain = bool(self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain"))
for source in self:
source.has_domain = bool(source.alias_id.alias_domain)
def create_alias(self):
campaign = self.env.ref('hr_recruitment.utm_campaign_job')
@@ -36,10 +37,9 @@ class RecruitmentSource(models.Model):
}
source.alias_id = self.env['mail.alias'].create(vals)
@api.model
def _get_view(self, view_id=None, view_type='form', **options):
arch, view = super()._get_view(view_id, view_type, **options)
if view_type == 'tree' and not bool(self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain")):
email = arch.xpath("//field[@name='email']")[0]
email.getparent().remove(email)
return arch, view
def unlink(self):
""" Cascade delete aliases to avoid useless / badly configured aliases. """
aliases = self.alias_id
res = super().unlink()
aliases.sudo().unlink()
return res
@@ -46,7 +46,8 @@
<field name="source_id" placeholder="e.g. LinkedIn" decoration-bf="1" attrs="{'readonly': [('id', '!=', False)]}"/>
<field name="medium_id" optional="hidden"/>
<field name="job_id" attrs="{'readonly': [('id', '!=', False)]}"/>
<field name="email" attrs="{'invisible': [('email', '=', False)]}" widget="email"/>
<field name="email" widget="email"
attrs="{'invisible': ['|', ('email', '=', False), ('has_domain', '=', False)]}"/>
<button name="create_alias" string="Generate Email" class="btn btn-primary" type="object" attrs="{'invisible': ['|', ('has_domain', '=', False), ('email', '!=', False)]}"/>
</tree>
</field>
@@ -22,11 +22,21 @@ class AliasMixinOptional(models.AbstractModel):
alias_name = fields.Char(related='alias_id.alias_name', readonly=False)
alias_domain = fields.Char('Alias domain', compute='_compute_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
or no name. Alias display name itself contains more info and cannot be
used as it is in views. """
self.alias_email = False
for record in self.filtered(lambda rec: rec.alias_name and rec.alias_domain):
record.alias_email = f"{record.alias_name}@{record.alias_domain}"
# --------------------------------------------------
# CRUD
# --------------------------------------------------
+1 -1
View File
@@ -7,7 +7,7 @@
'description': """
Manage your mailing lists from Odoo.
""",
'version': '1.0',
'version': '1.1',
'depends': [
'mail',
'portal',
+5 -15
View File
@@ -45,8 +45,6 @@ class MailGroup(models.Model):
active = fields.Boolean('Active', default=True)
name = fields.Char('Name', required=True, translate=True)
alias_name = fields.Char('Alias Name', copy=False, related='alias_id.alias_name', readonly=False)
alias_fullname = fields.Char('Alias Full Name', compute='_compute_alias_fullname')
description = fields.Text('Description')
image_128 = fields.Image('Image', max_width=128, max_height=128)
# Messages
@@ -85,14 +83,6 @@ class MailGroup(models.Model):
# UI
can_manage_group = fields.Boolean('Can Manage', help='Can manage the members', compute='_compute_can_manage_group')
@api.depends('alias_name', 'alias_domain')
def _compute_alias_fullname(self):
for group in self:
if group.alias_name and group.alias_domain:
group.alias_fullname = f'{group.alias_name}@{group.alias_domain}'
else:
group.alias_fullname = group.alias_name
@api.depends('mail_group_message_ids.create_date', 'mail_group_message_ids.moderation_status')
def _compute_mail_group_message_last_month_count(self):
month_date = datetime.today() - relativedelta.relativedelta(months=1)
@@ -436,11 +426,11 @@ class MailGroup(models.Model):
'Precedence': 'list',
'X-Auto-Response-Suppress': 'OOF', # avoid out-of-office replies from MS Exchange
}
if self.alias_name and self.alias_domain:
if self.alias_email:
headers.update({
'List-Id': f'<{self.alias_name}.{self.alias_domain}>',
'List-Post': f'<mailto:{self.alias_name}@{self.alias_domain}>',
'X-Forge-To': f'"{self.name}" <{self.alias_name}@{self.alias_domain}>',
'List-Id': f'<{self.alias_email}>',
'List-Post': f'<mailto:{self.alias_email}>',
'X-Forge-To': f'"{self.name}" <{self.alias_email}>',
})
if message.mail_message_id.parent_id:
@@ -448,7 +438,7 @@ class MailGroup(models.Model):
# Add the footer (member specific) in the body
template_values = {
'mailto': f'{self.alias_name}@{self.alias_domain}',
'mailto': f'{self.alias_email}',
'group_url': f'{base_url}/groups/{slug(self)}',
'unsub_label': f'{base_url}/groups?unsubscribe',
'unsub_url': f'{base_url}/groups?unsubscribe&group_id={self.id}&token={access_token}&email={email_url_encoded}',
+1 -1
View File
@@ -6,7 +6,7 @@
<field name="arch" type="xml">
<tree sample="1">
<field name="name"/>
<field name="alias_fullname" string="Alias"/>
<field name="alias_email" string="Alias"/>
<field name="moderation" string="Moderated"/>
<field name="member_count" string="Members"/>
</tree>
+4 -4
View File
@@ -30,10 +30,10 @@
<div t-else="" class="o_image_64_cover float-start me-3 d-lg-block d-md-none"/>
<div class="d-flex flex-row">
<strong><a t-attf-href="/groups/#{ slug(group) }" t-esc="group.name"/></strong>
<div t-if="group.alias_id and group.alias_id.alias_name and group.alias_id.alias_domain"
<div t-if="group.alias_email"
class="d-flex align-items-center ms-3">
<i class="fa fa-envelope-o me-1" role="img" aria-label="Alias" title="Alias"/>
<a class="text-break" t-attf-href="mailto:#{group.alias_id.alias_name}@#{group.alias_id.alias_domain}" t-field="group.alias_id"/>
<a class="text-break" t-attf-href="mailto:#{group.alias_email}" t-field="group.alias_email"/>
</div>
</div>
<div t-field="group.description" class="text-muted d-flex"/>
@@ -277,9 +277,9 @@
<template id="group_name">
<h1 class="text-center" t-esc="group.name"/>
<h4 class="text-center text-muted" t-if="group.alias_id and group.alias_id.sudo().alias_name and group.alias_id.sudo().alias_domain">
<h4 class="text-center text-muted" t-if="group.alias_email">
<i class="fa fa-envelope-o" role="img" aria-label="Alias" title="Alias"/>
<a class="text-break" t-attf-href="mailto:#{group.alias_id.sudo().display_name}" t-field="group.alias_id"/>
<a class="text-break" t-attf-href="mailto:#{group.alias_email}" t-field="group.alias_email"/>
</h4>
</template>
+1 -3
View File
@@ -47,9 +47,7 @@ class MaintenanceEquipmentCategory(models.Model):
equipment_count = fields.Integer(string="Equipment Count", compute='_compute_equipment_count')
maintenance_ids = fields.One2many('maintenance.request', 'category_id', copy=False)
maintenance_count = fields.Integer(string="Maintenance Count", compute='_compute_maintenance_count')
alias_id = fields.Many2one(
'mail.alias', 'Alias', ondelete='restrict', required=True,
help="Email alias for this equipment category. New emails will automatically "
alias_id = fields.Many2one(help="Email alias for this equipment category. New emails will automatically "
"create a new equipment under this category.")
fold = fields.Boolean(string='Folded in Maintenance Pipe', compute='_compute_fold', store=True)
+1 -1
View File
@@ -3,7 +3,7 @@
{
'name': 'Project',
'version': '1.2',
'version': '1.3',
'website': 'https://www.odoo.com/app/project',
'category': 'Services/Project',
'sequence': 45,
+2 -2
View File
@@ -30,8 +30,8 @@
<div>
<t t-set="project_record" t-value="object.env['project.project'].search([('alias_name', '!=', False)], limit=1, order='sequence asc')"/>
<p class="tip_title">Tip: Create tasks from incoming emails</p>
<t t-if="project_record and project_record.alias_domain">
<p class="tip_content">Emails sent to <a t-attf-href="mailto:{{project_record.alias_value}}" target="_blank" style="color: #875a7b; text-decoration: none;"><t t-out="project_record.alias_value" /></a> will generate tasks in your <t t-out="project_record.name"></t> project.</p>
<t t-if="project_record.alias_email">
<p class="tip_content">Emails sent to <a t-attf-href="mailto:{{project_record.alias_email}}" target="_blank" style="color: #875a7b; text-decoration: none;"><t t-out="project_record.alias_email" /></a> will generate tasks in your <t t-out="project_record.name"></t> project.</p>
</t>
<t t-else="">
<p class="tip_content">Create tasks by sending an email to the email address of your project.</p>
+3 -13
View File
@@ -130,10 +130,8 @@ class Project(models.Model):
color = fields.Integer(string='Color Index')
user_id = fields.Many2one('res.users', string='Project Manager', default=lambda self: self.env.user, tracking=True)
alias_enabled = fields.Boolean(string='Use Email Alias', compute='_compute_alias_enabled', readonly=False)
alias_id = fields.Many2one('mail.alias', string='Alias', ondelete="restrict", required=True,
help="Internal email associated with this project. Incoming emails are automatically synchronized "
"with Tasks (or optionally Issues if the Issue Tracker module is installed).")
alias_value = fields.Char(string='Alias email', compute='_compute_alias_value')
alias_id = fields.Many2one(help="Internal email associated with this project. Incoming emails are automatically synchronized "
"with Tasks (or optionally Issues if the Issue Tracker module is installed).")
privacy_visibility = fields.Selection([
('followers', 'Invited internal users (private)'),
('employees', 'All internal users'),
@@ -217,7 +215,7 @@ class Project(models.Model):
def _compute_alias_enabled(self):
for project in self:
project.alias_enabled = project.alias_domain and project.alias_id.alias_name
project.alias_enabled = bool(project.alias_email)
def _compute_access_url(self):
super(Project, self)._compute_access_url()
@@ -337,14 +335,6 @@ class Project(models.Model):
operator_new = 'not inselect'
return [('id', operator_new, (query, ()))]
@api.depends('alias_name', 'alias_domain')
def _compute_alias_value(self):
for project in self:
if not project.alias_name or not project.alias_domain:
project.alias_value = ''
else:
project.alias_value = "%s@%s" % (project.alias_name, project.alias_domain)
@api.depends('collaborator_ids', 'privacy_visibility')
def _compute_collaborator_count(self):
project_sharings = self.filtered(lambda project: project.privacy_visibility == 'portal')
@@ -96,7 +96,7 @@
<!-- Always display the whole alias in edit mode. It depends in read only -->
<field name="alias_enabled" invisible="1"/>
<label for="alias_name" class="fw-bold o_form_label" string="Create tasks by sending an email to"/>
<field name="alias_value" class="oe_read_only d-inline" readonly="1" widget="email" attrs="{'invisible': [('alias_name', '=', False)]}" />
<field name="alias_email" class="oe_read_only d-inline" readonly="1" widget="email" attrs="{'invisible': [('alias_name', '=', False)]}" />
<span class="oe_edit_only o_row">
<field name="alias_name" class="oe_inline"/>@<field name="alias_domain" class="oe_inline" readonly="1"/>
</span>
@@ -375,7 +375,7 @@
<field name="milestone_count"/>
<field name="allow_milestones"/>
<field name="label_tasks"/>
<field name="alias_id"/>
<field name="alias_email"/>
<field name="alias_name"/>
<field name="alias_domain"/>
<field name="is_favorite"/>
@@ -449,8 +449,8 @@
<i t-if="record.date.raw_value and record.date_start.raw_value" class="fa fa-long-arrow-right mx-2 oe_read_only" aria-label="Arrow icon" title="Arrow"/>
<field name="date"/>
</div>
<div t-if="record.alias_name.value and record.alias_domain.value" class="text-muted text-truncate" t-att-title="record.alias_id.value">
<span class="fa fa-envelope-o me-2" aria-label="Domain Alias" title="Domain Alias"></span><t t-esc="record.alias_id.value"/>
<div t-if="record.alias_email.value" class="text-muted text-truncate" t-att-title="record.alias_email.value">
<span class="fa fa-envelope-o me-2" aria-label="Domain Alias" title="Domain Alias"></span><t t-esc="record.alias_email.value"/>
</div>
<div t-if="record.rating_active.raw_value and record.rating_count.raw_value &gt; 0" class="text-muted" groups="project.group_project_rating">
<b class="me-1">
@@ -331,9 +331,6 @@ class MailTestContainer(models.Model):
name = fields.Char()
description = fields.Text()
customer_id = fields.Many2one('res.partner', 'Customer')
alias_id = fields.Many2one(
'mail.alias', 'Alias',
delegate=True)
def _mail_get_partner_fields(self, introspect_fields=False):
return ['customer_id']