[IMP] mail.compose.message, email.template: rendering is now done in batch.
authorThibault Delavallée <tde@openerp.com>
Wed, 28 Aug 2013 14:09:29 +0000 (16:09 +0200)
committerThibault Delavallée <tde@openerp.com>
Wed, 28 Aug 2013 14:09:29 +0000 (16:09 +0200)
Also refactored send_mail in mail.compose.message in order to be able to override
mail values without having to intefere with the send_mail behavior.

bzr revid: tde@openerp.com-20130828140929-xe9hbmbo6jxgs9mh

addons/email_template/email_template.py
addons/email_template/wizard/mail_compose_message.py
addons/mail/wizard/mail_compose_message.py

index 2e3405e..2e1c52d 100644 (file)
@@ -67,7 +67,7 @@ class email_template(osv.osv):
     _description = 'Email Templates'
     _order = 'name'
 
-    def render_template(self, cr, uid, template, model, res_id, context=None):
+    def render_template_batch(self, cr, uid, template, model, res_ids, context=None):
         """Render the given template text, replace mako expressions ``${expr}``
            with the result of evaluating these expressions with
            an evaluation context containing:
@@ -79,46 +79,60 @@ class email_template(osv.osv):
 
            :param str template: the template text to render
            :param str model: model name of the document record this mail is related to.
-           :param int res_id: id of the document record this mail is related to.
+           :param int res_ids: list of ids of document records those mails are related to.
         """
-        if not template:
-            return u""
         if context is None:
             context = {}
+        results = dict.fromkeys(res_ids, u"")
+
+        # try to load the template
         try:
-            template = tools.ustr(template)
-            record = None
-            if res_id:
-                record = self.pool[model].browse(cr, uid, res_id, context=context)
-            user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
-            variables = {
-                'object': record,
-                'user': user,
-                'ctx': context,     # context kw would clash with mako internals
-            }
-            result = mako_template_env.from_string(template).render(variables)
-            if result == u"False":
-                result = u""
-            return result
+            template = mako_template_env.from_string(tools.ustr(template))
         except Exception:
-            _logger.exception("failed to render mako template value %r", template)
-            return u""
-
-    def get_email_template(self, cr, uid, template_id=False, record_id=None, context=None):
+            _logger.exception("Failed to load template %r", template)
+            return results
+
+        # prepare template variables
+        user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
+        records = self.pool[model].browse(cr, uid, res_ids, context=context) or [None]
+        variables = {
+            'user': user,
+            'ctx': context,  # context kw would clash with mako internals
+        }
+        for record in records:
+            res_id = record.id if record else None
+            variables['object'] = record
+            try:
+                render_result = template.render(variables)
+            except Exception:
+                _logger.exception("Failed to render template %r using values %r" % (template, variables))
+                render_result = u""
+            if render_result == u"False":
+                render_result = u""
+            results[res_id] = render_result
+        return results
+
+    def get_email_template_batch(self, cr, uid, template_id=False, res_ids=None, context=None):
         if context is None:
             context = {}
+        if res_ids is None:
+            res_ids = [None]
+        results = dict.fromkeys(res_ids, False)
+
         if not template_id:
-            return False
+            return results
         template = self.browse(cr, uid, template_id, context)
-        lang = self.render_template(cr, uid, template.lang, template.model, record_id, context)
-        if lang:
-            # Use translated template if necessary
-            ctx = context.copy()
-            ctx['lang'] = lang
-            template = self.browse(cr, uid, template.id, ctx)
-        else:
-            template = self.browse(cr, uid, int(template_id), context)
-        return template
+        langs = self.render_template_batch(cr, uid, template.lang, template.model, res_ids, context)
+        for res_id, lang in langs.iteritems():
+            if lang:
+                # Use translated template if necessary
+                ctx = context.copy()
+                ctx['lang'] = lang
+                template = self.browse(cr, uid, template.id, ctx)
+            else:
+                template = self.browse(cr, uid, int(template_id), context)
+            results[res_id] = template
+        return results
 
     def onchange_model_id(self, cr, uid, ids, model_id, context=None):
         mod_name = False
@@ -300,64 +314,75 @@ class email_template(osv.osv):
                         })
         return {'value': result}
 
-    def generate_email(self, cr, uid, template_id, res_id, context=None):
-        """Generates an email from the template for given (model, res_id) pair.
-
-           :param template_id: id of the template to render.
-           :param res_id: id of the record to use for rendering the template (model
-                          is taken from template definition)
-           :returns: a dict containing all relevant fields for creating a new
-                     mail.mail entry, with one extra key ``attachments``, in the
-                     format expected by :py:meth:`mail_thread.message_post`.
+    def generate_email_batch(self, cr, uid, template_id, res_ids, context=None, fields=None):
+        """Generates an email from the template for given the given model based on
+        records given by res_ids.
+
+        :param template_id: id of the template to render.
+        :param res_id: id of the record to use for rendering the template (model
+                       is taken from template definition)
+        :returns: a dict containing all relevant fields for creating a new
+                  mail.mail entry, with one extra key ``attachments``, in the
+                  format expected by :py:meth:`mail_thread.message_post`.
         """
         if context is None:
             context = {}
+        if fields is None:
+            fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to']
+
         report_xml_pool = self.pool.get('ir.actions.report.xml')
-        template = self.get_email_template(cr, uid, template_id, res_id, context)
-        values = {}
-        for field in ['subject', 'body_html', 'email_from',
-                      'email_to', 'partner_to', 'email_cc', 'reply_to']:
-            values[field] = self.render_template(cr, uid, getattr(template, field),
-                                                 template.model, res_id, context=context) \
-                                                 or False
-        if template.user_signature:
-            signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature
-            values['body_html'] = tools.append_content_to_html(values['body_html'], signature)
-
-        if values['body_html']:
-            values['body'] = tools.html_sanitize(values['body_html'])
-
-        values.update(mail_server_id=template.mail_server_id.id or False,
-                      auto_delete=template.auto_delete,
-                      model=template.model,
-                      res_id=res_id or False)
-
-        attachments = []
-        # Add report in attachments
-        if template.report_template:
-            report_name = self.render_template(cr, uid, template.report_name, template.model, res_id, context=context)
-            report_service = report_xml_pool.browse(cr, uid, template.report_template.id, context).report_name
-            # Ensure report is rendered using template's language
-            ctx = context.copy()
-            if template.lang:
-                ctx['lang'] = self.render_template(cr, uid, template.lang, template.model, res_id, context)
-            result, format = openerp.report.render_report(cr, uid, [res_id], report_service, {'model': template.model}, ctx)
-            result = base64.b64encode(result)
-            if not report_name:
-                report_name = 'report.' + report_service
-            ext = "." + format
-            if not report_name.endswith(ext):
-                report_name += ext
-            attachments.append((report_name, result))
-
-        attachment_ids = []
-        # Add template attachments
-        for attach in template.attachment_ids:
-            attachment_ids.append(attach.id)
-
-        values['attachments'] = attachments
-        values['attachment_ids'] = attachment_ids
-        return values
+        res_ids_to_templates = self.get_email_template_batch(cr, uid, template_id, res_ids, context)
+
+        # templates: res_id -> template; template -> res_ids
+        templates_to_res_ids = {}
+        for res_id, template in res_ids_to_templates.iteritems():
+            templates_to_res_ids.setdefault(template, []).append(res_id)
+
+        results = dict()
+        for template, template_res_ids in templates_to_res_ids.iteritems():
+            # generate fields value for all res_ids linked to the current template
+            for field in ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to']:
+                generated_field_values = self.render_template_batch(cr, uid, getattr(template, field), template.model, template_res_ids, context=context)
+                for res_id, field_value in generated_field_values.iteritems():
+                    results.setdefault(res_id, dict())[field] = field_value
+            # update values for all res_ids
+            for res_id in template_res_ids:
+                values = results[res_id]
+                if template.user_signature:
+                    signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature
+                    values['body_html'] = tools.append_content_to_html(values['body_html'], signature)
+                if values['body_html']:
+                    values['body'] = tools.html_sanitize(values['body_html'])
+                values.update(
+                    mail_server_id=template.mail_server_id.id or False,
+                    auto_delete=template.auto_delete,
+                    model=template.model,
+                    res_id=res_id or False,
+                    attachment_ids=[attach.id for attach in template.attachment_ids],
+                )
+
+            # Add report in attachments
+            if template.report_template:
+                for res_id in template_res_ids:
+                    attachments = []
+                    report_name = self.render_template(cr, uid, template.report_name, template.model, res_id, context=context)
+                    report_service = report_xml_pool.browse(cr, uid, template.report_template.id, context).report_name
+                    # Ensure report is rendered using template's language
+                    ctx = context.copy()
+                    if template.lang:
+                        ctx['lang'] = self.render_template_batch(cr, uid, template.lang, template.model, res_id, context)  # take 0 ?
+                    result, format = openerp.report.render_report(cr, uid, [res_id], report_service, {'model': template.model}, ctx)
+                    result = base64.b64encode(result)
+                    if not report_name:
+                        report_name = 'report.' + report_service
+                    ext = "." + format
+                    if not report_name.endswith(ext):
+                        report_name += ext
+                    attachments.append((report_name, result))
+
+                    values['attachments'] = attachments
+
+        return results
 
     def send_mail(self, cr, uid, template_id, res_id, force_send=False, raise_exception=False, context=None):
         """Generates a new mail message for the given template and record,
@@ -404,4 +429,14 @@ class email_template(osv.osv):
             mail_mail.send(cr, uid, [msg_id], raise_exception=raise_exception, context=context)
         return msg_id
 
+    # Compatibility method
+    def render_template(self, cr, uid, template, model, res_id, context=None):
+        return self.render_template_batch(cr, uid, template, model, [res_id], context)[res_id]
+
+    def get_email_template(self, cr, uid, template_id=False, record_id=None, context=None):
+        return self.get_email_template_batch(cr, uid, template_id, [record_id], context)[record_id]
+
+    def generate_email(self, cr, uid, template_id, res_id, context=None):
+        return self.generate_email_batch(cr, uid, template_id, [res_id], context)[res_id]
+
 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
index 0aab765..bae7a0d 100644 (file)
@@ -62,6 +62,7 @@ class mail_compose_message(osv.TransientModel):
         for wizard in self.browse(cr, uid, ids, context=context):
             if wizard.template_id:
                 wizard_context['mail_notify_user_signature'] = False  # template user_signature is added when generating body_html
+                wizard_context['mail_auto_delete'] = wizard.template_id.auto_delete  # mass mailing: use template auto_delete value -> note, for emails mass mailing only
             if not wizard.attachment_ids or wizard.composition_mode == 'mass_mail' or not wizard.template_id:
                 continue
             new_attachment_ids = []
@@ -81,7 +82,7 @@ class mail_compose_message(osv.TransientModel):
             template_values = self.pool.get('email.template').read(cr, uid, template_id, fields, context)
             values = dict((field, template_values[field]) for field in fields if template_values.get(field))
         elif template_id:
-            values = self.generate_email_for_composer(cr, uid, template_id, res_id, context=context)
+            values = self.generate_email_for_composer_batch(cr, uid, template_id, [res_id], context=context)[res_id]
             # transform attachments into attachment_ids; not attached to the document because this will
             # be done further in the posting process, allowing to clean database if email not send
             values['attachment_ids'] = values.pop('attachment_ids', [])
@@ -147,45 +148,55 @@ class mail_compose_message(osv.TransientModel):
                     partner_ids.append(int(partner_id))
         return partner_ids
 
-    def generate_email_for_composer(self, cr, uid, template_id, res_id, context=None):
+    def generate_email_for_composer_batch(self, cr, uid, template_id, res_ids, context=None):
         """ Call email_template.generate_email(), get fields relevant for
             mail.compose.message, transform email_cc and email_to into partner_ids """
-        template_values = self.pool.get('email.template').generate_email(cr, uid, template_id, res_id, context=context)
         # filter template values
         fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc',  'reply_to', 'attachment_ids', 'attachments', 'mail_server_id']
-        values = dict((field, template_values[field]) for field in fields if template_values.get(field))
-        values['body'] = values.pop('body_html', '')
-
-        # transform email_to, email_cc into partner_ids
-        ctx = dict((k, v) for k, v in (context or {}).items() if not k.startswith('default_'))
-        partner_ids = self._get_or_create_partners_from_values(cr, uid, values, context=ctx)
-        # legacy template behavior: void values do not erase existing values and the
-        # related key is removed from the values dict
-        if partner_ids:
-            values['partner_ids'] = list(partner_ids)
-
+        values = dict.fromkeys(res_ids, False)
+
+        template_values = self.pool.get('email.template').generate_email_batch(cr, uid, template_id, res_ids, context=context)
+        for res_id in res_ids:
+            res_id_values = dict((field, template_values[res_id][field]) for field in fields if template_values[res_id].get(field))
+            res_id_values['body'] = res_id_values.pop('body_html', '')
+
+            # transform email_to, email_cc into partner_ids
+            ctx = dict((k, v) for k, v in (context or {}).items() if not k.startswith('default_'))
+            partner_ids = self._get_or_create_partners_from_values(cr, uid, res_id_values, context=ctx)
+            # legacy template behavior: void values do not erase existing values and the
+            # related key is removed from the values dict
+            if partner_ids:
+                res_id_values['partner_ids'] = list(partner_ids)
+
+            values[res_id] = res_id_values
         return values
 
-    def render_message(self, cr, uid, wizard, res_id, context=None):
+    def render_message_batch(self, cr, uid, wizard, res_ids, context=None):
         """ Override to handle templates. """
-        # generate the composer email
+        # generate template-based values
         if wizard.template_id:
-            values = self.generate_email_for_composer(cr, uid, wizard.template_id.id, res_id, context=context)
+            template_values = self.generate_email_for_composer_batch(cr, uid, wizard.template_id.id, res_ids, context=context)
         else:
-            values = {}
-        # remove attachments as they should not be rendered
-        values.pop('attachment_ids', None)
-        # get values to return
-        email_dict = super(mail_compose_message, self).render_message(cr, uid, wizard, res_id, context)
-        # those values are not managed; they are readonly
-        email_dict.pop('email_to', None)
-        email_dict.pop('email_cc', None)
-        email_dict.pop('partner_to', None)
-        # update template values by wizard values
-        values.update(email_dict)
-        return values
-
-    def render_template(self, cr, uid, template, model, res_id, context=None):
-        return self.pool.get('email.template').render_template(cr, uid, template, model, res_id, context=context)
+            template_values = dict.fromkeys(res_ids, dict())
+        # generate composer values
+        composer_values = super(mail_compose_message, self).render_message_batch(cr, uid, wizard, res_ids, context)
+
+        for res_id in res_ids:
+            # remove attachments from template values as they should not be rendered
+            template_values[res_id].pop('attachment_ids', None)
+            # remove some keys from composer that are readonly
+            composer_values[res_id].pop('email_to', None)
+            composer_values[res_id].pop('email_cc', None)
+            composer_values[res_id].pop('partner_to', None)
+            # update template values by composer values
+            template_values[res_id].update(composer_values[res_id])
+        return template_values
+
+    def render_template_batch(self, cr, uid, template, model, res_ids, context=None):
+        return self.pool.get('email.template').render_template_batch(cr, uid, template, model, res_ids, context=context)
+
+    # Compatibility methods
+    def generate_email_for_composer(self, cr, uid, template_id, res_id, context=None):
+        return self.generate_email_for_composer_batch(cr, uid, template_id, [res_id], context)[res_id]
 
 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
index 6ab1f0e..34c3db4 100644 (file)
@@ -233,7 +233,11 @@ class mail_compose_message(osv.TransientModel):
             email(s), rendering any template patterns on the fly if needed. """
         if context is None:
             context = {}
-        ir_attachment_obj = self.pool.get('ir.attachment')
+        # clean the context (hint: mass mailing sets some default values that
+        # could be wrongly interpreted by mail_mail)
+        context.pop('default_email_to', None)
+        context.pop('default_partner_ids', None)
+
         active_ids = context.get('active_ids')
         is_log = context.get('mail_compose_log', False)
 
@@ -252,43 +256,11 @@ class mail_compose_message(osv.TransientModel):
             else:
                 res_ids = [wizard.res_id]
 
-            for res_id in res_ids:
-                # mail.message values, according to the wizard options
-                post_values = {
-                    'subject': wizard.subject,
-                    'body': wizard.body,
-                    'parent_id': wizard.parent_id and wizard.parent_id.id,
-                    'partner_ids': [partner.id for partner in wizard.partner_ids],
-                    'attachment_ids': [attach.id 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)
-                    post_values['partner_ids'] += email_dict.pop('partner_ids', [])
-                    post_values['attachments'] = email_dict.pop('attachments', [])
-                    attachment_ids = []
-                    for attach_id in post_values.pop('attachment_ids'):
-                        new_attach_id = ir_attachment_obj.copy(cr, uid, attach_id, {'res_model': self._name, 'res_id': wizard.id}, context=context)
-                        attachment_ids.append(new_attach_id)
-                    post_values['attachment_ids'] = attachment_ids
-                    # email_from: mass mailing only can specify another email_from
-                    if email_dict.get('email_from'):
-                        post_values['email_from'] = email_dict.pop('email_from')
-                    # replies redirection: mass mailing only
-                    if not wizard.same_thread:
-                        post_values['reply_to'] = email_dict.pop('reply_to')
-                    else:
-                        email_dict.pop('reply_to')
-                    post_values.update(email_dict)
-                # clean the context (hint: mass mailing sets some default values that
-                # could be wrongly interpreted by mail_mail)
-                context.pop('default_email_to', None)
-                context.pop('default_partner_ids', None)
-                # post the message
+            all_mail_values = self.get_mail_values(cr, uid, wizard, res_ids, context=context)
+
+            for res_id, mail_values in all_mail_values.iteritems():
                 if mass_mail_mode and not wizard.post:
-                    post_values['body_html'] = post_values.get('body', '')
-                    post_values['recipient_ids'] = [(4, id) for id in post_values.pop('partner_ids', [])]
-                    self.pool.get('mail.mail').create(cr, uid, post_values, context=context)
+                    self.pool.get('mail.mail').create(cr, uid, mail_values, context=context)
                 else:
                     subtype = 'mail.mt_comment'
                     if is_log:  # log a note: subtype is False
@@ -297,46 +269,122 @@ class mail_compose_message(osv.TransientModel):
                         if not wizard.notify:
                             subtype = False
                         context = dict(context,
-                                    mail_notify_force_send=False,  # do not send emails directly but use the queue instead
-                                    mail_create_nosubscribe=True)  # add context key to avoid subscribing the author
-                    active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype=subtype, context=context, **post_values)
+                                       mail_notify_force_send=False,  # do not send emails directly but use the queue instead
+                                       mail_create_nosubscribe=True)  # add context key to avoid subscribing the author
+                    active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype=subtype, context=context, **mail_values)
 
         return {'type': 'ir.actions.act_window_close'}
 
-    def render_message(self, cr, uid, wizard, res_id, context=None):
-        """ Generate an email from the template for given (wizard.model, res_id)
-            pair. This method is meant to be inherited by email_template that
-            will produce a more complete dictionary. """
-        return {
-            'subject': self.render_template(cr, uid, wizard.subject, wizard.model, res_id, context),
-            'body': self.render_template(cr, uid, wizard.body, wizard.model, res_id, context),
-            'email_from': self.render_template(cr, uid, wizard.email_from, wizard.model, res_id, context),
-            'reply_to': self.render_template(cr, uid, wizard.reply_to, wizard.model, res_id, context),
-        }
-
-    def render_template(self, cr, uid, template, model, res_id, context=None):
+    def get_mail_values(self, cr, uid, wizard, res_ids, context=None):
+        """Generate the values that will be used by send_mail to create mail_messages
+        or mail_mails. """
+        results = dict.fromkeys(res_ids, False)
+        mass_mail_mode = wizard.composition_mode == 'mass_mail'
+
+        # render all template-based value at once
+        if mass_mail_mode and wizard.model:
+            rendered_values = self.render_message_batch(cr, uid, wizard, res_ids, context=context)
+
+        for res_id in res_ids:
+            # static wizard (mail.message) values
+            mail_values = {
+                'subject': wizard.subject,
+                'body': wizard.body,
+                'parent_id': wizard.parent_id and wizard.parent_id.id,
+                'partner_ids': [partner.id for partner in wizard.partner_ids],
+                'attachment_ids': [attach.id for attach in wizard.attachment_ids],
+            }
+            # mass mailing: rendering override wizard static values
+            if mass_mail_mode and wizard.model:
+                email_dict = rendered_values[res_id]
+                mail_values['partner_ids'] += email_dict.pop('partner_ids', [])
+                mail_values['attachments'] = email_dict.pop('attachments', [])
+                attachment_ids = []
+                for attach_id in mail_values.pop('attachment_ids'):
+                    new_attach_id = self.pool.get('ir.attachment').copy(cr, uid, attach_id, {'res_model': self._name, 'res_id': wizard.id}, context=context)
+                    attachment_ids.append(new_attach_id)
+                mail_values['attachment_ids'] = attachment_ids
+                # email_from: mass mailing only can specify another email_from
+                if email_dict.get('email_from'):
+                    mail_values['email_from'] = email_dict.pop('email_from')
+                # replies redirection: mass mailing only
+                if not wizard.same_thread:
+                    mail_values['reply_to'] = email_dict.pop('reply_to')
+                else:
+                    email_dict.pop('reply_to')
+                mail_values.update(email_dict)
+            # mass mailing without post: mail_mail values
+            if mass_mail_mode and not wizard.post:
+                if 'mail_auto_delete' in context:
+                    mail_values['auto_delete'] = context.get('mail_auto_delete')
+                mail_values['body_html'] = mail_values.get('body', '')
+                mail_values['recipient_ids'] = [(4, id) for id in mail_values.pop('partner_ids', [])]
+            results[res_id] = mail_values
+        return results
+
+    def render_message_batch(self, cr, uid, wizard, res_ids, context=None):
+        """Generate template-based values of wizard, for the document records given
+        by res_ids. This method is meant to be inherited by email_template that
+        will produce a more complete dictionary, using Jinja2 templates.
+
+        Each template is generated for all res_ids, allowing to parse the template
+        once, and render it multiple times. This is useful for mass mailing where
+        template rendering represent a significant part of the process.
+
+        :param browse wizard: current mail.compose.message browse record
+        :param list res_ids: list of record ids
+
+        :return dict results: for each res_id, the generated template values for
+                              subject, body, email_from and reply_to
+        """
+        subjects = self.render_template_batch(cr, uid, wizard.subject, wizard.model, res_ids, context)
+        bodies = self.render_template_batch(cr, uid, wizard.body, wizard.model, res_ids, context)
+        emails_from = self.render_template_batch(cr, uid, wizard.email_from, wizard.model, res_ids, context)
+        replies_to = self.render_template_batch(cr, uid, wizard.reply_to, wizard.model, res_ids, context)
+
+        results = dict.fromkeys(res_ids, False)
+        for res_id in res_ids:
+            results[res_id] = {
+                'subject': subjects[res_id],
+                'body': bodies[res_id],
+                'email_from': emails_from[res_id],
+                'reply_to': replies_to[res_id],
+            }
+        return results
+
+    def render_template_batch(self, cr, uid, template, model, res_ids, context=None):
         """ Render the given template text, replace mako-like expressions ``${expr}``
-            with the result of evaluating these expressions with an evaluation context
-            containing:
+        with the result of evaluating these expressions with an evaluation context
+        containing:
 
-                * ``user``: browse_record of the current user
-                * ``object``: browse_record of the document record this mail is
-                              related to
-                * ``context``: the context passed to the mail composition wizard
+            * ``user``: browse_record of the current user
+            * ``object``: browse_record of the document record this mail is
+                          related to
+            * ``context``: the context passed to the mail composition wizard
 
-            :param str template: the template text to render
-            :param str model: model name of the document record this mail is related to.
-            :param int res_id: id of the document record this mail is related to.
+        :param str template: the template text to render
+        :param str model: model name of the document record this mail is related to
+        :param list res_ids: list of record ids
         """
         if context is None:
             context = {}
-
-        def merge(match):
-            exp = str(match.group()[2:-1]).strip()
-            result = eval(exp, {
-                'user': self.pool.get('res.users').browse(cr, uid, uid, context=context),
-                'object': self.pool[model].browse(cr, uid, res_id, context=context),
-                'context': dict(context),  # copy context to prevent side-effects of eval
+        results = dict.fromkeys(res_ids, False)
+
+        for res_id in res_ids:
+            def merge(match):
+                exp = str(match.group()[2:-1]).strip()
+                result = eval(exp, {
+                    'user': self.pool.get('res.users').browse(cr, uid, uid, context=context),
+                    'object': self.pool[model].browse(cr, uid, res_id, context=context),
+                    '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)
+                return result and tools.ustr(result) or ''
+            results[res_id] = template and EXPRESSION_PATTERN.sub(merge, template)
+        return results
+
+    # Compatibility methods
+    def render_template(self, cr, uid, template, model, res_id, context=None):
+        return self.render_template_batch(cr, uid, template, model, [res_id], context)[res_id]
+
+    def render_message(self, cr, uid, wizard, res_id, context=None):
+        return self.render_message_batch(cr, uid, wizard, [res_id], context)[res_id]