Skip to content
Browse files

⚡️ mail_private New: added ability to select channels for private mes…

…sage sending
  • Loading branch information...
Ommo73 committed Mar 19, 2019
1 parent 32a27b6 commit 3650cc7dbbbebf1ee10f1a4467df24cec7a4fba2
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
# License LGPL-3.0 (

from . import models
@@ -1,10 +1,18 @@
# -*- coding: utf-8 -*-
# Copyright 2016 x620 <>
# Copyright 2016 Ilmir Karamov <>
# Copyright 2016 Ivan Yelizariev <>
# Copyright 2017 Artyom Losev <>
# Copyright 2018 Ruslan Ronzhin <>
# Copyright 2018 Kolushov Alexandr <>
# Copyright 2019 Artem Rafailov <>
# License LGPL-3.0 (
"name": """Internal Messaging""",
"summary": """Send private messages to specified recipients, regardless of who are in followers list.""",
"category": "Discuss",
"images": ['images/mail_private_image.png'],
"version": "",
"version": "",
"application": False,

"author": "IT-Projects LLC, Pavel Romanchenko",
@@ -1,3 +1,8 @@

- **New**: added ability to select channels for private message sending.


@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!--Copyright 2017 Artyom Losev <>
Copyright 2018 Kolushov Alexandr <>
License LGPL-3.0 (>


<record model="ir.ui.view" id="email_compose_message_wizard_form_private">
@@ -1,5 +1,11 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
# Copyright 2016 x620 <>
# Copyright 2016 manawi <>
# Copyright 2017 Artyom Losev <>
# Copyright 2019 Artem Rafailov <>
# License LGPL-3.0 (

from odoo import models, fields, api, exceptions, _, tools

class MailComposeMessage(models.TransientModel):
@@ -12,3 +18,201 @@ def send_mail(self, auto_commit=False):
for w in self:
w.is_log = True if w.is_private else w.is_log
super(MailComposeMessage, self).send_mail(auto_commit=False)

class MailMessage(models.Model):
_inherit = 'mail.message'

def _notify(self, force_send=False, send_after_commit=True, user_signature=True):
self_sudo = self.sudo()
if 'is_private' not in self_sudo._context or not self_sudo._context['is_private']:
super(MailMessage, self)._notify(force_send, send_after_commit, user_signature)
self._notify_mail_private(force_send, send_after_commit, user_signature)

def _notify_mail_private(self, force_send=False, send_after_commit=True, user_signature=True):
""" The method was partially copied from Odoo.
In the current method, the way of getting channels for a private message is changed.
# have a sudoed copy to manipulate partners (public can go here with
# website modules like forum / blog / ...

# TDE CHECK: add partners / channels as arguments to be able to notify a message with / without computation ??
self.ensure_one() # tde: not sure, just for testinh, will see
partners = self.env['res.partner'] | self.partner_ids
channels = self.env[''] | self.channel_ids

# update message, with maybe custom values
message_values = {
'channel_ids': [(6, 0, channels.ids)],
'needaction_partner_ids': [(6, 0, partners.ids)]
if self.model and self.res_id and hasattr(self.env[self.model], 'message_get_message_notify_values'):
self.env[self.model].browse(self.res_id).message_get_message_notify_values(self, message_values))

# notify partners and channels
partners._notify(self, force_send=force_send, send_after_commit=send_after_commit,

# Discard cache, because child / parent allow reading and therefore
# change access rights.
if self.parent_id:

return True

class MailThread(models.AbstractModel):
_inherit = 'mail.thread'

@api.returns('self', lambda value:
def message_post(self, body='', subject=None, message_type='notification',
subtype=None, parent_id=False, attachments=None,
content_subtype='html', **kwargs):
""" Post a new message in an existing thread, returning the new
mail.message ID.
:param int thread_id: thread ID to post into, or list with one ID;
if False/0, mail.message model will also be set as False
:param str body: body of the message, usually raw HTML that will
be sanitized
:param str type: see mail_message.type field
:param str content_subtype:: if plaintext: convert body into html
:param int parent_id: handle reply to a previous message by adding the
parent partners to the message in case of private discussion
:param tuple(str,str) attachments or list id: list of attachment tuples in the form
``(name,content)``, where content is NOT base64 encoded
Extra keyword arguments will be used as default column values for the
new mail.message record. Special cases:
- attachment_ids: supposed not attached to any document; attach them
to the related document. Should only be set by Chatter.
:return int: ID of newly created mail.message
if attachments is None:
attachments = {}
if self.ids and not self.ensure_one():
raise exceptions.Warning(
_('Invalid record set: should be called as model (without records) or on single-record recordset'))

# if we're processing a message directly coming from the gateway, the destination model was
# set in the context.
model = False
if self.ids:
model = self._context.get('thread_model', False) if self._name == 'mail.thread' else self._name
if model and model != self._name and hasattr(self.env[model], 'message_post'):
RecordModel = self.env[model].with_context(thread_model=None) # TDE: was removing the key ?
return RecordModel.browse(self.ids).message_post(
body=body, subject=subject, message_type=message_type,
subtype=subtype, parent_id=parent_id, attachments=attachments,
content_subtype=content_subtype, **kwargs)

# 0: Find the message's author, because we need it for private discussion
author_id = kwargs.get('author_id')
if author_id is None: # keep False values
author_id = self.env['mail.message']._get_default_author().id

# 1: Handle content subtype: if plaintext, converto into HTML
if content_subtype == 'plaintext':
body = tools.plaintext2html(body)

# 2: Private message: add recipients (recipients and author of parent message) - current author
# + legacy-code management (! we manage only 4 and 6 commands)
partner_ids = set()
kwargs_partner_ids = kwargs.pop('partner_ids', [])
for partner_id in kwargs_partner_ids:
if isinstance(partner_id, (list, tuple)) and partner_id[0] == 4 and len(partner_id) == 2:
if isinstance(partner_id, (list, tuple)) and partner_id[0] == 6 and len(partner_id) == 3:
partner_ids |= set(partner_id[2])
elif isinstance(partner_id, (int, long)):
pass # we do not manage anything else
if parent_id and not model:
parent_message = self.env['mail.message'].browse(parent_id)
private_followers = set([ for partner in parent_message.partner_ids])
if parent_message.author_id:
private_followers -= set([author_id])
partner_ids |= private_followers

# 4: mail.message.subtype
subtype_id = kwargs.get('subtype_id', False)
if not subtype_id:
subtype = subtype or 'mt_note'
if '.' not in subtype:
subtype = 'mail.%s' % subtype
subtype_id = self.env[''].xmlid_to_res_id(subtype)

# automatically subscribe recipients if asked to
if self._context.get('mail_post_autofollow') and self.ids and partner_ids:
partner_to_subscribe = partner_ids
if self._context.get('mail_post_autofollow_partner_ids'):
partner_to_subscribe = filter(
lambda item: item in self._context.get('mail_post_autofollow_partner_ids'), partner_ids)
self.message_subscribe(list(partner_to_subscribe), force=False)

# _mail_flat_thread: automatically set free messages to the first posted message
MailMessage = self.env['mail.message']
if self._mail_flat_thread and model and not parent_id and self.ids:
messages =
['&', ('res_id', '=', self.ids[0]), ('model', '=', model), ('message_type', '=', 'email')],
order="id ASC", limit=1)
if not messages:
messages =['&', ('res_id', '=', self.ids[0]), ('model', '=', model)],
order="id ASC", limit=1)
parent_id = messages and messages[0].id or False
# we want to set a parent: force to set the parent_id to the oldest ancestor, to avoid having more than 1 level of thread
elif parent_id:
messages = MailMessage.sudo().search([('id', '=', parent_id), ('parent_id', '!=', False)], limit=1)
# avoid loops when finding ancestors
processed_list = []
if messages:
message = messages[0]
while (message.parent_id and not in processed_list):
message = message.parent_id
parent_id =
values = kwargs
'author_id': author_id,
'model': model,
'res_id': model and self.ids[0] or False,
'body': body,
'subject': subject or False,
'message_type': message_type,
'parent_id': parent_id,
'subtype_id': subtype_id,
'partner_ids': [(4, pid) for pid in partner_ids],
'channel_ids': [(4, pid) for pid in kwargs['channel_ids']]

# 3. Attachments
# - HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message
attachment_ids = self._message_post_process_attachments(attachments, kwargs.pop('attachment_ids', []), values)
values['attachment_ids'] = attachment_ids

# Avoid warnings about non-existing fields
for x in ('from', 'to', 'cc'):
values.pop(x, None)

# Post the message
new_message = MailMessage.create(values)
# Post-process: subscribe author, update message_last_post
# Note: the message_last_post mechanism is no longer used. This
# will be removed in a later version.
if (self._context.get('mail_save_message_last_post') and model and model != 'mail.thread' and self.ids and subtype_id):
subtype_rec = self.env['mail.message.subtype'].sudo().browse(subtype_id)
if not subtype_rec.internal:
# done with SUPERUSER_ID, because on some models users can post only with read access, not necessarily write access
if new_message.author_id and model and self.ids and message_type != 'notification' and not self._context.get(
self.message_subscribe([], force=False)
return new_message

0 comments on commit 3650cc7

Please sign in to comment.
You can’t perform that action at this time.