diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py index 2e3405ed079..2e1c52d0ef9 100644 --- a/addons/email_template/email_template.py +++ b/addons/email_template/email_template.py @@ -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 = {} - 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 - except Exception: - _logger.exception("failed to render mako template value %r", template) - return u"" + results = dict.fromkeys(res_ids, u"") - def get_email_template(self, cr, uid, template_id=False, record_id=None, context=None): + # try to load the template + try: + template = mako_template_env.from_string(tools.ustr(template)) + except Exception: + _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. + 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`. + :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) + res_ids_to_templates = self.get_email_template_batch(cr, uid, template_id, res_ids, context) - if values['body_html']: - values['body'] = tools.html_sanitize(values['body_html']) + # 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) - 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) + 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], + ) - 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)) + # 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)) - attachment_ids = [] - # Add template attachments - for attach in template.attachment_ids: - attachment_ids.append(attach.id) + values['attachments'] = attachments - values['attachments'] = attachments - values['attachment_ids'] = attachment_ids - return values + 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: diff --git a/addons/email_template/wizard/mail_compose_message.py b/addons/email_template/wizard/mail_compose_message.py index 0aab765efbf..bae7a0d8f9c 100644 --- a/addons/email_template/wizard/mail_compose_message.py +++ b/addons/email_template/wizard/mail_compose_message.py @@ -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', '') + values = dict.fromkeys(res_ids, False) - # 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) + 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 + 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) - 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) + 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: diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 6ab1f0e81ff..34c3db4de9b 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -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 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' - def render_template(self, cr, uid, template, model, res_id, context=None): + # 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 = {} + results = dict.fromkeys(res_ids, False) - 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 + 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]