_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:
: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
})
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,
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:
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 = []
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', [])
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:
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)
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
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]