[MERGE] mail, various: introduce a composer mixin for invite-based wizards
PURPOSE Improve mail rendering tools and prepare ground for QWeb rendering. SPECIFICATIONS: MODEL FOR RENDERING Currently ``mail.render.mixin`` offers rendering tools, some of them being based on a ``model`` field. It allows to know which model to use to fetch records on which we perform rendering. However this field is not defined at mixin level but in inheriting models without being clearly implemented that way (see ``mail.template`` or ``sms.template`` models). In order to clean this mixin it is now defined at mixin level, using a not stored computed field allowing to define how to find this model. Sub models are updated accordingly. SPECIFICATIONS: COMPOSER MIXIN This merge introduces a new mixin ``mail.composer.mixin`` used when sending emails or notifications based on a mail template. Main current purpose is to hide details related to subject and body computation and rendering based on a mail.template. It also give the base tools to control who is allowed to edit body, notably when dealing with templating language like jinja or qweb. It is meant to evolve in a near future with upcoming support of qweb and fine grain control of rendering access. Use it in survey and slides invite wizards, as well as appraisal and appraisal survey (in enterprise). Use it in mail composer in a minimal way. SPECIFICATIONS: QWeb Temporarily and experimentally support Qweb rendering even if not used functionally, to prepare QWeb templates. LINKS COM PR odoo/odoo#70889 ENT PR odoo/enterprise#18352 UPG PR odoo/upgrade#2490 Related: odoo/upgrade#2496 Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -61,7 +61,7 @@
|
||||
<field name="model_id" ref="account.model_account_payment"/>
|
||||
<field name="subject">${object.company_id.name} Payment Receipt (Ref ${object.name or 'n/a' })</field>
|
||||
<field name="partner_to">${object.partner_id.id}</field>
|
||||
<field name="body_html" type="xml">
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin: 0px; padding: 0px;">
|
||||
<p style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||
Dear ${object.partner_id.name}<br/><br/>
|
||||
|
||||
@@ -187,15 +187,14 @@ class ResUsers(models.Model):
|
||||
template = self.env.ref('auth_signup.reset_password_email')
|
||||
assert template._name == 'mail.template'
|
||||
|
||||
template_values = {
|
||||
email_values = {
|
||||
'email_to': '${object.email|safe}',
|
||||
'email_cc': False,
|
||||
'auto_delete': True,
|
||||
'partner_to': False,
|
||||
'recipient_ids': [],
|
||||
'partner_ids': [],
|
||||
'scheduled_date': False,
|
||||
}
|
||||
if any(template[field] != value for (field, value) in template_values.items()):
|
||||
template.write(template_values)
|
||||
|
||||
for user in self:
|
||||
if not user.email:
|
||||
@@ -203,7 +202,7 @@ class ResUsers(models.Model):
|
||||
# TDE FIXME: make this template technical (qweb)
|
||||
with self.env.cr.savepoint():
|
||||
force_send = not(self.env.context.get('import_file', False))
|
||||
template.send_mail(user.id, force_send=force_send, raise_exception=True)
|
||||
template.send_mail(user.id, force_send=force_send, raise_exception=True, email_values=email_values)
|
||||
_logger.info("Password reset email sent for user <%s> to <%s>", user.login, user.email)
|
||||
|
||||
def send_unregistered_user_reminder(self, after_days=5):
|
||||
|
||||
@@ -121,7 +121,7 @@ class Digest(models.Model):
|
||||
'digest.digest_mail_main',
|
||||
'digest.digest',
|
||||
self.ids,
|
||||
engine='qweb',
|
||||
engine='qweb_view',
|
||||
add_context={
|
||||
'title': self.name,
|
||||
'top_button_label': _('Connect'),
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<field name="subject">New badge ${object.badge_id.name} granted</field>
|
||||
<field name="model_id" ref="gamification.model_gamification_badge_user"/>
|
||||
<field name="partner_to">${object.user_id.partner_id.id}</field>
|
||||
<field name="body_html" type="xml">
|
||||
<field name="body_html" type="html">
|
||||
<table border="0" cellpadding="0" style="padding-top: 16px; background-color: #F1F1F1; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
|
||||
<table border="0" width="590" cellpadding="0" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;" summary="o_mail_notification">
|
||||
<tbody>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
'version': '1.0',
|
||||
'depends': ['sale'],
|
||||
'data': [
|
||||
'data/gift_card_template_email.xml',
|
||||
'data/mail_template_data.xml',
|
||||
'data/gift_card_data.xml',
|
||||
'views/views.xml',
|
||||
'views/templates.xml',
|
||||
|
||||
+1
@@ -33,5 +33,6 @@
|
||||
</span>
|
||||
</div>
|
||||
</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -1,13 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mail_render_mixin
|
||||
from . import mail_composer_mixin
|
||||
|
||||
from . import mail_message_subtype
|
||||
from . import mail_tracking_value
|
||||
from . import mail_alias
|
||||
from . import mail_alias_mixin
|
||||
from . import mail_followers
|
||||
from . import mail_notification
|
||||
from . import mail_render_mixin
|
||||
from . import mail_message
|
||||
from . import mail_activity
|
||||
from . import mail_mail
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class MailComposerMixin(models.AbstractModel):
|
||||
""" Mixin used to edit and render some fields used when sending emails or
|
||||
notifications based on a mail template.
|
||||
|
||||
Main current purpose is to hide details related to subject and body computation
|
||||
and rendering based on a mail.template. It also give the base tools to control
|
||||
who is allowed to edit body, notably when dealing with templating language
|
||||
like jinja or qweb.
|
||||
|
||||
It is meant to evolve in a near future with upcoming support of qweb and fine
|
||||
grain control of rendering access.
|
||||
"""
|
||||
_name = 'mail.composer.mixin'
|
||||
_inherit = 'mail.render.mixin'
|
||||
_description = 'Mail Composer Mixin'
|
||||
|
||||
# Content
|
||||
subject = fields.Char('Subject', compute='_compute_subject', readonly=False, store=True)
|
||||
body = fields.Html('Contents', sanitize_style=True, compute='_compute_body', store=True, readonly=False)
|
||||
template_id = fields.Many2one('mail.template', 'Mail Template', domain="[('model', '=', render_model)]")
|
||||
# Access
|
||||
can_edit_body = fields.Boolean('Can Edit Body', compute='_compute_can_edit_body')
|
||||
|
||||
@api.depends('template_id')
|
||||
def _compute_subject(self):
|
||||
for composer_mixin in self:
|
||||
if composer_mixin.template_id:
|
||||
composer_mixin.subject = composer_mixin.template_id.subject
|
||||
elif not composer_mixin.subject:
|
||||
composer_mixin.subject = False
|
||||
|
||||
@api.depends('template_id')
|
||||
def _compute_body(self):
|
||||
for composer_mixin in self:
|
||||
if composer_mixin.template_id:
|
||||
composer_mixin.body = composer_mixin.template_id.body_html
|
||||
elif not composer_mixin.body:
|
||||
composer_mixin.body = False
|
||||
|
||||
@api.depends('template_id')
|
||||
@api.depends_context('uid')
|
||||
def _compute_can_edit_body(self):
|
||||
self.can_edit_body = True
|
||||
@@ -8,6 +8,7 @@ import logging
|
||||
import re
|
||||
|
||||
import dateutil.relativedelta as relativedelta
|
||||
from lxml import html
|
||||
from markupsafe import Markup
|
||||
from werkzeug import urls
|
||||
|
||||
@@ -37,6 +38,29 @@ def format_time(env, time, tz=False, time_format='medium', lang_code=False):
|
||||
except babel.core.UnknownLocaleError:
|
||||
return time
|
||||
|
||||
def relativedelta_proxy(*args, **kwargs):
|
||||
# dateutil.relativedelta is an old-style class and cannot be directly
|
||||
# instanciated wihtin a jinja2 expression, so a lambda "proxy" is
|
||||
# is needed, apparently
|
||||
return relativedelta.relativedelta(*args, **kwargs)
|
||||
|
||||
template_env_globals = {
|
||||
'str': str,
|
||||
'quote': urls.url_quote,
|
||||
'urlencode': urls.url_encode,
|
||||
'datetime': safe_eval.datetime,
|
||||
'len': len,
|
||||
'abs': abs,
|
||||
'min': min,
|
||||
'max': max,
|
||||
'sum': sum,
|
||||
'filter': filter,
|
||||
'reduce': functools.reduce,
|
||||
'map': map,
|
||||
'relativedelta': relativedelta_proxy,
|
||||
'round': round,
|
||||
}
|
||||
|
||||
try:
|
||||
# We use a jinja2 sandboxed environment to render mako templates.
|
||||
# Note that the rendering does not cover all the mako syntax, in particular
|
||||
@@ -58,26 +82,7 @@ try:
|
||||
trim_blocks=True, # do not output newline after blocks
|
||||
autoescape=True, # XML/HTML automatic escaping
|
||||
)
|
||||
jinja_template_env.globals.update({
|
||||
'str': str,
|
||||
'quote': urls.url_quote,
|
||||
'urlencode': urls.url_encode,
|
||||
'datetime': safe_eval.datetime,
|
||||
'len': len,
|
||||
'abs': abs,
|
||||
'min': min,
|
||||
'max': max,
|
||||
'sum': sum,
|
||||
'filter': filter,
|
||||
'reduce': functools.reduce,
|
||||
'map': map,
|
||||
'round': round,
|
||||
|
||||
# dateutil.relativedelta is an old-style class and cannot be directly
|
||||
# instanciated wihtin a jinja2 expression, so a lambda "proxy" is
|
||||
# is needed, apparently.
|
||||
'relativedelta': lambda *a, **kw : relativedelta.relativedelta(*a, **kw),
|
||||
})
|
||||
jinja_template_env.globals.update(template_env_globals)
|
||||
jinja_safe_template_env = copy.copy(jinja_template_env)
|
||||
jinja_safe_template_env.autoescape = False
|
||||
except ImportError:
|
||||
@@ -94,6 +99,8 @@ class MailRenderMixin(models.AbstractModel):
|
||||
help="Optional translation language (ISO code) to select when sending out an email. "
|
||||
"If not set, the english version will be used. This should usually be a placeholder expression "
|
||||
"that provides the appropriate language, e.g. ${object.partner_id.lang}.")
|
||||
# rendering context
|
||||
render_model = fields.Char("Rendering Model", compute='_compute_render_model', store=False)
|
||||
# expression builder
|
||||
model_object_field = fields.Many2one(
|
||||
'ir.model.fields', string="Field", store=False,
|
||||
@@ -114,6 +121,12 @@ class MailRenderMixin(models.AbstractModel):
|
||||
'Placeholder Expression', store=False,
|
||||
help="Final placeholder expression, to be copy-pasted in the desired template field.")
|
||||
|
||||
def _compute_render_model(self):
|
||||
""" Give the target model for rendering. Void by default as models
|
||||
inheriting from ``mail.render.mixin`` should define how to find this
|
||||
model. """
|
||||
self.render_model = False
|
||||
|
||||
@api.onchange('model_object_field', 'sub_model_object_field', 'null_value')
|
||||
def _onchange_dynamic_placeholder(self):
|
||||
""" Generate the dynamic placeholder """
|
||||
@@ -154,6 +167,14 @@ class MailRenderMixin(models.AbstractModel):
|
||||
expression += "}"
|
||||
return expression
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ORM
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _valid_field_parameter(self, field, name):
|
||||
# allow specifying rendering options directly from field when using the render mixin
|
||||
return name in ['render_engine', 'render_options'] or super()._valid_field_parameter(field, name)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# TOOLS
|
||||
# ------------------------------------------------------------
|
||||
@@ -243,97 +264,150 @@ class MailRenderMixin(models.AbstractModel):
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def _render_qweb_eval_context(self):
|
||||
""" Prepare qweb evaluation context, containing for all rendering
|
||||
def _get_common_eval_context(self):
|
||||
""" Evaluation context used in all rendering engines. Contains
|
||||
|
||||
* ``user``: current user browse record;
|
||||
* ``ctx```: current context;
|
||||
* various formatting tools;
|
||||
"""
|
||||
render_context = {
|
||||
return {
|
||||
'format_date': lambda date, date_format=False, lang_code=False: format_date(self.env, date, date_format, lang_code),
|
||||
'format_datetime': lambda dt, tz=False, dt_format=False, lang_code=False: format_datetime(self.env, dt, tz, dt_format, lang_code),
|
||||
'format_time': lambda time, tz=False, time_format=False, lang_code=False: format_time(self.env, time, tz, time_format, lang_code),
|
||||
'format_amount': lambda amount, currency, lang_code=False: tools.format_amount(self.env, amount, currency, lang_code),
|
||||
'format_duration': lambda value: tools.format_duration(value),
|
||||
'user': self.env.user,
|
||||
'ctx': self._context,
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _render_qweb_eval_context(self):
|
||||
""" Prepare qweb evaluation context, containing common context with
|
||||
some specific values for Qweb. """
|
||||
render_context = self._get_common_eval_context()
|
||||
render_context.update(copy.copy(template_env_globals))
|
||||
return render_context
|
||||
|
||||
@api.model
|
||||
def _render_template_qweb(self, template_src, model, res_ids, add_context=None):
|
||||
""" Render a QWeb template.
|
||||
def _render_template_qweb(self, template_src, model, res_ids,
|
||||
add_context=None, options=None):
|
||||
""" Render a raw QWeb template.
|
||||
|
||||
:param str template_src: source QWeb template. It should be a string
|
||||
XmlID allowing to fetch an ir.ui.view;
|
||||
:param str model: see ``MailRenderMixin._render_field)``;
|
||||
:param list res_ids: see ``MailRenderMixin._render_field)``;
|
||||
:param str template_src: raw QWeb template to render;
|
||||
:param str model: see ``MailRenderMixin._render_template()``;
|
||||
:param list res_ids: see ``MailRenderMixin._render_template()``;
|
||||
|
||||
:param dict add_context: additional context to give to renderer. It
|
||||
allows to add values to base rendering context generated by
|
||||
``MailRenderMixin._render_qweb_eval_context()``;
|
||||
allows to add or update values to base rendering context generated
|
||||
by ``MailRenderMixin._render_qweb_eval_context()``;
|
||||
:param dict options: options for rendering (not used currently);
|
||||
|
||||
:return dict: {res_id: string of rendered template based on record}
|
||||
|
||||
:notice: Experimental. Use at your own risks only.
|
||||
"""
|
||||
view = self.env.ref(template_src, raise_if_not_found=False) or self.env['ir.ui.view']
|
||||
results = dict.fromkeys(res_ids, u"")
|
||||
if not view:
|
||||
return results
|
||||
|
||||
# prepare template variables
|
||||
variables = self._render_qweb_eval_context()
|
||||
if add_context:
|
||||
variables.update(**add_context)
|
||||
|
||||
for record in self.env[model].browse(res_ids):
|
||||
variables['object'] = record
|
||||
try:
|
||||
render_result = self.env['ir.qweb']._render(html.fragment_fromstring(template_src), variables)
|
||||
except Exception as e:
|
||||
_logger.info("Failed to render template : %s", template_src, exc_info=True)
|
||||
raise UserError(_("Failed to render QWeb template : %s)", e))
|
||||
results[record.id] = render_result
|
||||
|
||||
return results
|
||||
|
||||
@api.model
|
||||
def _render_template_qweb_view(self, template_src, model, res_ids,
|
||||
add_context=None, options=None):
|
||||
""" Render a QWeb template based on an ir.ui.view content.
|
||||
|
||||
In addition to the generic evaluation context available, some other
|
||||
variables are added:
|
||||
* ``object``: record based on which the template is rendered;
|
||||
|
||||
:param str template_src: source QWeb template. It should be a string
|
||||
XmlID allowing to fetch an ``ir.ui.view``;
|
||||
:param str model: see ``MailRenderMixin._render_template()``;
|
||||
:param list res_ids: see ``MailRenderMixin._render_template()``;
|
||||
|
||||
:param dict add_context: additional context to give to renderer. It
|
||||
allows to add or update values to base rendering context generated
|
||||
by ``MailRenderMixin._render_qweb_eval_context()``;
|
||||
:param dict options: options for rendering (not used currently);
|
||||
|
||||
:return dict: {res_id: string of rendered template based on record}
|
||||
"""
|
||||
# prevent wrong values (rendering on a void record set, ...)
|
||||
if any(r is None for r in res_ids):
|
||||
raise ValueError(_('Template rendering should be called on a valid record IDs.'))
|
||||
|
||||
view = self.env.ref(template_src, raise_if_not_found=False) or self.env['ir.ui.view']
|
||||
results = dict.fromkeys(res_ids, u"")
|
||||
if not view:
|
||||
return results
|
||||
|
||||
# prepare template variables
|
||||
variables = self._render_qweb_eval_context()
|
||||
if add_context:
|
||||
variables.update(**add_context)
|
||||
safe_eval.check_values(variables)
|
||||
|
||||
for record in self.env[model].browse(res_ids):
|
||||
variables['object'] = record
|
||||
try:
|
||||
render_result = view._render(variables, engine='ir.qweb', minimal_qcontext=True)
|
||||
except Exception as e:
|
||||
_logger.info("Failed to render template : %s (%d)" % (template_src, view.id), exc_info=True)
|
||||
raise UserError(_("Failed to render template : %s (%d)", template_src, view.id))
|
||||
_logger.info("Failed to render template : %s (%d)", template_src, view.id, exc_info=True)
|
||||
raise UserError(_("Failed to render template : %(xml_id)s (%(view_id)d)",
|
||||
xml_id=template_src,
|
||||
view_id=view.id))
|
||||
results[record.id] = render_result
|
||||
|
||||
return results
|
||||
|
||||
@api.model
|
||||
def _render_jinja_eval_context(self):
|
||||
""" Prepare jinja evaluation context, containing for all rendering
|
||||
|
||||
* ``user``: current user browse record;
|
||||
* ``ctx```: current context, named ctx to avoid clash with jinja
|
||||
internals that already uses context;
|
||||
* various formatting tools;
|
||||
"""
|
||||
render_context = {
|
||||
'format_date': lambda date, date_format=False, lang_code=False: format_date(self.env, date, date_format, lang_code),
|
||||
'format_datetime': lambda dt, tz=False, dt_format=False, lang_code=False: format_datetime(self.env, dt, tz, dt_format, lang_code),
|
||||
'format_time': lambda time, tz=False, time_format=False, lang_code=False: format_time(self.env, time, tz, time_format, lang_code),
|
||||
'format_amount': lambda amount, currency, lang_code=False: tools.format_amount(self.env, amount, currency, lang_code),
|
||||
'format_duration': lambda value: tools.format_duration(value),
|
||||
'user': self.env.user,
|
||||
'ctx': self._context,
|
||||
}
|
||||
return render_context
|
||||
return self._get_common_eval_context()
|
||||
|
||||
@api.model
|
||||
def _render_template_jinja(self, template_txt, model, res_ids, add_context=None):
|
||||
def _render_template_jinja(self, template_txt, model, res_ids,
|
||||
add_context=None, options=None):
|
||||
""" Render a string-based template on records given by a model and a list
|
||||
of IDs, using jinja.
|
||||
|
||||
In addition to the generic evaluation context given by _render_jinja_eval_context
|
||||
some new variables are added, depending on each record
|
||||
|
||||
In addition to the generic evaluation context available, some other
|
||||
variables are added:
|
||||
* ``object``: record based on which the template is rendered;
|
||||
|
||||
:param str template_txt: template text to render
|
||||
:param str model: model name of records on which we want to perform rendering
|
||||
:param list res_ids: list of ids of records (all belonging to same model)
|
||||
:param str model: see ``MailRenderMixin._render_template()``;
|
||||
:param list res_ids: see ``MailRenderMixin._render_template()``;
|
||||
|
||||
:param dict add_context: additional context to give to renderer. It
|
||||
allows to add or update values to base rendering context generated
|
||||
by ``MailRenderMixin._render_jinja_eval_context()``;
|
||||
:param dict options: options for rendering;
|
||||
|
||||
:return dict: {res_id: string of rendered template based on record}
|
||||
"""
|
||||
# TDE FIXME: remove that brol (6dde919bb9850912f618b561cd2141bffe41340c)
|
||||
no_autoescape = self._context.get('safe')
|
||||
# prevent wrong values (rendering on a void record set, ...)
|
||||
if any(r is None for r in res_ids):
|
||||
raise ValueError(_('Template rendering should be called on a valid record IDs.'))
|
||||
|
||||
# TDE note: support 'safe' context key as backward compatibility for 6dde919bb9850912f618b561cd2141bffe41340c
|
||||
if options is None:
|
||||
options = {}
|
||||
no_autoescape = options.get('render_safe') or self._context.get('safe')
|
||||
|
||||
results = dict.fromkeys(res_ids, u"")
|
||||
if not template_txt:
|
||||
return results
|
||||
@@ -352,17 +426,13 @@ class MailRenderMixin(models.AbstractModel):
|
||||
variables.update(**add_context)
|
||||
safe_eval.check_values(variables)
|
||||
|
||||
# TDE CHECKME
|
||||
# records = self.env[model].browse(it for it in res_ids if it) # filter to avoid browsing [None]
|
||||
if any(r is None for r in res_ids):
|
||||
raise ValueError(_('Unsuspected None'))
|
||||
|
||||
for record in self.env[model].browse(res_ids):
|
||||
variables['object'] = record
|
||||
try:
|
||||
render_result = template.render(variables)
|
||||
except Exception as e:
|
||||
_logger.info("Failed to render template : %s" % e, exc_info=True)
|
||||
_logger.info("Failed to render template : %s", e, exc_info=True)
|
||||
raise UserError(_("Failed to render template : %s", e))
|
||||
if render_result == u"False":
|
||||
render_result = u""
|
||||
@@ -374,50 +444,66 @@ class MailRenderMixin(models.AbstractModel):
|
||||
def _render_template_postprocess(self, rendered):
|
||||
""" Tool method for post processing. In this method we ensure local
|
||||
links ('/shop/Basil-1') are replaced by global links ('https://www.
|
||||
mygardin.com/hop/Basil-1').
|
||||
mygarden.com/shop/Basil-1').
|
||||
|
||||
:param rendered: result of ``_render_template``
|
||||
:param rendered: result of ``_render_template``;
|
||||
|
||||
:return dict: updated version of rendered
|
||||
:return dict: updated version of rendered per record ID;
|
||||
"""
|
||||
for res_id, html in rendered.items():
|
||||
rendered[res_id] = self._replace_local_links(html)
|
||||
for res_id, rendered_html in rendered.items():
|
||||
rendered[res_id] = self._replace_local_links(rendered_html)
|
||||
return rendered
|
||||
|
||||
@api.model
|
||||
def _render_template(self, template_src, model, res_ids, engine='jinja', add_context=None, post_process=False):
|
||||
def _render_template(self, template_src, model, res_ids, engine='jinja',
|
||||
add_context=None, options=None, post_process=False):
|
||||
""" Render the given string on records designed by model / res_ids using
|
||||
the given rendering engine. Currently only jinja or qweb are supported.
|
||||
|
||||
:param str template_src: template text to render (jinja) or xml id of view (qweb)
|
||||
this could be cleaned but hey, we are in a rush
|
||||
:param str model: model name of records on which we want to perform rendering
|
||||
:param list res_ids: list of ids of records (all belonging to same model)
|
||||
:param string engine: jinja
|
||||
:param post_process: see ``MailRenderMixin._render_field``;
|
||||
:param str template_src: template text to render (jinja) or xml id of
|
||||
view (qweb);
|
||||
:param str model: model name of records on which we want to perform
|
||||
rendering (aka 'crm.lead');
|
||||
:param list res_ids: list of ids of records. All should belong to the
|
||||
Odoo model given by model;
|
||||
:param string engine: jinja or qweb_view;
|
||||
|
||||
:param dict add_context: additional context to give to renderer. It
|
||||
allows to add or update values to base rendering context generated
|
||||
by ``MailRenderMixin._render_<engine>_eval_context()``;
|
||||
:param dict options: options for rendering;
|
||||
:param boolean post_process: perform a post processing on rendered result
|
||||
(notably html links management). See``_render_template_postprocess``;
|
||||
|
||||
:return dict: {res_id: string of rendered template based on record}
|
||||
"""
|
||||
if not isinstance(res_ids, (list, tuple)):
|
||||
raise ValueError(_('Template rendering should be called only using on a list of IDs.'))
|
||||
if engine not in ('jinja', 'qweb'):
|
||||
raise ValueError(_('Template rendering supports only jinja or qweb.'))
|
||||
if engine not in ('jinja', 'qweb', 'qweb_view'):
|
||||
raise ValueError(_('Template rendering supports only raw jinja or qweb (view or raw).'))
|
||||
|
||||
if engine == 'qweb':
|
||||
rendered = self._render_template_qweb(template_src, model, res_ids, add_context=add_context)
|
||||
if engine == 'qweb_view':
|
||||
rendered = self._render_template_qweb_view(template_src, model, res_ids,
|
||||
add_context=add_context, options=options)
|
||||
elif engine == 'qweb':
|
||||
rendered = self._render_template_qweb(template_src, model, res_ids,
|
||||
add_context=add_context, options=options)
|
||||
else:
|
||||
rendered = self._render_template_jinja(template_src, model, res_ids, add_context=add_context)
|
||||
rendered = self._render_template_jinja(template_src, model, res_ids,
|
||||
add_context=add_context, options=options)
|
||||
if post_process:
|
||||
rendered = self._render_template_postprocess(rendered)
|
||||
|
||||
return rendered
|
||||
|
||||
def _render_lang(self, res_ids):
|
||||
def _render_lang(self, res_ids, engine='jinja'):
|
||||
""" Given some record ids, return the lang for each record based on
|
||||
lang field of template or through specific context-based key.
|
||||
lang field of template or through specific context-based key. Lang is
|
||||
computed by performing a rendering on res_ids, based on self.render_model.
|
||||
|
||||
:param list res_ids: list of ids of records (all belonging to same model
|
||||
defined by self.model)
|
||||
:param list res_ids: list of ids of records. All should belong to the
|
||||
Odoo model given by model;
|
||||
:param string engine: jinja or qweb_view;
|
||||
|
||||
:return dict: {res_id: lang code (i.e. en_US)}
|
||||
"""
|
||||
@@ -425,27 +511,30 @@ class MailRenderMixin(models.AbstractModel):
|
||||
if not isinstance(res_ids, (list, tuple)):
|
||||
raise ValueError(_('Template rendering for language should be called with a list of IDs.'))
|
||||
|
||||
if self.env.context.get('template_preview_lang'):
|
||||
return dict((res_id, self.env.context['template_preview_lang']) for res_id in res_ids)
|
||||
else:
|
||||
rendered_langs = self._render_template(self.lang, self.model, res_ids)
|
||||
return dict((res_id, lang)
|
||||
for res_id, lang in rendered_langs.items())
|
||||
rendered_langs = self._render_template(self.lang, self.render_model, res_ids, engine=engine)
|
||||
return dict(
|
||||
(res_id, lang)
|
||||
for res_id, lang in rendered_langs.items()
|
||||
)
|
||||
|
||||
def _classify_per_lang(self, res_ids):
|
||||
def _classify_per_lang(self, res_ids, engine='jinja'):
|
||||
""" Given some record ids, return for computed each lang a contextualized
|
||||
template and its subset of res_ids.
|
||||
|
||||
:param list res_ids: list of ids of records (all belonging to same model
|
||||
defined by self.model)
|
||||
defined by self.render_model)
|
||||
:param string engine: jinja or qweb_view;
|
||||
|
||||
:return dict: {lang: (template with lang=lang_code if specific lang computed
|
||||
or template, res_ids targeted by that language}
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
if self.env.context.get('template_preview_lang'):
|
||||
return dict((res_id, self.env.context['template_preview_lang']) for res_id in res_ids)
|
||||
|
||||
lang_to_res_ids = {}
|
||||
for res_id, lang in self._render_lang(res_ids).items():
|
||||
for res_id, lang in self._render_lang(res_ids, engine=engine).items():
|
||||
lang_to_res_ids.setdefault(lang, []).append(res_id)
|
||||
|
||||
return dict(
|
||||
@@ -453,16 +542,18 @@ class MailRenderMixin(models.AbstractModel):
|
||||
for lang, lang_res_ids in lang_to_res_ids.items()
|
||||
)
|
||||
|
||||
def _render_field(self, field, res_ids,
|
||||
def _render_field(self, field, res_ids, engine='jinja',
|
||||
compute_lang=False, set_lang=False,
|
||||
post_process=False):
|
||||
add_context=None, options=None, post_process=False):
|
||||
""" Given some record ids, render a template located on field on all
|
||||
records. ``field`` should be a field of self (i.e. ``body_html`` on
|
||||
``mail.template``). res_ids are record IDs linked to ``model`` field
|
||||
on self.
|
||||
|
||||
:param field: a field name existing on self;
|
||||
:param list res_ids: list of ids of records (all belonging to same model
|
||||
defined by ``self.model``)
|
||||
defined by ``self.render_model``)
|
||||
:param string engine: jinja or qweb_view;
|
||||
|
||||
:param boolean compute_lang: compute language to render on translated
|
||||
version of the template instead of default (probably english) one.
|
||||
@@ -470,11 +561,16 @@ class MailRenderMixin(models.AbstractModel):
|
||||
:param string set_lang: force language for rendering. It should be a
|
||||
valid lang code matching an activate res.lang. Checked only if
|
||||
``compute_lang`` is False;
|
||||
:param dict add_context: additional context to give to renderer;
|
||||
:param dict options: options for rendering;
|
||||
:param boolean post_process: perform a post processing on rendered result
|
||||
(notably html links management). See``_render_template_postprocess``);
|
||||
|
||||
:return dict: {res_id: string of rendered template based on record}
|
||||
"""
|
||||
if options is None:
|
||||
options = {}
|
||||
|
||||
self.ensure_one()
|
||||
if compute_lang:
|
||||
templates_res_ids = self._classify_per_lang(res_ids)
|
||||
@@ -483,11 +579,16 @@ class MailRenderMixin(models.AbstractModel):
|
||||
else:
|
||||
templates_res_ids = {self._context.get('lang'): (self, res_ids)}
|
||||
|
||||
# rendering options
|
||||
engine = getattr(self._fields[field], 'render_engine', engine)
|
||||
options.update(**getattr(self._fields[field], 'render_options', {}))
|
||||
post_process = options.get('post_process') or post_process
|
||||
|
||||
return dict(
|
||||
(res_id, rendered)
|
||||
for lang, (template, tpl_res_ids) in templates_res_ids.items()
|
||||
for res_id, rendered in template._render_template(
|
||||
template[field], template.model, tpl_res_ids,
|
||||
post_process=post_process
|
||||
template[field], template.render_model, tpl_res_ids, engine=engine,
|
||||
add_context=add_context, options=options, post_process=post_process
|
||||
).items()
|
||||
)
|
||||
|
||||
@@ -67,6 +67,16 @@ class MailTemplate(models.Model):
|
||||
help="Sidebar action to make this template available on records "
|
||||
"of the related document model")
|
||||
|
||||
# Overrides of mail.render.mixin
|
||||
@api.depends('model')
|
||||
def _compute_render_model(self):
|
||||
for template in self:
|
||||
template.render_model = template.model
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# CRUD
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def unlink(self):
|
||||
self.unlink_action()
|
||||
return super(MailTemplate, self).unlink()
|
||||
@@ -163,9 +173,9 @@ class MailTemplate(models.Model):
|
||||
results = dict()
|
||||
for lang, (template, template_res_ids) in self._classify_per_lang(res_ids).items():
|
||||
for field in fields:
|
||||
template = template.with_context(safe=(field == 'subject'))
|
||||
generated_field_values = template._render_field(
|
||||
field, template_res_ids,
|
||||
options={'render_safe': field == 'subject'},
|
||||
post_process=(field == 'body_html')
|
||||
)
|
||||
for res_id, field_value in generated_field_values.items():
|
||||
|
||||
@@ -52,20 +52,20 @@ class TestMailRender(common.MailCommon):
|
||||
</p>"""
|
||||
]
|
||||
|
||||
# some qweb templates and their xml ids
|
||||
cls.base_qweb_templates = cls.env['ir.ui.view'].create([
|
||||
{'name': 'TestRender', 'type': 'qweb',
|
||||
'arch': '<p>Hello</p>',
|
||||
},
|
||||
{'name': 'TestRender2', 'type': 'qweb',
|
||||
'arch': '<p>Hello <t t-esc="object.name"/></p>',
|
||||
},
|
||||
{'name': 'TestRender3', 'type': 'qweb',
|
||||
'arch': """<p>
|
||||
# some qweb templates, their views and their xml ids
|
||||
cls.base_qweb_bits = [
|
||||
'<p>Hello</p>',
|
||||
'<p>Hello <t t-esc="object.name"/></p>',
|
||||
"""<p>
|
||||
<span t-if="object.lang == 'en_US'">English Speaker</span>
|
||||
<span t-else="">Other Speaker</span>
|
||||
</p>""",
|
||||
},
|
||||
</p>"""
|
||||
]
|
||||
cls.base_qweb_templates = cls.env['ir.ui.view'].create([
|
||||
{'name': 'TestRender%d' % index,
|
||||
'type': 'qweb',
|
||||
'arch': qweb_content,
|
||||
} for index, qweb_content in enumerate(cls.base_qweb_bits)
|
||||
])
|
||||
cls.base_qweb_templates_data = cls.env['ir.model.data'].create([
|
||||
{'name': template.name, 'module': 'mail',
|
||||
@@ -127,23 +127,32 @@ class TestMailRender(common.MailCommon):
|
||||
})
|
||||
|
||||
@users('employee')
|
||||
def test_render_jinja(self):
|
||||
source = """<p>
|
||||
% set line_statement_variable = 3
|
||||
<span>We have ${line_statement_variable} cookies in stock</span>
|
||||
<span>We have <% set block_variable = 4 %>${block_variable} cookies in stock</span>
|
||||
</p>"""
|
||||
def test_evaluation_context(self):
|
||||
""" Test evaluation context and various ways of tweaking it. """
|
||||
partner = self.env['res.partner'].browse(self.render_object.ids)
|
||||
result = self.env['mail.render.mixin']._render_template(
|
||||
source,
|
||||
partner._name,
|
||||
partner.ids,
|
||||
engine='jinja',
|
||||
)[partner.id]
|
||||
self.assertEqual(result, """<p>
|
||||
<span>We have 3 cookies in stock</span>
|
||||
<span>We have 4 cookies in stock</span>
|
||||
</p>""")
|
||||
MailRenderMixin = self.env['mail.render.mixin']
|
||||
|
||||
custom_ctx = {'custom_ctx': 'Custom Context Value'}
|
||||
add_context = {
|
||||
'custom_value': 'Custom Render Value'
|
||||
}
|
||||
srces = [
|
||||
'<b>I am ${user.name}</b>',
|
||||
'<span>Datetime is ${format_datetime(datetime.datetime(2021, 6, 1), dt_format="MM - d - YYY")}</span>',
|
||||
'<span>Context ${ctx.get("custom_ctx")}, value ${custom_value}</span>',
|
||||
]
|
||||
results = [
|
||||
'<b>I am %s</b>' % self.env.user.name,
|
||||
'<span>Datetime is 06 - 1 - 2021</span>',
|
||||
'<span>Context Custom Context Value, value Custom Render Value</span>'
|
||||
]
|
||||
for src, expected in zip(srces, results):
|
||||
for engine in ['jinja']:
|
||||
result = MailRenderMixin.with_context(**custom_ctx)._render_template(
|
||||
src, partner._name, partner.ids,
|
||||
engine=engine, add_context=add_context
|
||||
)[partner.id]
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
@users('employee')
|
||||
def test_render_mail_template_jinja(self):
|
||||
@@ -179,13 +188,134 @@ class TestMailRender(common.MailCommon):
|
||||
self.assertEqual(rendered, expected)
|
||||
|
||||
@users('employee')
|
||||
def test_render_template_qweb(self):
|
||||
def test_render_template_qweb_view(self):
|
||||
partner = self.env['res.partner'].browse(self.render_object.ids)
|
||||
for source, expected in zip(self.base_qweb_templates_xmlids, self.base_rendered):
|
||||
rendered = self.env['mail.render.mixin']._render_template(
|
||||
source,
|
||||
partner._name,
|
||||
partner.ids,
|
||||
engine='qweb',
|
||||
engine='qweb_view',
|
||||
)[partner.id].decode()
|
||||
self.assertEqual(rendered, expected)
|
||||
|
||||
@users('employee')
|
||||
def test_template_rendering_impersonate(self):
|
||||
""" Test that the use of SUDO do not change the current user. """
|
||||
partner = self.env['res.partner'].browse(self.render_object.ids)
|
||||
src = '${user.name} - ${object.name}'
|
||||
expected = '%s - %s' % (self.env.user.name, partner.name)
|
||||
result = self.env['mail.render.mixin'].sudo()._render_template_jinja(
|
||||
src, partner._name, partner.ids
|
||||
)[partner.id]
|
||||
self.assertIn(expected, result)
|
||||
|
||||
@users('employee')
|
||||
def test_template_rendering_function_call(self):
|
||||
"""Test the case when the template call a custom function.
|
||||
This function should not be called when the template is not rendered.
|
||||
"""
|
||||
partner = self.env['res.partner'].browse(self.render_object.ids)
|
||||
|
||||
def cust_function():
|
||||
# Can not use "MagicMock" in a Jinja sand-boxed environment
|
||||
# so create our own function
|
||||
cust_function.call = True
|
||||
return 'return value'
|
||||
|
||||
cust_function.call = False
|
||||
|
||||
src = """<h1>This is a test</h1>
|
||||
<p>${cust_function()}</p>"""
|
||||
expected = """<h1>This is a test</h1>
|
||||
<p>return value</p>"""
|
||||
context = {'cust_function': cust_function}
|
||||
|
||||
result = self.env['mail.render.mixin']._render_template_jinja(
|
||||
src, partner._name, partner.ids,
|
||||
add_context=context
|
||||
)[partner.id]
|
||||
self.assertEqual(expected, result)
|
||||
self.assertTrue(cust_function.call)
|
||||
|
||||
@users('employee')
|
||||
def test_template_rendering_various(self):
|
||||
""" Test static rendering """
|
||||
partner = self.env['res.partner'].browse(self.render_object.ids)
|
||||
MailRenderMixin = self.env['mail.render.mixin']
|
||||
|
||||
# static string
|
||||
src = 'This is a string'
|
||||
expected = 'This is a string'
|
||||
for engine in ['jinja']:
|
||||
result = MailRenderMixin._render_template(
|
||||
src, partner._name, partner.ids, engine=engine,
|
||||
)[partner.id]
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
# code string
|
||||
src = 'This is a string with a number ${13+13}'
|
||||
expected = 'This is a string with a number 26'
|
||||
for engine in ['jinja']:
|
||||
result = MailRenderMixin._render_template(
|
||||
src, partner._name, partner.ids, engine=engine,
|
||||
)[partner.id]
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
# block string
|
||||
src = "This is a string with a block ${'hidden' if False else 'displayed'}"
|
||||
expected = 'This is a string with a block displayed'
|
||||
for engine in ['jinja']:
|
||||
result = MailRenderMixin._render_template(
|
||||
src, partner._name, partner.ids, engine=engine,
|
||||
)[partner.id]
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
# static xml
|
||||
src = '<p class="text-muted"><span>This is a string</span></p>'
|
||||
expected = '<p class="text-muted"><span>This is a string</span></p>'
|
||||
for engine in ['jinja', 'qweb']:
|
||||
result = MailRenderMixin._render_template(
|
||||
src, partner._name, partner.ids, engine=engine,
|
||||
)[partner.id]
|
||||
self.assertEqual(expected, result if engine == 'jinja' else result.decode()) # tde: checkme
|
||||
|
||||
# code xml
|
||||
srces = [
|
||||
'<p class="text-muted"><span>This is a string with a number ${13+13}</span></p>',
|
||||
'<p class="text-muted"><span>This is a string with a number <t t-out="13+13"/></span></p>',
|
||||
]
|
||||
expected = '<p class="text-muted"><span>This is a string with a number 26</span></p>'
|
||||
for engine, src in zip(['jinja', 'qweb'], srces):
|
||||
result = MailRenderMixin._render_template(
|
||||
src, partner._name, partner.ids, engine=engine,
|
||||
)[partner.id]
|
||||
self.assertEqual(expected, result if engine == 'jinja' else result.decode()) # tde: checkme
|
||||
|
||||
# condition block xml
|
||||
src = """<b>Test</b>
|
||||
% if False:
|
||||
<b>Code not executed</b>
|
||||
% endif"""
|
||||
expected = "<b>Test</b>\n" # TDE: to check
|
||||
for engine in ['jinja']:
|
||||
result = MailRenderMixin._render_template(
|
||||
src, partner._name, partner.ids, engine=engine,
|
||||
)[partner.id]
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
# complete block xml
|
||||
src = """<p>
|
||||
% set line_statement_variable = 3
|
||||
<span>We have ${line_statement_variable} cookies in stock</span>
|
||||
<span>We have <% set block_variable = 4 %>${block_variable} cookies in stock</span>
|
||||
</p>"""
|
||||
expected = """<p>
|
||||
<span>We have 3 cookies in stock</span>
|
||||
<span>We have 4 cookies in stock</span>
|
||||
</p>"""
|
||||
for engine in ['jinja']:
|
||||
result = MailRenderMixin._render_template(
|
||||
src, partner._name, partner.ids, engine=engine,
|
||||
)[partner.id]
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
@@ -37,6 +37,7 @@ class MailComposer(models.TransientModel):
|
||||
before being sent to each recipient.
|
||||
"""
|
||||
_name = 'mail.compose.message'
|
||||
_inherit = 'mail.composer.mixin'
|
||||
_description = 'Email composition wizard'
|
||||
_log_access = True
|
||||
_batch_size = 500
|
||||
@@ -81,8 +82,8 @@ class MailComposer(models.TransientModel):
|
||||
return filtered_result
|
||||
|
||||
# content
|
||||
subject = fields.Char('Subject')
|
||||
body = fields.Html('Contents', default='', sanitize_style=True)
|
||||
subject = fields.Char('Subject', compute=False)
|
||||
body = fields.Html('Contents', compute=False, default='', sanitize_style=True)
|
||||
parent_id = fields.Many2one(
|
||||
'mail.message', 'Parent Message', index=True, ondelete='set null',
|
||||
help="Initial thread message.")
|
||||
@@ -154,6 +155,12 @@ class MailComposer(models.TransientModel):
|
||||
for composer in self:
|
||||
composer.reply_to_force_new = composer.reply_to_mode == 'new'
|
||||
|
||||
# Overrides of mail.render.mixin
|
||||
@api.depends('model')
|
||||
def _compute_render_model(self):
|
||||
for composer in self:
|
||||
composer.render_model = composer.model
|
||||
|
||||
@api.model
|
||||
def get_record_data(self, values):
|
||||
""" Returns a defaults-like dict with initial values for the composition
|
||||
@@ -469,10 +476,10 @@ class MailComposer(models.TransientModel):
|
||||
multi_mode = False
|
||||
res_ids = [res_ids]
|
||||
|
||||
subjects = self.env['mail.render.mixin']._render_template(self.subject, self.model, res_ids)
|
||||
bodies = self.env['mail.render.mixin']._render_template(self.body, self.model, res_ids, post_process=True)
|
||||
emails_from = self.env['mail.render.mixin']._render_template(self.email_from, self.model, res_ids)
|
||||
replies_to = self.env['mail.render.mixin']._render_template(self.reply_to, self.model, res_ids)
|
||||
subjects = self._render_field('subject', res_ids, options={"render_safe": True})
|
||||
bodies = self._render_field('body', res_ids, post_process=True)
|
||||
emails_from = self._render_field('email_from', res_ids)
|
||||
replies_to = self._render_field('reply_to', res_ids)
|
||||
default_recipients = {}
|
||||
if not self.partner_ids:
|
||||
records = self.env[self.model].browse(res_ids).sudo()
|
||||
|
||||
@@ -113,7 +113,7 @@ class MassMailing(models.Model):
|
||||
string='Reply To', compute='_compute_reply_to', readonly=False, store=True,
|
||||
help='Preferred Reply-To Address')
|
||||
# recipients
|
||||
mailing_model_real = fields.Char(string='Recipients Real Model', compute='_compute_model')
|
||||
mailing_model_real = fields.Char(string='Recipients Real Model', compute='_compute_mailing_model_real')
|
||||
mailing_model_id = fields.Many2one(
|
||||
'ir.model', string='Recipients Model', ondelete='cascade', required=True,
|
||||
domain=[('is_mailing_enabled', '=', True)],
|
||||
@@ -239,9 +239,9 @@ class MassMailing(models.Model):
|
||||
mailing.medium_id = self.env.ref('utm.utm_medium_email').id
|
||||
|
||||
@api.depends('mailing_model_id')
|
||||
def _compute_model(self):
|
||||
for record in self:
|
||||
record.mailing_model_real = (record.mailing_model_name != 'mailing.list') and record.mailing_model_name or 'mailing.contact'
|
||||
def _compute_mailing_model_real(self):
|
||||
for mailing in self:
|
||||
mailing.mailing_model_real = (mailing.mailing_model_name != 'mailing.list') and mailing.mailing_model_name or 'mailing.contact'
|
||||
|
||||
@api.depends('mailing_model_real')
|
||||
def _compute_reply_to_mode(self):
|
||||
@@ -293,6 +293,12 @@ class MassMailing(models.Model):
|
||||
def _compute_mail_server_available(self):
|
||||
self.mail_server_available = self.env['ir.config_parameter'].sudo().get_param('mass_mailing.outgoing_mail_server')
|
||||
|
||||
# Overrides of mail.render.mixin
|
||||
@api.depends('mailing_model_real')
|
||||
def _compute_render_model(self):
|
||||
for mailing in self:
|
||||
mailing.render_model = mailing.mailing_model_real
|
||||
|
||||
# ------------------------------------------------------
|
||||
# ORM
|
||||
# ------------------------------------------------------
|
||||
|
||||
@@ -32,19 +32,22 @@ class TestMassMailing(models.TransientModel):
|
||||
mass_mail_layout = self.env.ref('mass_mailing.mass_mailing_mail_layout')
|
||||
|
||||
record = self.env[mailing.mailing_model_real].search([], limit=1)
|
||||
body = mailing._prepend_preview(mailing.body_html, mailing.preview)
|
||||
subject = mailing.subject
|
||||
|
||||
# If there is atleast 1 record for the model used in this mailing, then we use this one to render the template
|
||||
# Downside: Jinja syntax is only tested when there is atleast one record of the mailing's model
|
||||
if record:
|
||||
# Returns a proper error if there is a syntax error with jinja
|
||||
body = self.env['mail.render.mixin']._render_template(body, mailing.mailing_model_real, record.ids, post_process=True)[record.id]
|
||||
subject = self.env['mail.render.mixin']._render_template(subject, mailing.mailing_model_real, record.ids)[record.id]
|
||||
body = mailing._render_field('body_html', record.ids, post_process=True)[record.id]
|
||||
preview = mailing._render_field('preview', record.ids, post_process=True)[record.id]
|
||||
full_body = mailing._prepend_preview(body, preview)
|
||||
subject = mailing._render_field('subject', record.ids)[record.id]
|
||||
else:
|
||||
full_body = mailing._prepend_preview(mailing.body_html, mailing.preview)
|
||||
subject = mailing.subject
|
||||
|
||||
# Convert links in absolute URLs before the application of the shortener
|
||||
body = self.env['mail.render.mixin']._replace_local_links(body)
|
||||
body = tools.html_sanitize(body, sanitize_attributes=True, sanitize_style=True)
|
||||
full_body = self.env['mail.render.mixin']._replace_local_links(full_body)
|
||||
full_body = tools.html_sanitize(full_body, sanitize_attributes=True, sanitize_style=True)
|
||||
|
||||
for valid_email in valid_emails:
|
||||
mail_values = {
|
||||
@@ -52,7 +55,7 @@ class TestMassMailing(models.TransientModel):
|
||||
'reply_to': mailing.reply_to,
|
||||
'email_to': valid_email,
|
||||
'subject': subject,
|
||||
'body_html': mass_mail_layout._render({'body': body}, engine='ir.qweb', minimal_qcontext=True),
|
||||
'body_html': mass_mail_layout._render({'body': full_body}, engine='ir.qweb', minimal_qcontext=True),
|
||||
'notification': True,
|
||||
'mailing_id': mailing.id,
|
||||
'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<field name="subject">${object.partner_id.name} Repair Orders (Ref ${object.name or 'n/a' })</field>
|
||||
<field name="email_from">${(object.create_uid.email_formatted or user.email_formatted) | safe}</field>
|
||||
<field name="partner_to">${object.partner_id.id}</field>
|
||||
<field name="body_html" type="xml">
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin: 0px; padding: 0px;">
|
||||
<p style="margin: 0px; padding: 0px;font-size: 13px;">
|
||||
Hello ${object.partner_id.name},<br/>
|
||||
|
||||
@@ -29,6 +29,16 @@ class SMSTemplate(models.Model):
|
||||
help="Sidebar action to make this template available on records "
|
||||
"of the related document model")
|
||||
|
||||
# Overrides of mail.render.mixin
|
||||
@api.depends('model')
|
||||
def _compute_render_model(self):
|
||||
for template in self:
|
||||
template.render_model = template.model
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# CRUD
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@api.returns('self', lambda value: value.id)
|
||||
def copy(self, default=None):
|
||||
default = dict(default or {},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
'name': 'Surveys',
|
||||
'version': '3.3',
|
||||
'version': '3.4',
|
||||
'category': 'Marketing/Surveys',
|
||||
'description': """
|
||||
Create beautiful surveys and visualize answers
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<record id="mail_template_user_input_invite" model="mail.template">
|
||||
<field name="name">Survey: Invite</field>
|
||||
<field name="model_id" ref="model_survey_user_input" />
|
||||
<field name="subject">Participate to ${object.survey_id.title} survey</field>
|
||||
<field name="subject">Participate to ${object.survey_id.display_name} survey</field>
|
||||
<field name="email_to">${(object.partner_id.email_formatted or object.email) |safe}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||
@@ -43,31 +43,31 @@
|
||||
<field name="subject">Certification: ${object.survey_id.display_name}</field>
|
||||
<field name="email_from">${(object.survey_id.create_uid.email_formatted or user.email_formatted or user.company_id.catchall_formatted) |safe}</field>
|
||||
<field name="email_to">${(object.partner_id.email_formatted or object.email) |safe}</field>
|
||||
<field name="body_html" type="xml">
|
||||
<div style="background:#F0F0F0;color:#515166;padding:10px 0px;font-family:Arial,Helvetica,sans-serif;font-size:14px;">
|
||||
<table style="width:600px;margin:5px auto;">
|
||||
<tbody>
|
||||
<tr><td>
|
||||
<!-- We use the logo of the company that created the survey (to handle multi company cases) -->
|
||||
<a href="/"><img src="/logo.png?company=${object.survey_id.create_uid.company_id.id}" style="vertical-align:baseline;max-width:100px;" /></a>
|
||||
</td><td style="text-align:right;vertical-align:middle;">
|
||||
Certification: ${object.survey_id.display_name}
|
||||
</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table style="width:600px;margin:0px auto;background:white;border:1px solid #e1e1e1;">
|
||||
<tbody>
|
||||
<tr><td style="padding:15px 20px 10px 20px;">
|
||||
<p>Dear <span>${object.partner_id.name or 'participant'}</span></p>
|
||||
<p>
|
||||
Here is, in attachment, your certification document for
|
||||
<strong>${object.survey_id.display_name}</strong>
|
||||
</p>
|
||||
<p>Congratulations for succeeding the test!</p>
|
||||
</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<field name="body_html" type="html">
|
||||
<div style="background:#F0F0F0;color:#515166;padding:10px 0px;font-family:Arial,Helvetica,sans-serif;font-size:14px;">
|
||||
<table style="width:600px;margin:5px auto;">
|
||||
<tbody>
|
||||
<tr><td>
|
||||
<!-- We use the logo of the company that created the survey (to handle multi company cases) -->
|
||||
<a href="/"><img src="/logo.png?company=${object.survey_id.create_uid.company_id.id}" style="vertical-align:baseline;max-width:100px;" /></a>
|
||||
</td><td style="text-align:right;vertical-align:middle;">
|
||||
Certification: ${object.survey_id.display_name}
|
||||
</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table style="width:600px;margin:0px auto;background:white;border:1px solid #e1e1e1;">
|
||||
<tbody>
|
||||
<tr><td style="padding:15px 20px 10px 20px;">
|
||||
<p>Dear <span>${object.partner_id.name or 'participant'}</span></p>
|
||||
<p>
|
||||
Here is, in attachment, your certification document for
|
||||
<strong>${object.survey_id.display_name}</strong>
|
||||
</p>
|
||||
<p>Congratulations for succeeding the test!</p>
|
||||
</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</field>
|
||||
<field name="report_template" ref="certification_report"/>
|
||||
<field name="report_name">Certification Document</field>
|
||||
|
||||
@@ -15,6 +15,7 @@ emails_split = re.compile(r"[;,\n\r]+")
|
||||
|
||||
class SurveyInvite(models.TransientModel):
|
||||
_name = 'survey.invite'
|
||||
_inherit = 'mail.composer.mixin'
|
||||
_description = 'Survey Invitation Wizard'
|
||||
|
||||
@api.model
|
||||
@@ -28,14 +29,9 @@ class SurveyInvite(models.TransientModel):
|
||||
return self.env.user.partner_id
|
||||
|
||||
# composer content
|
||||
subject = fields.Char('Subject', compute='_compute_subject', readonly=False, store=True)
|
||||
body = fields.Html('Contents', sanitize_style=True, compute='_compute_body', readonly=False, store=True)
|
||||
attachment_ids = fields.Many2many(
|
||||
'ir.attachment', 'survey_mail_compose_message_ir_attachments_rel', 'wizard_id', 'attachment_id',
|
||||
string='Attachments')
|
||||
template_id = fields.Many2one(
|
||||
'mail.template', 'Use template', index=True,
|
||||
domain="[('model', '=', 'survey.user_input')]")
|
||||
# origin
|
||||
email_from = fields.Char('From', default=_get_default_from, help="Email address of the sender.")
|
||||
author_id = fields.Many2one(
|
||||
@@ -106,6 +102,11 @@ class SurveyInvite(models.TransientModel):
|
||||
for invite in self:
|
||||
invite.survey_start_url = werkzeug.urls.url_join(base_url, invite.survey_id.get_start_url()) if invite.survey_id else False
|
||||
|
||||
# Overrides of mail.composer.mixin
|
||||
@api.depends('survey_id') # fake trigger otherwise not computed in new mode
|
||||
def _compute_render_model(self):
|
||||
self.render_model = 'survey.user_input'
|
||||
|
||||
@api.onchange('emails')
|
||||
def _onchange_emails(self):
|
||||
if self.emails and (self.survey_users_login_required and not self.survey_id.users_can_signup):
|
||||
@@ -138,22 +139,6 @@ class SurveyInvite(models.TransientModel):
|
||||
', '.join(invalid_partners.mapped('name'))
|
||||
))
|
||||
|
||||
@api.depends('template_id')
|
||||
def _compute_subject(self):
|
||||
for invite in self:
|
||||
if invite.template_id:
|
||||
invite.subject = invite.template_id.subject
|
||||
elif not invite.subject:
|
||||
invite.subject = False
|
||||
|
||||
@api.depends('template_id')
|
||||
def _compute_body(self):
|
||||
for invite in self:
|
||||
if invite.template_id:
|
||||
invite.body = invite.template_id.body_html
|
||||
elif not invite.body:
|
||||
invite.body = False
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for values in vals_list:
|
||||
@@ -210,8 +195,8 @@ class SurveyInvite(models.TransientModel):
|
||||
|
||||
def _send_mail(self, answer):
|
||||
""" Create mail specific for recipient containing notably its access token """
|
||||
subject = self.env['mail.render.mixin'].with_context(safe=True)._render_template(self.subject, 'survey.user_input', answer.ids, post_process=True)[answer.id]
|
||||
body = self.env['mail.render.mixin']._render_template(self.body, 'survey.user_input', answer.ids, post_process=True)[answer.id]
|
||||
subject = self._render_field('subject', answer.ids, options={'render_safe': True})[answer.id]
|
||||
body = self._render_field('body', answer.ids, post_process=True)[answer.id]
|
||||
# post the message
|
||||
mail_values = {
|
||||
'email_from': self.email_from,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<field name="survey_users_can_signup" invisible="1"/>
|
||||
<field name="survey_id" readonly="context.get('default_survey_id')"/>
|
||||
<field name="existing_mode" widget="radio" invisible="1" />
|
||||
<field name="render_model" invisible="1"/>
|
||||
<field name="survey_start_url" label="Public share URL" readonly="1" widget="CopyClipboardChar"
|
||||
attrs="{'invisible':[('survey_access_mode', '!=', 'public')]}"
|
||||
class="mb16"/>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<field name="name">Mail Test Full: Tracking Template</field>
|
||||
<field name="subject">Test Template</field>
|
||||
<field name="partner_to">${object.customer_id.id | safe}</field>
|
||||
<field name="body_html" type="xml"><p>Hello ${object.name}</p></field>
|
||||
<field name="body_html" type="html"><p>Hello ${object.name}</p></field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_ticket"/>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
@@ -13,7 +13,7 @@
|
||||
<field name="name">Mail Test: Template</field>
|
||||
<field name="subject">Post on ${object.name}</field>
|
||||
<field name="partner_to">${object.customer_id.id}</field>
|
||||
<field name="body_html" type="xml"><p>Adding stuff on ${object.name}</p></field>
|
||||
<field name="body_html" type="html"><p>Adding stuff on ${object.name}</p></field>
|
||||
<field name="model_id" ref="test_mail.model_mail_test_container"/>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
@@ -122,3 +122,17 @@ class MailTestContainer(models.Model):
|
||||
values['alias_force_thread_id'] = self.id
|
||||
values['alias_parent_thread_id'] = self.id
|
||||
return values
|
||||
|
||||
class MailTestComposerMixin(models.Model):
|
||||
""" A simple invite-like wizard using the composer mixin, rendering on
|
||||
itself. """
|
||||
_description = 'Invite-like Wizard'
|
||||
_name = 'mail.test.composer.mixin'
|
||||
_inherit = ['mail.composer.mixin']
|
||||
|
||||
name = fields.Char('Name')
|
||||
author_id = fields.Many2one('res.partner')
|
||||
description = fields.Html('Description', render_engine="qweb", render_options={"post_process": True})
|
||||
|
||||
def _compute_render_model(self):
|
||||
self.render_model = self._name
|
||||
|
||||
@@ -11,6 +11,8 @@ access_mail_test_activity_portal,mail.test.activity.portal,model_mail_test_activ
|
||||
access_mail_test_activity_user,mail.test.activity.user,model_mail_test_activity,base.group_user,1,1,1,1
|
||||
access_mail_test_ticket_portal,mail.test.ticket.portal,model_mail_test_ticket,base.group_portal,0,0,0,0
|
||||
access_mail_test_ticket_user,mail.test.ticket.user,model_mail_test_ticket,base.group_user,1,1,1,1
|
||||
access_mail_test_composer_mixin_all,mail.test.composer.mixin.all,model_mail_test_composer_mixin,,0,0,0,0
|
||||
access_mail_test_composer_mixin_user,mail.test.composer.mixin.user,model_mail_test_composer_mixin,base.group_user,1,1,1,1
|
||||
access_mail_test_container_portal,mail.test.container_portal,model_mail_test_container,base.group_portal,1,1,0,0
|
||||
access_mail_test_container_user,mail.test.container.user,model_mail_test_container,base.group_user,1,1,1,1
|
||||
access_mail_test_cc_portal,mail.test.cc.portal,model_mail_test_cc,base.group_portal,1,0,0,0
|
||||
|
||||
|
@@ -4,6 +4,7 @@ from . import test_invite
|
||||
from . import test_ir_actions
|
||||
from . import test_mail_activity
|
||||
from . import test_mail_composer
|
||||
from . import test_mail_composer_mixin
|
||||
from . import test_mail_followers
|
||||
from . import test_mail_message
|
||||
from . import test_mail_mail
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon, TestRecipients
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import users
|
||||
|
||||
|
||||
@tagged('mail_composer_mixin')
|
||||
class TestMailComposerMixin(TestMailCommon, TestRecipients):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailComposerMixin, cls).setUpClass()
|
||||
cls._init_mail_gateway()
|
||||
|
||||
cls.mail_template = cls.env['mail.template'].create({
|
||||
'subject': 'Subject for ${object.name}',
|
||||
'body_html': '<p>Body for ${object.name}</p>',
|
||||
})
|
||||
|
||||
@users("employee")
|
||||
def test_content_sync(self):
|
||||
record = self.env['mail.test.composer.mixin'].create({
|
||||
'name': 'Invite',
|
||||
'template_id': self.mail_template.id,
|
||||
})
|
||||
self.assertEqual(record.subject, self.mail_template.subject)
|
||||
self.assertEqual(record.body, self.mail_template.body_html)
|
||||
|
||||
subject = record._render_field('subject', record.ids)[record.id]
|
||||
self.assertEqual(subject, 'Subject for %s' % record.name)
|
||||
body = record._render_field('body', record.ids)[record.id]
|
||||
self.assertEqual(body, '<p>Body for %s</p>' % record.name)
|
||||
|
||||
@users("employee")
|
||||
def test_rendering(self):
|
||||
record = self.env['mail.test.composer.mixin'].create({
|
||||
'name': 'Invite',
|
||||
'subject': 'Subject for ${object.name}',
|
||||
'body': '<p>Content from ${user.name}</p>',
|
||||
'description': '<p>Description for <t t-esc="object.name"/></p>',
|
||||
})
|
||||
self.assertEqual(record.subject, 'Subject for ${object.name}')
|
||||
self.assertEqual(record.body, '<p>Content from ${user.name}</p>')
|
||||
|
||||
subject = record._render_field('subject', record.ids)[record.id]
|
||||
self.assertEqual(subject, 'Subject for %s' % record.name)
|
||||
body = record._render_field('body', record.ids)[record.id]
|
||||
self.assertEqual(body, '<p>Content from %s</p>' % self.env.user.name)
|
||||
description = record._render_field('description', record.ids)[record.id]
|
||||
self.assertEqual(description, '<p>Description for </p>')
|
||||
@@ -23,6 +23,9 @@ class TestSmsTemplate(TestMailFullCommon, TestMailFullRecipients):
|
||||
rendered_body = self.sms_template._render_template(self.sms_template.body, self.sms_template.model, self.test_record.ids)
|
||||
self.assertEqual(rendered_body[self.test_record.id], 'Dear %s this is an SMS.' % self.test_record.display_name)
|
||||
|
||||
rendered_body = self.sms_template._render_field('body', self.test_record.ids)
|
||||
self.assertEqual(rendered_body[self.test_record.id], 'Dear %s this is an SMS.' % self.test_record.display_name)
|
||||
|
||||
def test_sms_template_lang(self):
|
||||
self.env['res.lang']._activate_lang('fr_FR')
|
||||
self.user_admin.write({'lang': 'en_US'})
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<field name="model_id" ref="website_event_track.model_event_track"/>
|
||||
<field name="subject">Confirmation of ${object.name}</field>
|
||||
<field name="use_default_to" eval="True"/>
|
||||
<field name="body_html" type="xml">
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
Dear ${object.partner_id.name or object.partner_name or ''}<br/>
|
||||
We are pleased to inform you that your proposal ${object.name} has been accepted and confirmed for the event ${object.event_id.name}.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<field name="subject">You left items in your cart!</field>
|
||||
<field name="email_from">${(object.user_id.email_formatted or user.email_formatted or '') | safe}</field>
|
||||
<field name="partner_to">${object.partner_id.id}</field>
|
||||
<field name="body_html" type="xml">
|
||||
<field name="body_html" type="html">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
|
||||
<tbody>
|
||||
|
||||
@@ -28,9 +28,6 @@
|
||||
<field name="model">slide.channel</field>
|
||||
<field name="inherit_id" ref="website_slides.slide_channel_view_kanban"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='website_published']" position="before">
|
||||
<field name="enroll"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='info_total_time']" position="after">
|
||||
<div class="d-flex" attrs="{'invisible': [('enroll', '!=', 'payment')]}">
|
||||
<span class="mr-auto"><label for="product_sale_revenues" class="mb0">Sales</label></span>
|
||||
@@ -38,9 +35,6 @@
|
||||
<field name="currency_id" attrs="{'invisible': True}"/>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//a[@name='action_channel_invite']" position="attributes">
|
||||
<attribute name="attrs">{'invisible': [('enroll', '=', 'payment')]}</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data></odoo>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'eLearning',
|
||||
'version': '2.3',
|
||||
'version': '2.4',
|
||||
'sequence': 125,
|
||||
'summary': 'Manage and publish an eLearning platform',
|
||||
'website': 'https://www.odoo.com/page/slides',
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Channels">
|
||||
<header>
|
||||
<button name="action_channel_invite" string="Invite" type="object" class="oe_highlight" attrs="{'invisible': [('enroll', '!=', 'invite')]}"/>
|
||||
<button name="action_channel_invite" string="Invite" type="object" class="oe_highlight" attrs="{'invisible': [('enroll', '!=', 'invite')]}"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
@@ -207,6 +207,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<kanban string="eLearning Overview" class="o_emphasize_colors o_kanban_dashboard o_slide_kanban breadcrumb_item active" edit="false" sample="1">
|
||||
<field name="color"/>
|
||||
<field name="enroll"/>
|
||||
<field name="website_published"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
@@ -231,7 +232,8 @@
|
||||
<div role="menuitem">
|
||||
<a name="action_view_slides" type="object">Lessons</a>
|
||||
</div>
|
||||
<div role="menuitem" name="action_channel_invite">
|
||||
<div role="menuitem" name="action_channel_invite"
|
||||
attrs="{'invisible': [('enroll', '!=', 'invite')]}">
|
||||
<a name="action_channel_invite" type="object">Invite</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,6 @@ import re
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import formataddr
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -15,35 +14,20 @@ emails_split = re.compile(r"[;,\n\r]+")
|
||||
|
||||
class SlideChannelInvite(models.TransientModel):
|
||||
_name = 'slide.channel.invite'
|
||||
_inherit = 'mail.composer.mixin'
|
||||
_description = 'Channel Invitation Wizard'
|
||||
|
||||
# composer content
|
||||
subject = fields.Char('Subject', compute='_compute_subject', readonly=False, store=True)
|
||||
body = fields.Html('Contents', sanitize_style=True, compute='_compute_body', readonly=False, store=True)
|
||||
attachment_ids = fields.Many2many('ir.attachment', string='Attachments')
|
||||
template_id = fields.Many2one(
|
||||
'mail.template', 'Use template',
|
||||
domain="[('model', '=', 'slide.channel.partner')]")
|
||||
# recipients
|
||||
partner_ids = fields.Many2many('res.partner', string='Recipients')
|
||||
# slide channel
|
||||
channel_id = fields.Many2one('slide.channel', string='Slide channel', required=True)
|
||||
|
||||
@api.depends('template_id')
|
||||
def _compute_subject(self):
|
||||
for invite in self:
|
||||
if invite.template_id:
|
||||
invite.subject = invite.template_id.subject
|
||||
elif not invite.subject:
|
||||
invite.subject = False
|
||||
|
||||
@api.depends('template_id')
|
||||
def _compute_body(self):
|
||||
for invite in self:
|
||||
if invite.template_id:
|
||||
invite.body = invite.template_id.body_html
|
||||
elif not invite.body:
|
||||
invite.body = False
|
||||
# Overrides of mail.composer.mixin
|
||||
@api.depends('channel_id') # fake trigger otherwise not computed in new mode
|
||||
def _compute_render_model(self):
|
||||
self.render_model = 'slide.channel.partner'
|
||||
|
||||
@api.onchange('partner_ids')
|
||||
def _onchange_partner_ids(self):
|
||||
@@ -77,6 +61,8 @@ class SlideChannelInvite(models.TransientModel):
|
||||
|
||||
if not self.env.user.email:
|
||||
raise UserError(_("Unable to post message, please configure the sender's email address."))
|
||||
if not self.partner_ids:
|
||||
raise UserError(_("Please select at least one recipient."))
|
||||
|
||||
mail_values = []
|
||||
for partner_id in self.partner_ids:
|
||||
@@ -84,16 +70,14 @@ class SlideChannelInvite(models.TransientModel):
|
||||
if slide_channel_partner:
|
||||
mail_values.append(self._prepare_mail_values(slide_channel_partner))
|
||||
|
||||
# TODO awa: change me to create multi when mail.mail supports it
|
||||
for mail_value in mail_values:
|
||||
self.env['mail.mail'].sudo().create(mail_value)
|
||||
self.env['mail.mail'].sudo().create(mail_values)
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
def _prepare_mail_values(self, slide_channel_partner):
|
||||
""" Create mail specific for recipient """
|
||||
subject = self.env['mail.render.mixin']._render_template(self.subject, 'slide.channel.partner', slide_channel_partner.ids, post_process=True)[slide_channel_partner.id]
|
||||
body = self.env['mail.render.mixin']._render_template(self.body, 'slide.channel.partner', slide_channel_partner.ids, post_process=True)[slide_channel_partner.id]
|
||||
subject = self._render_field('subject', slide_channel_partner.ids, options={'render_safe': True})[slide_channel_partner.id]
|
||||
body = self._render_field('body', slide_channel_partner.ids, post_process=True)[slide_channel_partner.id]
|
||||
# post the message
|
||||
mail_values = {
|
||||
'email_from': self.env.user.email_formatted,
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
<field name="partner_ids"
|
||||
widget="many2many_tags_email"
|
||||
placeholder="Add existing contacts..."
|
||||
context="{'force_email':True, 'show_email':True, 'no_create_edit': True}"/>
|
||||
required="1"
|
||||
context="{'force_email':True, 'show_email':True, 'no_create_edit': True, 'no_quick_create': True}"/>
|
||||
</group>
|
||||
<group col="2">
|
||||
<field name="render_model" invisible="1"/>
|
||||
<field name="subject" placeholder="Subject..."/>
|
||||
</group>
|
||||
<field name="body" options="{'style-inline': true}"/>
|
||||
|
||||
Reference in New Issue
Block a user