[MERGE] Chatter and CRM: usability fixes and improvements

Chatter:
- now displays the 'To' of messages, aka notified people,
- now allows to Send a message or to Log a note, that is a message not pushed to anyone; however users that see the document still see the log message,
- fixed suggested recipient behavior: canceling the partner creation popup now correctly avoids creating a partner,
- moved 'Advanced wizard' button on top-right,
- unfollowing or removing someone from followers now displays a warning,
CRM, Recruitment, Issues:
- better management of customers/applicants: Chatter suggests to notify and add as follower the customer, or to create a partner based on the email_from,
CRM, Recruitment, Tasks, Issues:
- changing the user_id (salesman, responsible) still adds the related partner as follower; but now an unread notification is pushed, with the first email or first message if no email,
- updated and added if missing message_summary in kanban views, now displayed only when having unread messages, and the related number, to be more visible,
CRM:
- crm_partner_assign: fixed forward-to-partner wizard,
- crm_partner_assign: geo localization now also sets salesteam along with salesman,
- crm: fixed opportunity email_template,
- crm: removed 'Send mail' button, as the functionality should be covered by Chatter (but the code of the action is left untouched to avoid errors),
Mail:
- merged message_post and message_post_user_api because there was an opportunity to avoid doing similar things in two different methods,
- fixed email_from of incoming email not always stored,
- followers: authors of discussion messages are now added as followers, this is not only limited to incoming emails,
- followers: recipients of emails coming through the mail gateway are not automatically added as followers of the target documents,
- followers: slightly updated _notify, to be able to notify a partner of a specific message; the notification process is therefore accessible outside of the mail_message.create() process

bzr revid: tde@openerp.com-20130301101122-l18mr6hb0j5k4atv
This commit is contained in:
Thibault Delavallée
2013-03-01 11:11:22 +01:00
25 changed files with 565 additions and 326 deletions
+1 -2
View File
@@ -80,8 +80,7 @@
basis for email recipients, name and to ease the definition of a further elaborated template. -->
<record id="email_template_opportunity_mail" model="email.template">
<field name="name">Opportunity - Send Emails</field>
<field name="email_from">${object.user_id.email or ''}</field>
<field name="subject">Opportunity ${object.name | h})</field>
<field name="subject">${object.name}</field>
<field name="model_id" ref="crm.model_crm_lead"/>
<field name="auto_delete" eval="True"/>
<field name="email_recipients">${object.partner_id.id}</field>
+10 -1
View File
@@ -930,7 +930,7 @@ class crm_lead(base_stage, format_address, osv.osv):
try:
compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
except ValueError:
compose_form_id = False
compose_form_id = False
if context is None:
context = {}
ctx = context.copy()
@@ -961,6 +961,15 @@ class crm_lead(base_stage, format_address, osv.osv):
return [lead.section_id.message_get_reply_to()[0] if lead.section_id else False
for lead in self.browse(cr, uid, ids, context=context)]
def message_get_suggested_recipients(self, cr, uid, ids, context=None):
recipients = super(crm_lead, self).message_get_suggested_recipients(cr, uid, ids, context=context)
for lead in self.browse(cr, uid, ids, context=context):
if lead.partner_id:
self._message_add_suggested_recipient(cr, uid, recipients, lead, partner=lead.partner_id, reason=_('Customer'))
elif lead.email_from:
self._message_add_suggested_recipient(cr, uid, recipients, lead, email=lead.email_from, reason=_('Customer Email'))
return recipients
def message_new(self, cr, uid, msg, custom_values=None, context=None):
""" Overrides mail_thread message_new that is called by the mailgateway
through message_process.
-1
View File
@@ -377,7 +377,6 @@
states="draft,open,pending" class="oe_highlight"/>
<button name="case_mark_lost" string="Mark Lost" type="object"
states="draft,open" class="oe_highlight"/>
<button name="new_mail_send" string="Send Mail" type="object" states="draft,open,pending"/>
<field name="stage_id" widget="statusbar" clickable="True"/>
</header>
<sheet>
@@ -157,8 +157,9 @@ class crm_lead(osv.osv):
self.assign_geo_localize(cr, uid, [lead.id], lead.partner_latitude, lead.partner_longitude, context=context)
partner = res_partner.browse(cr, uid, partner_id, context=context)
if partner.user_id:
salesteam_id = partner.section_id and partner.section_id.id or False
for lead_id in ids:
self.allocate_salesman(cr, uid, [lead_id], [partner.user_id.id], context=context)
self.allocate_salesman(cr, uid, [lead_id], [partner.user_id.id], team_id=salesteam_id, context=context)
self.write(cr, uid, [lead.id], {'date_assign': fields.date.context_today(self,cr,uid,context=context), 'partner_assigned_id': partner_id}, context=context)
return res
@@ -20,14 +20,11 @@
#
##############################################################################
import re
import time
from openerp import tools
from openerp.osv import fields, osv
from openerp.tools.translate import _
class crm_lead_forward_to_partner(osv.osv_memory):
class crm_lead_forward_to_partner(osv.TransientModel):
""" Forward info history to partners. """
_name = 'crm.lead.forward.to.partner'
_inherit = "mail.compose.message"
@@ -116,15 +113,7 @@ class crm_lead_forward_to_partner(osv.osv_memory):
value = self.default_get(cr, uid, ['body', 'email_to', 'email_cc', 'subject', 'history_mode'], context=context)
self.write(cr, uid, ids, value, context=context)
self.send_mail(cr, uid, ids, context=context)
# for case in lead.browse(cr, uid, lead_ids, context=context):
# TODO: WHAT TO DO WITH THAT ?
# if (this.send_to == 'partner' and this.partner_id):
# lead.assign_partner(cr, uid, [case.id], this.partner_id.id, context=context)
# if this.send_to == 'user':
# lead.allocate_salesman(cr, uid, [case.id], [this.user_id.id], context=context)
return res
return self.send_mail(cr, uid, ids, context=context)
def _get_info_body(self, cr, uid, lead, context=None):
field_names = []
@@ -15,8 +15,7 @@
<field name="history_mode" colspan="4"
on_change="on_change_history_mode(history_mode, model, res_id)"/>
<field name="subject" colspan="4"/>
<field name="partner_ids" colspan="4" widget="many2many_tags"
on_change="onchange_partner_ids(partner_ids)"/>
<field name="partner_ids" colspan="4" widget="many2many_tags_email"/>
<notebook colspan="4">
<page string="Body">
<field name="body"/>
+9
View File
@@ -340,6 +340,15 @@ class hr_applicant(base_stage, osv.Model):
value = self.pool.get("survey").action_print_survey(cr, uid, ids, context=context)
return value
def message_get_suggested_recipients(self, cr, uid, ids, context=None):
recipients = super(hr_applicant, self).message_get_suggested_recipients(cr, uid, ids, context=context)
for applicant in self.browse(cr, uid, ids, context=context):
if applicant.partner_id:
self._message_add_suggested_recipient(cr, uid, recipients, applicant, partner=applicant.partner_id, reason=_('Contact'))
elif applicant.email_from:
self._message_add_suggested_recipient(cr, uid, recipients, applicant, email=applicant.email_from, reason=_('Contact Email'))
return recipients
def message_new(self, cr, uid, msg, custom_values=None, context=None):
""" Overrides mail_thread message_new that is called by the mailgateway
through message_process.
@@ -237,6 +237,7 @@
<field name="title_action"/>
<field name="department_id"/>
<field name="categ_ids"/>
<field name="message_summary"/>
<templates>
<t t-name="kanban-tooltip">
<ul class="oe_kanban_tooltip">
@@ -282,6 +283,7 @@
</div>
<div class="oe_kanban_footer_left" style="margin-top:5px;">
<t t-raw="record.message_summary.raw_value"/>
<field name="categ_ids"/>
</div>
</div>
+28 -8
View File
@@ -75,16 +75,21 @@ class mail_notification(osv.Model):
if not cr.fetchone():
cr.execute('CREATE INDEX mail_notification_partner_id_read_starred_message_id ON mail_notification (partner_id, read, starred, message_id)')
def get_partners_to_notify(self, cr, uid, message, context=None):
def get_partners_to_notify(self, cr, uid, message, partners_to_notify=None, context=None):
""" Return the list of partners to notify, based on their preferences.
:param browse_record message: mail.message to notify
:param list partners_to_notify: optional list of partner ids restricting
the notifications to process
"""
notify_pids = []
for notification in message.notification_ids:
if notification.read:
continue
partner = notification.partner_id
# If partners_to_notify specified: restrict to them
if partners_to_notify and partner.id not in partners_to_notify:
continue
# Do not send to partners without email address defined
if not partner.email:
continue
@@ -100,16 +105,35 @@ class mail_notification(osv.Model):
notify_pids.append(partner.id)
return notify_pids
def _notify(self, cr, uid, msg_id, context=None):
""" Send by email the notification depending on the user preferences """
def _notify(self, cr, uid, msg_id, partners_to_notify=None, context=None):
""" Send by email the notification depending on the user preferences
:param list partners_to_notify: optional list of partner ids restricting
the notifications to process
"""
if context is None:
context = {}
mail_message_obj = self.pool.get('mail.message')
# optional list of partners to notify: subscribe them if not already done or update the notification
if partners_to_notify:
notifications_to_update = []
notified_partners = []
notif_ids = self.search(cr, SUPERUSER_ID, [('message_id', '=', msg_id), ('partner_id', 'in', partners_to_notify)], context=context)
for notification in self.browse(cr, SUPERUSER_ID, notif_ids, context=context):
notified_partners.append(notification.partner_id.id)
notifications_to_update.append(notification.id)
partners_to_notify = filter(lambda item: item not in notified_partners, partners_to_notify)
if notifications_to_update:
self.write(cr, SUPERUSER_ID, notifications_to_update, {'read': False}, context=context)
mail_message_obj.write(cr, uid, msg_id, {'notified_partner_ids': [(4, id) for id in partners_to_notify]}, context=context)
# mail_notify_noemail (do not send email) or no partner_ids: do not send, return
if context.get('mail_notify_noemail'):
return True
# browse as SUPERUSER_ID because of access to res_partner not necessarily allowed
msg = self.pool.get('mail.message').browse(cr, SUPERUSER_ID, msg_id, context=context)
notify_partner_ids = self.get_partners_to_notify(cr, uid, msg, context=context)
notify_partner_ids = self.get_partners_to_notify(cr, uid, msg, partners_to_notify=partners_to_notify, context=context)
if not notify_partner_ids:
return True
@@ -136,16 +160,12 @@ class mail_notification(osv.Model):
mail_values = {
'mail_message_id': msg.id,
'email_to': [],
'auto_delete': True,
'body_html': body_html,
'email_from': email_from,
'state': 'outgoing',
}
mail_values['email_to'] = ', '.join(mail_values['email_to'])
email_notif_id = mail_mail.create(cr, uid, mail_values, context=context)
try:
return mail_mail.send(cr, uid, [email_notif_id], recipient_ids=notify_partner_ids, context=context)
except Exception:
return False
+1 -1
View File
@@ -106,7 +106,7 @@
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/>
<field name="message_ids" widget="mail_thread" options='{"thread_level": 1}' placeholder="Send a message to the group..."/>
<field name="message_ids" widget="mail_thread" options='{"thread_level": 1}' placeholder="Send a message to the group"/>
</div>
</form>
</field>
+5 -6
View File
@@ -301,8 +301,8 @@ class mail_message(osv.Model):
for key, message in message_tree.iteritems():
if message.author_id:
partner_ids |= set([message.author_id.id])
if message.partner_ids:
partner_ids |= set([partner.id for partner in message.partner_ids])
if message.notified_partner_ids:
partner_ids |= set([partner.id for partner in message.notified_partner_ids])
if message.attachment_ids:
attachment_ids |= set([attachment.id for attachment in message.attachment_ids])
# Read partners as SUPERUSER -> display the names like classic m2o even if no access
@@ -322,7 +322,7 @@ class mail_message(osv.Model):
else:
author = (0, message.email_from)
partner_ids = []
for partner in message.partner_ids:
for partner in message.notified_partner_ids:
if partner.id in partner_tree:
partner_ids.append(partner_tree[partner.id])
attachment_ids = []
@@ -861,7 +861,7 @@ class mail_message(osv.Model):
# message has no subtype_id: pure log message -> no partners, no one notified
if not message.subtype_id:
return True
# all followers of the mail.message document have to be added as partners and notified
if message.model and message.res_id:
fol_obj = self.pool.get("mail.followers")
@@ -884,8 +884,7 @@ class mail_message(osv.Model):
# notify
if partners_to_notify:
self.write(cr, SUPERUSER_ID, [newid], {'notified_partner_ids': [(4, p.id) for p in partners_to_notify]}, context=context)
notification_obj._notify(cr, uid, newid, context=context)
notification_obj._notify(cr, uid, newid, partners_to_notify=[p.id for p in partners_to_notify], context=context)
message.refresh()
# An error appear when a user receive a notification without notifying
+148 -137
View File
@@ -35,6 +35,7 @@ from openerp import SUPERUSER_ID
from openerp.addons.mail.mail_message import decode
from openerp.osv import fields, osv
from openerp.tools.safe_eval import safe_eval as eval
from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
@@ -92,7 +93,7 @@ class mail_thread(osv.AbstractModel):
""" Computes:
- message_unread: has uid unread message for the document
- message_summary: html snippet summarizing the Chatter for kanban views """
res = dict((id, dict(message_unread=False, message_summary='')) for id in ids)
res = dict((id, dict(message_unread=False, message_unread_count=0, message_summary=' ')) for id in ids)
user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
# search for unread messages, directly in SQL to improve performances
@@ -101,14 +102,14 @@ class mail_thread(osv.AbstractModel):
ON (n.message_id = m.id AND n.partner_id = %s AND (n.read = False or n.read IS NULL))
WHERE m.model = %s AND m.res_id in %s""",
(user_pid, self._name, tuple(ids),))
msg_ids = [result[0] for result in cr.fetchall()]
for msg_id in msg_ids:
res[msg_id]['message_unread'] = True
for thread in self.browse(cr, uid, ids, context=context):
cls = res[thread.id]['message_unread'] and ' class="oe_kanban_mail_new"' or ''
res[thread.id]['message_summary'] = "<span%s><span class='oe_e'>9</span> %d</span> <span><span class='oe_e'>+</span> %d</span>" % (cls, len(thread.message_ids), len(thread.message_follower_ids))
for result in cr.fetchall():
res[result[0]]['message_unread'] = True
res[result[0]]['message_unread_count'] += 1
for id in ids:
if res[id]['message_unread_count']:
title = res[id]['message_unread_count'] > 1 and _("You have %d unread messages") % res[id]['message_unread_count'] or _("You have one unread message")
res[id]['message_summary'] = "<span class='oe_kanban_mail_new' title='%s'><span class='oe_e'>9</span> %d %s</span>" % (title, res[id].pop('message_unread_count'), _("New"))
return res
def _get_subscription_data(self, cr, uid, ids, name, args, context=None):
@@ -606,15 +607,12 @@ class mail_thread(osv.AbstractModel):
if thread_id and hasattr(model_pool, 'message_update'):
model_pool.message_update(cr, user_id, [thread_id], msg, context=nosub_ctx)
else:
nosub_ctx = dict(nosub_ctx, mail_create_nolog=True)
thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=nosub_ctx)
else:
assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message."
model_pool = self.pool.get('mail.thread')
new_msg_id = model_pool.message_post_user_api(cr, uid, [thread_id], context=context, content_subtype='html', **msg)
# when posting an incoming email to a document: subscribe the author, if a partner, as follower
if model and thread_id and msg.get('author_id'):
model_pool.message_subscribe(cr, uid, [thread_id], [msg.get('author_id')], context=context)
new_msg_id = model_pool.message_post(cr, uid, [thread_id], context=context, subtype='mail.mt_comment', **msg)
if partner_ids:
# postponed after message_post, because this is an external message and we don't want to create
@@ -771,8 +769,7 @@ class mail_thread(osv.AbstractModel):
author_ids = self._message_find_partners(cr, uid, message, ['From'], context=context)
if author_ids:
msg_dict['author_id'] = author_ids[0]
else:
msg_dict['email_from'] = decode(message.get('from'))
msg_dict['email_from'] = decode(message.get('from'))
partner_ids = self._message_find_partners(cr, uid, message, ['To', 'Cc'], context=context)
msg_dict['partner_ids'] = [(4, partner_id) for partner_id in partner_ids]
@@ -820,77 +817,140 @@ class mail_thread(osv.AbstractModel):
"now deprecated res.log.")
self.message_post(cr, uid, [id], message, context=context)
def message_create_partners_from_emails(self, cr, uid, emails, context=None):
def _message_add_suggested_recipient(self, cr, uid, result, obj, partner=None, email=None, reason='', context=None):
""" Called by message_get_suggested_recipients, to add a suggested
recipient in the result dictionary. The form is :
partner_id, partner_name<partner_email> or partner_name, reason """
if email and not partner:
partner_info = self.message_get_partner_info_from_emails(cr, uid, [email], context=context)[0]
if partner_info.get('partner_id'):
partner = self.pool.get('res.partner').browse(cr, SUPERUSER_ID, [partner_info.get('partner_id')], context=context)[0]
if email and email in [val[1] for val in result[obj.id]]: # already existing email -> skip
return result
if partner and partner in obj.message_follower_ids: # recipient already in the followers -> skip
return result
if partner and partner in [val[0] for val in result[obj.id]]: # already existing partner ID -> skip
return result
if partner and partner.email: # complete profile: id, name <email>
result[obj.id].append((partner.id, '%s<%s>' % (partner.name, partner.email), reason))
elif partner: # incomplete profile: id, name
result[obj.id].append((partner.id, '%s' % (partner.name), reason))
else: # unknown partner, we are probably managing an email address
result[obj.id].append((False, email, reason))
return result
def message_get_suggested_recipients(self, cr, uid, ids, context=None):
""" Returns suggested recipients for ids. Those are a list of
tuple (partner_id, partner_name, reason), to be managed by Chatter. """
result = dict.fromkeys(ids, list())
if self._all_columns.get('user_id'):
for obj in self.browse(cr, SUPERUSER_ID, ids, context=context): # SUPERUSER because of a read on res.users that would crash otherwise
if not obj.user_id or not obj.user_id.partner_id:
continue
self._message_add_suggested_recipient(cr, uid, result, obj, partner=obj.user_id.partner_id, reason=self._all_columns['user_id'].column.string, context=context)
return result
def message_get_partner_info_from_emails(self, cr, uid, emails, link_mail=False, context=None):
""" Convert a list of emails into a list partner_ids and a list
new_partner_ids. The return value is non conventional because
it is meant to be used by the mail widget.
:return dict: partner_ids and new_partner_ids
"""
partner_obj = self.pool.get('res.partner')
mail_message_obj = self.pool.get('mail.message')
partner_ids = []
new_partner_ids = []
partner_obj = self.pool.get('res.partner')
result = list()
for email in emails:
partner_info = {'full_name': email, 'partner_id': False}
m = re.search(r"((.+?)\s*<)?([^<>]+@[^<>]+)>?", email, re.IGNORECASE | re.DOTALL)
name = m.group(2) or m.group(0)
email = m.group(3)
ids = partner_obj.search(cr, SUPERUSER_ID, [('email', '=', email)], context=context)
if not m:
continue
email_address = m.group(3)
ids = partner_obj.search(cr, SUPERUSER_ID, [('email', '=', email_address)], context=context)
if ids:
partner_ids.append(ids[0])
partner_id = ids[0]
else:
partner_id = partner_obj.create(cr, uid, {
'name': name or email,
'email': email,
}, context=context)
new_partner_ids.append(partner_id)
partner_info['partner_id'] = ids[0]
result.append(partner_info)
# link mail with this from mail to the new partner id
message_ids = mail_message_obj.search(cr, SUPERUSER_ID, ['|', ('email_from', '=', email), ('email_from', 'ilike', '<%s>' % email), ('author_id', '=', False)], context=context)
if message_ids:
mail_message_obj.write(cr, SUPERUSER_ID, message_ids, {'email_from': None, 'author_id': partner_id}, context=context)
return {
'partner_ids': partner_ids,
'new_partner_ids': new_partner_ids,
}
if link_mail and ids:
message_ids = mail_message_obj.search(cr, SUPERUSER_ID, [
'|',
('email_from', '=', email),
('email_from', 'ilike', '<%s>' % email),
('author_id', '=', False)
], context=context)
if message_ids:
mail_message_obj.write(cr, SUPERUSER_ID, message_ids, {'author_id': ids[0]}, context=context)
return result
def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification',
subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
def message_post(self, cr, uid, thread_id, body='', subject=None,
content_subtype='html', type='notification', subtype=None,
parent_id=False, attachments=None, context=None, **kwargs):
""" Post a new message in an existing thread, returning the new
mail.message ID. Extra keyword arguments will be used as default
column values for the new mail.message record.
Auto link messages for same id and object
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 subject: optional subject
:param str type: mail_message.type
:param int parent_id: optional ID of parent message in this thread
: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
:return: ID of newly created mail.message
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 context is None:
context = {}
if attachments is None:
attachments = {}
assert (not thread_id) or isinstance(thread_id, (int, long)) or \
(isinstance(thread_id, (list, tuple)) and len(thread_id) == 1), "Invalid thread_id; should be 0, False, an ID or a list with one ID"
if isinstance(thread_id, (list, tuple)):
thread_id = thread_id and thread_id[0]
mail_message = self.pool.get('mail.message')
ir_attachment = self.pool.get('ir.attachment')
assert (not thread_id) or \
isinstance(thread_id, (int, long)) or \
(isinstance(thread_id, (list, tuple)) and len(thread_id) == 1), \
"Invalid thread_id; should be 0, False, an ID or a list with one ID"
if isinstance(thread_id, (list, tuple)):
thread_id = thread_id[0]
# if we're processing a message directly coming from the gateway, the destination model was
# set in the context.
# set in the context.
model = False
if thread_id:
model = context.get('thread_model', self._name) if self._name == 'mail.thread' else self._name
attachment_ids = kwargs.pop('attachment_ids', [])
# 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)
partner_ids = set(kwargs.pop('partner_ids', []))
if parent_id and model == 'mail.thread':
parent_message = mail_message.browse(cr, uid, parent_id, context=context)
partner_ids |= set([partner.id for partner in parent_message.partner_ids])
if parent_message.author_id:
partner_ids.add(parent_message.author_id.id)
# 3. Attachments
# - HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message
attachment_ids = kwargs.pop('attachment_ids', []) or [] # because we could receive None (some old code sends None)
if attachment_ids:
filtered_attachment_ids = ir_attachment.search(cr, SUPERUSER_ID, [
('res_model', '=', 'mail.compose.message'),
('res_id', '=', 0),
('create_uid', '=', uid),
('id', 'in', attachment_ids)], context=context)
if filtered_attachment_ids:
ir_attachment.write(cr, SUPERUSER_ID, filtered_attachment_ids, {'res_model': model, 'res_id': thread_id}, context=context)
attachment_ids = [(4, id) for id in attachment_ids]
# Handle attachments parameter, that is a dictionary of attachments
for name, content in attachments:
if isinstance(content, unicode):
content = content.encode('utf-8')
@@ -899,20 +959,25 @@ class mail_thread(osv.AbstractModel):
'datas': base64.b64encode(str(content)),
'datas_fname': name,
'description': name,
'res_model': context.get('thread_model') or self._name,
'res_model': model,
'res_id': thread_id,
}
attachment_ids.append((0, 0, data_attach))
# fetch subtype
# 4: mail.message.subtype
subtype_id = False
if subtype:
s_data = subtype.split('.')
if len(s_data) == 1:
s_data = ('mail', s_data[0])
ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, s_data[0], s_data[1])
if '.' not in subtype:
subtype = 'mail.%s' % subtype
ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, *subtype.split('.'))
subtype_id = ref and ref[1] or False
else:
subtype_id = False
# automatically subscribe recipients if asked to
if context.get('mail_post_autofollow') and thread_id and partner_ids:
partner_to_subscribe = partner_ids
if context.get('mail_post_autofollow_partner_ids'):
partner_to_subscribe = filter(lambda item: item in context.get('mail_post_autofollow_partner_ids'), partner_ids)
self.message_subscribe(cr, uid, [thread_id], list(partner_to_subscribe), context=context)
# _mail_flat_thread: automatically set free messages to the first posted message
if self._mail_flat_thread and not parent_id and thread_id:
@@ -940,86 +1005,19 @@ class mail_thread(osv.AbstractModel):
'parent_id': parent_id,
'attachment_ids': attachment_ids,
'subtype_id': subtype_id,
'partner_ids': [(4, pid) for pid in partner_ids],
})
# Avoid warnings about non-existing fields
for x in ('from', 'to', 'cc'):
values.pop(x, None)
return mail_message.create(cr, uid, values, context=context)
def message_post_user_api(self, cr, uid, thread_id, body='', parent_id=False,
attachment_ids=None, content_subtype='plaintext',
context=None, **kwargs):
""" Wrapper on message_post, used for user input :
- mail gateway
- quick reply in Chatter (refer to mail.js), not
the mail.compose.message wizard
The purpose is to perform some pre- and post-processing:
- if body is plaintext: convert it into html
- if parent_id: handle reply to a previous message by adding the
parent partners to the message
- type and subtype: comment and mail.mt_comment by default
- attachment_ids: supposed not attached to any document; attach them
to the related document. Should only be set by Chatter.
"""
mail_message_obj = self.pool.get('mail.message')
ir_attachment = self.pool.get('ir.attachment')
# 1.A.1: add recipients of parent message (# TDE FIXME HACK: mail.thread -> private message)
partner_ids = set([])
if parent_id and self._name == 'mail.thread':
parent_message = mail_message_obj.browse(cr, uid, parent_id, context=context)
partner_ids |= set([(4, partner.id) for partner in parent_message.partner_ids])
if parent_message.author_id.id:
partner_ids.add((4, parent_message.author_id.id))
# 1.A.2: add specified recipients
param_partner_ids = set()
for item in kwargs.pop('partner_ids', []):
if isinstance(item, (list)):
param_partner_ids.add((item[0], item[1]))
elif isinstance(item, (int, long)):
param_partner_ids.add((4, item))
else:
param_partner_ids.add(item)
partner_ids |= param_partner_ids
# 1.A.3: add parameters recipients as follower
# TDE FIXME in 7.1: should check whether this comes from email_list or partner_ids
if param_partner_ids and self._name != 'mail.thread':
self.message_subscribe(cr, uid, [thread_id], [pid[1] for pid in param_partner_ids], context=context)
# 1.B: handle body, message_type and message_subtype
if content_subtype == 'plaintext':
body = tools.plaintext2html(body)
msg_type = kwargs.pop('type', 'comment')
msg_subtype = kwargs.pop('subtype', 'mail.mt_comment')
# 2. Pre-processing: attachments
# HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message
if attachment_ids:
# TDE FIXME (?): when posting a private message, we use mail.thread as a model
# However, attaching doc to mail.thread is not possible, mail.thread does not have any table
model = self._name
if model == 'mail.thread':
model = False
filtered_attachment_ids = ir_attachment.search(cr, SUPERUSER_ID, [
('res_model', '=', 'mail.compose.message'),
('res_id', '=', 0),
('create_uid', '=', uid),
('id', 'in', attachment_ids)], context=context)
if filtered_attachment_ids:
if thread_id and model:
ir_attachment.write(cr, SUPERUSER_ID, attachment_ids, {'res_model': model, 'res_id': thread_id}, context=context)
else:
attachment_ids = []
attachment_ids = [(4, id) for id in attachment_ids]
# 3. Post message
return self.message_post(cr, uid, thread_id=thread_id, body=body,
type=msg_type, subtype=msg_subtype, parent_id=parent_id,
attachment_ids=attachment_ids, partner_ids=list(partner_ids), context=context, **kwargs)
# Create and auto subscribe the author
msg_id = mail_message.create(cr, uid, values, context=context)
message = mail_message.browse(cr, uid, msg_id, context=context)
if message.author_id and thread_id and type != 'notification':
self.message_subscribe(cr, uid, [thread_id], [message.author_id.id], context=context)
return msg_id
#------------------------------------------------------
# Followers API
@@ -1141,12 +1139,25 @@ class mail_thread(osv.AbstractModel):
# add followers coming from res.users relational fields that are tracked
user_ids = [getattr(record, name).id for name in user_field_lst if getattr(record, name)]
for partner_id in [user.partner_id.id for user in self.pool.get('res.users').browse(cr, SUPERUSER_ID, user_ids, context=context)]:
user_id_partner_ids = [user.partner_id.id for user in self.pool.get('res.users').browse(cr, SUPERUSER_ID, user_ids, context=context)]
for partner_id in user_id_partner_ids:
new_followers.setdefault(partner_id, None)
for pid, subtypes in new_followers.items():
subtypes = list(subtypes) if subtypes is not None else None
self.message_subscribe(cr, uid, [record.id], [pid], subtypes, context=context)
# find first email message, set it as unread for auto_subscribe fields for them to have a notification
if user_id_partner_ids:
msg_ids = self.pool.get('mail.message').search(cr, uid, [
('model', '=', self._name),
('res_id', '=', record.id),
('type', '=', 'email')], limit=1, context=context)
if not msg_ids and record.message_ids:
msg_ids = [record.message_ids[-1].id]
if msg_ids:
self.pool.get('mail.notification')._notify(cr, uid, msg_ids[0], partners_to_notify=user_id_partner_ids, context=context)
return True
#------------------------------------------------------
+8 -1
View File
@@ -18,9 +18,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.tools.translate import _
from openerp.osv import fields, osv
class res_partner_mail(osv.Model):
""" Update partner to add a field about notification preferences """
_name = "res.partner"
@@ -42,6 +43,12 @@ class res_partner_mail(osv.Model):
'notification_email_send': lambda *args: 'comment'
}
def message_get_suggested_recipients(self, cr, uid, ids, context=None):
recipients = super(res_partner_mail, self).message_get_suggested_recipients(cr, uid, ids, context=context)
for partner in self.browse(cr, uid, ids, context=context):
self._message_add_suggested_recipient(cr, uid, recipients, partner, partner=partner, reason=_('Partner Profile'))
return recipients
def message_post(self, cr, uid, thread_id, **kwargs):
""" Override related to res.partner. In case of email message, set it as
private:
+8 -9
View File
@@ -113,13 +113,6 @@ class res_users(osv.Model):
thread_id = thread_id[0]
return self.browse(cr, SUPERUSER_ID, thread_id).partner_id.id
def message_post_user_api(self, cr, uid, thread_id, context=None, **kwargs):
""" Redirect the posting of message on res.users to the related partner.
This is done because when giving the context of Chatter on the
various mailboxes, we do not have access to the current partner_id. """
partner_id = self._message_post_get_pid(cr, uid, thread_id, context=context)
return self.pool.get('res.partner').message_post_user_api(cr, uid, partner_id, context=context, **kwargs)
def message_post(self, cr, uid, thread_id, context=None, **kwargs):
""" Redirect the posting of message on res.users to the related partner.
This is done because when giving the context of Chatter on the
@@ -139,8 +132,14 @@ class res_users(osv.Model):
self.pool.get('res.partner').message_subscribe(cr, uid, [partner_id], partner_ids, subtype_ids=subtype_ids, context=context)
return True
def message_create_partners_from_emails(self, cr, uid, emails, context=None):
return self.pool.get('res.partner').message_create_partners_from_emails(cr, uid, emails, context=context)
def message_get_partner_info_from_emails(self, cr, uid, emails, link_mail=False, context=None):
return self.pool.get('res.partner').message_get_partner_info_from_emails(cr, uid, emails, link_mail=link_mail, context=context)
def message_get_suggested_recipients(self, cr, uid, ids, context=None):
partner_ids = []
for id in ids:
partner_ids.append(self.browse(cr, SUPERUSER_ID, id).partner_id.id)
return self.pool.get('res.partner').message_get_suggested_recipients(cr, uid, partner_ids, context=context)
class res_users_mail_group(osv.Model):
+30 -17
View File
@@ -54,6 +54,9 @@
min-height: 42px;
border: solid 1px rgba(0,0,0,0.03);
}
.openerp .oe_mail .oe_msg.oe_msg_nobody{
background: #F8F8F8;
}
.openerp .oe_mail .oe_msg .oe_msg_left{
position: absolute;
left:0; top: 0; bottom: 0; width: 40px;
@@ -228,16 +231,23 @@
height: 24px;
width: 100%;
padding: 2px 4px;
color: #AAA;
word-spacing: 3px;
cursor: text;
}
.openerp .oe_mail .oe_msg.oe_msg_composer_compact .oe_compact_record {
font-size: 13px;
font-style: bold;
text-align: center;
}
.openerp .oe_mail .oe_msg.oe_msg_composer_compact .oe_compact_inbox {
border: 1px solid #CCC;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
background: white;
font-size: 14px;
color: #AAA;
font-style: italic;
word-spacing: 3px;
cursor: text;
}
/* d) I.E. tweaks for Message action icons */
@@ -269,20 +279,20 @@
margin-top: 4px;
margin-bottom: 4px;
}
.openerp .oe_mail .oe_msg_composer .oe_msg_attachment_list{
.openerp .oe_mail .oe_msg_composer .oe_msg_attachment_list {
display: block;
}
.openerp .oe_mail .oe_msg_composer .oe_emails_from{
.openerp .oe_mail .oe_msg_composer .oe_recipients {
font-size: 12px;
margin-left: 20px;
margin-bottom: 2px;
}
.openerp .oe_mail .oe_msg_composer .oe_emails_from label{
.openerp .oe_mail .oe_msg_composer .oe_recipients label{
vertical-align: middle;
display: block;
line-height: 14px;
}
.openerp .oe_mail .oe_msg_composer .oe_emails_from input{
.openerp .oe_mail .oe_msg_composer .oe_recipients input{
vertical-align: middle;
}
.openerp .oe_mail .oe_attachment{
@@ -439,7 +449,8 @@
line-height: 12px;
vertical-align: middle;
}
.openerp .oe_mail .oe_msg_footer button.oe_post{
.openerp .oe_mail .oe_msg_footer button.oe_post,
.openerp .oe_mail .oe_msg_footer button.oe_log{
position: relative;
z-index: 2;
}
@@ -458,18 +469,20 @@
.openerp .oe_mail .oe_hidden_input_file, .openerp .oe_mail .oe_hidden_input_file form{
display:inline;
}
.openerp .oe_mail .oe_msg_footer button.oe_full{
width:24px;
overflow:hidden;
.openerp .oe_mail .oe_msg_center button.oe_full{
width: 24px;
height: 22px;
overflow: hidden;
float: right;
filter:none;
filter: none;
}
.openerp .oe_mail .oe_msg_footer button.oe_full .oe_e{
.openerp .oe_mail .oe_msg_center button.oe_full .oe_e{
position: relative;
top: -4px;
margin-left: -9px;
vertical-align: middle;
filter:none;
top: -9px;
margin-left: -5px;
vertical-align: top;
filter: none;
height: 14px;
}
.openerp .oe_mail button.oe_attach, .openerp .oe_mail button.oe_full{
background: transparent;
+212 -78
View File
@@ -21,6 +21,19 @@ openerp.mail = function (session) {
mail.ChatterUtils = {
/** parse text to find email: Tagada <address@mail.fr> -> [Tagada, address@mail.fr] or False */
parse_email: function (text) {
var result = text.match(/(.*)<(.*@.*)>/);
if (result) {
return [_.str.trim(result[1]), _.str.trim(result[2])];
}
result = text.match(/(.*@.*)/);
if (result) {
return [_.str.trim(result[1]), _.str.trim(result[1])];
}
return [text, false];
},
/* Get an image in /web/binary/image?... */
get_image: function (session, model, field, id, resize) {
var r = resize ? encodeURIComponent(resize) : '';
@@ -255,11 +268,15 @@ openerp.mail = function (session) {
this.avatar = mail.ChatterUtils.get_image(this.session, 'res.users', 'image_small', this.session.uid);
}
if (this.author_id && this.author_id[1]) {
var email = this.author_id[1].match(/(.*)<(.*@.*)>/);
if (!email) {
this.author_id.push(_.str.escapeHTML(this.author_id[1]), '', this.author_id[1]);
} else {
this.author_id.push(_.str.escapeHTML(email[0]), _.str.trim(email[1]), email[2]);
var parsed_email = mail.ChatterUtils.parse_email(this.author_id[1]);
this.author_id.push(parsed_email[0], parsed_email[1]);
}
if (this.partner_ids && this.partner_ids.length > 3) {
this.extra_partners_nbr = this.partner_ids.length - 3;
this.extra_partners_str = ''
var extra_partners = this.partner_ids.slice(3);
for (var key in extra_partners) {
this.extra_partners_str += extra_partners[key][1];
}
}
},
@@ -371,14 +388,24 @@ openerp.mail = function (session) {
* @param {Object} [context] context passed to the
* mail.compose.message DataSetSearch. Please refer to this model
* for more details about fields and default values.
* @param {Object} recipients = [
{
'email_address': [str],
'partner_id': False/[int],
'name': [str],
'full_name': name<email_address>,
},
{ ... },
]
*/
init: function (parent, datasets, options) {
this._super(parent, datasets, options);
this.show_compact_message = false;
this.show_delete_attachment = true;
this.emails_from = [];
this.partners_from = [];
this.is_log = false;
this.recipients = [];
this.recipient_ids = [];
},
start: function () {
@@ -479,15 +506,13 @@ openerp.mail = function (session) {
bind_events: function () {
var self = this;
this.$('.oe_compact').on('click', _.bind( this.on_compose_expandable, this));
// set the function called when attachments are added
this.$('input.oe_form_binary_file').on('change', _.bind( this.on_attachment_change, this) );
this.$('.oe_cancel').on('click', _.bind( this.on_cancel, this) );
this.$('.oe_post').on('click', _.bind( this.on_message_post, this) );
this.$('.oe_full').on('click', _.bind( this.on_compose_fullmail, this, this.id ? 'reply' : 'comment') );
this.$('.oe_compact_inbox').on('click', self.on_toggle_quick_composer);
this.$('.oe_compose_post').on('click', self.on_toggle_quick_composer);
this.$('.oe_compose_log').on('click', self.on_toggle_quick_composer);
this.$('input.oe_form_binary_file').on('change', _.bind( this.on_attachment_change, this));
this.$('.oe_cancel').on('click', _.bind( this.on_cancel, this));
this.$('.oe_post').on('click', self.on_message_post);
this.$('.oe_full').on('click', _.bind( this.on_compose_fullmail, this, this.id ? 'reply' : 'comment'));
/* stack for don't close the compose form if the user click on a button */
this.$('.oe_msg_left, .oe_msg_center').on('mousedown', _.bind( function () { this.stay_open = true; }, this));
this.$('.oe_msg_left, .oe_msg_content').on('mouseup', _.bind( function () { this.$('textarea').focus(); }, this));
@@ -497,13 +522,12 @@ openerp.mail = function (session) {
this.$('textarea').autosize();
// auto close
this.$('textarea').on('blur', _.bind( this.on_compose_expandable, this));
this.$('textarea').on('blur', self.on_toggle_quick_composer);
// event: delete child attachments off the oe_msg_attachment_list box
this.$(".oe_msg_attachment_list").on('click', '.oe_delete', this.on_attachment_delete);
this.$(".oe_emails_from").on('change', 'input', this.on_checked_email_from);
this.$(".oe_partners_from").on('change', 'input', this.on_checked_partner_from);
this.$(".oe_recipients").on('change', 'input', this.on_checked_recipient);
},
on_compose_fullmail: function (default_composition_mode) {
@@ -511,16 +535,26 @@ openerp.mail = function (session) {
if(!this.do_check_attachment_upload()) {
return false;
}
// create list of new partners
this.check_recipient_partners().done(function (partner_ids) {
var recipient_done = $.Deferred();
if (this.is_log) {
recipient_done.resolve([]);
}
else {
recipient_done = this.check_recipient_partners();
}
$.when(recipient_done).done(function (partner_ids) {
var context = {
'default_composition_mode': default_composition_mode,
'default_parent_id': self.id,
'default_body': mail.ChatterUtils.get_text2html(self.$el ? (self.$el.find('textarea:not(.oe_compact)').val() || '') : ''),
'default_attachment_ids': self.attachment_ids,
'default_partner_ids': partner_ids,
'mail_post_autofollow': true,
'mail_post_autofollow_partner_ids': partner_ids,
};
if (self.is_log) {
_.extend(context, {'mail_compose_log': true});
}
if (default_composition_mode != 'reply' && self.context.default_model && self.context.default_res_id) {
context.default_model = self.context.default_model;
context.default_res_id = self.context.default_res_id;
@@ -573,68 +607,122 @@ openerp.mail = function (session) {
check_recipient_partners: function () {
var self = this;
var partners_from = [];
var emails = [];
_.each(this.emails_from, function (email_from) {
if (email_from[1] && !_.find(emails, function (email) {return email == email_from[0][4];})) {
emails.push(email_from[0][1]);
}
});
var deferred_check = $.Deferred();
if (emails.length == 0) {
return deferred_check.resolve(partners_from);
var check_done = $.Deferred();
var recipients = _.filter(this.recipients, function (recipient) { return recipient.checked });
var recipients_to_find = _.filter(recipients, function (recipient) { return (! recipient.partner_id) });
var names_to_find = _.pluck(recipients_to_find, 'full_name');
var recipients_to_check = _.filter(recipients, function (recipient) { return (recipient.partner_id && ! recipient.email_address) });
var recipient_ids = _.pluck(_.filter(recipients, function (recipient) { return recipient.partner_id && recipient.email_address }), 'partner_id');
var names_to_remove = [];
var recipient_ids_to_remove = [];
// have unknown names -> call message_get_partner_info_from_emails to try to find partner_id
var find_done = $.Deferred();
if (names_to_find.length > 0) {
find_done = self.parent_thread.ds_thread._model.call('message_get_partner_info_from_emails', [names_to_find]);
}
self.parent_thread.ds_thread._model.call('message_create_partners_from_emails', [emails]).then(function (partners) {
partners_from = _.clone(partners.partner_ids);
var deferreds = [];
_.each(partners.new_partner_ids, function (id) {
else {
find_done.resolve([]);
}
// for unknown names + incomplete partners -> open popup - cancel = remove from recipients
$.when(find_done).pipe(function (result) {
var emails_deferred = [];
var recipient_popups = result.concat(recipients_to_check);
_.each(recipient_popups, function (partner_info) {
var deferred = $.Deferred()
deferreds.push(deferred);
var pop = new session.web.form.FormOpenPopup(this);
emails_deferred.push(deferred);
var partner_name = partner_info.full_name;
var partner_id = partner_info.partner_id;
var parsed_email = mail.ChatterUtils.parse_email(partner_name);
var pop = new session.web.form.FormOpenPopup(this);
pop.show_element(
'res.partner',
id,
{
'force_email': true,
partner_id,
{ 'force_email': true,
'ref': "compound_context",
},
{
'default_name': parsed_email[0],
'default_email': parsed_email[1],
}, {
title: _t("Please complete partner's informations"),
}
);
pop.on('closed', self, function () {
deferred.resolve();
});
partners_from.push(id);
pop.view_form.on('on_button_cancel', self, function () {
names_to_remove.push(partner_name);
if (partner_id) {
recipient_ids_to_remove.push(partner_id);
}
});
});
$.when.apply( $, deferreds ).then(function () {
deferred_check.resolve(partners_from);
$.when.apply($, emails_deferred).then(function () {
var new_names_to_find = _.difference(names_to_find, names_to_remove);
find_done = $.Deferred();
if (new_names_to_find.length > 0) {
find_done = self.parent_thread.ds_thread._model.call('message_get_partner_info_from_emails', [new_names_to_find, true]);
}
else {
find_done.resolve([]);
}
$.when(find_done).pipe(function (result) {
var recipient_popups = result.concat(recipients_to_check);
_.each(recipient_popups, function (partner_info) {
if (partner_info.partner_id && _.indexOf(partner_info.partner_id, recipient_ids_to_remove) == -1) {
recipient_ids.push(partner_info.partner_id);
}
});
}).pipe(function () {
check_done.resolve(recipient_ids);
});
});
});
return deferred_check;
return check_done;
},
on_message_post: function (event) {
var self = this;
if (this.do_check_attachment_upload() && (this.attachment_ids.length || this.$('textarea').val().match(/\S+/))) {
// create list of new partners
this.check_recipient_partners().done(function (partner_ids) {
self.do_send_message_post(partner_ids);
});
if (this.is_log) {
this.do_send_message_post([], this.is_log);
}
else {
this.check_recipient_partners().done(function (partner_ids) {
self.do_send_message_post(partner_ids, self.is_log);
});
}
}
},
/*do post a message and fetch the message*/
do_send_message_post: function (partner_ids) {
/* do post a message and fetch the message */
do_send_message_post: function (partner_ids, log) {
var self = this;
this.parent_thread.ds_thread._model.call('message_post_user_api', [this.context.default_res_id], {
var values = {
'body': this.$('textarea').val(),
'subject': false,
'parent_id': this.context.default_parent_id,
'attachment_ids': _.map(this.attachment_ids, function (file) {return file.id;}),
'partner_ids': partner_ids,
'context': this.parent_thread.context,
}).done(function (message_id) {
'context': _.extend(this.parent_thread.context, {
'mail_post_autofollow': true,
'mail_post_autofollow_partner_ids': partner_ids,
}),
'type': 'comment',
'content_subtype': 'plaintext',
};
if (log) {
values['subtype'] = false;
}
else {
values['subtype'] = 'mail.mt_comment';
}
this.parent_thread.ds_thread._model.call('message_post', [this.context.default_res_id], values).done(function (message_id) {
var thread = self.parent_thread;
var root = thread == self.options.root_thread;
if (self.options.display_indented_thread < self.thread_level && thread.parent_message) {
@@ -650,18 +738,55 @@ openerp.mail = function (session) {
});
},
/* convert the compact mode into the compose message
*/
on_compose_expandable: function (event) {
this.get_emails_from();
if ((!this.stay_open || (event && event.type == 'click')) && (!this.show_composer || !this.$('textarea:not(.oe_compact)').val().match(/\S+/) && !this.attachment_ids.length)) {
this.show_composer = !this.show_composer || this.stay_open;
this.reinit();
/* Quick composer: toggle minimal / expanded mode
* - toggle minimal (one-liner) / expanded (textarea, buttons) mode
* - when going into expanded mode:
* - call `message_get_suggested_recipients` to have a list of partners to add
* - compute email_from list (list of unknown email_from to propose to create partners)
*/
on_toggle_quick_composer: function (event) {
var self = this;
var $input = $(event.target);
this.compute_emails_from();
var email_addresses = _.pluck(this.recipients, 'email_address');
var suggested_partners = $.Deferred();
// if clicked: call for suggested recipients
if (event.type == 'click') {
this.is_log = $input.hasClass('oe_compose_log');
suggested_partners = this.parent_thread.ds_thread.call('message_get_suggested_recipients', [[this.context.default_res_id]]).done(function (additional_recipients) {
var thread_recipients = additional_recipients[self.context.default_res_id];
_.each(thread_recipients, function (recipient) {
var parsed_email = mail.ChatterUtils.parse_email(recipient[1]);
if (_.indexOf(email_addresses, parsed_email[1]) == -1) {
self.recipients.push({
'checked': true,
'partner_id': recipient[0],
'full_name': recipient[1],
'name': parsed_email[0],
'email_address': parsed_email[1],
'reason': recipient[2],
})
}
});
});
}
if (!this.stay_open && this.show_composer && (!event || event.type != 'blur')) {
this.$('textarea:not(.oe_compact):first').focus();
else {
suggested_partners.resolve({});
}
return true;
// when call for suggested partners finished: re-render the widget
$.when(suggested_partners).pipe(function (additional_recipients) {
if ((!self.stay_open || (event && event.type == 'click')) && (!self.show_composer || !self.$('textarea:not(.oe_compact)').val().match(/\S+/) && !self.attachment_ids.length)) {
self.show_composer = !self.show_composer || self.stay_open;
self.reinit();
}
if (!self.stay_open && self.show_composer && (!event || event.type != 'blur')) {
self.$('textarea:not(.oe_compact):first').focus();
}
});
return suggested_partners;
},
do_hide_compact: function () {
@@ -678,7 +803,10 @@ openerp.mail = function (session) {
}
},
get_emails_from: function () {
/** Compute the list of unknown email_from the the given thread
* TDE FIXME: seems odd to delegate to the composer
* TDE TODO: please de-obfuscate and comment your code */
compute_emails_from: function () {
var self = this;
var messages = [];
@@ -691,22 +819,28 @@ openerp.mail = function (session) {
// get all wall messages if is not a mail.Wall
_.each(this.options.root_thread.messages, function (msg) {messages.push(msg); messages.concat(msg.get_childs());});
}
_.each(messages, function (thread) {
if (thread.author_id && !thread.author_id[0] &&
!_.find(self.emails_from, function (from) {return from[0][4] == thread.author_id[4];})) {
self.emails_from.push([thread.author_id, true]);
!_.find(self.recipients, function (recipient) {return recipient.email_address == thread.author_id[3];})) {
self.recipients.push({ 'full_name': thread.author_id[1],
'name': thread.author_id[2],
'email_address': thread.author_id[3],
'partner_id': false,
'checked': true,
'reason': 'Incoming email author'
});
}
});
return self.emails_from;
return self.recipients;
},
on_checked_email_from: function (event) {
on_checked_recipient: function (event) {
var $input = $(event.target);
var email = $input.attr("data");
_.each(this.emails_from, function (email_from) {
if (email_from[0][4] == email) {
email_from[1] = $input.is(":checked");
_.each(this.recipients, function (recipient) {
if (recipient.email_address == email) {
recipient.checked = $input.is(":checked");
}
});
},
@@ -1236,12 +1370,12 @@ openerp.mail = function (session) {
/**
*If compose_message doesn't exist, instantiate the compose message.
* Call the on_compose_expandable method to allow the user to write his message.
* Call the on_toggle_quick_composer method to allow the user to write his message.
* (Is call when a user click on "Reply" button)
*/
on_compose_message: function (event) {
this.instantiate_compose_message();
this.compose_message.on_compose_expandable(event);
this.compose_message.on_toggle_quick_composer(event);
return false;
},
@@ -1810,7 +1944,7 @@ openerp.mail = function (session) {
};
session.client.action_manager.do_action(action);
});
this.$(".oe_write_onwall").click(function(){ self.root.thread.on_compose_message(); });
this.$(".oe_write_onwall").click(function (event) { self.root.thread.on_compose_message(event); });
}
});
+18 -10
View File
@@ -106,9 +106,12 @@ openerp_mail_followers = function(session, mail) {
on_remove_follower: function (event) {
var partner_id = $(event.target).data('id');
var context = new session.web.CompoundContext(this.build_context(), {});
return this.ds_model.call('message_unsubscribe', [[this.view.datarecord.id], [partner_id], context])
.then(this.proxy('read_value'));
var name = $(event.target).parent().find("a").html();
if (confirm(_.str.sprintf(_t("Warning! \n %s won't be notified of any email or discussion on this document. Do you really want to remove him from the followers ?"), name))) {
var context = new session.web.CompoundContext(this.build_context(), {});
return this.ds_model.call('message_unsubscribe', [[this.view.datarecord.id], [partner_id], context])
.then(this.proxy('read_value'));
}
},
read_value: function () {
@@ -248,12 +251,15 @@ openerp_mail_followers = function(session, mail) {
},
do_unfollow: function () {
_(this.$('.oe_msg_subtype_check')).each(function (record) {
$(record).attr('checked',false);
});
var context = new session.web.CompoundContext(this.build_context(), {});
return this.ds_model.call('message_unsubscribe_users', [[this.view.datarecord.id], [this.session.uid], context])
.then(this.proxy('read_value'));
if (confirm(_t("Warning! \nYou won't be notified of any email or discussion on this document. Do you really want to unfollow this document ?"))) {
_(this.$('.oe_msg_subtype_check')).each(function (record) {
$(record).attr('checked',false);
});
var context = new session.web.CompoundContext(this.build_context(), {});
return this.ds_model.call('message_unsubscribe_users', [[this.view.datarecord.id], [this.session.uid], context])
.then(this.proxy('read_value'));
}
return false;
},
do_update_subscription: function (event) {
@@ -267,7 +273,9 @@ openerp_mail_followers = function(session, mail) {
});
if (!checklist.length) {
this.do_unfollow();
if (!this.do_unfollow()) {
$(event.target).attr("checked", "checked");
}
} else {
var context = new session.web.CompoundContext(this.build_context(), {});
return this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], [this.session.uid], checklist, context])
+45 -16
View File
@@ -26,24 +26,37 @@
</div>
<div class="oe_msg_center">
<div class="oe_msg_content">
<button class="oe_full" title="Open the full mail composer"><span class='oe_e'>&amp;ograve</span></button>
<t t-call="mail.thread.list_recipients"/>
<textarea class="field_text"></textarea>
</div>
<div class="oe_msg_footer">
<div class="oe_msg_attachment_list"></div>
<button class="oe_post">Post</button>
<button class="oe_post">
<t t-if="!widget.is_log">Send</t>
<t t-if="widget.is_log">Log a note</t>
</button>
<t t-call="mail.compose_message.add_attachment"/>
<!--<a class="oe_cancel oe_e">X</a>-->
<button class="oe_full" title="Open the full mail composer"><span class='oe_e'>&amp;ograve</span></button>
</div>
</div>
</div>
<div t-if="widget.show_compact_message and !widget.show_composer and !widget.options.readonly" t-attf-class="oe_msg oe_msg_composer_compact #{widget.thread_level and widget.options.display_indented_thread > -1 ? 'oe_msg_indented' : ''}">
<div class="field_text oe_compact">
<t t-if="widget.options.compose_placeholder" t-raw="widget.options.compose_placeholder"/>
<t t-if="!widget.options.compose_placeholder and !widget.options.view_mailbox">Write to the followers of this document...</t>
<t t-if="!widget.options.compose_placeholder and widget.options.view_mailbox">Share with my followers...</t>
</div>
<t t-if="widget.options.view_mailbox">
<div class="field_text oe_compact oe_compact_inbox">
<t t-if="widget.options.compose_placeholder" t-raw="widget.options.compose_placeholder"/>
<t t-if="!widget.options.compose_placeholder and widget.options.view_mailbox">Share with my followers...</t>
</div>
</t>
<t t-if="!widget.options.view_mailbox">
<div class="field_text oe_compact oe_compact_record">
<a class="oe_compose_post">
<t t-if="widget.options.compose_placeholder" t-raw="widget.options.compose_placeholder"/>
<t t-if="!widget.options.compose_placeholder and !widget.options.view_mailbox">Send a message</t>
</a>
<span class="oe_grey">or</span>
<a class="oe_compose_log">Log a note</a>
</div>
</t>
</div>
<span t-if="!(widget.show_compact_message and !widget.show_composer) and !widget.show_composer" class="oe_placeholder_compose"></span>
</t>
@@ -57,7 +70,7 @@
<span class="oe_add">
<!-- uploader of file -->
<button class="oe_attach"><span class="oe_e">'</span></button>
<span class='oe_attach_label'>File</span>
<span class='oe_attach_label'>Attach a File</span>
<t t-call="HiddenInputFile">
<t t-set="fileupload_id" t-value="widget.fileupload_id"/>
<t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t>
@@ -106,7 +119,7 @@
template to the recipients list
-->
<t t-name="mail.thread.list_recipients">
<div class="oe_mail_list_recipients">
<div class="oe_mail_list_recipients" t-if="!widget.is_log">
To:
<t t-if="!widget.is_private">
<span class="oe_all_follower">
@@ -129,11 +142,11 @@
<a class="oe_more_hidden">&lt;&lt;&lt;</a>
</t>
</div>
<div class="oe_emails_from">
<t t-foreach='widget.emails_from' t-as='email_from'>
<label title="Add them into recipients and followers">
<input type="checkbox" t-att-checked="email_from[1] ? 'checked' : undefind" t-att-data="email_from[0][4]"/>
<t t-raw="email_from[0][2]"/>
<div class="oe_recipients" t-if="!widget.is_log">
<t t-foreach='widget.recipients' t-as='recipient'>
<label t-attf-title="Add as recipient and follower (reason: #{recipient.reason})">
<input type="checkbox" t-att-checked="recipient.checked ? 'checked' : undefined" t-att-data="recipient.email_address"/>
<t t-raw="recipient.name"/> (<t t-raw="recipient.email_address"/>)
</label>
</t>
</div>
@@ -212,7 +225,7 @@
<!-- default layout -->
<t t-name="mail.thread.message">
<div t-attf-class="oe_msg #{widget.thread_level and widget.options.display_indented_thread > -1 ? 'oe_msg_indented' : ''} oe_msg_#{widget.type}">
<div t-attf-class="oe_msg #{widget.thread_level and widget.options.display_indented_thread > -1 ? 'oe_msg_indented' : ''} #{widget.partner_ids.length == 0 ? 'oe_msg_nobody' : ''} oe_msg_#{widget.type}">
<div class='oe_msg_left'>
<a t-if="widget.options.show_link" t-attf-href="#model=res.partner&amp;id=#{widget.author_id[0]}" t-att-title="widget.author_id[1]">
@@ -245,6 +258,22 @@
</t>
<a t-if="widget.author_id and widget.options.show_link and widget.author_id[0]" t-attf-href="#model=res.partner&amp;id=#{widget.author_id[0]}"><t t-raw="widget.author_id[2]"/></a>
<span t-if="widget.author_id and (!widget.options.show_link or !widget.author_id[0])"><t t-raw="widget.author_id[2]"/></span>
<t t-if="widget.partner_ids.length == 0">
logged a note
</t>
<t t-if="widget.partner_ids.length > 0">
to
</t>
<t t-foreach="widget.partner_ids.slice(0, 3)" t-as="partner">
<span t-attf-class="oe_partner_follower">
<a t-if="widget.options.show_link" t-attf-href="#model=res.partner&amp;id=#{partner[0]}"><t t-raw="partner[1]"/></a>
<t t-if="!widget.options.show_link" t-raw="partner[1]"/>
</span>
<t t-if="!partner_last">,</t>
</t>
<t t-if="widget.partner_ids.length > 3">
<span t-att-title="widget.extra_partners_str">and <t t-raw="widget.extra_partners_nbr"/> more</span>
</t>
<span class='oe_subtle'>•</span>
<span t-att-title="widget.date"><t t-if="widget.timerelative" t-raw="widget.timerelative"/><t t-if="!widget.timerelative" t-raw="widget.date"/></span>
<span t-if="!widget.options.readonly" class='oe_subtle'>•</span>
+7 -6
View File
@@ -119,18 +119,18 @@ class test_mail(TestMailBase):
# Previously-created group can be emailed now - it should have an implicit alias group+frogs@...
frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
group_messages = frog_group.message_ids
self.assertTrue(len(group_messages) == 2, 'New group should only have the original message + creation log')
self.assertTrue(len(group_messages) == 1, 'New group should only have the original message')
mail_frog_news = MAIL_TEMPLATE.format(to='Friendly Frogs <group+frogs@example.com>', subject='news', extra='')
self.mail_thread.message_process(cr, uid, None, mail_frog_news)
frog_group.refresh()
self.assertTrue(len(frog_group.message_ids) == 3, 'Group should contain 3 messages now')
self.assertTrue(len(frog_group.message_ids) == 2, 'Group should contain 2 messages now')
# Even with a wrong destination, a reply should end up in the correct thread
mail_reply = MAIL_TEMPLATE.format(to='erroneous@example.com>', subject='Re: news',
extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
self.mail_thread.message_process(cr, uid, None, mail_reply)
frog_group.refresh()
self.assertTrue(len(frog_group.message_ids) == 4, 'Group should contain 4 messages now')
self.assertTrue(len(frog_group.message_ids) == 3, 'Group should contain 3 messages now')
# No model passed and no matching alias must raise
mail_spam = MAIL_TEMPLATE.format(to='noone@example.com', subject='spam', extra='')
@@ -154,7 +154,7 @@ class test_mail(TestMailBase):
new_mail = self.mail_message.browse(cr, uid, self.mail_message.search(cr, uid, [('message_id', '=', test_msg_id)])[0])
# Test: author_id set, not email_from
self.assertEqual(new_mail.author_id, user_raoul.partner_id, 'message process wrong author found')
self.assertFalse(new_mail.email_from, 'message process should not set the email_from when an author is found')
self.assertEqual(new_mail.email_from, user_raoul.email, 'message process wrong email_from')
# Do: post a new message, with a unknown partner
test_msg_id = '<deadcafe.1337-3@smtp.agrolait.com>'
@@ -391,7 +391,8 @@ class test_mail(TestMailBase):
# 1. Post a new email comment on Pigs
self._init_mock_build_email()
msg2_id = self.mail_group.message_post(cr, user_raoul.id, self.group_pigs_id, body=_body2, type='email', subtype='mt_comment',
partner_ids=[(6, 0, [p_d_id])], parent_id=msg1_id, attachments=_attachments)
partner_ids=[p_d_id], parent_id=msg1_id, attachments=_attachments,
context={'mail_post_autofollow': True})
message2 = self.mail_message.browse(cr, uid, msg2_id)
sent_emails = self._build_email_kwargs_list
self.assertFalse(self.mail_mail.search(cr, uid, [('mail_message_id', '=', msg2_id)]), 'mail.mail notifications should have been auto-deleted!')
@@ -482,7 +483,7 @@ class test_mail(TestMailBase):
self.assertEqual(compose.res_id, self.group_pigs_id, 'mail.compose.message incorrect res_id')
# 2. Post the comment, get created message
mail_compose.send_mail(cr, uid, [compose_id])
mail_compose.send_mail(cr, uid, [compose_id], {'mail_post_autofollow': True})
group_pigs.refresh()
message = group_pigs.message_ids[0]
# Test: mail.message: subject, body inside pre
+2 -2
View File
@@ -311,8 +311,8 @@ class test_mail_access_rights(TestMailBase):
user_bert_id, user_raoul_id = self.user_bert_id, self.user_raoul_id
# Prepare groups: Pigs (employee), Jobs (public)
pigs_msg_id = self.mail_group.message_post(cr, uid, self.group_pigs_id, body='Message', partner_ids=[(4, self.partner_admin_id)])
jobs_msg_id = self.mail_group.message_post(cr, uid, self.group_jobs_id, body='Message', partner_ids=[(4, self.partner_admin_id)])
pigs_msg_id = self.mail_group.message_post(cr, uid, self.group_pigs_id, body='Message', partner_ids=[self.partner_admin_id])
jobs_msg_id = self.mail_group.message_post(cr, uid, self.group_jobs_id, body='Message', partner_ids=[self.partner_admin_id])
# ----------------------------------------
# CASE1: Bert, without groups
+8 -11
View File
@@ -191,6 +191,7 @@ class mail_compose_message(osv.TransientModel):
if context is None:
context = {}
active_ids = context.get('active_ids')
is_log = context.get('mail_compose_log', False)
for wizard in self.browse(cr, uid, ids, context=context):
mass_mail_mode = wizard.composition_mode == 'mass_mail'
@@ -204,26 +205,22 @@ class mail_compose_message(osv.TransientModel):
'subject': wizard.subject,
'body': wizard.body,
'parent_id': wizard.parent_id and wizard.parent_id.id,
'partner_ids': [(4, partner.id) for partner in wizard.partner_ids],
'partner_ids': [partner.id for partner in wizard.partner_ids],
'attachments': [(attach.datas_fname or attach.name, base64.b64decode(attach.datas)) for attach in wizard.attachment_ids],
}
# mass mailing: render and override default values
if mass_mail_mode and wizard.model:
email_dict = self.render_message(cr, uid, wizard, res_id, context=context)
new_partner_ids = email_dict.pop('partner_ids', [])
post_values['partner_ids'] += [(4, partner_id) for partner_id in new_partner_ids]
post_values['partner_ids'] += new_partner_ids
new_attachments = email_dict.pop('attachments', [])
post_values['attachments'] += new_attachments
post_values.update(email_dict)
# automatically subscribe recipients if asked to
if context.get('mail_post_autofollow') and wizard.model and post_values.get('partner_ids'):
active_model_pool.message_subscribe(cr, uid, [res_id], [item[1] for item in post_values.get('partner_ids')], context=context)
# post the message
active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype='mt_comment', context=context, **post_values)
# post process: update attachments, because id is not necessarily known when adding attachments in Chatter
# self.pool.get('ir.attachment').write(cr, uid, [attach.id for attach in wizard.attachment_ids], {
# 'res_id': wizard.id, 'res_model': wizard.model or False}, context=context)
subtype = 'mail.mt_comment'
if is_log:
subtype = False
active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype=subtype, context=context, **post_values)
return {'type': 'ir.actions.act_window_close'}
@@ -258,7 +255,7 @@ class mail_compose_message(osv.TransientModel):
result = eval(exp, {
'user': self.pool.get('res.users').browse(cr, uid, uid, context=context),
'object': self.pool.get(model).browse(cr, uid, res_id, context=context),
'context': dict(context), # copy context to prevent side-effects of eval
'context': dict(context), # copy context to prevent side-effects of eval
})
return result and tools.ustr(result) or ''
return template and EXPRESSION_PATTERN.sub(merge, template)
@@ -13,8 +13,9 @@
<field name="res_id" invisible="1"/>
<field name="parent_id" invisible="1"/>
<!-- visible wizard -->
<label for="partner_ids" string="Recipients"/>
<div groups="base.group_user">
<label for="partner_ids" string="Recipients"
invisible="context.get('mail_compose_log', False)"/>
<div groups="base.group_user" invisible="context.get('mail_compose_log', False)">
<span attrs="{'invisible':['|', ('model', '=', False), ('composition_mode', '!=', 'mass_mail')]}">
Followers of selected items and
</span>
+1 -1
View File
@@ -476,7 +476,6 @@
<field name="remaining_hours" sum="Remaining Time" groups="project.group_time_work_estimation_tasks"/>
<field name="date_deadline"/>
<field name="message_summary"/>
<field name="message_unread"/>
<field name="categ_ids"/>
<templates>
<t t-name="kanban-box">
@@ -523,6 +522,7 @@
</span>
</div>
<div class="oe_kanban_footer_left" style="margin-top:5px;">
<t t-raw="record.message_summary.raw_value"/>
<field name="categ_ids"/>
</div>
</div>
+9
View File
@@ -485,6 +485,15 @@ class project_issue(base_stage, osv.osv):
return [issue.project_id.message_get_reply_to()[0] if issue.project_id else False
for issue in self.browse(cr, uid, ids, context=context)]
def message_get_suggested_recipients(self, cr, uid, ids, context=None):
recipients = super(project_issue, self).message_get_suggested_recipients(cr, uid, ids, context=context)
for issue in self.browse(cr, uid, ids, context=context):
if issue.partner_id:
self._message_add_suggested_recipient(cr, uid, recipients, issue, partner=issue.partner_id, reason=_('Customer'))
elif issue.email_from:
self._message_add_suggested_recipient(cr, uid, recipients, issue, email=issue.email_from, reason=_('Customer Email'))
return recipients
def message_new(self, cr, uid, msg, custom_values=None, context=None):
""" Overrides mail_thread message_new that is called by the mailgateway
through message_process.
@@ -190,6 +190,7 @@
<field name="user_id"/>
<field name="date_deadline"/>
<field name="kanban_state"/>
<field name="message_summary"/>
<templates>
<t t-name="kanban-tooltip">
<ul class="oe_kanban_tooltip">
@@ -240,6 +241,9 @@
<img t-att-src="kanban_image('res.users', 'image_small', record.user_id.raw_value)" t-att-title="record.user_id.value" width="24" height="24" class="oe_kanban_avatar"/>
</div>
</div>
<div class="oe_kanban_footer_left">
<t t-raw="record.message_summary.raw_value"/>
</div>
</div>
<div class="oe_clear"></div>
</div>