Launchpad automatic translations update.
[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 from openerp import netsvc
27 from openerp.osv import osv, fields
28 from openerp.osv import fields
29 from openerp import tools
30 from openerp.tools.translate import _
31 from urllib import urlencode, quote as quote
32
33 _logger = logging.getLogger(__name__)
34
35 try:
36     # We use a jinja2 sandboxed environment to render mako templates.
37     # Note that the rendering does not cover all the mako syntax, in particular
38     # arbitrary Python statements are not accepted, and not all expressions are
39     # allowed: only "public" attributes (not starting with '_') of objects may
40     # be accessed.
41     # This is done on purpose: it prevents incidental or malicious execution of
42     # Python code that may break the security of the server.
43     from jinja2.sandbox import SandboxedEnvironment
44     mako_template_env = SandboxedEnvironment(
45         block_start_string="<%",
46         block_end_string="%>",
47         variable_start_string="${",
48         variable_end_string="}",
49         comment_start_string="<%doc>",
50         comment_end_string="</%doc>",
51         line_statement_prefix="%",
52         line_comment_prefix="##",
53         trim_blocks=True,               # do not output newline after blocks
54         autoescape=True,                # XML/HTML automatic escaping
55     )
56     mako_template_env.globals.update({
57         'str': str,
58         'quote': quote,
59         'urlencode': urlencode,
60     })
61 except ImportError:
62     _logger.warning("jinja2 not available, templating features will not work!")
63
64 class email_template(osv.osv):
65     "Templates for sending email"
66     _name = "email.template"
67     _description = 'Email Templates'
68
69     def render_template(self, cr, uid, template, model, res_id, context=None):
70         """Render the given template text, replace mako expressions ``${expr}``
71            with the result of evaluating these expressions with
72            an evaluation context containing:
73
74                 * ``user``: browse_record of the current user
75                 * ``object``: browse_record of the document record this mail is
76                               related to
77                 * ``context``: the context passed to the mail composition wizard
78
79            :param str template: the template text to render
80            :param str model: model name of the document record this mail is related to.
81            :param int res_id: id of the document record this mail is related to.
82         """
83         if not template:
84             return u""
85         if context is None:
86             context = {}
87         try:
88             template = tools.ustr(template)
89             record = None
90             if res_id:
91                 record = self.pool.get(model).browse(cr, uid, res_id, context=context)
92             user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
93             variables = {
94                 'object': record,
95                 'user': user,
96                 'ctx': context,     # context kw would clash with mako internals
97             }
98             result = mako_template_env.from_string(template).render(variables)
99             if result == u"False":
100                 result = u""
101             return result
102         except Exception:
103             _logger.exception("failed to render mako template value %r", template)
104             return u""
105
106     def get_email_template(self, cr, uid, template_id=False, record_id=None, context=None):
107         if context is None:
108             context = {}
109         if not template_id:
110             return False
111         template = self.browse(cr, uid, template_id, context)
112         lang = self.render_template(cr, uid, template.lang, template.model, record_id, context)
113         if lang:
114             # Use translated template if necessary
115             ctx = context.copy()
116             ctx['lang'] = lang
117             template = self.browse(cr, uid, template.id, ctx)
118         else:
119             template = self.browse(cr, uid, int(template_id), context)
120         return template
121
122     def onchange_model_id(self, cr, uid, ids, model_id, context=None):
123         mod_name = False
124         if model_id:
125             mod_name = self.pool.get('ir.model').browse(cr, uid, model_id, context).model
126         return {'value': {'model': mod_name}}
127
128     _columns = {
129         'name': fields.char('Name'),
130         'model_id': fields.many2one('ir.model', 'Applies to', help="The kind of document with with this template can be used"),
131         'model': fields.related('model_id', 'model', type='char', string='Related Document Model',
132                                 size=128, select=True, store=True, readonly=True),
133         'lang': fields.char('Language',
134                             help="Optional translation language (ISO code) to select when sending out an email. "
135                                  "If not set, the english version will be used. "
136                                  "This should usually be a placeholder expression "
137                                  "that provides the appropriate language code, e.g. "
138                                  "${object.partner_id.lang.code}.",
139                             placeholder="${object.partner_id.lang.code}"),
140         'user_signature': fields.boolean('Add Signature',
141                                          help="If checked, the user's signature will be appended to the text version "
142                                               "of the message"),
143         'subject': fields.char('Subject', translate=True, help="Subject (placeholders may be used here)",),
144         'email_from': fields.char('From', help="Sender address (placeholders may be used here)"),
145         'email_to': fields.char('To (Emails)', help="Comma-separated recipient addresses (placeholders may be used here)"),
146         'email_recipients': fields.char('To (Partners)', help="Comma-separated ids of recipient partners (placeholders may be used here)"),
147         'email_cc': fields.char('Cc', help="Carbon copy recipients (placeholders may be used here)"),
148         'reply_to': fields.char('Reply-To', help="Preferred response address (placeholders may be used here)"),
149         'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing Mail Server', readonly=False,
150                                           help="Optional preferred server for outgoing mails. If not set, the highest "
151                                                "priority one will be used."),
152         'body_html': fields.text('Body', translate=True, help="Rich-text/HTML version of the message (placeholders may be used here)"),
153         'report_name': fields.char('Report Filename', translate=True,
154                                    help="Name to use for the generated report file (may contain placeholders)\n"
155                                         "The extension can be omitted and will then come from the report type."),
156         'report_template': fields.many2one('ir.actions.report.xml', 'Optional report to print and attach'),
157         'ref_ir_act_window': fields.many2one('ir.actions.act_window', 'Sidebar action', readonly=True,
158                                             help="Sidebar action to make this template available on records "
159                                                  "of the related document model"),
160         'ref_ir_value': fields.many2one('ir.values', 'Sidebar Button', readonly=True,
161                                        help="Sidebar button to open the sidebar action"),
162         'attachment_ids': fields.many2many('ir.attachment', 'email_template_attachment_rel', 'email_template_id',
163                                            'attachment_id', 'Attachments',
164                                            help="You may attach files to this template, to be added to all "
165                                                 "emails created from this template"),
166         'auto_delete': fields.boolean('Auto Delete', help="Permanently delete this email after sending it, to save space"),
167
168         # Fake fields used to implement the placeholder assistant
169         'model_object_field': fields.many2one('ir.model.fields', string="Field",
170                                               help="Select target field from the related document model.\n"
171                                                    "If it is a relationship field you will be able to select "
172                                                    "a target field at the destination of the relationship."),
173         'sub_object': fields.many2one('ir.model', 'Sub-model', readonly=True,
174                                       help="When a relationship field is selected as first field, "
175                                            "this field shows the document model the relationship goes to."),
176         'sub_model_object_field': fields.many2one('ir.model.fields', 'Sub-field',
177                                                   help="When a relationship field is selected as first field, "
178                                                        "this field lets you select the target field within the "
179                                                        "destination document model (sub-model)."),
180         'null_value': fields.char('Default Value', help="Optional value to use if the target field is empty"),
181         'copyvalue': fields.char('Placeholder Expression', help="Final placeholder expression, to be copy-pasted in the desired template field."),
182     }
183
184     _defaults = {
185         'auto_delete': True,
186     }
187
188     def create_action(self, cr, uid, ids, context=None):
189         vals = {}
190         action_obj = self.pool.get('ir.actions.act_window')
191         data_obj = self.pool.get('ir.model.data')
192         for template in self.browse(cr, uid, ids, context=context):
193             src_obj = template.model_id.model
194             model_data_id = data_obj._get_id(cr, uid, 'mail', 'email_compose_message_wizard_form')
195             res_id = data_obj.browse(cr, uid, model_data_id, context=context).res_id
196             button_name = _('Send Mail (%s)') % template.name
197             vals['ref_ir_act_window'] = action_obj.create(cr, uid, {
198                  'name': button_name,
199                  'type': 'ir.actions.act_window',
200                  'res_model': 'mail.compose.message',
201                  'src_model': src_obj,
202                  'view_type': 'form',
203                  'context': "{'default_composition_mode': 'mass_mail', 'default_template_id' : %d, 'default_use_template': True}" % (template.id),
204                  'view_mode':'form,tree',
205                  'view_id': res_id,
206                  'target': 'new',
207                  'auto_refresh':1
208             }, context)
209             vals['ref_ir_value'] = self.pool.get('ir.values').create(cr, uid, {
210                  'name': button_name,
211                  'model': src_obj,
212                  'key2': 'client_action_multi',
213                  'value': "ir.actions.act_window," + str(vals['ref_ir_act_window']),
214                  'object': True,
215              }, context)
216         self.write(cr, uid, ids, {
217                     'ref_ir_act_window': vals.get('ref_ir_act_window',False),
218                     'ref_ir_value': vals.get('ref_ir_value',False),
219                 }, context)
220         return True
221
222     def unlink_action(self, cr, uid, ids, context=None):
223         for template in self.browse(cr, uid, ids, context=context):
224             try:
225                 if template.ref_ir_act_window:
226                     self.pool.get('ir.actions.act_window').unlink(cr, uid, template.ref_ir_act_window.id, context)
227                 if template.ref_ir_value:
228                     ir_values_obj = self.pool.get('ir.values')
229                     ir_values_obj.unlink(cr, uid, template.ref_ir_value.id, context)
230             except Exception:
231                 raise osv.except_osv(_("Warning"), _("Deletion of the action record failed."))
232         return True
233
234     def unlink(self, cr, uid, ids, context=None):
235         self.unlink_action(cr, uid, ids, context=context)
236         return super(email_template, self).unlink(cr, uid, ids, context=context)
237
238     def copy(self, cr, uid, id, default=None, context=None):
239         template = self.browse(cr, uid, id, context=context)
240         if default is None:
241             default = {}
242         default = default.copy()
243         default.update(
244             name=_("%s (copy)") % (template.name),
245             ref_ir_act_window=False,
246             ref_ir_value=False)
247         return super(email_template, self).copy(cr, uid, id, default, context)
248
249     def build_expression(self, field_name, sub_field_name, null_value):
250         """Returns a placeholder expression for use in a template field,
251            based on the values provided in the placeholder assistant.
252
253           :param field_name: main field name
254           :param sub_field_name: sub field name (M2O)
255           :param null_value: default value if the target value is empty
256           :return: final placeholder expression
257         """
258         expression = ''
259         if field_name:
260             expression = "${object." + field_name
261             if sub_field_name:
262                 expression += "." + sub_field_name
263             if null_value:
264                 expression += " or '''%s'''" % null_value
265             expression += "}"
266         return expression
267
268     def onchange_sub_model_object_value_field(self, cr, uid, ids, model_object_field, sub_model_object_field=False, null_value=None, context=None):
269         result = {
270             'sub_object': False,
271             'copyvalue': False,
272             'sub_model_object_field': False,
273             'null_value': False
274             }
275         if model_object_field:
276             fields_obj = self.pool.get('ir.model.fields')
277             field_value = fields_obj.browse(cr, uid, model_object_field, context)
278             if field_value.ttype in ['many2one', 'one2many', 'many2many']:
279                 res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_value.relation)], context=context)
280                 sub_field_value = False
281                 if sub_model_object_field:
282                     sub_field_value = fields_obj.browse(cr, uid, sub_model_object_field, context)
283                 if res_ids:
284                     result.update({
285                         'sub_object': res_ids[0],
286                         'copyvalue': self.build_expression(field_value.name, sub_field_value and sub_field_value.name or False, null_value or False),
287                         'sub_model_object_field': sub_model_object_field or False,
288                         'null_value': null_value or False
289                         })
290             else:
291                 result.update({
292                         'copyvalue': self.build_expression(field_value.name, False, null_value or False),
293                         'null_value': null_value or False
294                         })
295         return {'value':result}
296
297
298     def generate_email(self, cr, uid, template_id, res_id, context=None):
299         """Generates an email from the template for given (model, res_id) pair.
300
301            :param template_id: id of the template to render.
302            :param res_id: id of the record to use for rendering the template (model
303                           is taken from template definition)
304            :returns: a dict containing all relevant fields for creating a new
305                      mail.mail entry, with one extra key ``attachments``, in the
306                      format expected by :py:meth:`mail_thread.message_post`.
307         """
308         if context is None:
309             context = {}
310         report_xml_pool = self.pool.get('ir.actions.report.xml')
311         template = self.get_email_template(cr, uid, template_id, res_id, context)
312         values = {}
313         for field in ['subject', 'body_html', 'email_from',
314                       'email_to', 'email_recipients', 'email_cc', 'reply_to']:
315             values[field] = self.render_template(cr, uid, getattr(template, field),
316                                                  template.model, res_id, context=context) \
317                                                  or False
318         if template.user_signature:
319             signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature
320             values['body_html'] = tools.append_content_to_html(values['body_html'], signature)
321
322         if values['body_html']:
323             values['body'] = tools.html_sanitize(values['body_html'])
324
325         values.update(mail_server_id=template.mail_server_id.id or False,
326                       auto_delete=template.auto_delete,
327                       model=template.model,
328                       res_id=res_id or False)
329
330         attachments = []
331         # Add report in attachments
332         if template.report_template:
333             report_name = self.render_template(cr, uid, template.report_name, template.model, res_id, context=context)
334             report_service = 'report.' + report_xml_pool.browse(cr, uid, template.report_template.id, context).report_name
335             # Ensure report is rendered using template's language
336             ctx = context.copy()
337             if template.lang:
338                 ctx['lang'] = self.render_template(cr, uid, template.lang, template.model, res_id, context)
339             service = netsvc.LocalService(report_service)
340             (result, format) = service.create(cr, uid, [res_id], {'model': template.model}, ctx)
341             result = base64.b64encode(result)
342             if not report_name:
343                 report_name = report_service
344             ext = "." + format
345             if not report_name.endswith(ext):
346                 report_name += ext
347             attachments.append((report_name, result))
348
349         # Add template attachments
350         for attach in template.attachment_ids:
351             attachments.append((attach.datas_fname, attach.datas))
352
353         values['attachments'] = attachments
354         return values
355
356     def send_mail(self, cr, uid, template_id, res_id, force_send=False, context=None):
357         """Generates a new mail message for the given template and record,
358            and schedules it for delivery through the ``mail`` module's scheduler.
359
360            :param int template_id: id of the template to render
361            :param int res_id: id of the record to render the template with
362                               (model is taken from the template)
363            :param bool force_send: if True, the generated mail.message is
364                 immediately sent after being created, as if the scheduler
365                 was executed for this message only.
366            :returns: id of the mail.message that was created
367         """
368         if context is None: context = {}
369         mail_mail = self.pool.get('mail.mail')
370         ir_attachment = self.pool.get('ir.attachment')
371         values = self.generate_email(cr, uid, template_id, res_id, context=context)
372         assert 'email_from' in values, 'email_from is missing or empty after template rendering, send_mail() cannot proceed'
373         attachments = values.pop('attachments') or {}
374         del values['email_recipients'] # TODO Properly use them.
375         msg_id = mail_mail.create(cr, uid, values, context=context)
376         # link attachments
377         attachment_ids = []
378         for fname, fcontent in attachments.iteritems():
379             attachment_data = {
380                     'name': fname,
381                     'datas_fname': fname,
382                     'datas': fcontent,
383                     'res_model': mail_mail._name,
384                     'res_id': msg_id,
385             }
386             context.pop('default_type', None)
387             attachment_ids.append(ir_attachment.create(cr, uid, attachment_data, context=context))
388         if attachment_ids:
389             mail_mail.write(cr, uid, msg_id, {'attachment_ids': [(6, 0, attachment_ids)]}, context=context)
390         if force_send:
391             mail_mail.send(cr, uid, [msg_id], context=context)
392         return msg_id
393
394 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: