[MERGE] mail, various: cleanup alias usage
RATIONALE
We plan to move from a single alias-domain configuration to a multi company
enabled support for alias domains and aliases. First step to prepare this
change is to cleanup alias usage through various Odoo addons.
ALIAS USAGE CLEANUP
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;
ALIAS MIXIN WITH OPTIONAL ALIAS_ID
Some models would like to use the 'mail.alias.mixin' but it creates an alias
for each record in the parent model. This leads to a lot of unused aliases
if only a subset of those records really use aliases i.e. a lot of aliases
with 'alias_name' being 'False'.
In this commit we introduce a new mixin 'mail.alias.mixin.optional' that
behaves like the old 'mail.alias.mixin' but without having the 'alias_id'
field required i.e. without the "inherits". When creating a record without
giving an 'alias_name' no alias is created.
In this PR we use it notably to remove custom code in account journal model
and make it more standard. Using it in more models will be done later, but
it is a candidate to cleanup unused aliases related to discuss channel model.
ALIAS NAME SANITIZE
Currently there is a constraint on alias name as we allow only a subset of
valid latin characters in it aka `[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~]`. There
is also an automatic sanitize of alias name at create / write that replaces
any non-word characters by an hyphen. This sanitize is stricter than the
constraint and it is not really coherent.
In this commit we make the sanitize inlined with the constraint, allowing
more characters to go through the 'mail.alias.mixin' cleaning pass notably.
Enforce that void alias names are forced to False to avoid any constraint
issue. Sanitize method is now independent from the check method, to avoid
calling multiple times the sanitization as it is often used for other checks.
MISC
Lint / reorder code, prepare future changes. Add tests, notably for alias
name sanitize and management.
Provide various fixes, see sub commits for more details.
LINKS
After odoo/odoo#130768 and odoo/enterprise#45204 this is another preparation
for multi-company aliases (see odoo/odoo#76734 and odoo/enterprise#20983).
Task-3453343 (Mail: Cleanup Alias Usage)
Prepares Task-36879 (Mail: Support MultiCompany Aliases)
closes odoo/odoo#130632
Related: odoo/enterprise#45118
Related: odoo/upgrade#5012
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from ast import literal_eval
|
||||
|
||||
from odoo import api, Command, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.addons.base.models.res_bank import sanitize_account_number
|
||||
@@ -9,13 +12,6 @@ import re
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
def is_encodable_as_ascii(string):
|
||||
try:
|
||||
remove_accents(string).encode('ascii')
|
||||
except UnicodeEncodeError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class AccountJournalGroup(models.Model):
|
||||
_name = 'account.journal.group'
|
||||
@@ -37,7 +33,11 @@ class AccountJournal(models.Model):
|
||||
_name = "account.journal"
|
||||
_description = "Journal"
|
||||
_order = 'sequence, type, code'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin', 'portal.mixin']
|
||||
_inherit = ['portal.mixin',
|
||||
'mail.alias.mixin.optional',
|
||||
'mail.thread',
|
||||
'mail.activity.mixin',
|
||||
]
|
||||
_check_company_auto = True
|
||||
_check_company_domain = models.check_company_domain_parent_of
|
||||
_rec_names_search = ['name', 'code']
|
||||
@@ -81,7 +81,6 @@ class AccountJournal(models.Model):
|
||||
('bank', 'Bank'),
|
||||
('general', 'Miscellaneous'),
|
||||
], required=True,
|
||||
inverse='_inverse_type',
|
||||
help="Select 'Sale' for customer invoices journals.\n"\
|
||||
"Select 'Purchase' for vendor bills journals.\n"\
|
||||
"Select 'Cash' or 'Bank' for journals that are used in customer or vendor payments.\n"\
|
||||
@@ -186,11 +185,9 @@ class AccountJournal(models.Model):
|
||||
sale_activity_note = fields.Text('Activity Summary')
|
||||
|
||||
# alias configuration for journals
|
||||
alias_id = fields.Many2one('mail.alias', string='Email Alias', help="Send one separate email for each invoice.\n\n"
|
||||
"Any file extension will be accepted.\n\n"
|
||||
"Only PDF and XML files will be interpreted by Odoo", copy=False)
|
||||
alias_domain = fields.Char('Alias domain', compute='_compute_alias_domain')
|
||||
alias_name = fields.Char('Alias Name', copy=False, compute='_compute_alias_name', inverse='_inverse_type', help="It creates draft invoices and bills by sending an email.")
|
||||
alias_id = fields.Many2one(help="Send one separate email for each invoice.\n\n"
|
||||
"Any file extension will be accepted.\n\n"
|
||||
"Only PDF and XML files will be interpreted by Odoo")
|
||||
|
||||
journal_group_ids = fields.Many2many('account.journal.group',
|
||||
check_company=True,
|
||||
@@ -358,52 +355,14 @@ class AccountJournal(models.Model):
|
||||
else:
|
||||
journal.suspense_account_id = False
|
||||
|
||||
def _inverse_type(self):
|
||||
# Create an alias for purchase/sales journals
|
||||
for journal in self:
|
||||
if journal.type not in ('purchase', 'sale'):
|
||||
if journal.alias_id:
|
||||
journal.alias_id.sudo().unlink()
|
||||
continue
|
||||
|
||||
alias_name = next(string for string in (
|
||||
journal.alias_name,
|
||||
journal.name,
|
||||
journal.code,
|
||||
journal.type,
|
||||
) if string and is_encodable_as_ascii(string))
|
||||
|
||||
if journal.company_id != self.env.ref('base.main_company'):
|
||||
if is_encodable_as_ascii(journal.company_id.name):
|
||||
alias_name = f"{alias_name}-{journal.company_id.name}"
|
||||
else:
|
||||
alias_name = f"{alias_name}-{journal.company_id.id}"
|
||||
|
||||
alias_values = {
|
||||
'alias_defaults': {
|
||||
'move_type': 'in_invoice' if journal.type == 'purchase' else 'out_invoice',
|
||||
'company_id': journal.company_id.id,
|
||||
'journal_id': journal.id,
|
||||
},
|
||||
'alias_parent_thread_id': journal.id,
|
||||
'alias_name': alias_name,
|
||||
}
|
||||
if journal.alias_id:
|
||||
journal.alias_id.sudo().write(alias_values)
|
||||
else:
|
||||
alias_values['alias_model_id'] = self.env['ir.model']._get('account.move').id
|
||||
alias_values['alias_parent_model_id'] = self.env['ir.model']._get('account.journal').id
|
||||
journal.alias_id = self.env['mail.alias'].sudo().create(alias_values)
|
||||
self.invalidate_recordset(['alias_name'])
|
||||
|
||||
@api.depends('name')
|
||||
def _compute_alias_domain(self):
|
||||
self.alias_domain = self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain")
|
||||
|
||||
@api.depends('alias_id', 'alias_id.alias_name')
|
||||
def _compute_alias_name(self):
|
||||
for journal in self:
|
||||
journal.alias_name = journal.alias_id.alias_name
|
||||
@api.onchange('type')
|
||||
def _onchange_type_for_alias(self):
|
||||
self.filtered(lambda journal: journal.type not in {'sale', 'purchase'}).alias_name = False
|
||||
for journal in self.filtered(lambda journal: (
|
||||
not journal.alias_name and journal.type in {'sale', 'purchase'})
|
||||
):
|
||||
journal.alias_name = self._alias_prepare_alias_name(
|
||||
False, journal.name, journal.code, journal.type, journal.company_id)
|
||||
|
||||
@api.constrains('account_control_ids')
|
||||
def _constrains_account_control_ids(self):
|
||||
@@ -512,7 +471,6 @@ class AccountJournal(models.Model):
|
||||
accounts = self.search([('bank_account_id', '=', bank_account.id)])
|
||||
if accounts <= self:
|
||||
bank_accounts += bank_account
|
||||
self.mapped('alias_id').sudo().unlink()
|
||||
ret = super(AccountJournal, self).unlink()
|
||||
bank_accounts.unlink()
|
||||
return ret
|
||||
@@ -549,6 +507,14 @@ class AccountJournal(models.Model):
|
||||
return super(AccountJournal, self).copy(default)
|
||||
|
||||
def write(self, vals):
|
||||
# for journals, force a readable name instead of a sanitized name e.g. non ascii in journal names
|
||||
if vals.get('alias_name') and 'type' not in vals:
|
||||
# will raise if writing name on more than 1 record, using self[0] is safe
|
||||
if (not self.env['mail.alias']._is_encodable(vals['alias_name']) or
|
||||
not self.env['mail.alias']._sanitize_alias_name(vals['alias_name'])):
|
||||
vals['alias_name'] = self._alias_prepare_alias_name(
|
||||
False, vals.get('name', self.name), vals.get('code', self.code), self[0].type, self[0].company_id)
|
||||
|
||||
for journal in self:
|
||||
company = journal.company_id
|
||||
if ('company_id' in vals and journal.company_id.id != vals['company_id']):
|
||||
@@ -573,6 +539,16 @@ class AccountJournal(models.Model):
|
||||
raise UserError(_("You cannot modify the field %s of a journal that already has accounting entries.", field_string))
|
||||
result = super(AccountJournal, self).write(vals)
|
||||
|
||||
# Ensure alias coherency when changing type
|
||||
if 'type' in vals:
|
||||
for journal in self:
|
||||
alias_vals = journal._alias_get_creation_values()
|
||||
alias_vals = {
|
||||
'alias_defaults': alias_vals['alias_defaults'],
|
||||
'alias_name': alias_vals['alias_name'],
|
||||
}
|
||||
journal.update(alias_vals)
|
||||
|
||||
# Ensure the liquidity accounts are sharing the same foreign currency.
|
||||
if 'currency_id' in vals:
|
||||
for journal in self.filtered(lambda journal: journal.type in ('bank', 'cash')):
|
||||
@@ -588,6 +564,37 @@ class AccountJournal(models.Model):
|
||||
|
||||
return result
|
||||
|
||||
def _alias_get_creation_values(self):
|
||||
values = super()._alias_get_creation_values()
|
||||
values['alias_model_id'] = self.env['ir.model']._get_id('account.move')
|
||||
if self.id:
|
||||
values['alias_name'] = self._alias_prepare_alias_name(self.alias_name, self.name, self.code, self.type, self.company_id)
|
||||
values['alias_defaults'] = defaults = literal_eval(self.alias_defaults or "{}")
|
||||
defaults['company_id'] = self.company_id.id
|
||||
defaults['move_type'] = 'in_invoice' if self.type == 'purchase' else 'out_invoice'
|
||||
defaults['journal_id'] = self.id
|
||||
return values
|
||||
|
||||
@api.model
|
||||
def _alias_prepare_alias_name(self, alias_name, name, code, jtype, company):
|
||||
""" Tool method generating standard journal alias, to ensure uniqueness
|
||||
and readability; reset for other journals than purchase / sale """
|
||||
if jtype not in ('purchase', 'sale'):
|
||||
return False
|
||||
|
||||
alias_name = next(
|
||||
(
|
||||
string for string in (alias_name, name, code, jtype)
|
||||
if (string and self.env['mail.alias']._is_encodable(string) and
|
||||
self.env['mail.alias']._sanitize_alias_name(string))
|
||||
), False
|
||||
)
|
||||
if company != self.env.ref('base.main_company'):
|
||||
company_identifier = company.name if self.env['mail.alias']._is_encodable(company.name) else company.id
|
||||
if f'-{company_identifier}' not in alias_name:
|
||||
alias_name = f"{alias_name}-{company_identifier}"
|
||||
return self.env['mail.alias']._sanitize_alias_name(alias_name)
|
||||
|
||||
@api.model
|
||||
def get_next_bank_cash_default_code(self, journal_type, company, cache=None):
|
||||
journal_code_base = (journal_type == 'cash' and 'CSH' or 'BNK')
|
||||
@@ -661,6 +668,12 @@ class AccountJournal(models.Model):
|
||||
if 'refund_sequence' not in vals:
|
||||
vals['refund_sequence'] = vals['type'] in ('sale', 'purchase')
|
||||
|
||||
# === Fill missing alias name for sale / purchase, to force alias creation ===
|
||||
if journal_type in {'sale', 'purchase'} and 'alias_name' not in vals:
|
||||
vals['alias_name'] = self._alias_prepare_alias_name(
|
||||
False, vals.get('name'), vals.get('code'), journal_type, company
|
||||
)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
|
||||
@@ -28,17 +28,6 @@ class TestAccountIncomingSupplierInvoice(AccountTestInvoicingCommon):
|
||||
|
||||
cls.journal = cls.company_data['default_journal_purchase']
|
||||
|
||||
journal_alias = cls.env['mail.alias'].create({
|
||||
'alias_name': 'test-bill',
|
||||
'alias_model_id': cls.env.ref('account.model_account_move').id,
|
||||
'alias_defaults': json.dumps({
|
||||
'move_type': 'in_invoice',
|
||||
'company_id': cls.env.user.company_id.id,
|
||||
'journal_id': cls.journal.id,
|
||||
}),
|
||||
})
|
||||
cls.journal.write({'alias_id': journal_alias.id})
|
||||
|
||||
def test_supplier_invoice_mailed_from_supplier(self):
|
||||
message_parsed = {
|
||||
'message_id': 'message-id-dead-beef',
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from ast import literal_eval
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from odoo.addons.account.models.account_payment_method import AccountPaymentMethod
|
||||
from odoo.tests import tagged
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.tests import Form, tagged
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
@@ -137,24 +140,149 @@ class TestAccountJournal(AccountTestInvoicingCommon):
|
||||
|
||||
self.assertFalse(second_method.exists())
|
||||
|
||||
def test_account_journal_alias_name(self):
|
||||
journal = self.company_data['default_journal_purchase']
|
||||
self.assertEqual(journal.alias_name, 'vendor-bills-company_1_data')
|
||||
journal.name = 'ぁ'
|
||||
journal.alias_name = False
|
||||
self.assertEqual(journal.alias_name, 'bill-company_1_data')
|
||||
journal.code = 'ぁ'
|
||||
journal.alias_name = False
|
||||
self.assertEqual(journal.alias_name, 'purchase-company_1_data')
|
||||
|
||||
company_2_id = str(self.company_data_2['company'].id)
|
||||
journal_2 = self.company_data_2['default_journal_sale']
|
||||
self.company_data_2['company'].name = 'ぁ'
|
||||
journal_2.alias_name = False
|
||||
self.assertEqual(journal_2.alias_name, 'customer-invoices-' + company_2_id)
|
||||
journal_2.name = 'ぁ'
|
||||
journal_2.alias_name = False
|
||||
self.assertEqual(journal_2.alias_name, 'inv-' + company_2_id)
|
||||
journal_2.code = 'ぁ'
|
||||
journal_2.alias_name = False
|
||||
self.assertEqual(journal_2.alias_name, 'sale-' + company_2_id)
|
||||
@tagged('post_install', '-at_install', 'mail_alias')
|
||||
class TestAccountJournalAlias(AccountTestInvoicingCommon, MailCommon):
|
||||
|
||||
def test_alias_name_creation(self):
|
||||
""" Test alias creation, notably avoid raising constraints due to ascii
|
||||
characters removal. See odoo/odoo@339cdffb68f91eb1455d447d1bdd7133c68723bd """
|
||||
# check base test data
|
||||
journal1 = self.company_data['default_journal_purchase']
|
||||
company1 = journal1.company_id
|
||||
journal2 = self.company_data_2['default_journal_sale']
|
||||
company2 = journal2.company_id
|
||||
# have a non ascii company name
|
||||
company2.name = 'ぁ'
|
||||
|
||||
for (aname, jname, jcode, jtype, jcompany), expected_alias_name in zip(
|
||||
[
|
||||
('youpie', 'Journal Name', 'NEW1', 'purchase', company1),
|
||||
(False, 'Journal Other Name', 'NEW2', 'purchase', company1),
|
||||
(False, 'ぁ', 'NEW3', 'purchase', company1),
|
||||
(False, 'ぁ', 'ぁ', 'purchase', company1),
|
||||
('youpie', 'Journal Name', 'NEW1', 'purchase', company2),
|
||||
(False, 'Journal Other Name', 'NEW2', 'purchase', company2),
|
||||
(False, 'ぁ', 'NEW3', 'purchase', company2),
|
||||
(False, 'ぁ', 'ぁ', 'purchase', company2),
|
||||
],
|
||||
[
|
||||
f'youpie-{company1.name}',
|
||||
f'journal-other-name-{company1.name}',
|
||||
f'new3-{company1.name}',
|
||||
f'purchase-{company1.name}',
|
||||
f'youpie-{company2.id}',
|
||||
f'journal-other-name-{company2.id}',
|
||||
f'new3-{company2.id}',
|
||||
f'purchase-{company2.id}',
|
||||
]
|
||||
):
|
||||
with self.subTest(aname=aname, jname=jname, jcode=jcode, jtype=jtype, jcompany=jcompany):
|
||||
new_journal = self.env['account.journal'].create({
|
||||
'code': jcode,
|
||||
'company_id': jcompany.id,
|
||||
'name': jname,
|
||||
'type': jtype,
|
||||
# force alias_name only if given, to check default value otherwise
|
||||
**({'alias_name': aname} if aname else {}),
|
||||
})
|
||||
self.assertEqual(new_journal.alias_name, expected_alias_name)
|
||||
|
||||
# other types: no mail support by default
|
||||
journals = self.env['account.journal'].create([{
|
||||
'code': f'NEW{jtype}',
|
||||
'name': f'Type {jtype}',
|
||||
'type': jtype}
|
||||
for jtype in ('general', 'cash', 'bank')
|
||||
])
|
||||
self.assertFalse(journals.alias_id, 'Do not create useless aliases')
|
||||
self.assertFalse(list(filter(None, journals.mapped('alias_name'))))
|
||||
|
||||
def test_alias_name_form(self):
|
||||
""" Test alias name update using Form tool (onchange) """
|
||||
journal = Form(self.env['account.journal'])
|
||||
journal.name = 'Test With Form'
|
||||
self.assertFalse(journal.alias_name)
|
||||
journal.type = 'sale'
|
||||
self.assertEqual(journal.alias_name, f'test-with-form-{self.env.company.name}')
|
||||
journal.type = 'cash'
|
||||
self.assertFalse(journal.alias_name)
|
||||
|
||||
def test_alias_from_type(self):
|
||||
""" Test alias behavior on journal, especially alias_name management as
|
||||
well as defaults update, see odoo/odoo@400b6860271a11b9914166ff7e42939c4c6192dc """
|
||||
journal = self.company_data['default_journal_purchase']
|
||||
|
||||
# assert base test data
|
||||
company_name = 'company_1_data'
|
||||
journal_code = 'BILL'
|
||||
journal_name = 'Vendor Bills'
|
||||
journal_alias = journal.alias_id
|
||||
self.assertEqual(journal.code, journal_code)
|
||||
self.assertEqual(journal.company_id.name, company_name)
|
||||
self.assertEqual(journal.name, journal_name)
|
||||
self.assertEqual(journal.type, 'purchase')
|
||||
|
||||
# assert default creation data
|
||||
self.assertEqual(journal_alias.alias_contact, 'everyone')
|
||||
self.assertDictEqual(
|
||||
dict(literal_eval(journal_alias.alias_defaults)),
|
||||
{
|
||||
'move_type': 'in_invoice',
|
||||
'company_id': journal.company_id.id,
|
||||
'journal_id': journal.id,
|
||||
}
|
||||
)
|
||||
self.assertFalse(journal_alias.alias_force_thread_id, 'Journal alias should create new moves')
|
||||
self.assertEqual(journal_alias.alias_model_id, self.env['ir.model']._get('account.move'),
|
||||
'Journal alias targets moves')
|
||||
self.assertEqual(journal_alias.alias_name, f'vendor-bills-{company_name}')
|
||||
self.assertEqual(journal_alias.alias_parent_model_id, self.env['ir.model']._get('account.journal'),
|
||||
'Journal alias owned by journal itself')
|
||||
self.assertEqual(journal_alias.alias_parent_thread_id, journal.id,
|
||||
'Journal alias owned by journal itself')
|
||||
|
||||
# update alias_name, ensure a fallback on a real name when not explicit reset
|
||||
for alias_name, expected in [
|
||||
(False, False),
|
||||
('', False),
|
||||
(' ', f'vendor-bills-{company_name}'), # error recuperation
|
||||
('.', f'vendor-bills-{company_name}'), # error recuperation
|
||||
('😊', f'vendor-bills-{company_name}'), # resets, unicode not supported
|
||||
('ぁ', f'vendor-bills-{company_name}'), # resets, non ascii not supported
|
||||
('Youpie Boum', 'youpie-boum'),
|
||||
]:
|
||||
with self.subTest(alias_name=alias_name):
|
||||
journal.write({'alias_name': alias_name})
|
||||
self.assertEqual(journal.alias_name, expected)
|
||||
self.assertEqual(journal_alias.alias_name, expected)
|
||||
|
||||
# changing type should void if not purchase or sale
|
||||
for jtype in ('general', 'cash', 'bank'):
|
||||
journal.write({'type': jtype})
|
||||
self.assertEqual(journal.alias_id, journal_alias,
|
||||
'Dà not unlink aliases, just reset their value')
|
||||
self.assertFalse(journal.alias_name)
|
||||
self.assertFalse(journal_alias.alias_name)
|
||||
|
||||
# changing type should reset if sale or purchase
|
||||
journal.company_id.write({'name': 'New Company Name'})
|
||||
journal.write({'name': 'Reset Journal', 'type': 'sale'})
|
||||
journal_alias_2 = journal.alias_id
|
||||
self.assertEqual(journal_alias_2.alias_contact, 'everyone')
|
||||
self.assertDictEqual(
|
||||
dict(literal_eval(journal_alias_2.alias_defaults)),
|
||||
{
|
||||
'move_type': 'out_invoice',
|
||||
'company_id': journal.company_id.id,
|
||||
'journal_id': journal.id,
|
||||
}
|
||||
)
|
||||
self.assertFalse(journal_alias_2.alias_force_thread_id, 'Journal alias should create new moves')
|
||||
self.assertEqual(journal_alias_2.alias_model_id, self.env['ir.model']._get('account.move'),
|
||||
'Journal alias targets moves')
|
||||
self.assertEqual(journal_alias_2.alias_name, 'reset-journal-new-company-name')
|
||||
self.assertEqual(journal_alias_2.alias_parent_model_id, self.env['ir.model']._get('account.journal'),
|
||||
'Journal alias owned by journal itself')
|
||||
self.assertEqual(journal_alias_2.alias_parent_thread_id, journal.id,
|
||||
'Journal alias owned by journal itself')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>'
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
@@ -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="">
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
{
|
||||
'name': 'Discuss',
|
||||
'version': '1.14',
|
||||
'version': '1.15',
|
||||
'category': 'Productivity/Discuss',
|
||||
'sequence': 145,
|
||||
'summary': 'Chat, mail gateway and private channels',
|
||||
|
||||
@@ -7,6 +7,7 @@ from . import models
|
||||
|
||||
# mixin
|
||||
from . import mail_activity_mixin
|
||||
from . import mail_alias_mixin_optional
|
||||
from . import mail_alias_mixin
|
||||
from . import mail_render_mixin
|
||||
from . import mail_composer_mixin
|
||||
|
||||
@@ -7,19 +7,6 @@ from odoo import api, models
|
||||
class IrConfigParameter(models.Model):
|
||||
_inherit = 'ir.config_parameter'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('key') in ['mail.bounce.alias', 'mail.catchall.alias']:
|
||||
vals['value'] = self.env['mail.alias']._clean_and_check_unique([vals.get('value')])[0]
|
||||
return super().create(vals_list)
|
||||
|
||||
def write(self, vals):
|
||||
for parameter in self:
|
||||
if 'value' in vals and parameter.key in ['mail.bounce.alias', 'mail.catchall.alias'] and vals['value'] != parameter.value:
|
||||
vals['value'] = self.env['mail.alias']._clean_and_check_unique([vals.get('value')])[0]
|
||||
return super().write(vals)
|
||||
|
||||
@api.model
|
||||
def set_param(self, key, value):
|
||||
if key == 'mail.restrict.template.rendering':
|
||||
@@ -33,8 +20,14 @@ class IrConfigParameter(models.Model):
|
||||
# remove existing users, including inactive template user
|
||||
# admin will regain the right via implied_ids on group_system
|
||||
group_user._remove_group(group_mail_template_editor)
|
||||
# sanitize, normalize and check uniqueness of catchall / bounce aliases
|
||||
# note that False = unlink ICP
|
||||
elif key in {'mail.bounce.alias', 'mail.catchall.alias'} and value:
|
||||
value = self.env['mail.alias']._sanitize_alias_name(value)
|
||||
if value:
|
||||
self.env['mail.alias']._check_unique([value], skip_icp_keys={key})
|
||||
# sanitize and normalize allowed catchall domains
|
||||
elif key == 'mail.catchall.domain.allowed' and value:
|
||||
value = self.env['mail.alias']._clean_and_check_mail_catchall_allowed_domains(value)
|
||||
|
||||
return super(IrConfigParameter, self).set_param(key, value)
|
||||
return super().set_param(key, value)
|
||||
|
||||
@@ -33,7 +33,12 @@ class Alias(models.Model):
|
||||
_rec_name = 'alias_name'
|
||||
_order = 'alias_model_id, alias_name'
|
||||
|
||||
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 <jobs@example.odoo.com>")
|
||||
# email definition
|
||||
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 <jobs@example.odoo.com>")
|
||||
alias_domain = fields.Char('Alias domain', compute='_compute_alias_domain')
|
||||
# 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 "
|
||||
"corresponds. Any incoming email that does not reply to an "
|
||||
@@ -54,17 +59,22 @@ class Alias(models.Model):
|
||||
'Record Thread ID',
|
||||
help="Optional ID of a thread (record) to which all incoming messages will be attached, even "
|
||||
"if they did not reply to it. If set, this will disable the creation of new records completely.")
|
||||
alias_domain = fields.Char('Alias domain', compute='_compute_alias_domain')
|
||||
# owner
|
||||
alias_parent_model_id = fields.Many2one(
|
||||
'ir.model', 'Parent Model',
|
||||
help="Parent model holding the alias. The model holding the alias reference "
|
||||
"is not necessarily the model given by alias_model_id "
|
||||
"(example: project (parent_model) and task (model))")
|
||||
alias_parent_thread_id = fields.Integer('Parent Record Thread ID', help="ID of the parent record holding the alias (example: project holding the task creation alias)")
|
||||
alias_contact = fields.Selection([
|
||||
('everyone', 'Everyone'),
|
||||
('partners', 'Authenticated Partners'),
|
||||
('followers', 'Followers only')], default='everyone',
|
||||
alias_parent_thread_id = fields.Integer(
|
||||
'Parent Record Thread ID',
|
||||
help="ID of the parent record holding the alias (example: project holding the task creation alias)")
|
||||
# incoming configuration (mailgateway)
|
||||
alias_contact = fields.Selection(
|
||||
[
|
||||
('everyone', 'Everyone'),
|
||||
('partners', 'Authenticated Partners'),
|
||||
('followers', 'Followers only')
|
||||
], default='everyone',
|
||||
string='Alias Contact Security', required=True,
|
||||
help="Policy to post a message on the document using the mailgateway.\n"
|
||||
"- everyone: everyone can post\n"
|
||||
@@ -73,12 +83,12 @@ class Alias(models.Model):
|
||||
alias_bounced_content = fields.Html(
|
||||
"Custom Bounced Message", translate=True,
|
||||
help="If set, this content will automatically be sent out to unauthorized users instead of the default message.")
|
||||
alias_status = fields.Selection([
|
||||
('not_tested', 'Not Tested'),
|
||||
('valid', 'Valid'),
|
||||
('invalid', 'Invalid'),
|
||||
],
|
||||
compute='_compute_alias_status', store=True,
|
||||
alias_status = fields.Selection(
|
||||
[
|
||||
('not_tested', 'Not Tested'),
|
||||
('valid', 'Valid'),
|
||||
('invalid', 'Invalid'),
|
||||
], compute='_compute_alias_status', store=True,
|
||||
help='Alias status assessed on the last message received.')
|
||||
|
||||
_sql_constraints = [
|
||||
@@ -86,66 +96,29 @@ class Alias(models.Model):
|
||||
]
|
||||
|
||||
@api.constrains('alias_name')
|
||||
def _alias_is_ascii(self):
|
||||
def _check_alias_is_ascii(self):
|
||||
""" The local-part ("display-name" <local-part@domain>) of an
|
||||
address only contains limited range of ascii characters.
|
||||
We DO NOT allow anything else than ASCII dot-atom formed
|
||||
local-part. Quoted-string and internationnal characters are
|
||||
to be rejected. See rfc5322 sections 3.4.1 and 3.2.3
|
||||
"""
|
||||
for alias in self:
|
||||
if alias.alias_name and not dot_atom_text.match(alias.alias_name):
|
||||
raise ValidationError(_(
|
||||
"You cannot use anything else than unaccented latin characters in the alias address (%s).",
|
||||
alias.alias_name,
|
||||
))
|
||||
|
||||
@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."""
|
||||
self.alias_status = 'not_tested'
|
||||
|
||||
@api.depends('alias_name')
|
||||
def _compute_alias_domain(self):
|
||||
self.alias_domain = self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain")
|
||||
for alias in self.filtered('alias_name'):
|
||||
if not dot_atom_text.match(alias.alias_name):
|
||||
raise ValidationError(
|
||||
_("You cannot use anything else than unaccented latin characters in the alias address %(alias_name)s.",
|
||||
alias_name=alias.alias_name)
|
||||
)
|
||||
|
||||
@api.constrains('alias_defaults')
|
||||
def _check_alias_defaults(self):
|
||||
for alias in self:
|
||||
try:
|
||||
dict(ast.literal_eval(alias.alias_defaults))
|
||||
except Exception:
|
||||
raise ValidationError(_('Invalid expression, it must be a literal python dictionary definition e.g. "{\'field\': \'value\'}"'))
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
""" Creates email.alias records according to the values provided in
|
||||
``vals`` with 1 alteration:
|
||||
|
||||
* ``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;
|
||||
"""
|
||||
alias_names = [vals['alias_name'] for vals in vals_list if vals.get('alias_name')]
|
||||
if alias_names:
|
||||
sanitized_names = self._clean_and_check_unique(alias_names)
|
||||
for vals in vals_list:
|
||||
if vals.get('alias_name'):
|
||||
vals['alias_name'] = sanitized_names[alias_names.index(vals['alias_name'])]
|
||||
return super(Alias, self).create(vals_list)
|
||||
|
||||
def write(self, vals):
|
||||
""""Raises UserError if given alias name is already assigned"""
|
||||
if vals.get('alias_name') and self.ids:
|
||||
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)
|
||||
))
|
||||
vals['alias_name'] = self._clean_and_check_unique([vals.get('alias_name')])[0]
|
||||
return super(Alias, self).write(vals)
|
||||
except Exception as e:
|
||||
raise ValidationError(
|
||||
_('Invalid expression, it must be a literal python dictionary definition e.g. "{\'field\': \'value\'}"')
|
||||
) from e
|
||||
|
||||
@api.depends('alias_domain', 'alias_name')
|
||||
def _compute_display_name(self):
|
||||
@@ -161,70 +134,154 @@ 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."""
|
||||
self.alias_status = 'not_tested'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
""" Creates email.alias records according to the values provided in
|
||||
``vals`` with 1 alteration:
|
||||
|
||||
* ``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;
|
||||
"""
|
||||
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
|
||||
return super().create(vals_list)
|
||||
|
||||
def write(self, vals):
|
||||
""" Raise UserError with a meaningfull message instead of letting the
|
||||
unicity constraint give its error. """
|
||||
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))
|
||||
)
|
||||
return super().write(vals)
|
||||
|
||||
def _clean_and_check_mail_catchall_allowed_domains(self, value):
|
||||
""" The purpose of this system parameter is to avoid the creation
|
||||
of records from incoming emails with a domain != alias_domain
|
||||
but that have a pattern matching an internal mail.alias . """
|
||||
value = [domain.strip().lower() for domain in value.split(',') if domain.strip()]
|
||||
if not value:
|
||||
raise ValidationError(_("Value for `mail.catchall.domain.allowed` cannot be validated.\n"
|
||||
"It should be a comma separated list of domains e.g. example.com,example.org."))
|
||||
raise ValidationError(
|
||||
_("Value for `mail.catchall.domain.allowed` cannot be validated.\n"
|
||||
"It should be a comma separated list of domains e.g. example.com,example.org.")
|
||||
)
|
||||
return ",".join(value)
|
||||
|
||||
def _clean_and_check_unique(self, names):
|
||||
"""When an alias name appears to already be an email, we keep the local
|
||||
part only. A sanitizing / cleaning is also performed on the name. If
|
||||
name already exists an UserError is raised. """
|
||||
def _check_unique(self, sanitized_names, skip_icp_keys=None):
|
||||
""" Check unicity constraint won't be raised, otherwise raise a UserError
|
||||
with a complete error message. Also check unicity against alias config
|
||||
parameters.
|
||||
|
||||
def _sanitize_alias_name(name):
|
||||
""" Cleans and sanitizes the alias name """
|
||||
sanitized_name = remove_accents(name).lower().split('@')[0]
|
||||
sanitized_name = re.sub(r'[^\w+.]+', '-', sanitized_name)
|
||||
sanitized_name = re.sub(r'^\.+|\.+$|\.+(?=\.)', '', sanitized_name)
|
||||
sanitized_name = sanitized_name.encode('ascii', errors='replace').decode()
|
||||
return sanitized_name
|
||||
:param list sanitized_names: a list of names (considered as sanitized
|
||||
and ready to be sent to DB);
|
||||
"""
|
||||
valid_names = list(filter(None, sanitized_names))
|
||||
|
||||
sanitized_names = [_sanitize_alias_name(name) for name in names]
|
||||
# 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))
|
||||
)
|
||||
|
||||
catchall_alias = self.env['ir.config_parameter'].sudo().get_param('mail.catchall.alias')
|
||||
bounce_alias = self.env['ir.config_parameter'].sudo().get_param('mail.bounce.alias')
|
||||
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 sanitized_names:
|
||||
if sanitized_name in [catchall_alias, bounce_alias]:
|
||||
matching_alias_name = '%s@%s' % (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=_('catchall') if sanitized_name == catchall_alias else _('bounce'))
|
||||
)
|
||||
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])
|
||||
)
|
||||
|
||||
# matches existing alias
|
||||
domain = [('alias_name', 'in', sanitized_names)]
|
||||
if self:
|
||||
domain += [('id', 'not in', self.ids)]
|
||||
matching_alias = self.search(domain, limit=1)
|
||||
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:
|
||||
return sanitized_names
|
||||
|
||||
sanitized_alias_name = _sanitize_alias_name(matching_alias.alias_name)
|
||||
matching_alias_name = '%s@%s' % (sanitized_alias_name, alias_domain) if alias_domain else sanitized_alias_name
|
||||
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_name,
|
||||
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_name,
|
||||
matching_alias_name=matching_alias.display_name,
|
||||
alias_model_name=matching_alias.alias_model_id.name)
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _sanitize_alias_name(self, name):
|
||||
""" Cleans and sanitizes the alias name """
|
||||
sanitized_name = name.strip() if name else ''
|
||||
if sanitized_name:
|
||||
sanitized_name = remove_accents(sanitized_name).lower().split('@')[0]
|
||||
# cannot start and end with dot
|
||||
sanitized_name = re.sub(r'^\.+|\.+$|\.+(?=\.)', '', sanitized_name)
|
||||
# subset of allowed characters
|
||||
sanitized_name = re.sub(r'[^\w!#$%&\'*+\-/=?^_`{|}~.]+', '-', sanitized_name)
|
||||
sanitized_name = sanitized_name.encode('ascii', errors='replace').decode()
|
||||
if not sanitized_name.strip():
|
||||
return False
|
||||
return sanitized_name
|
||||
|
||||
@api.model
|
||||
def _is_encodable(self, alias_name, charset='ascii'):
|
||||
""" Check if alias_name is encodable. Standard charset is ascii, as
|
||||
UTF-8 requires a specific extension. Not recommended for outgoing
|
||||
aliases. 'remove_accents' is performed as sanitization process of
|
||||
the name will do it anyway. """
|
||||
try:
|
||||
remove_accents(alias_name).encode(charset)
|
||||
except UnicodeEncodeError:
|
||||
return False
|
||||
return True
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ACTIONS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def open_document(self):
|
||||
if not self.alias_model_id or not self.alias_force_thread_id:
|
||||
return False
|
||||
@@ -245,6 +302,10 @@ class Alias(models.Model):
|
||||
'type': 'ir.actions.act_window',
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# MAIL GATEWAY
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _get_alias_bounced_body_fallback(self, message_dict):
|
||||
contact_description = self._get_alias_contact_description()
|
||||
default_email = self.env.company.partner_id.email_formatted if self.env.company.partner_id.email else self.env.company.name
|
||||
|
||||
@@ -3,93 +3,34 @@
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo import fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AliasMixin(models.AbstractModel):
|
||||
""" A mixin for models that inherits mail.alias. This mixin initializes the
|
||||
alias_id column in database, and manages the expected one-to-one
|
||||
relation between your model and mail aliases.
|
||||
"""
|
||||
""" A mixin for models that inherits mail.alias to have a one-to-one relation
|
||||
between the model and its alias. """
|
||||
_name = 'mail.alias.mixin'
|
||||
_inherit = 'mail.alias.mixin.optional'
|
||||
_inherits = {'mail.alias': 'alias_id'}
|
||||
_description = 'Email Aliases Mixin'
|
||||
ALIAS_WRITEABLE_FIELDS = ['alias_name', 'alias_contact', 'alias_defaults', 'alias_bounced_content']
|
||||
|
||||
alias_id = fields.Many2one('mail.alias', string='Alias', ondelete="restrict", required=True)
|
||||
alias_id = fields.Many2one(required=True)
|
||||
alias_name = fields.Char(inherited=True)
|
||||
alias_defaults = fields.Text(inherited=True)
|
||||
|
||||
# --------------------------------------------------
|
||||
# CRUD
|
||||
# --------------------------------------------------
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
""" Create a record with each ``vals`` or ``vals_list`` and create a corresponding alias. """
|
||||
# prepare all alias values
|
||||
alias_vals_list, record_vals_list = [], []
|
||||
for vals in vals_list:
|
||||
new_alias = not vals.get('alias_id')
|
||||
if new_alias:
|
||||
alias_vals, record_vals = self._alias_filter_fields(vals)
|
||||
alias_vals.update(self._alias_get_creation_values())
|
||||
alias_vals_list.append(alias_vals)
|
||||
record_vals_list.append(record_vals)
|
||||
|
||||
# create all aliases
|
||||
alias_ids = []
|
||||
if alias_vals_list:
|
||||
alias_ids = iter(self.env['mail.alias'].sudo().create(alias_vals_list).ids)
|
||||
|
||||
# update alias values in create vals directly
|
||||
valid_vals_list = []
|
||||
record_vals_iter = iter(record_vals_list)
|
||||
for vals in vals_list:
|
||||
new_alias = not vals.get('alias_id')
|
||||
if new_alias:
|
||||
record_vals = next(record_vals_iter)
|
||||
record_vals['alias_id'] = next(alias_ids)
|
||||
valid_vals_list.append(record_vals)
|
||||
else:
|
||||
valid_vals_list.append(vals)
|
||||
|
||||
records = super(AliasMixin, self).create(valid_vals_list)
|
||||
|
||||
for record in records:
|
||||
record.alias_id.sudo().write(record._alias_get_creation_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 """
|
||||
alias_vals, record_vals = self._alias_filter_fields(vals, filters=self.ALIAS_WRITEABLE_FIELDS)
|
||||
if record_vals:
|
||||
super(AliasMixin, self).write(record_vals)
|
||||
if alias_vals and (record_vals or self.check_access_rights('write', raise_exception=False)):
|
||||
self.mapped('alias_id').sudo().write(alias_vals)
|
||||
|
||||
return True
|
||||
|
||||
def unlink(self):
|
||||
""" Delete the given records, and cascade-delete their corresponding alias. """
|
||||
aliases = self.mapped('alias_id')
|
||||
res = super(AliasMixin, self).unlink()
|
||||
aliases.sudo().unlink()
|
||||
return res
|
||||
|
||||
@api.returns(None, lambda value: value[0])
|
||||
def copy_data(self, default=None):
|
||||
data = super(AliasMixin, self).copy_data(default)[0]
|
||||
for fields_not_writable in set(self.env['mail.alias']._fields.keys()) - set(self.ALIAS_WRITEABLE_FIELDS):
|
||||
if fields_not_writable in data:
|
||||
del data[fields_not_writable]
|
||||
return [data]
|
||||
def _require_new_alias(self, record_vals):
|
||||
""" alias_id field is always required, due to inherits """
|
||||
return not record_vals.get('alias_id')
|
||||
|
||||
def _init_column(self, name):
|
||||
""" Create aliases for existing rows. """
|
||||
super(AliasMixin, self)._init_column(name)
|
||||
super()._init_column(name)
|
||||
if name == 'alias_id':
|
||||
# as 'mail.alias' records refer to 'ir.model' records, create
|
||||
# aliases after the reflection of models
|
||||
@@ -109,29 +50,3 @@ class AliasMixin(models.AbstractModel):
|
||||
record.with_context(mail_notrack=True).alias_id = alias
|
||||
_logger.info('Mail alias created for %s %s (id %s)',
|
||||
record._name, record.display_name, record.id)
|
||||
|
||||
# --------------------------------------------------
|
||||
# MIXIN TOOL OVERRIDE METHODS
|
||||
# --------------------------------------------------
|
||||
|
||||
def _alias_get_creation_values(self):
|
||||
""" Return values to create an alias, or to write on the alias after its
|
||||
creation.
|
||||
"""
|
||||
return {
|
||||
'alias_parent_thread_id': self.id if self.id else False,
|
||||
'alias_parent_model_id': self.env['ir.model']._get(self._name).id,
|
||||
}
|
||||
|
||||
def _alias_filter_fields(self, values, filters=False):
|
||||
""" Split the vals dict into two dictionnary of vals, one for alias
|
||||
field and the other for other fields """
|
||||
if not filters:
|
||||
filters = self.env['mail.alias']._fields.keys()
|
||||
alias_values, record_values = {}, {}
|
||||
for fname in values.keys():
|
||||
if fname in filters:
|
||||
alias_values[fname] = values.get(fname)
|
||||
else:
|
||||
record_values[fname] = values.get(fname)
|
||||
return alias_values, record_values
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AliasMixinOptional(models.AbstractModel):
|
||||
""" A mixin for models that handles underlying 'mail.alias' records to use
|
||||
the mail gateway. Field is not mandatory and its creation is done dynamically
|
||||
based on given 'alias_name', allowing to gradually populate the alias table
|
||||
without having void aliases as when used with an inherits-like implementation.
|
||||
"""
|
||||
_name = 'mail.alias.mixin.optional'
|
||||
_description = 'Email Aliases Mixin (light)'
|
||||
ALIAS_WRITEABLE_FIELDS = ['alias_name', 'alias_contact', 'alias_defaults', 'alias_bounced_content']
|
||||
|
||||
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_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
|
||||
# --------------------------------------------------
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
""" Create aliases using sudo if an alias is required, notably if its
|
||||
name is given. """
|
||||
# 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):
|
||||
alias_vals, record_vals = self._alias_filter_fields(vals)
|
||||
alias_vals.update(self._alias_get_creation_values())
|
||||
alias_vals_list.append(alias_vals)
|
||||
record_vals_list.append(record_vals)
|
||||
|
||||
# create all aliases
|
||||
alias_ids = []
|
||||
if alias_vals_list:
|
||||
alias_ids = iter(self.env['mail.alias'].sudo().create(alias_vals_list).ids)
|
||||
|
||||
# update alias values in create vals directly
|
||||
valid_vals_list = []
|
||||
record_vals_iter = iter(record_vals_list)
|
||||
for vals in vals_list:
|
||||
if self._require_new_alias(vals):
|
||||
record_vals = next(record_vals_iter)
|
||||
record_vals['alias_id'] = next(alias_ids)
|
||||
valid_vals_list.append(record_vals)
|
||||
else:
|
||||
valid_vals_list.append(vals)
|
||||
|
||||
records = super().create(valid_vals_list)
|
||||
|
||||
for record in records.filtered('alias_id'):
|
||||
record.alias_id.sudo().write(record._alias_get_creation_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. """
|
||||
# create missing aliases
|
||||
if vals.get('alias_name'):
|
||||
alias_create_values = [
|
||||
dict(
|
||||
record._alias_get_creation_values(),
|
||||
alias_name=self.env['mail.alias']._sanitize_alias_name(vals['alias_name']),
|
||||
)
|
||||
for record in self.filtered(lambda rec: not rec.alias_id)
|
||||
]
|
||||
if alias_create_values:
|
||||
aliases = self.env['mail.alias'].sudo().create(alias_create_values)
|
||||
for record, alias in zip(self.filtered(lambda rec: not rec.alias_id), aliases):
|
||||
record.alias_id = alias.id
|
||||
|
||||
alias_vals, record_vals = self._alias_filter_fields(vals, filters=self.ALIAS_WRITEABLE_FIELDS)
|
||||
if record_vals:
|
||||
super().write(record_vals)
|
||||
if alias_vals and (record_vals or self.check_access_rights('write', raise_exception=False)):
|
||||
self.mapped('alias_id').sudo().write(alias_vals)
|
||||
|
||||
return True
|
||||
|
||||
def unlink(self):
|
||||
""" Delete the given records, and cascade-delete their corresponding alias. """
|
||||
aliases = self.mapped('alias_id')
|
||||
res = super().unlink()
|
||||
aliases.sudo().unlink()
|
||||
return res
|
||||
|
||||
@api.returns(None, lambda value: value[0])
|
||||
def copy_data(self, default=None):
|
||||
data = super().copy_data(default)[0]
|
||||
for fields_not_writable in set(self.env['mail.alias']._fields.keys()) - set(self.ALIAS_WRITEABLE_FIELDS):
|
||||
if fields_not_writable in data:
|
||||
del data[fields_not_writable]
|
||||
return [data]
|
||||
|
||||
def _require_new_alias(self, record_vals):
|
||||
""" Create only if no existing alias, and if a name is given, to avoid
|
||||
creating inactive aliases (falsy name). """
|
||||
return not record_vals.get('alias_id') and record_vals.get('alias_name')
|
||||
|
||||
# --------------------------------------------------
|
||||
# MIXIN TOOL OVERRIDE METHODS
|
||||
# --------------------------------------------------
|
||||
|
||||
def _alias_get_creation_values(self):
|
||||
""" Return values to create an alias, or to write on the alias after its
|
||||
creation.
|
||||
"""
|
||||
return {
|
||||
'alias_parent_thread_id': self.id if self.id else False,
|
||||
'alias_parent_model_id': self.env['ir.model']._get_id(self._name),
|
||||
}
|
||||
|
||||
def _alias_filter_fields(self, values, filters=False):
|
||||
""" Split the vals dict into two dictionnary of vals, one for alias
|
||||
field and the other for other fields """
|
||||
if not filters:
|
||||
filters = self.env['mail.alias']._fields.keys()
|
||||
alias_values, record_values = {}, {}
|
||||
for fname in values.keys():
|
||||
if fname in filters:
|
||||
alias_values[fname] = values.get(fname)
|
||||
else:
|
||||
record_values[fname] = values.get(fname)
|
||||
return alias_values, record_values
|
||||
@@ -3,8 +3,8 @@
|
||||
<data>
|
||||
|
||||
<!-- Alias Form View -->
|
||||
<record model="ir.ui.view" id="view_mail_alias_form">
|
||||
<field name="name">mail.alias.form</field>
|
||||
<record model="ir.ui.view" id="mail_alias_view_form">
|
||||
<field name="name">mail.alias.view.form</field>
|
||||
<field name="model">mail.alias</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Alias">
|
||||
@@ -50,8 +50,8 @@
|
||||
</record>
|
||||
|
||||
<!-- Alias List View -->
|
||||
<record model="ir.ui.view" id="view_mail_alias_tree">
|
||||
<field name="name">mail.alias.tree</field>
|
||||
<record model="ir.ui.view" id="mail_alias_view_tree">
|
||||
<field name="name">mail.alias.view.tree</field>
|
||||
<field name="model">mail.alias</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Alias">
|
||||
@@ -78,8 +78,8 @@
|
||||
</record>
|
||||
|
||||
<!-- Alias Search View -->
|
||||
<record model="ir.ui.view" id="view_mail_alias_search">
|
||||
<field name="name">mail.alias.search</field>
|
||||
<record model="ir.ui.view" id="mail_alias_view_search">
|
||||
<field name="name">mail.alias.view.search</field>
|
||||
<field name="model">mail.alias</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Alias">
|
||||
@@ -102,7 +102,7 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_view_mail_alias" model="ir.actions.act_window">
|
||||
<record id="mail_alias_action" model="ir.actions.act_window">
|
||||
<field name="name">Aliases</field>
|
||||
<field name="res_model">mail.alias</field>
|
||||
<field name="context">{
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
sequence="10"/>
|
||||
<menuitem id="mail_alias_menu"
|
||||
parent="base.menu_email"
|
||||
action="action_view_mail_alias"
|
||||
action="mail_alias_action"
|
||||
sequence="11"
|
||||
groups="base.group_no_one"/>
|
||||
<menuitem id="mail.discuss_channel_menu_settings"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
'description': """
|
||||
Manage your mailing lists from Odoo.
|
||||
""",
|
||||
'version': '1.0',
|
||||
'version': '1.1',
|
||||
'depends': [
|
||||
'mail',
|
||||
'portal',
|
||||
|
||||
@@ -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}',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
{
|
||||
'name': 'Project',
|
||||
'version': '1.2',
|
||||
'version': '1.3',
|
||||
'website': 'https://www.odoo.com/app/project',
|
||||
'category': 'Services/Project',
|
||||
'sequence': 45,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 > 0" class="text-muted" groups="project.group_project_rating">
|
||||
<b class="me-1">
|
||||
|
||||
@@ -51,6 +51,27 @@ class MailTestSimpleUnfollow(models.Model):
|
||||
email_from = fields.Char()
|
||||
|
||||
|
||||
class MailTestAliasOptional(models.Model):
|
||||
""" A chatter model inheriting from the alias mixin using optional alias_id
|
||||
field, hence no inherits. """
|
||||
_description = 'Chatter Model using Optional Alias Mixin'
|
||||
_name = 'mail.test.alias.optional'
|
||||
_inherit = ['mail.alias.mixin.optional']
|
||||
|
||||
name = fields.Char()
|
||||
company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
|
||||
email_from = fields.Char()
|
||||
|
||||
def _alias_get_creation_values(self):
|
||||
""" Updates itself """
|
||||
values = super()._alias_get_creation_values()
|
||||
values['alias_model_id'] = self.env['ir.model']._get_id('mail.test.alias.optional')
|
||||
if self.id:
|
||||
values['alias_force_thread_id'] = self.id
|
||||
values['alias_defaults'] = {'company_id': self.company_id.id}
|
||||
return values
|
||||
|
||||
|
||||
class MailTestGateway(models.Model):
|
||||
""" A very simple model only inheriting from mail.thread to test pure mass
|
||||
mailing features and base performances. """
|
||||
@@ -310,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']
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_mail_performance_thread,access_mail_performance_thread,model_mail_performance_thread,base.group_user,1,1,1,1
|
||||
access_mail_performance_tracking_user,mail.performance.tracking,model_mail_performance_tracking,base.group_user,1,1,1,1
|
||||
access_mail_test_alias_optional_portal,mail.test.alias.optional.portal,model_mail_test_alias_optional,base.group_portal,1,0,0,0
|
||||
access_mail_test_alias_optional_user,mail.test.alias.optional.user,model_mail_test_alias_optional,base.group_user,1,1,1,1
|
||||
access_mail_test_simple_portal,mail.test.simple.portal,model_mail_test_simple,base.group_portal,1,0,0,0
|
||||
access_mail_test_simple_user,mail.test.simple.user,model_mail_test_simple,base.group_user,1,1,1,1
|
||||
access_mail_test_simple_unfollow_portal,mail.test.simple.unfollow.portal,model_mail_test_simple_unfollow,base.group_portal,0,0,0,0
|
||||
|
||||
|
@@ -1,6 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import psycopg2
|
||||
|
||||
from ast import literal_eval
|
||||
|
||||
from odoo import exceptions
|
||||
from odoo.addons.mail.tests.common import MailCommon
|
||||
from odoo.tests import tagged
|
||||
@@ -15,11 +19,17 @@ class TestMailAliasCommon(MailCommon):
|
||||
super().setUpClass()
|
||||
cls._activate_multi_company()
|
||||
|
||||
cls.test_alias_mc = cls.env['mail.alias'].create({
|
||||
'alias_model_id': cls.env['ir.model']._get('mail.test.container.mc').id,
|
||||
'alias_name': 'test.alias',
|
||||
})
|
||||
|
||||
|
||||
@tagged('mail_gateway', 'mail_alias', 'multi_company')
|
||||
class TestMailAlias(TestMailAliasCommon):
|
||||
""" Test alias model features, constraints and behavior. """
|
||||
|
||||
@users('admin')
|
||||
def test_alias_domain_allowed_validation(self):
|
||||
""" Check the validation of `mail.catchall.domain.allowed` system parameter"""
|
||||
for value in [',', ',,', ', ,']:
|
||||
@@ -37,6 +47,50 @@ class TestMailAlias(TestMailAliasCommon):
|
||||
self.env['ir.config_parameter'].set_param('mail.catchall.domain.allowed', value)
|
||||
self.assertEqual(self.env['ir.config_parameter'].get_param('mail.catchall.domain.allowed'), expected)
|
||||
|
||||
@users('admin')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_alias_domain_parameters(self):
|
||||
""" Check the validation of ``mail.bounce.alias`` and ``mail.catchall.alias``
|
||||
parameters. """
|
||||
ICP = self.env['ir.config_parameter']
|
||||
# sanitization
|
||||
for (bounce_value, catchall_value), (expected_bounce, expected_catchall) in zip(
|
||||
[
|
||||
('bounce+b4r=*R3wl_#_-$€{}[]()~|\\/!?&%^\'"`~', 'catchall+b4r=*R3wl_#_-$€{}[]()~|\\/!?&%^\'"`~'),
|
||||
('bounce+😊', 'catchall+😊'),
|
||||
('Bouncâïde 😊', 'Catchôïee 😊'),
|
||||
('ぁ', 'ぁぁ'),
|
||||
],
|
||||
[
|
||||
('bounce+b4r=*r3wl_#_-$-{}-~|-/!?&%^\'-`~', 'catchall+b4r=*r3wl_#_-$-{}-~|-/!?&%^\'-`~'),
|
||||
('bounce+-', 'catchall+-'),
|
||||
('bouncaide-', 'catchoiee-'),
|
||||
('?', '??'),
|
||||
]
|
||||
):
|
||||
with self.subTest(bounce_value=bounce_value):
|
||||
ICP.set_param('mail.bounce.alias', bounce_value)
|
||||
self.assertEqual(ICP.get_param('mail.bounce.alias'), expected_bounce)
|
||||
with self.subTest(catchall_value=catchall_value):
|
||||
ICP.set_param('mail.catchall.alias', catchall_value)
|
||||
self.assertEqual(ICP.get_param('mail.catchall.alias'), expected_catchall)
|
||||
|
||||
# falsy values
|
||||
for config_value in [False, None, '', ' ']:
|
||||
with self.subTest(config_value=config_value):
|
||||
ICP.set_param('mail.bounce.alias', config_value)
|
||||
self.assertFalse(ICP.get_param('mail.bounce.alias'))
|
||||
ICP.set_param('mail.catchall.alias', config_value)
|
||||
self.assertFalse(ICP.get_param('mail.catchall.alias'))
|
||||
|
||||
# check successive param set, should not raise for unicity against itself
|
||||
for _ in range(2):
|
||||
ICP.set_param('mail.bounce.alias', 'bounce+double.test')
|
||||
ICP.set_param('mail.catchall.alias', 'catchall+double.test')
|
||||
self.assertEqual(ICP.get_param('mail.bounce.alias'), 'bounce+double.test')
|
||||
self.assertEqual(ICP.get_param('mail.catchall.alias'), 'catchall+double.test')
|
||||
|
||||
@users('admin')
|
||||
def test_alias_name_unique(self):
|
||||
alias_model_id = self.env['ir.model']._get('mail.test.gateway').id
|
||||
catchall_alias = self.env['ir.config_parameter'].sudo().get_param('mail.catchall.alias')
|
||||
@@ -52,22 +106,48 @@ class TestMailAlias(TestMailAliasCommon):
|
||||
'alias_model_id': alias_model_id,
|
||||
'alias_name': 'unused.test.alias'
|
||||
})
|
||||
other_alias = self.env['mail.alias'].create({
|
||||
'alias_model_id': alias_model_id,
|
||||
'alias_name': 'other.test.alias'
|
||||
})
|
||||
|
||||
# test that alias names should be unique
|
||||
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
|
||||
self.env['mail.alias'].create({
|
||||
'alias_model_id': alias_model_id,
|
||||
'alias_name': 'unused.test.alias',
|
||||
})
|
||||
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
|
||||
self.env['mail.alias'].create([
|
||||
{
|
||||
'alias_model_id': alias_model_id,
|
||||
'alias_name': alias_name,
|
||||
}
|
||||
for alias_name in ('new.alias.1', 'new.alias.2', 'new.alias.1')
|
||||
])
|
||||
with self.assertRaises(psycopg2.errors.UniqueViolation), self.cr.savepoint(), mute_logger('odoo.sql_db'):
|
||||
new_mail_alias.copy({'alias_name': 'unused.test.alias'})
|
||||
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
|
||||
other_alias.write({'alias_name': 'unused.test.alias'})
|
||||
|
||||
# test that re-using catchall and bounce alias raises UserError
|
||||
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
|
||||
new_mail_alias.write({
|
||||
'alias_name': catchall_alias
|
||||
})
|
||||
new_mail_alias.write({'alias_name': catchall_alias})
|
||||
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
|
||||
new_mail_alias.write({
|
||||
'alias_name': bounce_alias
|
||||
})
|
||||
new_mail_alias.write({'alias_name': bounce_alias})
|
||||
|
||||
new_mail_alias.write({'alias_name': 'another.unused.test.alias'})
|
||||
|
||||
# test that duplicating an alias should have blank name
|
||||
copy_new_mail_alias = new_mail_alias.copy()
|
||||
self.assertFalse(copy_new_mail_alias.alias_name)
|
||||
copy_1 = new_mail_alias.copy()
|
||||
self.assertFalse(copy_1.alias_name)
|
||||
# test sanitize of copy with new name
|
||||
copy_2 = new_mail_alias.copy({'alias_name': 'test.alias.2.éè#'})
|
||||
self.assertEqual(copy_2.alias_name, 'test.alias.2.ee#')
|
||||
|
||||
# cannot batch update, would create duplicates
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
(copy_1 + copy_2).write({'alias_name': 'test.alias.other'})
|
||||
|
||||
# cannot set catchall / bounce to used alias
|
||||
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
|
||||
@@ -75,21 +155,88 @@ class TestMailAlias(TestMailAliasCommon):
|
||||
with self.assertRaises(exceptions.UserError), self.cr.savepoint():
|
||||
self.env['ir.config_parameter'].sudo().set_param('mail.bounce.alias', new_mail_alias.alias_name)
|
||||
|
||||
@users('admin')
|
||||
@mute_logger('odoo.models.unlink')
|
||||
def test_alias_name_sanitize(self):
|
||||
alias = self.env['mail.alias'].create({
|
||||
'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
|
||||
'alias_name': 'bidule...inc.',
|
||||
})
|
||||
self.assertEqual(alias.alias_name, 'bidule.inc', 'Emails cannot start or end with a dot, there cannot be a sequence of dots.')
|
||||
""" Check sanitizer, at both create, copy and write on alias name. """
|
||||
alias_names = [
|
||||
'bidule...inc.',
|
||||
'b4r+=*R3wl_#_-$€{}[]()~|\\/!?&%^\'"`~',
|
||||
'hélène.prôvâïder',
|
||||
'😊',
|
||||
'Déboulonneur 😊',
|
||||
'ぁ',
|
||||
]
|
||||
expected_names = [
|
||||
'bidule.inc',
|
||||
'b4r+=*r3wl_#_-$-{}-~|-/!?&%^\'-`~',
|
||||
'helene.provaider',
|
||||
'-',
|
||||
'deboulonneur-',
|
||||
'?',
|
||||
]
|
||||
msgs = [
|
||||
'Emails cannot start or end with a dot, there cannot be a sequence of dots.',
|
||||
'Disallowed chars should be replaced by hyphens',
|
||||
'Email alias should be unaccented',
|
||||
'Only a subset of unaccented latin chars are valid, others are replaced',
|
||||
'Only a subset of unaccented latin chars are valid, others are replaced',
|
||||
'Only a subset of unaccented latin chars are valid, others are replaced',
|
||||
]
|
||||
for alias_name, expected, msg in zip(alias_names, expected_names, msgs):
|
||||
with self.subTest(alias_name=alias_name):
|
||||
alias = self.env['mail.alias'].create({
|
||||
'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
|
||||
'alias_name': alias_name,
|
||||
})
|
||||
self.assertEqual(alias.alias_name, expected, msg)
|
||||
alias.unlink()
|
||||
|
||||
alias = self.env['mail.alias'].create({
|
||||
'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
|
||||
'alias_name': 'b4r+_#_R3wl$$',
|
||||
})
|
||||
self.assertEqual(alias.alias_name, 'b4r+_-_r3wl-', 'Disallowed chars should be replaced by hyphens')
|
||||
# check at write
|
||||
for alias_name, expected, msg in zip(alias_names, expected_names, msgs):
|
||||
with self.subTest(alias_name=alias_name):
|
||||
alias.write({'alias_name': alias_name})
|
||||
self.assertEqual(alias.alias_name, expected, msg)
|
||||
|
||||
@users('admin')
|
||||
def test_alias_name_sanitize_false(self):
|
||||
""" Check empty-like aliases are forced to False, as otherwise unique
|
||||
constraint might fail with empty strings. """
|
||||
aliases = self.env['mail.alias'].create([
|
||||
{
|
||||
'alias_model_id': self.env['ir.model']._get('mail.test.container').id,
|
||||
'alias_name': falsy_name,
|
||||
}
|
||||
# '.' -> not allowed to start with a "." hence False
|
||||
for falsy_name in [False, None, '', ' ', '.']
|
||||
])
|
||||
for alias in aliases:
|
||||
with self.subTest(alias_name=alias.alias_name):
|
||||
self.assertFalse(alias.alias_name, 'Void values should resolve to False')
|
||||
|
||||
# try to reset names in batch: should work
|
||||
for idx, alias in enumerate(aliases):
|
||||
alias.write({'alias_name': f'unique-{idx}'})
|
||||
aliases.write({'alias_name': ''})
|
||||
for alias in aliases:
|
||||
self.assertEqual(alias.alias_name, False)
|
||||
|
||||
@users('admin')
|
||||
def test_alias_setup(self):
|
||||
""" Test various constraints / configuration of alias model"""
|
||||
alias = self.env['mail.alias'].create({
|
||||
'alias_model_id': self.env['ir.model']._get('mail.test.container.mc').id,
|
||||
'alias_name': 'unused.test.alias'
|
||||
})
|
||||
self.assertEqual(alias.alias_status, 'not_tested')
|
||||
|
||||
# validation of alias_defaults
|
||||
with self.assertRaises(exceptions.ValidationError):
|
||||
alias.write({'alias_defaults': "{'custom_field': brokendict"})
|
||||
alias.write({'alias_defaults': "{'custom_field': 'validdict'}"})
|
||||
|
||||
|
||||
@tagged('mail_gateway', 'mail_alias', 'multi_company')
|
||||
@@ -99,7 +246,7 @@ class TestMailAliasMixin(TestMailAliasCommon):
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.addons.base.models.ir_model')
|
||||
def test_alias_creation(self):
|
||||
def test_alias_mixin(self):
|
||||
record = self.env['mail.test.container'].create({
|
||||
'name': 'Test Record',
|
||||
'alias_name': 'alias.test',
|
||||
@@ -132,6 +279,60 @@ class TestMailAliasMixin(TestMailAliasCommon):
|
||||
with self.assertRaises(exceptions.ValidationError):
|
||||
record.write({'alias_defaults': "{'custom_field': brokendict"})
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.addons.base.models.ir_model')
|
||||
def test_alias_mixin_alias_id_management(self):
|
||||
""" Test alias_id being not mandatory """
|
||||
record_wo_alias, record_w_alias = self.env['mail.test.alias.optional'].create([
|
||||
{
|
||||
'name': 'Test WoAlias Name',
|
||||
}, {
|
||||
'alias_name': 'Alias Name',
|
||||
'name': 'Test WoAlias Name',
|
||||
}
|
||||
])
|
||||
self.assertFalse(record_wo_alias.alias_id, 'Alias record not created if not necessary (no alias_name)')
|
||||
self.assertFalse(record_wo_alias.alias_id.alias_name)
|
||||
self.assertFalse(record_wo_alias.alias_id.alias_defaults)
|
||||
self.assertFalse(record_wo_alias.alias_name)
|
||||
self.assertTrue(record_w_alias.alias_id, 'Alias record created as alias_name was given')
|
||||
self.assertEqual(record_w_alias.alias_id.alias_name, 'alias-name', 'Alias name should go through sanitize')
|
||||
self.assertEqual(
|
||||
literal_eval(record_w_alias.alias_id.alias_defaults),
|
||||
{'company_id': self.env.company.id}
|
||||
)
|
||||
self.assertEqual(record_w_alias.alias_name, 'alias-name', 'Alias name should go through sanitize')
|
||||
self.assertEqual(
|
||||
literal_eval(record_w_alias.alias_defaults),
|
||||
{'company_id': self.env.company.id}
|
||||
)
|
||||
|
||||
# update existing alias
|
||||
record_w_alias.write({'alias_contact': 'followers', 'alias_name': 'Updated Alias Name'})
|
||||
self.assertEqual(record_w_alias.alias_id.alias_contact, 'followers')
|
||||
self.assertEqual(record_w_alias.alias_id.alias_name, 'updated-alias-name')
|
||||
self.assertEqual(record_w_alias.alias_name, 'updated-alias-name')
|
||||
|
||||
# update non existing alias -> creates alias
|
||||
record_wo_alias.write({'alias_name': 'trying a name'})
|
||||
self.assertTrue(record_wo_alias.alias_id, 'Alias record should have been created to store the name')
|
||||
self.assertEqual(record_wo_alias.alias_id.alias_name, 'trying-a-name')
|
||||
self.assertEqual(
|
||||
literal_eval(record_wo_alias.alias_id.alias_defaults),
|
||||
{'company_id': self.env.company.id}
|
||||
)
|
||||
self.assertEqual(record_wo_alias.alias_name, 'trying-a-name')
|
||||
self.assertEqual(
|
||||
literal_eval(record_wo_alias.alias_defaults),
|
||||
{'company_id': self.env.company.id}
|
||||
)
|
||||
|
||||
# reset alias -> keep the alias as void, don't remove it
|
||||
existing_aliases = record_wo_alias.alias_id + record_w_alias.alias_id
|
||||
(record_wo_alias + record_w_alias).write({'alias_name': False})
|
||||
self.assertEqual((record_wo_alias + record_w_alias).alias_id, existing_aliases)
|
||||
self.assertFalse(list(filter(None, existing_aliases.mapped('alias_name'))))
|
||||
|
||||
@users('employee')
|
||||
def test_alias_mixin_copy_content(self):
|
||||
self.assertFalse(self.env.user.has_group('base.group_system'), 'Test user should not have Administrator access')
|
||||
@@ -151,3 +352,23 @@ class TestMailAliasMixin(TestMailAliasCommon):
|
||||
self.assertEqual(record_copy.alias_bounced_content, new_content)
|
||||
record_copy2 = record_copy.copy()
|
||||
self.assertEqual(record_copy2.alias_bounced_content, new_content)
|
||||
|
||||
@users('erp_manager')
|
||||
def test_alias_mixin_mc(self):
|
||||
""" Test company change does not impact anything at alias domain level """
|
||||
record = self.env['mail.test.container.mc'].create({
|
||||
'name': 'Test Record',
|
||||
'alias_name': 'alias.test',
|
||||
'alias_contact': 'followers',
|
||||
'company_id': self.env.user.company_id.id,
|
||||
})
|
||||
self.assertEqual(record.alias_domain, self.alias_domain)
|
||||
self.assertEqual(record.company_id, self.company_2)
|
||||
|
||||
record.write({'company_id': self.company_admin.id})
|
||||
self.assertEqual(record.alias_domain, self.alias_domain)
|
||||
self.assertEqual(record.company_id, self.company_admin)
|
||||
|
||||
record.write({'company_id': False})
|
||||
self.assertEqual(record.alias_domain, self.alias_domain)
|
||||
self.assertFalse(record.company_id)
|
||||
|
||||
Reference in New Issue
Block a user