[IMP] mail: chatter: composition widget now correctly manage default email_mode and...
[odoo/odoo.git] / addons / mail / wizard / mail_compose_message.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>)
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU General Public License as published by
9 #    the Free Software Foundation, either version 3 of the License, or
10 #    (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU General Public License for more details.
16 #
17 #    You should have received a copy of the GNU General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>
19 #
20 ##############################################################################
21
22 import ast
23 import re
24
25 import tools
26 from osv import osv
27 from osv import fields
28 from tools.safe_eval import safe_eval as eval
29 from tools.translate import _
30
31 from ..mail_message import to_email
32
33 # main mako-like expression pattern
34 EXPRESSION_PATTERN = re.compile('(\$\{.+?\})')
35
36 class mail_compose_message(osv.TransientModel):
37     """Generic Email composition wizard. This wizard is meant to be inherited
38        at model and view level to provide specific wizard features.
39
40        The behavior of the wizard can be modified through the use of context
41        parameters, among which are:
42
43          * mail.compose.message.mode: if set to 'reply', the wizard is in 
44             reply to a previous message mode and pre-populated with the original
45             quote. If set to 'comment', it means you are writing a new message to
46             be attached to a document. If set to 'mass_mail', the wizard is in
47             mass mailing where the mail details can contain template placeholders
48             that will be merged with actual data before being sent to each
49             recipient.
50          * active_model: model name of the document to which the mail being
51                         composed is related
52          * active_id: id of the document to which the mail being composed is
53                       related, or id of the message to which user is replying,
54                       in case ``mail.compose.message.mode == 'reply'``
55          * active_ids: ids of the documents to which the mail being composed is
56                       related, in case ``mail.compose.message.mode == 'mass_mail'``.
57     """
58     _name = 'mail.compose.message'
59     _inherit = 'mail.message.common'
60     _description = 'Email composition wizard'
61
62     def default_get(self, cr, uid, fields, context=None):
63         """ Overridden to provide specific defaults depending on the context
64             parameters.
65
66             Composition mode
67             - comment: default mode; active_model, active_id = model and ID of a
68             document we are commenting,
69             - reply: active_id = ID of a mail.message to which we are replying.
70             From this message we can find the related model and res_id,
71             - mass_mailing mode: active_model, active_id  = model and ID of a
72             document we are commenting,
73
74            :param dict context: several context values will modify the behavior
75                                 of the wizard, cfr. the class description.
76         """
77         if context is None:
78             context = {}
79         compose_mode = context.get('mail.compose.message.mode', 'comment')
80         active_model = context.get('active_model')
81         active_id = context.get('active_id')
82         result = super(mail_compose_message, self).default_get(cr, uid, fields, context=context)
83
84         # get default values according to the composition mode
85         vals = {}
86         if compose_mode in ['reply']:
87             vals = self.get_message_data(cr, uid, int(context['active_id']), context=context)
88         elif compose_mode in ['comment', 'mass_mail'] and active_model and active_id:
89             vals = self.get_value(cr, uid, active_model, active_id, context)
90         for field in vals:
91             if field in fields:
92                 result[field] = vals[field]
93
94         # link to model and record if not done yet
95         if not result.get('model') and active_model:
96             result['model'] = active_model
97         if not result.get('res_id') and active_id:
98             result['res_id'] = active_id
99             # if result['model'] == 'mail.message' and not result.get('parent_id'):
100             #     result['parent_id'] = context.get('active_id')
101
102         # Try to provide default email_from if not specified yet
103         if not result.get('email_from'):
104             current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
105             result['email_from'] = current_user.user_email or False
106
107         return result
108
109     _columns = {
110         'dest_partner_ids': fields.many2many('res.partner',
111             'email_message_send_partner_rel',
112             'wizard_id', 'partner_id', 'Destination partners',
113             help="When sending emails through the social network composition wizard"\
114                  "you may choose to send a copy of the mail to partners."),
115         'attachment_ids': fields.many2many('ir.attachment','email_message_send_attachment_rel', 'wizard_id', 'attachment_id', 'Attachments'),
116         'auto_delete': fields.boolean('Auto Delete', help="Permanently delete emails after sending"),
117         'filter_id': fields.many2one('ir.filters', 'Filters'),
118     }
119
120     def get_value(self, cr, uid, model, res_id, context=None):
121         """ Returns a defaults-like dict with initial values for the composition
122             wizard when sending an email related to the document record
123             identified by ``model`` and ``res_id``.
124
125             The default implementation returns an empty dictionary, and is meant
126             to be overridden by subclasses.
127
128             :param str model: model name of the document record this mail is
129                 related to.
130             :param int res_id: id of the document record this mail is related to.
131             :param dict context: several context values will modify the behavior
132                 of the wizard, cfr. the class description.
133         """
134         result = {}
135         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
136         result.update({
137             'model': model,
138             'res_id': res_id,
139             'email_from': user.user_email or tools.config.get('email_from', False),
140             'body_html': '<br />---<br />' + tools.ustr(user.signature or ''),
141         })
142         return result
143
144     def get_message_data(self, cr, uid, message_id, context=None):
145         """ Returns a defaults-like dict with initial values for the composition
146             wizard when replying to the given message (e.g. including the quote
147             of the initial message, and the correct recipient). It should not be
148             called unless ``context['mail.compose.message.mode'] == 'reply'``.
149
150             :param int message_id: id of the mail.message to which the user
151                 is replying.
152             :param dict context: several context values will modify the behavior
153                 of the wizard, cfr. the class description.
154         """
155         if context is None:
156             context = {}
157         result = {}
158         if not message_id:
159             return result
160
161         current_user = self.pool.get('res.users').browse(cr, uid, uid, context)
162         message_data = self.pool.get('mail.message').browse(cr, uid, message_id, context)
163         # Form the subject
164         re_prefix = _("Re:")
165         reply_subject = tools.ustr(message_data.subject or '')
166         if not (reply_subject.startswith('Re:') or reply_subject.startswith(re_prefix)):
167             reply_subject = "%s %s" % (re_prefix, reply_subject)
168         # Form the bodies (text and html). We use the plain text version of the
169         # original mail, by default, as it is easier to quote than the HTML
170         # version. TODO: make it possible to switch to HTML on the fly
171         sent_date = _('On %(date)s, ') % {'date': message_data.date} if message_data.date else ''
172         sender = _('%(sender_name)s wrote:') % {'sender_name': tools.ustr(message_data.email_from or _('You'))}
173         body_text = message_data.body_text or ''
174         quoted_body_text = '> %s' % tools.ustr(body_text.replace('\n', "\n> ") or '')
175         quoted_body_html = '> %s' % tools.ustr(body_text.replace('\n', "<br />&gt; ") or '')
176         reply_body_text = '\n%s%s\n%s\n%s' % (sent_date, sender, quoted_body_text, current_user.signature)
177         reply_body_html = '\n%s%s\n%s\n%s' % (sent_date, sender, quoted_body_html, current_user.signature)
178         # Update header and references
179         reply_headers = {}
180         reply_references = message_data.references and tools.ustr(message_data.references) or False
181         reply_message_id = message_data.message_id or False
182         if reply_message_id:
183             reply_references = (reply_references or '') + " " + mail_wiz.message_id
184             reply_headers['In-Reply-To'] = mail_wiz.message_id
185         # update the result
186         result.update({
187             'body_text': reply_body_text,
188             'body_html': quoted_body_html,
189             'subject': reply_subject,
190             'attachment_ids': [],
191             'dest_partner_ids': [1],
192             'model': message_data.model or False,
193             'res_id': message_data.res_id or False,
194             'email_from': current_user.user_email or message_data.email_to or False,
195             'email_to': message_data.reply_to or message_data.email_from or False,
196             'email_cc': message_data.email_cc or False,
197             'user_id': uid,
198             # pass msg-id and references of mail we're replying to, to construct the
199             # new ones later when sending
200             'message_id': reply_message_id,
201             'references': reply_references,
202             'headers': reply_headers,
203         })
204         return result
205
206     def send_mail(self, cr, uid, ids, context=None):
207         '''Process the wizard contents and proceed with sending the corresponding
208            email(s), rendering any template patterns on the fly if needed.
209            If the wizard is in mass-mail mode (context['mail.compose.message.mode'] is
210            set to ``'mass_mail'``), the resulting email(s) are scheduled for being
211            sent the next time the mail.message scheduler runs, or the next time
212            ``mail.message.process_email_queue`` is called.
213            Otherwise the new message is sent immediately.
214
215            :param dict context: several context values will modify the behavior
216                                 of the wizard, cfr. the class description.
217         '''
218         if context is None:
219             context = {}
220         mail_message = self.pool.get('mail.message')
221         for mail_wiz in self.browse(cr, uid, ids, context=context):
222             # attachments
223             attachment = {}
224             for attach in mail_wiz.attachment_ids:
225                 attachment[attach.datas_fname] = attach.datas and attach.datas.decode('base64')
226
227             # composition wizard options
228             email_mode = context.get('email_mode')
229             formatting = context.get('formatting')
230
231             # default message values according to the wizard options
232             if formatting:
233                 content_subtype = 'html'
234                 subject = mail_wiz.subject
235             else:
236                 content_subtype = 'text'
237                 subject = False
238             if email_mode:
239                 type = 'email'
240             else:
241                 type = 'comment'
242             references = None
243             headers = {}
244             body = mail_wiz.body_html if mail_wiz.content_subtype == 'html' else mail_wiz.body_text
245
246             # Get model, and check whether it is OpenChatter enabled, aka inherit from mail.thread
247             if context.get('mail.compose.message.mode') == 'mass_mail':
248                 if context.get('active_ids') and context.get('active_model'):
249                     active_ids = context['active_ids']
250                     active_model = context['active_model']
251                 else:
252                     active_model = mail_wiz.model
253                     active_model_pool = self.pool.get(active_model)
254                     active_ids = active_model_pool.search(cr, uid, ast.literal_eval(mail_wiz.filter_id.domain), context=ast.literal_eval(mail_wiz.filter_id.context))
255             else:
256                 active_model = mail_wiz.model
257                 active_ids = [int(mail_wiz.res_id)]
258             active_model_pool = self.pool.get(active_model)
259             if hasattr(active_model_pool, 'message_append'):
260                 mail_thread_enabled = True
261             else:
262                 mail_thread_enabled = False
263
264             if context.get('mail.compose.message.mode') == 'mass_mail':
265                 # Mass mailing: must render the template patterns
266                 for active_id in active_ids:
267                     rendered_subject = self.render_template(cr, uid, subject, active_model, active_id)
268                     rendered_body = self.render_template(cr, uid, body, active_model, active_id)
269                     email_from = self.render_template(cr, uid, mail_wiz.email_from, active_model, active_id)
270                     email_to = self.render_template(cr, uid, mail_wiz.email_to, active_model, active_id)
271                     email_cc = self.render_template(cr, uid, mail_wiz.email_cc, active_model, active_id)
272                     email_bcc = self.render_template(cr, uid, mail_wiz.email_bcc, active_model, active_id)
273                     reply_to = self.render_template(cr, uid, mail_wiz.reply_to, active_model, active_id)
274     
275                     # in mass-mailing mode we only schedule the mail for sending, it will be 
276                     # processed as soon as the mail scheduler runs.
277                     if mail_thread_enabled:
278                         active_model_pool.message_append(cr, uid, [active_id],
279                             rendered_subject, body_text=mail_wiz.body_text, body_html=mail_wiz.body_html, content_subtype=mail_wiz.content_subtype, state='outgoing',
280                             email_to=email_to, email_from=email_from, email_cc=email_cc, email_bcc=email_bcc,
281                             reply_to=reply_to, references=references, attachments=attachment, headers=headers, context=context)
282                     else:
283                         mail_message.schedule_with_attach(cr, uid, email_from, to_email(email_to), subject, rendered_body,
284                             model=mail_wiz.model, email_cc=to_email(email_cc), email_bcc=to_email(email_bcc), reply_to=reply_to,
285                             attachments=attachment, references=references, res_id=active_id,
286                             content_subtype=mail_wiz.content_subtype, headers=headers, context=context)
287             else:
288                 # normal mode - no mass-mailing
289                 if mail_thread_enabled:
290                     msg_ids = active_model_pool.message_append(cr, uid, active_ids,
291                             subject, body_text=mail_wiz.body_text, body_html=mail_wiz.body_html, content_subtype=content_subtype, state='outgoing',
292                             email_to=mail_wiz.email_to, email_from=mail_wiz.email_from, email_cc=mail_wiz.email_cc, email_bcc=mail_wiz.email_bcc,
293                             reply_to=mail_wiz.reply_to, references=references, attachments=attachment, headers=headers, context=context,
294                             type=type)
295                 else:
296                     msg_ids = [mail_message.schedule_with_attach(cr, uid, mail_wiz.email_from, to_email(mail_wiz.email_to), subject, body_text,
297                         model=mail_wiz.model, email_cc=to_email(mail_wiz.email_cc), email_bcc=to_email(mail_wiz.email_bcc), reply_to=mail_wiz.reply_to,
298                         attachments=attachment, references=references, res_id=int(mail_wiz.res_id),
299                         content_subtype=mail_wiz.content_subtype, headers=headers, context=context)]
300                 # in normal mode, we send the email immediately, as the user expects us to (delay should be sufficiently small)
301                 mail_message.send(cr, uid, msg_ids, context=context)
302
303         return {'type': 'ir.actions.act_window_close'}
304
305     def render_template(self, cr, uid, template, model, res_id, context=None):
306         """Render the given template text, replace mako-like expressions ``${expr}``
307            with the result of evaluating these expressions with an evaluation context
308            containing:
309
310                 * ``user``: browse_record of the current user
311                 * ``object``: browse_record of the document record this mail is
312                               related to
313                 * ``context``: the context passed to the mail composition wizard
314
315            :param str template: the template text to render
316            :param str model: model name of the document record this mail is related to.
317            :param int res_id: id of the document record this mail is related to.
318         """
319         if context is None:
320             context = {}
321         def merge(match):
322             exp = str(match.group()[2:-1]).strip()
323             result = eval(exp,
324                           {
325                             'user' : self.pool.get('res.users').browse(cr, uid, uid, context=context),
326                             'object' : self.pool.get(model).browse(cr, uid, res_id, context=context),
327                             'context': dict(context), # copy context to prevent side-effects of eval
328                           })
329             if result in (None, False):
330                 return ""
331             return tools.ustr(result)
332         return template and EXPRESSION_PATTERN.sub(merge, template)
333
334
335 class mail_compose_message_extended(osv.TransientModel):
336     """ Extension of 'mail.compose.message' to support default field values related
337         to CRM-like models that follow the following conventions:
338
339         1. The model object must have an attribute '_mail_compose_message' equal to True.
340
341         2. The model should define the following fields:
342             - 'name' as subject of the message (required);
343             - 'email_from' as destination email address (required);
344             - 'email_cc' as cc email addresses (required);
345             - 'section_id.reply_to' as reply-to address (optional).
346     """
347     _inherit = 'mail.compose.message'
348
349     def get_value(self, cr, uid, model, res_id, context=None):
350         """ Overrides the default implementation to provide more default field values
351             related to the corresponding CRM case.
352         """
353         result = super(mail_compose_message_extended, self).get_value(cr, uid,  model, res_id, context=context)
354         model_obj = self.pool.get(model)
355         if getattr(model_obj, '_mail_compose_message', False) and res_id:
356             data = model_obj.browse(cr, uid , res_id, context)
357             result.update({
358                 'email_to': data.email_from or False,
359                 'email_cc': tools.ustr(data.email_cc or ''),
360                 'subject': data.name or False,
361             })
362             if hasattr(data, 'section_id'):
363                 result['reply_to'] = data.section_id and data.section_id.reply_to or False
364         return result
365
366 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: