19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -5,9 +5,10 @@ from collections import defaultdict
import itertools
from odoo import api, fields, models, Command
from odoo.addons.mail.tools.discuss import Store
class Followers(models.Model):
class MailFollowers(models.Model):
""" mail_followers holds the data related to the follow mechanism inside
Odoo. Partners can choose to follow documents (records) of any kind
that inherits from mail.thread. Following documents allow to receive
@ -17,7 +18,6 @@ class Followers(models.Model):
:param: res_id: ID of resource (may be 0 for every objects)
"""
_name = 'mail.followers'
_rec_name = 'partner_id'
_log_access = False
_description = 'Document Followers'
@ -29,7 +29,7 @@ class Followers(models.Model):
res_id = fields.Many2oneReference(
'Related Document ID', index=True, help='Id of the followed resource', model_field='res_model')
partner_id = fields.Many2one(
'res.partner', string='Related Partner', index=True, ondelete='cascade', required=True, domain=[('type', '!=', 'private')])
'res.partner', string='Related Partner', index=True, ondelete='cascade', required=True)
subtype_ids = fields.Many2many(
'mail.message.subtype', string='Subtype',
help="Message subtypes followed, meaning subtypes that will be pushed onto the user's Wall.")
@ -50,69 +50,113 @@ class Followers(models.Model):
@api.model_create_multi
def create(self, vals_list):
res = super(Followers, self).create(vals_list)
res = super().create(vals_list)
res._invalidate_documents(vals_list)
return res
def write(self, vals):
if 'res_model' in vals or 'res_id' in vals:
self._invalidate_documents()
res = super(Followers, self).write(vals)
res = super().write(vals)
if any(x in vals for x in ['res_model', 'res_id', 'partner_id']):
self._invalidate_documents()
return res
def unlink(self):
self._invalidate_documents()
return super(Followers, self).unlink()
return super().unlink()
_sql_constraints = [
('mail_followers_res_partner_res_model_id_uniq', 'unique(res_model,res_id,partner_id)', 'Error, a partner cannot follow twice the same object.'),
]
_mail_followers_res_partner_res_model_id_uniq = models.Constraint(
'unique(res_model,res_id,partner_id)',
'Error, a partner cannot follow twice the same object.',
)
@api.depends("partner_id")
def _compute_display_name(self):
for follower in self:
# sudo: res.partner - can read partners of accessible followers, in particular allows
# by-passing multi-company ACL for portal partners
follower.display_name = follower.partner_id.sudo().display_name
# --------------------------------------------------
# Private tools methods to fetch followers data
# --------------------------------------------------
@api.model
def _get_mail_doc_to_followers(self, mail_ids):
""" Get partner mail recipients that follows the related record of the mails.
:param list mail_ids: mail_mail ids
:return: for each (model, document_id): list of partner ids that are followers
:rtype: dict
"""
if not mail_ids:
return {}
self.env['mail.mail'].flush_model(['mail_message_id', 'recipient_ids'])
self.env['mail.followers'].flush_model(['partner_id', 'res_model', 'res_id'])
self.env['mail.message'].flush_model(['model', 'res_id'])
# mail_mail_res_partner_rel is the join table for the m2m recipient_ids field
self.env.cr.execute("""
SELECT message.model, message.res_id, mail_partner.res_partner_id
FROM mail_mail mail
JOIN mail_mail_res_partner_rel mail_partner ON mail_partner.mail_mail_id = mail.id
JOIN mail_message message ON mail.mail_message_id = message.id
JOIN mail_followers follower ON message.model = follower.res_model
AND message.res_id = follower.res_id
AND mail_partner.res_partner_id = follower.partner_id
WHERE mail.id IN %(mail_ids)s
""", {'mail_ids': tuple(mail_ids)})
res = defaultdict(list)
for model, doc_id, partner_id in self.env.cr.fetchall():
res[(model, doc_id)].append(partner_id)
return res
def _get_recipient_data(self, records, message_type, subtype_id, pids=None):
""" Private method allowing to fetch recipients data based on a subtype.
Purpose of this method is to fetch all data necessary to notify recipients
in a single query. It fetches data from
* followers (partners and channels) of records that follow the given
subtype if records and subtype are set;
* followers of records that follow the given subtype if records and
subtype are set;
* partners if pids is given;
:param records: fetch data from followers of ``records`` that follow
``subtype_id``;
:param message_type: mail.message.message_type in order to allow custom
:param str message_type: mail.message.message_type in order to allow custom
behavior depending on it (SMS for example);
:param subtype_id: mail.message.subtype to check against followers;
:param int subtype_id: mail.message.subtype to check against followers;
:param pids: additional set of partner IDs from which to fetch recipient
data independently from following status;
:return dict: recipients data based on record.ids if given, else a generic
:returns: recipients data based on record.ids if given, else a generic
'0' key to keep a dict-like return format. Each item is a dict based on
recipients partner ids formatted like
{'active': whether partner is active;
'id': res.partner ID;
'is_follower': True if linked to a record and if partner is a follower;
'lang': lang of the partner;
'groups': groups of the partner's user. If several users exist preference
is given to internal user, then share users. In case of multiples
users of same kind groups are unioned;
recipients partner ids formatted like {
'active': partner.active;
'email_normalized': partner.email_normalized;
'id': res.partner ID;
'is_follower': True if linked to a record and if partner is a follower;
'lang': partner.lang;
'name': partner.name;
'groups': groups of the partner's user (see 'uid'). If several users
of the same kind (e.g. several internal users) exist groups are
concatenated;
'notif': notification type ('inbox' or 'email'). Overrides may change
this value (e.g. 'sms' in sms module);
'share': if partner is a customer (no user or share user);
'ushare': if partner has users, whether all are shared (public or portal);
'type': summary of partner 'usage' (portal, customer, internal user);
'type': summary of partner 'usage' (a string among 'portal', 'customer',
'internal user');
'uid': linked 'res.users' ID. If several users exist preference is
given to internal user, then share users;
}
:rtype: dict
"""
self.env['mail.followers'].flush_model(['partner_id', 'subtype_ids'])
self.env['mail.message.subtype'].flush_model(['internal'])
self.env['res.users'].flush_model(['notification_type', 'active', 'partner_id', 'groups_id'])
self.env['res.partner'].flush_model(['active', 'partner_share'])
self.env['res.groups'].flush_model(['users'])
self.env['res.users'].flush_model(['notification_type', 'active', 'partner_id', 'group_ids'])
self.env['res.partner'].flush_model(['active', 'email_normalized', 'name', 'partner_share'])
self.env['res.groups'].flush_model(['user_ids'])
# if we have records and a subtype: we have to fetch followers, unless being
# in user notification mode (contact only pids)
if message_type != 'user_notification' and records and subtype_id:
@ -148,7 +192,9 @@ class Followers(models.Model):
)
SELECT partner.id as pid,
partner.active as active,
partner.email_normalized AS email_normalized,
partner.lang as lang,
partner.name as name,
partner.partner_share as pshare,
sub_user.uid as uid,
COALESCE(sub_user.share, FALSE) as ushare,
@ -185,7 +231,9 @@ class Followers(models.Model):
query = """
SELECT partner.id as pid,
partner.active as active,
partner.email_normalized AS email_normalized,
partner.lang as lang,
partner.name as name,
partner.partner_share as pshare,
sub_user.uid as uid,
COALESCE(sub_user.share, FALSE) as ushare,
@ -237,7 +285,9 @@ class Followers(models.Model):
query = """
SELECT partner.id as pid,
partner.active as active,
partner.email_normalized AS email_normalized,
partner.lang as lang,
partner.name as name,
partner.partner_share as pshare,
sub_user.uid as uid,
COALESCE(sub_user.share, FALSE) as ushare,
@ -276,17 +326,26 @@ class Followers(models.Model):
res_ids = records.ids if records else [0]
doc_infos = dict((res_id, {}) for res_id in res_ids)
for (partner_id, is_active, lang, pshare, uid, ushare, notif, groups, res_id, is_follower) in res:
for (
partner_id, is_active, email_normalized, lang, name,
pshare, uid, ushare, notif, groups, res_id, is_follower
) in res:
to_update = [res_id] if res_id else res_ids
# add transitive closure of implied groups; note that the field
# all_implied_ids relies on ormcache'd data, which shouldn't add
# more queries
groups = self.env['res.groups'].browse(set(groups or [])).all_implied_ids.ids
for res_id_to_update in to_update:
# avoid updating already existing information, unnecessary dict update
if not res_id and partner_id in doc_infos[res_id_to_update]:
continue
follower_data = {
'active': is_active,
'email_normalized': email_normalized,
'id': partner_id,
'is_follower': is_follower,
'lang': lang,
'name': name,
'groups': set(groups or []),
'notif': notif,
'share': pshare,
@ -306,7 +365,7 @@ class Followers(models.Model):
def _get_subscription_data(self, doc_data, pids, include_pshare=False, include_active=False):
""" Private method allowing to fetch follower data from several documents of a given model.
Followers can be filtered given partner IDs and channel IDs.
MailFollowers can be filtered given partner IDs and channel IDs.
:param doc_data: list of pair (res_model, res_ids) that are the documents from which we
want to have subscription data;
@ -322,8 +381,8 @@ class Followers(models.Model):
share status of partner (returned only if include_pshare is True)
active flag status of partner (returned only if include_active is True)
"""
self.env['mail.followers'].flush_model()
self.env['res.partner'].flush_model()
self.env['mail.followers'].flush_model(['partner_id', 'res_id', 'res_model', 'subtype_ids'])
self.env['res.partner'].flush_model(['active', 'partner_share'])
# base query: fetch followers of given documents
where_clause = ' OR '.join(['fol.res_model = %s AND fol.res_id IN %s'] * len(doc_data))
where_params = list(itertools.chain.from_iterable((rm, tuple(rids)) for rm, rids in doc_data))
@ -433,14 +492,11 @@ GROUP BY fol.id%s%s""" % (
:param subtypes: optional subtypes for new partner followers. This
is a dict whose keys are partner IDs and value subtype IDs for that
partner.
:param channel_subtypes: optional subtypes for new channel followers. This
is a dict whose keys are channel IDs and value subtype IDs for that
channel.
:param check_existing: if True, check for existing followers for given
documents and handle them according to existing_policy parameter.
Setting to False allows to save some computation if caller is sure
there are no conflict for followers;
:param existing policy: if check_existing, tells what to do with already
:param existing_policy: if check_existing, tells what to do with already
existing followers:
* skip: simply skip existing followers, do not touch them;
@ -483,3 +539,19 @@ GROUP BY fol.id%s%s""" % (
update[fol_id] = {'subtype_ids': update_cmd}
return new, update
# --------------------------------------------------
# Misc discuss
# --------------------------------------------------
def _to_store_defaults(self, target):
return [
"display_name",
"email",
"is_active",
"name",
# sudo: res.partner - can read partners of found followers, in particular allows
# by-passing multi-company ACL for portal partners
Store.One("partner_id", sudo=True),
Store.One("thread", [], as_thread=True),
]