[IMP] email_template: use raw template for msg composition in mass_mail mode
[odoo/odoo.git] / addons / email_template / email_template.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2009 Sharoon Thomas
6 #    Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>)
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU General Public License as published by
10 #    the Free Software Foundation, either version 3 of the License, or
11 #    (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU General Public License for more details.
17 #
18 #    You should have received a copy of the GNU General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>
20 #
21 ##############################################################################
22
23 import base64
24 import logging
25
26 import netsvc
27 from osv import osv
28 from osv import fields
29 import tools
30 from tools.translate import _
31
32 try:
33     from mako.template import Template as MakoTemplate
34 except ImportError:
35     logging.getLogger('init').warning("email_template: mako templates not available, templating features will not work!")
36
37 class email_template(osv.osv):
38     "Templates for sending email"
39     _inherit = 'mail.message'
40     _name = "email.template"
41     _description = 'Email Templates'
42     _rec_name = 'name' # override mail.message's behavior
43
44     def render_template(self, cr, uid, template, model, res_id, context=None):
45         """Render the given template text, replace mako expressions ``${expr}``
46            with the result of evaluating these expressions with
47            an evaluation context containing:
48
49                 * ``user``: browse_record of the current user
50                 * ``object``: browse_record of the document record this mail is
51                               related to
52                 * ``context``: the context passed to the mail composition wizard
53
54            :param str template: the template text to render
55            :param str model: model name of the document record this mail is related to.
56            :param int res_id: id of the document record this mail is related to.
57         """
58         if not template: return u""
59         try:
60             template = tools.ustr(template)
61             record = None
62             if res_id:
63                 record = self.pool.get(model).browse(cr, uid, res_id, context=context)
64             user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
65             result = MakoTemplate(template).render_unicode(object=record,
66                                                            user=user,
67                                                            # context kw would clash with mako internals
68                                                            ctx=context,
69                                                            format_exceptions=True)
70             if result == u'False':
71                 result = u''
72             return result
73         except Exception:
74             logging.exception("failed to render mako template value %r", template)
75             return u""
76
77     def get_email_template(self, cr, uid, template_id=False, record_id=None, context=None):
78         if context is None:
79             context = {}
80         if not template_id:
81             return False
82         template = self.browse(cr, uid, template_id, context)
83         lang = self.render_template(cr, uid, template.lang, template.model, record_id, context)
84         if lang:
85             # Use translated template if necessary
86             ctx = context.copy()
87             ctx['lang'] = lang
88             template = self.browse(cr, uid, template.id, ctx)
89         else:
90             template = self.browse(cr, uid, int(template_id), context)
91         return template
92
93     def onchange_model_id(self, cr, uid, ids, model_id, context=None):
94         mod_name = False
95         if model_id:
96             mod_name = self.pool.get('ir.model').browse(cr, uid, model_id, context).model
97         return {'value':{'model': mod_name}}
98
99     _columns = {
100         'name': fields.char('Name', size=250),
101         'model_id': fields.many2one('ir.model', 'Related document model'),
102         'lang': fields.char('Language Selection', size=250,
103                             help="Optional translation language (ISO code) to select when sending out an email. "
104                                  "If not set, the english version will be used. "
105                                  "This should usually be a placeholder expression "
106                                  "that provides the appropriate language code, e.g. "
107                                  "${object.partner_id.lang.code}."),
108         'user_signature': fields.boolean('Add Signature',
109                                          help="If checked, the user's signature will be appended to the text version "
110                                               "of the message"),
111         'report_name': fields.char('Report Filename', size=200, translate=True,
112                                    help="Name to use for the generated report file (may contain placeholders)\n"
113                                         "The extension can be omitted and will then come from the report type."),
114         'report_template':fields.many2one('ir.actions.report.xml', 'Optional report to print and attach'),
115         'ref_ir_act_window':fields.many2one('ir.actions.act_window', 'Sidebar action', readonly=True,
116                                             help="Sidebar action to make this template available on records "
117                                                  "of the related document model"),
118         'ref_ir_value':fields.many2one('ir.values', 'Sidebar button', readonly=True,
119                                        help="Sidebar button to open the sidebar action"),
120         'track_campaign_item': fields.boolean('Resource Tracking',
121                                               help="Enable this is you wish to include a special tracking marker "
122                                                    "in outgoing emails so you can identify replies and link "
123                                                    "them back to the corresponding resource record. "
124                                                    "This is useful for CRM leads for example"),
125
126         # Overridden mail.message.common fields for technical reasons:
127         'model': fields.related('model_id','model', type='char', string='Related Document model',
128                                 size=128, select=True, store=True, readonly=True),
129         # we need a separate m2m table to avoid ID collisions with the original mail.message entries
130         'attachment_ids': fields.many2many('ir.attachment', 'email_template_attachment_rel', 'email_template_id',
131                                            'attachment_id', 'Files to attach',
132                                            help="You may attach files to this template, to be added to all "
133                                                 "emails created from this template"),
134
135         # Overridden mail.message.common fields to make tooltips more appropriate:
136         'subject':fields.char('Subject', size=512, translate=True, help="Subject (placeholders may be used here)",),
137         'email_from': fields.char('From', size=128, help="Sender address (placeholders may be used here)"),
138         'email_to': fields.char('To', size=256, help="Comma-separated recipient addresses (placeholders may be used here)"),
139         'email_cc': fields.char('Cc', size=256, help="Carbon copy recipients (placeholders may be used here)"),
140         'email_bcc': fields.char('Bcc', size=256, help="Blind carbon copy recipients (placeholders may be used here)"),
141         'reply_to': fields.char('Reply-To', size=250, help="Preferred response address (placeholders may be used here)"),
142         'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing Mail Server', readonly=False,
143                                           help="Optional preferred server for outgoing mails. If not set, the highest "
144                                                "priority one will be used."),
145         'body_text': fields.text('Text contents', translate=True, help="Plaintext version of the message (placeholders may be used here)"),
146         'body_html': fields.text('Rich-text contents', help="Rich-text/HTML version of the message (placeholders may be used here)"),
147         'message_id': fields.char('Message-Id', size=256, help="Message-ID SMTP header to use in outgoing messages based on this template. "
148                                                                "Please note that this overrides the 'Resource Tracking' option, "
149                                                                "so if you simply need to track replies to outgoing emails, enable "
150                                                                "that option instead.\n"
151                                                                "Placeholders must be used here, as this value always needs to be unique!"),
152
153         # Fake fields used to implement the placeholder assistant
154         'model_object_field': fields.many2one('ir.model.fields', string="Field",
155                                               help="Select target field from the related document model.\n"
156                                                    "If it is a relationship field you will be able to select "
157                                                    "a target field at the destination of the relationship."),
158         'sub_object': fields.many2one('ir.model', 'Sub-model', readonly=True,
159                                       help="When a relationship field is selected as first field, "
160                                            "this field shows the document model the relationship goes to."),
161         'sub_model_object_field': fields.many2one('ir.model.fields', 'Sub-field',
162                                                   help="When a relationship field is selected as first field, "
163                                                        "this field lets you select the target field within the "
164                                                        "destination document model (sub-model)."),
165         'null_value': fields.char('Null value', help="Optional value to use if the target field is empty", size=128),
166         'copyvalue': fields.char('Expression', size=256, help="Final placeholder expression, to be copy-pasted in the desired template field."),
167     }
168
169     _defaults = {
170         'track_campaign_item': True
171     }
172
173     def create_action(self, cr, uid, ids, context=None):
174         vals = {}
175         action_obj = self.pool.get('ir.actions.act_window')
176         data_obj = self.pool.get('ir.model.data')
177         for template in self.browse(cr, uid, ids, context=context):
178             src_obj = template.model_id.model
179             model_data_id = data_obj._get_id(cr, uid, 'mail', 'email_compose_message_wizard_form')
180             res_id = data_obj.browse(cr, uid, model_data_id, context=context).res_id
181             vals['ref_ir_act_window'] = action_obj.create(cr, uid, {
182                  'name': template.name,
183                  'type': 'ir.actions.act_window',
184                  'res_model': 'mail.compose.message',
185                  'src_model': src_obj,
186                  'view_type': 'form',
187                  'context': "{'mail.compose.message.mode':'mass_mail'}",
188                  'view_mode':'form,tree',
189                  'view_id': res_id,
190                  'target': 'new',
191                  'auto_refresh':1
192             }, context)
193             vals['ref_ir_value'] = self.pool.get('ir.values').create(cr, uid, {
194                  'name': _('Send Mail (%s)') % template.name,
195                  'model': src_obj,
196                  'key2': 'client_action_multi',
197                  'value': "ir.actions.act_window," + str(vals['ref_ir_act_window']),
198                  'object': True,
199              }, context)
200         self.write(cr, uid, ids, {
201                     'ref_ir_act_window': vals.get('ref_ir_act_window',False),
202                     'ref_ir_value': vals.get('ref_ir_value',False),
203                 }, context)
204         return True
205
206     def unlink_action(self, cr, uid, ids, context=None):
207         for template in self.browse(cr, uid, ids, context=context):
208             try:
209                 if template.ref_ir_act_window:
210                     self.pool.get('ir.actions.act_window').unlink(cr, uid, template.ref_ir_act_window.id, context)
211                 if template.ref_ir_value:
212                     ir_values_obj = self.pool.get('ir.values')
213                     ir_values_obj.unlink(cr, uid, template.ref_ir_value.id, context)
214             except:
215                 raise osv.except_osv(_("Warning"), _("Deletion of Record failed"))
216         return True
217
218     def unlink(self, cr, uid, ids, context=None):
219         self.unlink_action(cr, uid, ids, context=context)
220         return super(email_template, self).unlink(cr, uid, ids, context=context)
221
222     def copy(self, cr, uid, id, default=None, context=None):
223         template = self.browse(cr, uid, id, context=context)
224         if default is None:
225             default = {}
226         default = default.copy()
227         default['name'] = template.name + _('(copy)')
228         return super(email_template, self).copy(cr, uid, id, default, context)
229
230     def build_expression(self, field_name, sub_field_name, null_value):
231         """Returns a placeholder expression for use in a template field,
232            based on the values provided in the placeholder assistant.
233
234           :param field_name: main field name
235           :param sub_field_name: sub field name (M2O)
236           :param null_value: default value if the target value is empty
237           :return: final placeholder expression
238         """
239         expression = ''
240         if field_name:
241             expression = "${object." + field_name
242             if sub_field_name:
243                 expression += "." + sub_field_name
244             if null_value:
245                 expression += " or '''%s'''" % null_value
246             expression += "}"
247         return expression
248
249     def onchange_sub_model_object_value_field(self, cr, uid, ids, model_object_field, sub_model_object_field=False, null_value=None, context=None):
250         result = {
251             'sub_object': False,
252             'copyvalue': False,
253             'sub_model_object_field': False,
254             'null_value': False
255             }
256         if model_object_field:
257             fields_obj = self.pool.get('ir.model.fields')
258             field_value = fields_obj.browse(cr, uid, model_object_field, context)
259             if field_value.ttype in ['many2one', 'one2many', 'many2many']:
260                 res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_value.relation)], context=context)
261                 sub_field_value = False
262                 if sub_model_object_field:
263                     sub_field_value = fields_obj.browse(cr, uid, sub_model_object_field, context)
264                 if res_ids:
265                     result.update({
266                         'sub_object': res_ids[0],
267                         'copyvalue': self.build_expression(field_value.name, sub_field_value and sub_field_value.name or False, null_value or False),
268                         'sub_model_object_field': sub_model_object_field or False,
269                         'null_value': null_value or False
270                         })
271             else:
272                 result.update({
273                         'copyvalue': self.build_expression(field_value.name, False, null_value or False),
274                         'null_value': null_value or False
275                         })
276         return {'value':result}
277
278
279     def generate_email(self, cr, uid, template_id, res_id, context=None):
280         """Generates an email from the template for given (model, res_id) pair.
281
282            :param template_id: id of the template to render.
283            :param res_id: id of the record to use for rendering the template (model
284                           is taken from template definition)
285            :returns: a dict containing all relevant fields for creating a new
286                      mail.message entry, with the addition one additional
287                      special key ``attachments`` containing a list of
288         """
289         if context is None:
290             context = {}
291         values = {
292                   'subject': False,
293                   'body_text': False,
294                   'body_html': False,
295                   'email_from': False,
296                   'email_to': False,
297                   'email_cc': False,
298                   'email_bcc': False,
299                   'reply_to': False,
300                   'auto_delete': False,
301                   'model': False,
302                   'res_id': False,
303                   'mail_server_id': False,
304                   'attachments': False,
305                   'attachment_ids': False,
306                   'message_id': False,
307                   'state': 'outgoing',
308         }
309         if not template_id:
310             return values
311
312         report_xml_pool = self.pool.get('ir.actions.report.xml')
313         template = self.get_email_template(cr, uid, template_id, res_id, context)
314
315         for field in ['subject', 'body_text', 'body_html', 'email_from',
316                       'email_to', 'email_cc', 'email_bcc', 'reply_to',
317                       'message_id']:
318             values[field] = self.render_template(cr, uid, getattr(template, field),
319                                                  template.model, res_id, context=context) \
320                                                  or False
321
322         if template.user_signature:
323             signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature
324             values['body_text'] += '\n\n' + signature
325
326         values.update(mail_server_id = template.mail_server_id.id or False,
327                       auto_delete = template.auto_delete,
328                       model=template.model,
329                       res_id=res_id or False)
330
331         attachments = {}
332         # Add report as a Document
333         if template.report_template:
334             report_name = template.report_name
335             report_service = 'report.' + report_xml_pool.browse(cr, uid, template.report_template.id, context).report_name
336             # Ensure report is rendered using template's language
337             ctx = context.copy()
338             if template.lang:
339                 ctx['lang'] = self.render_template(cr, uid, template.lang, template.model, res_id, context)
340             service = netsvc.LocalService(report_service)
341             (result, format) = service.create(cr, uid, [res_id], {'model': template.model}, ctx)
342             result = base64.b64encode(result)
343             if not report_name:
344                 report_name = report_service
345             ext = "." + format
346             if not report_name.endswith(ext):
347                 report_name += ext
348             attachments[report_name] = result
349
350         # Add document attachments
351         for attach in template.attachment_ids:
352             # keep the bytes as fetched from the db, base64 encoded
353             attachments[attach.datas_fname] = attach.datas
354
355         values['attachments'] = attachments
356         return values
357
358     def send_mail(self, cr, uid, template_id, res_id, context=None):
359         """Generates a new mail message for the given template and record,
360            and schedule it for delivery through the ``mail`` module's scheduler.
361
362            :param int template_id: id of the template to render
363            :param int record_id: id of the record to render the template with
364                                 (model is taken from the template)
365         """
366         mail_message = self.pool.get('mail.message')
367         ir_attachment = self.pool.get('ir.attachment')
368         template = self.browse(cr, uid, template_id, context)
369         values = self.generate_email(cr, uid, template_id, res_id, context=context)
370         attachments = values.pop('attachments')
371         message_id = mail_message.create(values)
372         # link attachments
373         attachment_ids = []
374         for fname, fcontent in values['attachments'].iteritems():
375             attachment_data = {
376                     'name': fname,
377                     'datas_fname': fname,
378                     'datas': fcontent,
379                     'res_model': mail_message._name,
380                     'res_id': message_id,
381             }
382             if context.has_key('default_type'):
383                 del context['default_type']
384             attachment_ids.append(ir_attachment.create(cr, uid, attachment_data, context))
385
386 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: