[REF] mail: move generic methods from thread to BaseModel

PURPOSE

Since v13 some commits added code in mail thread file. This file is already
quite long and keeping it organized allow to understand its content.

SPECIFICATIONS

Some methods defined on mail.thread may actually be called on models
not inheriting from mail.thread . Instead of having model methods on mail
thread receiving a records parameter it seems better to have those available
directly on BaseModel.

Move static_message_track on BaseModel in models.py. This method is now called
_mail_track, to be coherent with othe rmail-related naming. It is used in
accounting to track values in line model that does not inherit from mail.
thread. Update accounting accordingly (followup of d862965).

Move _message_get_default_recipients_on_records in models.py. Renaming it
_message_get_default_recipients() allow to be compatible with current behavior
and current override available in some addons (like CRM, event, ...).

Move _notify_get_reply_to_on_records in models.py. Renaming it
_notify_get_reply_to() allows to be shorter and coherent.

Move _alias_check_contact_on_record in models.py. Renaming it
_alias_check_contact_() allows to be shorter and coherent. Its override in
hr is also moved on BaseModel.

LINKS

Task ID-2327096 (code cleaning)
PR #56631

X-original-commit: f5df1ed912455e5ed52a65df3149f30a9d424de0
This commit is contained in:
Thibault Delavallée
2020-08-28 07:59:23 +00:00
parent 371ab515bb
commit f5c6f14bb1
11 changed files with 203 additions and 213 deletions
+5 -5
View File
@@ -3713,16 +3713,16 @@ class AccountMoveLine(models.Model):
for move_id, modified_lines in move_initial_values.items():
tmp_move = {move_id: []}
for line in self.filtered(lambda l: l.move_id.id == move_id):
tracked_field = self.env['mail.thread'].static_message_track(line, ref_fields, modified_lines) # Return a tuple like (changed field, ORM command)
changes, tracking_value_ids = line._mail_track(ref_fields, modified_lines) # Return a tuple like (changed field, ORM command)
tmp = {'line_id': line.id}
if len(tracked_field[1]) > 0:
selected_field = tracked_field[1][0][2] # Get the last element of the tuple in the list of ORM command. (changed, [(0, 0, THIS)])
if tracking_value_ids:
selected_field = tracking_value_ids[0][2] # Get the last element of the tuple in the list of ORM command. (changed, [(0, 0, THIS)])
tmp.update({
**{'field_name': selected_field.get('field_desc')},
**self._get_formated_values(selected_field)
})
elif len(tracked_field[0]):
field_name = line._fields[tracked_field[0].pop()].string # Get the field name
elif changes:
field_name = line._fields[changes.pop()].string # Get the field name
tmp.update({
'error': True,
'field_error': field_name
+1
View File
@@ -10,6 +10,7 @@ from . import hr_job
from . import hr_plan
from . import mail_alias
from . import mail_channel
from . import models
from . import res_config_settings
from . import res_partner
from . import res_users
+2 -19
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, tools, _
from odoo import fields, models, _
class Alias(models.Model):
@@ -16,21 +16,4 @@ class Alias(models.Model):
return _("""Hi,<br/>
Your document has not been created because your email address is not recognized.<br/>
Please send emails with the email address recorded on your employee information, or contact your HR manager.""")
else:
return super(Alias, self)._get_alias_bounced_body_fallback(message_dict)
class AliasMixin(models.AbstractModel):
_inherit = 'mail.alias.mixin'
def _alias_check_contact_on_record(self, record, message, message_dict, alias):
if alias.alias_contact == 'employees':
email_from = tools.decode_message_header(message, 'From')
email_address = tools.email_split(email_from)[0]
employee = self.env['hr.employee'].search([('work_email', 'ilike', email_address)], limit=1)
if not employee:
employee = self.env['hr.employee'].search([('user_id.email', 'ilike', email_address)], limit=1)
if not employee:
return _('restricted to employees')
return True
return super(AliasMixin, self)._alias_check_contact_on_record(record, message, message_dict, alias)
return super(Alias, self)._get_alias_bounced_body_fallback(message_dict)
+20
View File
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, tools, _
class BaseModel(models.AbstractModel):
_inherit = 'base'
def _alias_get_error_message(self, message, message_dict, alias):
if alias.alias_contact == 'employees':
email_from = tools.decode_message_header(message, 'From')
email_address = tools.email_split(email_from)[0]
employee = self.env['hr.employee'].search([('work_email', 'ilike', email_address)], limit=1)
if not employee:
employee = self.env['hr.employee'].search([('user_id.email', 'ilike', email_address)], limit=1)
if not employee:
return _('restricted to employees')
return True
return super(BaseModel, self)._alias_get_error_message(message, message_dict, alias)
-25
View File
@@ -120,28 +120,3 @@ class AliasMixin(models.AbstractModel):
else:
record_values[fname] = values.get(fname)
return alias_values, record_values
# --------------------------------------------------
# GATEWAY
# --------------------------------------------------
def _alias_check_contact(self, message, message_dict, alias):
""" Main mixin method that inheriting models may inherit in order
to implement a specifc behavior. """
return self._alias_check_contact_on_record(self, message, message_dict, alias)
def _alias_check_contact_on_record(self, record, message, message_dict, alias):
""" Generic method that takes a record not necessarily inheriting from
mail.alias.mixin. """
author = self.env['res.partner'].browse(message_dict.get('author_id', False))
if alias.alias_contact == 'followers':
if not record.ids:
return _('incorrectly configured alias (unknown reference record)')
if not hasattr(record, "message_partner_ids") or not hasattr(record, "message_channel_ids"):
return _('incorrectly configured alias')
accepted_partner_ids = record.message_partner_ids | record.message_channel_ids.mapped('channel_partner_ids')
if not author or author not in accepted_partner_ids:
return _('restricted to followers')
elif alias.alias_contact == 'partners' and not author:
return _('restricted to known authors')
return True
+2 -2
View File
@@ -386,13 +386,13 @@ class Channel(models.Model):
})
return message
def _alias_check_contact(self, message, message_dict, alias):
def _alias_get_error_message(self, message, message_dict, alias):
if alias.alias_contact == 'followers' and self.ids:
author = self.env['res.partner'].browse(message_dict.get('author_id', False))
if not author or author not in self.channel_partner_ids:
return _('restricted to channel members')
return True
return super(Channel, self)._alias_check_contact(message, message_dict, alias)
return super(Channel, self)._alias_get_error_message(message, message_dict, alias)
def init(self):
self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_channel_partner_seen_message_id_idx',))
+3 -3
View File
@@ -1167,15 +1167,15 @@ class Message(models.Model):
def _get_reply_to(self, values):
""" Return a specific reply_to for the document """
model = values.get('model', self._context.get('default_model'))
res_id = values.get('res_id', self._context.get('default_res_id'))
res_id = values.get('res_id', self._context.get('default_res_id')) or False
email_from = values.get('email_from')
message_type = values.get('message_type')
records = None
if self.is_thread_message({'model': model, 'res_id': res_id, 'message_type': message_type}):
records = self.env[model].browse([res_id])
else:
res_id = False
return self.env['mail.thread']._notify_get_reply_to_on_records(default=email_from, records=records)[res_id]
records = self.env[model] if model else self.env['mail.thread']
return records._notify_get_reply_to(default=email_from)[res_id]
@api.model
def _get_message_id(self, values):
+1 -1
View File
@@ -116,7 +116,7 @@ class MailTemplate(models.Model):
if self.use_default_to or self._context.get('tpl_force_default_to'):
records = self.env[self.model].browse(res_ids).sudo()
default_recipients = self.env['mail.thread']._message_get_default_recipients_on_records(records)
default_recipients = records._message_get_default_recipients()
for res_id, recipients in default_recipients.items():
results[res_id].pop('partner_to', None)
results[res_id].update(recipients)
+4 -154
View File
@@ -574,38 +574,6 @@ class MailThread(models.AbstractModel):
def _track_template(self, changes):
return dict()
@api.model
def static_message_track(self, record, tracked_fields, initial):
""" For a given record, fields to check (tuple column name, column info)
and initial values, return a structure that is a tuple containing :
- a set of updated column names
- a list of ORM (0, 0, values) commands to create 'mail.tracking.value'
This static method is usefull when you don't want to inherit from mail.thread but
you want to use '_message_track()' method.
"""
record.ensure_one()
changes = set() # contains onchange tracked fields that changed
tracking_value_ids = []
# generate tracked_values data structure: {'col_name': {col_info, new_value, old_value}}
for col_name, col_info in tracked_fields.items():
if col_name not in initial:
continue
initial_value = initial[col_name]
new_value = record[col_name]
if new_value != initial_value and (new_value or initial_value): # because browse null != False
tracking_sequence = getattr(record._fields[col_name], 'tracking',
getattr(record._fields[col_name], 'track_sequence', 100)) # backward compatibility with old parameter name
if tracking_sequence is True:
tracking_sequence = 100
tracking = record.env['mail.tracking.value'].create_tracking_values(initial_value, new_value, col_name, col_info, tracking_sequence, record._name)
if tracking:
tracking_value_ids.append([0, 0, tracking])
changes.add(col_name)
return changes, tracking_value_ids
def message_track(self, tracked_fields, initial_values):
""" Track updated values. Comparing the initial and current values of
the fields given in tracked_fields, it generates a message containing
@@ -624,7 +592,7 @@ class MailThread(models.AbstractModel):
tracking = dict()
for record in self:
try:
tracking[record.id] = record._message_track(tracked_fields, initial_values[record.id])
tracking[record.id] = record._mail_track(tracked_fields, initial_values[record.id])
except MissingError:
continue
@@ -648,13 +616,6 @@ class MailThread(models.AbstractModel):
return tracking
def _message_track(self, tracked_fields, initial):
""" For a given record, fields to check (tuple column name, column info)
and initial values, return a structure that is a tuple containing :
- a set of updated column names
- a list of ORM (0, 0, values) commands to create 'mail.tracking.value' """
return self.static_message_track(self, tracked_fields, initial)
def _track_subtype(self, init_values):
""" Give the subtypes triggered by the changes on the record according
to values that have been updated.
@@ -835,13 +796,10 @@ class MailThread(models.AbstractModel):
obj = self.env[alias.alias_parent_model_id.model].browse(alias.alias_parent_thread_id)
else:
obj = self.env[model]
if hasattr(obj, '_alias_check_contact'):
error_code = obj._alias_check_contact(message, message_dict, alias)
else:
error_code = self.env['mail.alias.mixin']._alias_check_contact_on_record(obj, message, message_dict, alias)
if error_code is not True:
error_message = obj._alias_get_error_message(message, message_dict, alias)
if error_message:
self._routing_warn(
_('alias %(name)s: %(error)s', name=alias.alias_name, error=error_code or _('unknown error')),
_('alias %(name)s: %(error)s', name=alias.alias_name, error=error_message or _('unknown error')),
message_id,
route,
False
@@ -1480,30 +1438,6 @@ class MailThread(models.AbstractModel):
# RECIPIENTS MANAGEMENT TOOLS
# ------------------------------------------------------
@api.model
def _message_get_default_recipients_on_records(self, records):
""" Generic implementation for finding default recipient to mail on
a recordset. ``_message_get_default_recipients`` may be defined to
implement custom behavior. """
if hasattr(records, '_message_get_default_recipients'):
return records._message_get_default_recipients()
res = {}
for record in records:
recipient_ids, email_to, email_cc = [], False, False
if 'partner_id' in record and record.partner_id:
recipient_ids.append(record.partner_id.id)
elif 'email_normalized' in record and record.email_normalized:
email_to = record.email_normalized
elif 'email_from' in record and record.email_from:
email_to = record.email_from
elif 'partner_email' in record and record.partner_email:
email_to = record.partner_email
elif 'email' in record and record.email:
email_to = record.email
res[record.id] = {'partner_ids': recipient_ids, 'email_to': email_to, 'email_cc': email_cc}
return res
def _message_add_suggested_recipient(self, result, partner=None, email=None, reason=''):
""" Called by _message_get_suggested_recipients, to add a suggested
recipient in the result dictionary. The form is :
@@ -2637,90 +2571,6 @@ class MailThread(models.AbstractModel):
return result
def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None):
""" Returns the preferred reply-to email address when replying to a thread
on documents. Documents are either given by self it this method is called
as a true multi method on a record set or can be given using records to
have a generic implementation available for all models.
Reply-to is formatted like "MyCompany MyDocument <reply.to@domain>".
Heuristic it the following:
* search for specific aliases as they always have priority; it is limited
to aliases linked to documents (like project alias for task for example);
* use catchall address;
* use default;
This method works on documents
* as a true multi method for models inheriting from mail.thread; call
``records._notify_get_reply_to(...)``;
* as a generic implementation if records are given; call ``MailThread.
_notify_get_reply_to(records=records, ...)``;
* as a generic implementation is self is a void mail.thread record set
meaning catchall is computed; call ``MailThread._notify_get_reply_to
(records=None)``;
Tweak this method on a specific model by overriding if it inherits from
mail.thread. An example would be tasks taking their reply-to alias from
their project.
:param default: default email if no alias or catchall is found;
:param records: record_set if self if a generic mail.thread record allowing
generic implementation of finding reply-to;
:param company: used to compute company name part of the from name; provide
it if already known, otherwise fall back on user company;
:param doc_names: dict(res_id, doc_name) used to compute doc name part of
the from name; provide it if already known to avoid queries, otherwise
name_get on document will be performed;
:return result: dictionary. Keys are record IDs and value is formatted
like an email "Company_name Document_name <reply_to@email>"/
"""
_records = self if self and self._name != 'mail.thread' else records
model = _records._name if _records and _records._name != 'mail.thread' else False
res_ids = _records.ids if _records and model else []
_res_ids = res_ids or [False] # always have a default value located in False
alias_domain = self.env['ir.config_parameter'].sudo().get_param("mail.catchall.domain")
result = dict.fromkeys(_res_ids, False)
result_email = dict()
doc_names = doc_names if doc_names else dict()
if alias_domain:
if model and res_ids:
if not doc_names:
doc_names = dict((rec.id, rec.display_name) for rec in _records)
mail_aliases = self.env['mail.alias'].sudo().search([
('alias_parent_model_id.model', '=', model),
('alias_parent_thread_id', 'in', res_ids),
('alias_name', '!=', False)])
# take only first found alias for each thread_id, to match order (1 found -> limit=1 for each res_id)
for alias in mail_aliases:
result_email.setdefault(alias.alias_parent_thread_id, '%s@%s' % (alias.alias_name, alias_domain))
# left ids: use catchall
left_ids = set(_res_ids) - set(result_email)
if left_ids:
catchall = self.env['ir.config_parameter'].sudo().get_param("mail.catchall.alias")
if catchall:
result_email.update(dict((rid, '%s@%s' % (catchall, alias_domain)) for rid in left_ids))
# compute name of reply-to - TDE tocheck: quotes and stuff like that
company_name = company.name if company else self.env.company.name
for res_id in result_email.keys():
name = '%s%s%s' % (company_name, ' ' if doc_names.get(res_id) else '', doc_names.get(res_id, ''))
result[res_id] = tools.formataddr((name, result_email[res_id]))
left_ids = set(_res_ids) - set(result_email)
if left_ids:
result.update(dict((res_id, default) for res_id in left_ids))
return result
@api.model
def _notify_get_reply_to_on_records(self, default=None, records=None, company=None, doc_names=None):
""" Generic wrapper on ``_notify_get_reply_to`` checking mail.thread inheritance
and allowing to call model-specific implementation in a one liner. This
method should not be overridden. """
if records and hasattr(records, '_notify_get_reply_to'):
return records._notify_get_reply_to(default=default, company=company, doc_names=doc_names)
return self._notify_get_reply_to(default=default, records=records, company=company, doc_names=doc_names)
def _notify_email_recipient_values(self, recipient_ids):
""" Format email notification recipient values to store on the notification
mail.mail. Basic method just set the recipient partners as mail_mail
+163 -2
View File
@@ -3,15 +3,176 @@
from lxml.builder import E
from odoo import models, api
from odoo import api, models, tools, _
class BaseModel(models.AbstractModel):
_inherit = 'base'
# ------------------------------------------------------------
# GENERIC MAIL FEATURES
# ------------------------------------------------------------
def _mail_track(self, tracked_fields, initial):
""" For a given record, fields to check (tuple column name, column info)
and initial values, return a valid command to create tracking values.
:param tracked_fields: fields_get of updated fields on which tracking
is checked and performed;
:param initial: dict of initial values for each updated fields;
:return: a tuple (changes, tracking_value_ids) where
changes: set of updated column names;
tracking_value_ids: a list of ORM (0, 0, values) commands to create
``mail.tracking.value`` records;
Override this method on a specific model to implement model-specific
behavior. Also consider inheriting from ``mail.thread``. """
self.ensure_one()
changes = set() # contains onchange tracked fields that changed
tracking_value_ids = []
# generate tracked_values data structure: {'col_name': {col_info, new_value, old_value}}
for col_name, col_info in tracked_fields.items():
if col_name not in initial:
continue
initial_value = initial[col_name]
new_value = self[col_name]
if new_value != initial_value and (new_value or initial_value): # because browse null != False
tracking_sequence = getattr(self._fields[col_name], 'tracking',
getattr(self._fields[col_name], 'track_sequence', 100)) # backward compatibility with old parameter name
if tracking_sequence is True:
tracking_sequence = 100
tracking = self.env['mail.tracking.value'].create_tracking_values(initial_value, new_value, col_name, col_info, tracking_sequence, self._name)
if tracking:
tracking_value_ids.append([0, 0, tracking])
changes.add(col_name)
return changes, tracking_value_ids
def _message_get_default_recipients(self):
""" Generic implementation for finding default recipient to mail on
a recordset. This method is a generic implementation available for
all models as we could send an email through mail templates on models
not inheriting from mail.thread.
Override this method on a specific model to implement model-specific
behavior. Also consider inheriting from ``mail.thread``. """
res = {}
for record in self:
recipient_ids, email_to, email_cc = [], False, False
if 'partner_id' in record and record.partner_id:
recipient_ids.append(record.partner_id.id)
elif 'email_normalized' in record and record.email_normalized:
email_to = record.email_normalized
elif 'email_from' in record and record.email_from:
email_to = record.email_from
elif 'partner_email' in record and record.partner_email:
email_to = record.partner_email
elif 'email' in record and record.email:
email_to = record.email
res[record.id] = {'partner_ids': recipient_ids, 'email_to': email_to, 'email_cc': email_cc}
return res
def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None):
""" Returns the preferred reply-to email address when replying to a thread
on documents. This method is a generic implementation available for
all models as we could send an email through mail templates on models
not inheriting from mail.thread.
Reply-to is formatted like "MyCompany MyDocument <reply.to@domain>".
Heuristic it the following:
* search for specific aliases as they always have priority; it is limited
to aliases linked to documents (like project alias for task for example);
* use catchall address;
* use default;
This method can be used as a generic tools if self is a void recordset.
Override this method on a specific model to implement model-specific
behavior. Also consider inheriting from ``mail.thread``.
An example would be tasks taking their reply-to alias from their project.
:param default: default email if no alias or catchall is found;
:param records: DEPRECATED, self should be a valid record set or an
empty recordset if a generic reply-to is required;
:param company: used to compute company name part of the from name; provide
it if already known, otherwise fall back on user company;
:param doc_names: dict(res_id, doc_name) used to compute doc name part of
the from name; provide it if already known to avoid queries, otherwise
name_get on document will be performed;
:return result: dictionary. Keys are record IDs and value is formatted
like an email "Company_name Document_name <reply_to@email>"/
"""
if records:
raise ValueError('Use of records is deprecated as this method is available on BaseModel.')
_records = self
model = _records._name if _records and _records._name != 'mail.thread' else False
res_ids = _records.ids if _records and model else []
_res_ids = res_ids or [False] # always have a default value located in False
alias_domain = self.env['ir.config_parameter'].sudo().get_param("mail.catchall.domain")
result = dict.fromkeys(_res_ids, False)
result_email = dict()
doc_names = doc_names if doc_names else dict()
if alias_domain:
if model and res_ids:
if not doc_names:
doc_names = dict((rec.id, rec.display_name) for rec in _records)
mail_aliases = self.env['mail.alias'].sudo().search([
('alias_parent_model_id.model', '=', model),
('alias_parent_thread_id', 'in', res_ids),
('alias_name', '!=', False)])
# take only first found alias for each thread_id, to match order (1 found -> limit=1 for each res_id)
for alias in mail_aliases:
result_email.setdefault(alias.alias_parent_thread_id, '%s@%s' % (alias.alias_name, alias_domain))
# left ids: use catchall
left_ids = set(_res_ids) - set(result_email)
if left_ids:
catchall = self.env['ir.config_parameter'].sudo().get_param("mail.catchall.alias")
if catchall:
result_email.update(dict((rid, '%s@%s' % (catchall, alias_domain)) for rid in left_ids))
# compute name of reply-to - TDE tocheck: quotes and stuff like that
company_name = company.name if company else self.env.company.name
for res_id in result_email:
name = '%s%s%s' % (company_name, ' ' if doc_names.get(res_id) else '', doc_names.get(res_id, ''))
result[res_id] = tools.formataddr((name, result_email[res_id]))
left_ids = set(_res_ids) - set(result_email)
if left_ids:
result.update(dict((res_id, default) for res_id in left_ids))
return result
# ------------------------------------------------------------
# ALIAS MANAGEMENT
# ------------------------------------------------------------
def _alias_get_error_message(self, message, message_dict, alias):
""" Generic method that takes a record not necessarily inheriting from
mail.alias.mixin. """
author = self.env['res.partner'].browse(message_dict.get('author_id', False))
if alias.alias_contact == 'followers':
if not self.ids:
return _('incorrectly configured alias (unknown reference record)')
if not hasattr(self, "message_partner_ids") or not hasattr(self, "message_channel_ids"):
return _('incorrectly configured alias')
accepted_partner_ids = self.message_partner_ids | self.message_channel_ids.mapped('channel_partner_ids')
if not author or author not in accepted_partner_ids:
return _('restricted to followers')
elif alias.alias_contact == 'partners' and not author:
return _('restricted to known authors')
return False
# ------------------------------------------------------------
# ACTIVITY
# -----------------------------------------------------------
# ------------------------------------------------------------
@api.model
def _get_default_activity_view(self):
+2 -2
View File
@@ -278,7 +278,7 @@ class MailComposer(models.TransientModel):
reply_to_value = dict.fromkeys(res_ids, None)
if mass_mail_mode and not self.no_auto_thread:
records = self.env[self.model].browse(res_ids)
reply_to_value = self.env['mail.thread']._notify_get_reply_to_on_records(default=self.email_from, records=records)
reply_to_value = records._notify_get_reply_to(default=self.email_from)
blacklisted_rec_ids = []
if mass_mail_mode and issubclass(type(self.env[self.model]), self.pool['mail.thread.blacklist']):
@@ -462,7 +462,7 @@ class MailComposer(models.TransientModel):
default_recipients = {}
if not self.partner_ids:
records = self.env[self.model].browse(res_ids).sudo()
default_recipients = self.env['mail.thread']._message_get_default_recipients_on_records(records)
default_recipients = records._message_get_default_recipients()
results = dict.fromkeys(res_ids, False)
for res_id in res_ids: