572 lines
27 KiB
Python
572 lines
27 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
|
|
|
import babel
|
|
import base64
|
|
import copy
|
|
import datetime
|
|
import dateutil.relativedelta as relativedelta
|
|
import logging
|
|
|
|
import functools
|
|
import lxml
|
|
from werkzeug import urls
|
|
|
|
from flectra import _, api, fields, models, tools
|
|
from flectra.exceptions import UserError
|
|
from flectra.tools import pycompat
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def format_date(env, date, pattern=False):
|
|
if not date:
|
|
return ''
|
|
try:
|
|
return tools.format_date(env, date, date_format=pattern)
|
|
except babel.core.UnknownLocaleError:
|
|
return date
|
|
|
|
|
|
def format_tz(env, dt, tz=False, format=False):
|
|
record_user_timestamp = env.user.sudo().with_context(tz=tz or env.user.sudo().tz or 'UTC')
|
|
timestamp = datetime.datetime.strptime(dt, tools.DEFAULT_SERVER_DATETIME_FORMAT)
|
|
|
|
ts = fields.Datetime.context_timestamp(record_user_timestamp, timestamp)
|
|
|
|
# Babel allows to format datetime in a specific language without change locale
|
|
# So month 1 = January in English, and janvier in French
|
|
# Be aware that the default value for format is 'medium', instead of 'short'
|
|
# medium: Jan 5, 2016, 10:20:31 PM | 5 janv. 2016 22:20:31
|
|
# short: 1/5/16, 10:20 PM | 5/01/16 22:20
|
|
if env.context.get('use_babel'):
|
|
# Formatting available here : http://babel.pocoo.org/en/latest/dates.html#date-fields
|
|
from babel.dates import format_datetime
|
|
return format_datetime(ts, format or 'medium', locale=env.context.get("lang") or 'en_US')
|
|
|
|
if format:
|
|
return pycompat.text_type(ts.strftime(format))
|
|
else:
|
|
lang = env.context.get("lang")
|
|
langs = env['res.lang']
|
|
if lang:
|
|
langs = env['res.lang'].search([("code", "=", lang)])
|
|
format_date = langs.date_format or '%B-%d-%Y'
|
|
format_time = langs.time_format or '%I-%M %p'
|
|
|
|
fdate = pycompat.text_type(ts.strftime(format_date))
|
|
ftime = pycompat.text_type(ts.strftime(format_time))
|
|
return u"%s %s%s" % (fdate, ftime, (u' (%s)' % tz) if tz else u'')
|
|
|
|
def format_amount(env, amount, currency):
|
|
fmt = "%.{0}f".format(currency.decimal_places)
|
|
lang = env['res.lang']._lang_get(env.context.get('lang') or 'en_US')
|
|
|
|
formatted_amount = lang.format(fmt, currency.round(amount), grouping=True, monetary=True)\
|
|
.replace(r' ', u'\N{NO-BREAK SPACE}').replace(r'-', u'\u2011')
|
|
|
|
pre = post = u''
|
|
if currency.position == 'before':
|
|
pre = u'{symbol}\N{NO-BREAK SPACE}'.format(symbol=currency.symbol or '')
|
|
else:
|
|
post = u'\N{NO-BREAK SPACE}{symbol}'.format(symbol=currency.symbol or '')
|
|
|
|
return u'{pre}{0}{post}'.format(formatted_amount, pre=pre, post=post)
|
|
|
|
try:
|
|
# We use a jinja2 sandboxed environment to render mako templates.
|
|
# Note that the rendering does not cover all the mako syntax, in particular
|
|
# arbitrary Python statements are not accepted, and not all expressions are
|
|
# allowed: only "public" attributes (not starting with '_') of objects may
|
|
# be accessed.
|
|
# This is done on purpose: it prevents incidental or malicious execution of
|
|
# Python code that may break the security of the server.
|
|
from jinja2.sandbox import SandboxedEnvironment
|
|
mako_template_env = SandboxedEnvironment(
|
|
block_start_string="<%",
|
|
block_end_string="%>",
|
|
variable_start_string="${",
|
|
variable_end_string="}",
|
|
comment_start_string="<%doc>",
|
|
comment_end_string="</%doc>",
|
|
line_statement_prefix="%",
|
|
line_comment_prefix="##",
|
|
trim_blocks=True, # do not output newline after blocks
|
|
autoescape=True, # XML/HTML automatic escaping
|
|
)
|
|
mako_template_env.globals.update({
|
|
'str': str,
|
|
'quote': urls.url_quote,
|
|
'urlencode': urls.url_encode,
|
|
'datetime': 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),
|
|
})
|
|
mako_safe_template_env = copy.copy(mako_template_env)
|
|
mako_safe_template_env.autoescape = False
|
|
except ImportError:
|
|
_logger.warning("jinja2 not available, templating features will not work!")
|
|
|
|
|
|
class MailTemplate(models.Model):
|
|
"Templates for sending email"
|
|
_name = "mail.template"
|
|
_description = 'Email Templates'
|
|
_order = 'name'
|
|
|
|
@api.model
|
|
def default_get(self, fields):
|
|
res = super(MailTemplate, self).default_get(fields)
|
|
if res.get('model'):
|
|
res['model_id'] = self.env['ir.model']._get(res.pop('model')).id
|
|
return res
|
|
|
|
name = fields.Char('Name')
|
|
model_id = fields.Many2one('ir.model', 'Applies to', help="The type of document this template can be used with")
|
|
model = fields.Char('Related Document Model', related='model_id.model', index=True, store=True, readonly=True)
|
|
lang = fields.Char('Language',
|
|
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}.",
|
|
placeholder="${object.partner_id.lang}")
|
|
user_signature = fields.Boolean('Add Signature',
|
|
help="If checked, the user's signature will be appended to the text version "
|
|
"of the message")
|
|
subject = fields.Char('Subject', translate=True, help="Subject (placeholders may be used here)")
|
|
email_from = fields.Char('From',
|
|
help="Sender address (placeholders may be used here). If not set, the default "
|
|
"value will be the author's email alias if configured, or email address.")
|
|
use_default_to = fields.Boolean(
|
|
'Default recipients',
|
|
help="Default recipients of the record:\n"
|
|
"- partner (using id on a partner or the partner_id field) OR\n"
|
|
"- email (using email_from or email field)")
|
|
email_to = fields.Char('To (Emails)', help="Comma-separated recipient addresses (placeholders may be used here)")
|
|
partner_to = fields.Char('To (Partners)', oldname='email_recipients',
|
|
help="Comma-separated ids of recipient partners (placeholders may be used here)")
|
|
email_cc = fields.Char('Cc', help="Carbon copy recipients (placeholders may be used here)")
|
|
reply_to = fields.Char('Reply-To', help="Preferred response address (placeholders may be used here)")
|
|
mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing Mail Server', readonly=False,
|
|
help="Optional preferred server for outgoing mails. If not set, the highest "
|
|
"priority one will be used.")
|
|
body_html = fields.Html('Body', translate=True, sanitize=False)
|
|
report_name = fields.Char('Report Filename', translate=True,
|
|
help="Name to use for the generated report file (may contain placeholders)\n"
|
|
"The extension can be omitted and will then come from the report type.")
|
|
report_template = fields.Many2one('ir.actions.report', 'Optional report to print and attach')
|
|
ref_ir_act_window = fields.Many2one('ir.actions.act_window', 'Sidebar action', readonly=True, copy=False,
|
|
help="Sidebar action to make this template available on records "
|
|
"of the related document model")
|
|
attachment_ids = fields.Many2many('ir.attachment', 'email_template_attachment_rel', 'email_template_id',
|
|
'attachment_id', 'Attachments',
|
|
help="You may attach files to this template, to be added to all "
|
|
"emails created from this template")
|
|
auto_delete = fields.Boolean('Auto Delete', default=True, help="Permanently delete this email after sending it, to save space")
|
|
|
|
# Fake fields used to implement the placeholder assistant
|
|
model_object_field = fields.Many2one('ir.model.fields', string="Field",
|
|
help="Select target field from the related document model.\n"
|
|
"If it is a relationship field you will be able to select "
|
|
"a target field at the destination of the relationship.")
|
|
sub_object = fields.Many2one('ir.model', 'Sub-model', readonly=True,
|
|
help="When a relationship field is selected as first field, "
|
|
"this field shows the document model the relationship goes to.")
|
|
sub_model_object_field = fields.Many2one('ir.model.fields', 'Sub-field',
|
|
help="When a relationship field is selected as first field, "
|
|
"this field lets you select the target field within the "
|
|
"destination document model (sub-model).")
|
|
null_value = fields.Char('Default Value', help="Optional value to use if the target field is empty")
|
|
copyvalue = fields.Char('Placeholder Expression', help="Final placeholder expression, to be copy-pasted in the desired template field.")
|
|
scheduled_date = fields.Char('Scheduled Date', help="If set, the queue manager will send the email after the date. If not set, the email will be send as soon as possible. Jinja2 placeholders may be used.")
|
|
|
|
@api.onchange('model_id')
|
|
def onchange_model_id(self):
|
|
# TDE CLEANME: should'nt it be a stored related ?
|
|
if self.model_id:
|
|
self.model = self.model_id.model
|
|
else:
|
|
self.model = False
|
|
|
|
def build_expression(self, field_name, sub_field_name, null_value):
|
|
"""Returns a placeholder expression for use in a template field,
|
|
based on the values provided in the placeholder assistant.
|
|
|
|
:param field_name: main field name
|
|
:param sub_field_name: sub field name (M2O)
|
|
:param null_value: default value if the target value is empty
|
|
:return: final placeholder expression """
|
|
expression = ''
|
|
if field_name:
|
|
expression = "${object." + field_name
|
|
if sub_field_name:
|
|
expression += "." + sub_field_name
|
|
if null_value:
|
|
expression += " or '''%s'''" % null_value
|
|
expression += "}"
|
|
return expression
|
|
|
|
@api.onchange('model_object_field', 'sub_model_object_field', 'null_value')
|
|
def onchange_sub_model_object_value_field(self):
|
|
if self.model_object_field:
|
|
if self.model_object_field.ttype in ['many2one', 'one2many', 'many2many']:
|
|
model = self.env['ir.model']._get(self.model_object_field.relation)
|
|
if model:
|
|
self.sub_object = model.id
|
|
self.copyvalue = self.build_expression(self.model_object_field.name, self.sub_model_object_field and self.sub_model_object_field.name or False, self.null_value or False)
|
|
else:
|
|
self.sub_object = False
|
|
self.sub_model_object_field = False
|
|
self.copyvalue = self.build_expression(self.model_object_field.name, False, self.null_value or False)
|
|
else:
|
|
self.sub_object = False
|
|
self.copyvalue = False
|
|
self.sub_model_object_field = False
|
|
self.null_value = False
|
|
|
|
@api.multi
|
|
def unlink(self):
|
|
self.unlink_action()
|
|
return super(MailTemplate, self).unlink()
|
|
|
|
@api.multi
|
|
def copy(self, default=None):
|
|
default = dict(default or {},
|
|
name=_("%s (copy)") % self.name)
|
|
return super(MailTemplate, self).copy(default=default)
|
|
|
|
@api.multi
|
|
def unlink_action(self):
|
|
for template in self:
|
|
if template.ref_ir_act_window:
|
|
template.ref_ir_act_window.sudo().unlink()
|
|
return True
|
|
|
|
@api.multi
|
|
def create_action(self):
|
|
ActWindowSudo = self.env['ir.actions.act_window'].sudo()
|
|
view = self.env.ref('mail.email_compose_message_wizard_form')
|
|
|
|
for template in self:
|
|
button_name = _('Send Mail (%s)') % template.name
|
|
action = ActWindowSudo.create({
|
|
'name': button_name,
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'mail.compose.message',
|
|
'src_model': template.model_id.model,
|
|
'view_type': 'form',
|
|
'context': "{'default_composition_mode': 'mass_mail', 'default_template_id' : %d, 'default_use_template': True}" % (template.id),
|
|
'view_mode': 'form,tree',
|
|
'view_id': view.id,
|
|
'target': 'new',
|
|
'binding_model_id': template.model_id.id,
|
|
})
|
|
template.write({'ref_ir_act_window': action.id})
|
|
|
|
return True
|
|
|
|
# ----------------------------------------
|
|
# RENDERING
|
|
# ----------------------------------------
|
|
|
|
@api.model
|
|
def _replace_local_links(self, html):
|
|
""" Post-processing of html content to replace local links to absolute
|
|
links, using web.base.url as base url. """
|
|
if not html:
|
|
return html
|
|
|
|
# form a tree
|
|
root = lxml.html.fromstring(html)
|
|
if not len(root) and root.text is None and root.tail is None:
|
|
html = u'<div>%s</div>' % html
|
|
root = lxml.html.fromstring(html, encoding='unicode')
|
|
|
|
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
|
base = urls.url_parse(base_url)
|
|
|
|
def _process_link(url):
|
|
new_url = urls.url_parse(url)
|
|
if new_url.scheme and new_url.netloc:
|
|
return url
|
|
return new_url.replace(scheme=base.scheme, netloc=base.netloc).to_url()
|
|
|
|
# check all nodes, replace :
|
|
# - img src -> check URL
|
|
# - a href -> check URL
|
|
for node in root.iter():
|
|
if node.tag == 'a' and node.get('href'):
|
|
node.set('href', _process_link(node.get('href')))
|
|
elif node.tag == 'img' and not node.get('src', 'data').startswith(u'data'):
|
|
node.set('src', _process_link(node.get('src')))
|
|
|
|
html = lxml.html.tostring(root, pretty_print=False, method='html', encoding='unicode')
|
|
# this is ugly, but lxml/etree tostring want to put everything in a 'div' that breaks the editor -> remove that
|
|
if html.startswith(u'<div>') and html.endswith(u'</div>'):
|
|
html = html[5:-6]
|
|
return html
|
|
|
|
@api.model
|
|
def render_post_process(self, html):
|
|
html = self._replace_local_links(html)
|
|
return html
|
|
|
|
@api.model
|
|
def render_template(self, template_txt, model, res_ids, post_process=False):
|
|
""" Render the given template text, replace mako expressions ``${expr}``
|
|
with the result of evaluating these expressions with an evaluation
|
|
context containing:
|
|
|
|
- ``user``: Model of the current user
|
|
- ``object``: record of the document record this mail is related to
|
|
- ``context``: the context passed to the mail composition wizard
|
|
|
|
:param str template_txt: the template text to render
|
|
:param str model: model name of the document record this mail is related to.
|
|
:param int res_ids: list of ids of document records those mails are related to.
|
|
"""
|
|
multi_mode = True
|
|
if isinstance(res_ids, pycompat.integer_types):
|
|
multi_mode = False
|
|
res_ids = [res_ids]
|
|
|
|
results = dict.fromkeys(res_ids, u"")
|
|
|
|
# try to load the template
|
|
try:
|
|
mako_env = mako_safe_template_env if self.env.context.get('safe') else mako_template_env
|
|
template = mako_env.from_string(tools.ustr(template_txt))
|
|
except Exception:
|
|
_logger.info("Failed to load template %r", template_txt, exc_info=True)
|
|
return multi_mode and results or results[res_ids[0]]
|
|
|
|
# prepare template variables
|
|
records = self.env[model].browse(it for it in res_ids if it) # filter to avoid browsing [None]
|
|
res_to_rec = dict.fromkeys(res_ids, None)
|
|
for record in records:
|
|
res_to_rec[record.id] = record
|
|
variables = {
|
|
'format_date': lambda date, format=False, context=self._context: format_date(self.env, date, format),
|
|
'format_tz': lambda dt, tz=False, format=False, context=self._context: format_tz(self.env, dt, tz, format),
|
|
'format_amount': lambda amount, currency, context=self._context: format_amount(self.env, amount, currency),
|
|
'user': self.env.user,
|
|
'ctx': self._context, # context kw would clash with mako internals
|
|
}
|
|
for res_id, record in res_to_rec.items():
|
|
variables['object'] = record
|
|
try:
|
|
render_result = template.render(variables)
|
|
except Exception:
|
|
_logger.info("Failed to render template %r using values %r" % (template, variables), exc_info=True)
|
|
raise UserError(_("Failed to render template %r using values %r")% (template, variables))
|
|
if render_result == u"False":
|
|
render_result = u""
|
|
results[res_id] = render_result
|
|
|
|
if post_process:
|
|
for res_id, result in results.items():
|
|
results[res_id] = self.render_post_process(result)
|
|
|
|
return multi_mode and results or results[res_ids[0]]
|
|
|
|
@api.multi
|
|
def get_email_template(self, res_ids):
|
|
multi_mode = True
|
|
if isinstance(res_ids, pycompat.integer_types):
|
|
res_ids = [res_ids]
|
|
multi_mode = False
|
|
|
|
if res_ids is None:
|
|
res_ids = [None]
|
|
results = dict.fromkeys(res_ids, False)
|
|
|
|
if not self.ids:
|
|
return results
|
|
self.ensure_one()
|
|
|
|
langs = self.render_template(self.lang, self.model, res_ids)
|
|
for res_id, lang in langs.items():
|
|
if lang:
|
|
template = self.with_context(lang=lang)
|
|
else:
|
|
template = self
|
|
results[res_id] = template
|
|
|
|
return multi_mode and results or results[res_ids[0]]
|
|
|
|
@api.multi
|
|
def generate_recipients(self, results, res_ids):
|
|
"""Generates the recipients of the template. Default values can ben generated
|
|
instead of the template values if requested by template or context.
|
|
Emails (email_to, email_cc) can be transformed into partners if requested
|
|
in the context. """
|
|
self.ensure_one()
|
|
|
|
if self.use_default_to or self._context.get('tpl_force_default_to'):
|
|
default_recipients = self.env['mail.thread'].message_get_default_recipients(res_model=self.model, res_ids=res_ids)
|
|
for res_id, recipients in default_recipients.items():
|
|
results[res_id].pop('partner_to', None)
|
|
results[res_id].update(recipients)
|
|
|
|
for res_id, values in results.items():
|
|
partner_ids = values.get('partner_ids', list())
|
|
if self._context.get('tpl_partners_only'):
|
|
mails = tools.email_split(values.pop('email_to', '')) + tools.email_split(values.pop('email_cc', ''))
|
|
for mail in mails:
|
|
partner_id = self.env['res.partner'].find_or_create(mail)
|
|
partner_ids.append(partner_id)
|
|
partner_to = values.pop('partner_to', '')
|
|
if partner_to:
|
|
# placeholders could generate '', 3, 2 due to some empty field values
|
|
tpl_partner_ids = [int(pid) for pid in partner_to.split(',') if pid]
|
|
partner_ids += self.env['res.partner'].sudo().browse(tpl_partner_ids).exists().ids
|
|
results[res_id]['partner_ids'] = partner_ids
|
|
return results
|
|
|
|
@api.multi
|
|
def generate_email(self, res_ids, 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 [(report_name, data)] where data is base64 encoded.
|
|
"""
|
|
self.ensure_one()
|
|
multi_mode = True
|
|
if isinstance(res_ids, pycompat.integer_types):
|
|
res_ids = [res_ids]
|
|
multi_mode = False
|
|
if fields is None:
|
|
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'scheduled_date']
|
|
|
|
res_ids_to_templates = self.get_email_template(res_ids)
|
|
|
|
# templates: res_id -> template; template -> res_ids
|
|
templates_to_res_ids = {}
|
|
for res_id, template in res_ids_to_templates.items():
|
|
templates_to_res_ids.setdefault(template, []).append(res_id)
|
|
|
|
results = dict()
|
|
for template, template_res_ids in templates_to_res_ids.items():
|
|
Template = self.env['mail.template']
|
|
# generate fields value for all res_ids linked to the current template
|
|
if template.lang:
|
|
Template = Template.with_context(lang=template._context.get('lang'))
|
|
for field in fields:
|
|
Template = Template.with_context(safe=field in {'subject'})
|
|
generated_field_values = Template.render_template(
|
|
getattr(template, field), template.model, template_res_ids,
|
|
post_process=(field == 'body_html'))
|
|
for res_id, field_value in generated_field_values.items():
|
|
results.setdefault(res_id, dict())[field] = field_value
|
|
# compute recipients
|
|
if any(field in fields for field in ['email_to', 'partner_to', 'email_cc']):
|
|
results = template.generate_recipients(results, template_res_ids)
|
|
# update values for all res_ids
|
|
for res_id in template_res_ids:
|
|
values = results[res_id]
|
|
# body: add user signature, sanitize
|
|
if 'body_html' in fields and template.user_signature:
|
|
signature = self.env.user.signature
|
|
if signature:
|
|
values['body_html'] = tools.append_content_to_html(values['body_html'], signature, plaintext=False)
|
|
if values.get('body_html'):
|
|
values['body'] = tools.html_sanitize(values['body_html'])
|
|
# technical settings
|
|
values.update(
|
|
mail_server_id=template.mail_server_id.id or False,
|
|
auto_delete=template.auto_delete,
|
|
model=template.model,
|
|
res_id=res_id or False,
|
|
attachment_ids=[attach.id for attach in template.attachment_ids],
|
|
)
|
|
|
|
# Add report in attachments: generate once for all template_res_ids
|
|
if template.report_template:
|
|
for res_id in template_res_ids:
|
|
attachments = []
|
|
report_name = self.render_template(template.report_name, template.model, res_id)
|
|
report = template.report_template
|
|
report_service = report.report_name
|
|
|
|
if report.report_type not in ['qweb-html', 'qweb-pdf']:
|
|
raise UserError(_('Unsupported report type %s found.') % report.report_type)
|
|
result, format = report.render_qweb_pdf([res_id])
|
|
|
|
# TODO in trunk, change return format to binary to match message_post expected format
|
|
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))
|
|
results[res_id]['attachments'] = attachments
|
|
|
|
return multi_mode and results or results[res_ids[0]]
|
|
|
|
@api.multi
|
|
def send_mail(self, res_id, force_send=False, raise_exception=False, email_values=None):
|
|
"""Generates a new mail message for the given template and record,
|
|
and schedules it for delivery through the ``mail`` module's scheduler.
|
|
|
|
:param int res_id: id of the record to render the template with
|
|
(model is taken from the template)
|
|
:param bool force_send: if True, the generated mail.message is
|
|
immediately sent after being created, as if the scheduler
|
|
was executed for this message only.
|
|
:param dict email_values: if set, the generated mail.message is
|
|
updated with given values dict
|
|
:returns: id of the mail.message that was created
|
|
"""
|
|
self.ensure_one()
|
|
Mail = self.env['mail.mail']
|
|
Attachment = self.env['ir.attachment'] # TDE FIXME: should remove dfeault_type from context
|
|
|
|
# create a mail_mail based on values, without attachments
|
|
values = self.generate_email(res_id)
|
|
values['recipient_ids'] = [(4, pid) for pid in values.get('partner_ids', list())]
|
|
values.update(email_values or {})
|
|
attachment_ids = values.pop('attachment_ids', [])
|
|
attachments = values.pop('attachments', [])
|
|
# add a protection against void email_from
|
|
if 'email_from' in values and not values.get('email_from'):
|
|
values.pop('email_from')
|
|
mail = Mail.create(values)
|
|
|
|
# manage attachments
|
|
for attachment in attachments:
|
|
attachment_data = {
|
|
'name': attachment[0],
|
|
'datas_fname': attachment[0],
|
|
'datas': attachment[1],
|
|
'type': 'binary',
|
|
'res_model': 'mail.message',
|
|
'res_id': mail.mail_message_id.id,
|
|
}
|
|
attachment_ids.append(Attachment.create(attachment_data).id)
|
|
if attachment_ids:
|
|
values['attachment_ids'] = [(6, 0, attachment_ids)]
|
|
mail.write({'attachment_ids': [(6, 0, attachment_ids)]})
|
|
|
|
if force_send:
|
|
mail.send(raise_exception=raise_exception)
|
|
return mail.id # TDE CLEANME: return mail + api.returns ?
|