diff --git a/CHANGELOG b/CHANGELOG
index 95617995eb..120570727e 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,5 @@
+* Add e-mail template
+* Send e-mail on behalf of user
* Register mixins to generic Report class
* Cache in memory the report template instances
* Support Genshi's MsgDirective in report
diff --git a/trytond/ir/__init__.py b/trytond/ir/__init__.py
index cb003cb51d..bec2e0c672 100644
--- a/trytond/ir/__init__.py
+++ b/trytond/ir/__init__.py
@@ -21,6 +21,7 @@ from . import session
from . import queue
from . import calendar_
from . import message
+from . import email_
from . import routes
__all__ = ['register', 'routes']
@@ -94,6 +95,10 @@ def register():
calendar_.Month,
calendar_.Day,
message.Message,
+ email_.Email,
+ email_.EmailAddress,
+ email_.EmailTemplate,
+ email_.EmailTemplate_Report,
module='ir', type_='model')
Pool.register(
translation.TranslationSet,
diff --git a/trytond/ir/action.py b/trytond/ir/action.py
index afc23ef73a..d708d08e72 100644
--- a/trytond/ir/action.py
+++ b/trytond/ir/action.py
@@ -107,7 +107,7 @@ class Action(DeactivableMixin, ModelSQL, ModelView):
columns = []
columns += ['id', 'name', 'type', 'icon.rec_name']
if type_ == 'ir.action.report':
- columns += ['report_name', 'direct_print', 'email']
+ columns += ['report_name', 'direct_print']
elif type_ == 'ir.action.act_window':
columns += [
'views', 'domains', 'res_model', 'limit',
@@ -115,7 +115,7 @@ class Action(DeactivableMixin, ModelSQL, ModelView):
'pyson_domain', 'pyson_context', 'pyson_order',
'pyson_search_value']
elif type_ == 'ir.action.wizard':
- columns += ['wiz_name', 'window', 'email']
+ columns += ['wiz_name', 'window']
elif type_ == 'ir.action.url':
columns += ['url']
return Action.read(action_ids, columns)
@@ -540,10 +540,6 @@ class ActionReport(ActionMixin, ModelSQL, ModelView):
string='Extension', help='Leave empty for the same as template, '
'see LibreOffice documentation for compatible format.')
module = fields.Char('Module', readonly=True, select=True)
- email = fields.Char('Email',
- help='Python dictonary where keys define "to" "cc" "subject"\n'
- "Example: {'to': 'test@example.com', 'cc': 'user@example.com'}")
- pyson_email = fields.Function(fields.Char('PySON Email'), 'get_pyson')
_template_cache = MemoryCache('ir.action.report.template', context=False)
@classmethod
@@ -596,31 +592,6 @@ class ActionReport(ActionMixin, ModelSQL, ModelView):
def default_module():
return Transaction().context.get('module') or ''
- @classmethod
- def validate(cls, reports):
- super(ActionReport, cls).validate(reports)
- cls.check_email(reports)
-
- @classmethod
- def check_email(cls, reports):
- "Check email"
- for report in reports:
- if report.email:
- try:
- value = PYSONDecoder().decode(report.email)
- except Exception:
- value = None
- if isinstance(value, dict):
- inkeys = set(value)
- if not inkeys <= EMAIL_REFKEYS:
- raise EmailError(
- gettext('ir.msg_report_invalid_email',
- name=report.rec_name))
- else:
- raise EmailError(
- gettext('ir.msg_report_invalid_email',
- name=report.rec_name))
-
def get_is_custom(self, name):
return bool(self.report_content_custom)
@@ -670,9 +641,7 @@ class ActionReport(ActionMixin, ModelSQL, ModelView):
def get_pyson(cls, reports, name):
pysons = {}
field = name[6:]
- defaults = {
- 'email': '{}',
- }
+ defaults = {}
for report in reports:
pysons[report.id] = (getattr(report, field)
or defaults.get(field, 'null'))
@@ -1051,7 +1020,6 @@ class ActionWizard(ActionMixin, ModelSQL, ModelView):
action = fields.Many2One('ir.action', 'Action', required=True,
ondelete='CASCADE')
model = fields.Char('Model')
- email = fields.Char('Email')
window = fields.Boolean('Window', help='Run wizard in a new window.')
@staticmethod
diff --git a/trytond/ir/email.xml b/trytond/ir/email.xml
new file mode 100644
index 0000000000..dba0ddde5d
--- /dev/null
+++ b/trytond/ir/email.xml
@@ -0,0 +1,91 @@
+
+
+
+
+
+ ir.email
+ form
+ email_form
+
+
+ ir.email
+ tree
+ email_list
+
+
+
+ E-mails
+ ir.email
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ E-mails
+ ir.email
+
+
+
+
+
+
+
+
+
+
+
+
+
+ form_relate
+
+
+
+
+ ir.email.template
+ form
+ email_template_form
+
+
+ ir.email.template
+ tree
+ email_template_list
+
+
+
+ E-mail Templates
+ ir.email.template
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/trytond/ir/email_.py b/trytond/ir/email_.py
new file mode 100644
index 0000000000..b4a9b96449
--- /dev/null
+++ b/trytond/ir/email_.py
@@ -0,0 +1,478 @@
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+import heapq
+import mimetypes
+import re
+from email.encoders import encode_base64
+from email.header import Header
+from email.mime.application import MIMEApplication
+from email.mime.multipart import MIMEMultipart
+from email.mime.nonmultipart import MIMENonMultipart
+from email.mime.text import MIMEText
+from email.utils import formataddr, getaddresses
+
+try:
+ import html2text
+except ImportError:
+ html2text = None
+from genshi.template import TextTemplate
+
+from trytond.i18n import gettext
+from trytond.config import config
+from trytond.model import ModelSQL, ModelView, fields
+from trytond.model.exceptions import ValidationError, AccessError
+from trytond.pool import Pool
+from trytond.pyson import Eval
+from trytond.rpc import RPC
+from trytond.sendmail import sendmail_transactional, SMTPDataManager
+from trytond.tools import escape_wildcard
+from trytond.tools.string_ import StringMatcher
+from trytond.transaction import Transaction
+
+from .resource import ResourceAccessMixin
+
+HTML_EMAIL = """
+
+
%(subject)s
+%(body)s
+
+%(signature)s
+"""
+specialsre = re.compile(r'[][\\()<>@,:;".]')
+escapesre = re.compile(r'[\\"]')
+
+
+class EmailTemplateError(ValidationError):
+ pass
+
+
+def _get_emails(value):
+ "Return list of email from the comma separated list"
+ return [e for n, e in getaddresses([value]) if e]
+
+
+def _formataddr(pair):
+ "Format address without encoding"
+ name, address = pair
+ address.encode('ascii')
+ if name:
+ quotes = ''
+ if specialsre.search(name):
+ quotes = '"'
+ name = escapesre.sub(r'\\\g<0>', name)
+ return '%s%s%s <%s>' % (quotes, name, quotes, address)
+ return address
+
+
+class Email(ResourceAccessMixin, ModelSQL, ModelView):
+ "Email"
+ __name__ = 'ir.email'
+
+ user = fields.Function(fields.Char("User"), 'get_user')
+ at = fields.Function(fields.DateTime("At"), 'get_at')
+ recipients = fields.Char("Recipients")
+ recipients_secondary = fields.Char("Secondary Recipients")
+ recipients_hidden = fields.Char("Hidden Recipients")
+ addresses = fields.One2Many('ir.email.address', 'email', "Addresses")
+ subject = fields.Char("Subject")
+ body = fields.Char("Body")
+
+ @classmethod
+ def __setup__(cls):
+ super().__setup__()
+ cls._order.insert(0, ('create_date', 'DESC'))
+ cls.__rpc__.update({
+ 'send': RPC(readonly=False, result=int),
+ 'complete': RPC(),
+ })
+ del cls.__rpc__['create']
+
+ def get_user(self, name):
+ return self.create_uid.rec_name
+
+ def get_at(self, name):
+ return self.create_date.replace(microsecond=0)
+
+ @classmethod
+ def send(cls, to='', cc='', bcc='', subject='', body='',
+ attachments=None, record=None, reports=None):
+ pool = Pool()
+ User = pool.get('res.user')
+ ActionReport = pool.get('ir.action.report')
+ Attachment = pool.get('ir.attachment')
+ transaction = Transaction()
+ user = User(transaction.user)
+
+ Model = pool.get(record[0])
+ record = Model(record[1])
+
+ body_html = HTML_EMAIL % {
+ 'subject': subject,
+ 'body': body,
+ 'signature': user.signature or '',
+ }
+ content = MIMEMultipart('alternative')
+ if html2text:
+ body_text = HTML_EMAIL % {
+ 'subject': subject,
+ 'body': body,
+ 'signature': '',
+ }
+ converter = html2text.HTML2Text()
+ body_text = converter.handle(body_text)
+ if user.signature:
+ body_text += '\n-- \n' + converter.handle(user.signature)
+ part = MIMEText(body_text, 'plain', _charset='utf-8')
+ content.attach(part)
+ part = MIMEText(body_html, 'html', _charset='utf-8')
+ content.attach(part)
+ if reports or attachments:
+ msg = MIMEMultipart('mixed')
+ msg.attach(content)
+ if attachments is None:
+ attachments = []
+ else:
+ attachments = list(attachments)
+
+ for report_id in (reports or []):
+ report = ActionReport(report_id)
+ Report = pool.get(report.report_name, type='report')
+ ext, content, _, title = Report.execute(
+ [record.id], {
+ 'action_id': report.id,
+ })
+ name = '%s.%s' % (title, ext)
+ if isinstance(content, str):
+ content = content.encode('utf-8')
+ attachments.append((name, content))
+
+ for name, data in attachments:
+ mimetype, _ = mimetypes.guess_type(name)
+ if mimetype:
+ attachment = MIMENonMultipart(*mimetype.split('/'))
+ attachment.set_payload(data)
+ encode_base64(attachment)
+ else:
+ attachment = MIMEApplication(data)
+ attachment.add_header(
+ 'Content-Disposition', 'attachment',
+ filename=('utf-8', '', name))
+ msg.attach(attachment)
+ else:
+ msg = content
+ msg['From'] = from_ = config.get('email', 'from')
+ if user.email:
+ if user.name:
+ user_email = formataddr((user.name, user.email))
+ else:
+ user_email = user.email
+ msg['Behalf-Of'] = user_email
+ msg['Reply-To'] = user_email
+ msg['To'] = ', '.join(formataddr(a) for a in getaddresses([to]))
+ msg['Cc'] = ', '.join(formataddr(a) for a in getaddresses([cc]))
+ msg['Subject'] = Header(subject, 'utf-8')
+
+ to_addrs = list(filter(None, map(
+ str.strip,
+ _get_emails(to) + _get_emails(cc) + _get_emails(bcc))))
+ sendmail_transactional(
+ from_, to_addrs, msg, datamanager=SMTPDataManager(strict=True))
+
+ email = cls(
+ recipients=to,
+ recipients_secondary=cc,
+ recipients_hidden=bcc,
+ addresses=[{'address': a} for a in to_addrs],
+ subject=subject,
+ body=body,
+ resource=record)
+ email.save()
+ with Transaction().set_context(_check_access=False):
+ attachments_ = []
+ for name, data in attachments:
+ attachments_.append(
+ Attachment(resource=email, name=name, data=data))
+ Attachment.save(attachments_)
+ return email
+
+ @classmethod
+ def complete(cls, text, limit):
+ limit = int(limit)
+ if not limit > 0:
+ raise ValueError('limit must be > 0: %r' % (limit,))
+ emails = getaddresses([text])
+ if not emails:
+ return []
+ name, email = map(str.strip, emails[-1])
+ if not name and not email:
+ return []
+ s = StringMatcher()
+ s.set_seq2(_formataddr((name, email)))
+
+ def generate(name, email):
+ for name, email in cls._match(name, email):
+ address = _formataddr((name, email))
+ s.set_seq1(address)
+ yield (
+ s.ratio(), address,
+ ', '.join(map(_formataddr, emails[:-1] + [(name, email)])))
+ return heapq.nlargest(limit, generate(name, email))
+
+ @classmethod
+ def _match(cls, name, email):
+ pool = Pool()
+ User = pool.get('res.user')
+ domain = ['OR']
+ for field in ['name', 'login', 'email']:
+ for value in [name, email]:
+ if value and len(value) >= 3:
+ domain.append(
+ (field, 'ilike', '%' + escape_wildcard(value) + '%'))
+ for user in User.search([
+ ('email', '!=', ''),
+ domain,
+ ], order=[]):
+ yield user.name, user.email
+
+
+class EmailAddress(ModelSQL):
+ "Email Address"
+ __name__ = 'ir.email.address'
+
+ email = fields.Many2One(
+ 'ir.email', "E-mail", required=True, ondelete='CASCADE', select=True)
+ address = fields.Char("Address", required=True, select=True)
+
+
+class EmailTemplate(ModelSQL, ModelView):
+ "Email Template"
+ __name__ = 'ir.email.template'
+
+ model = fields.Many2One('ir.model', "Model", required=True)
+ name = fields.Char("Name", required=True, translate=True)
+ recipients = fields.Many2One(
+ 'ir.model.field', "Recipients",
+ domain=[
+ ('model', '=', Eval('model')),
+ ],
+ depends=['model'],
+ help="The field that contains the recipient(s).")
+ recipients_secondary = fields.Many2One(
+ 'ir.model.field', "Secondary Recipients",
+ domain=[
+ ('model', '=', Eval('model')),
+ ],
+ depends=['model'],
+ help="The field that contains the secondary recipient(s).")
+ recipients_hidden = fields.Many2One(
+ 'ir.model.field', "Hidden Recipients",
+ domain=[
+ ('model', '=', Eval('model')),
+ ],
+ depends=['model'],
+ help="The field that contains the secondary recipient(s).")
+ subject = fields.Char("Subject", translate=True)
+ body = fields.Text("Body", translate=True)
+ reports = fields.Many2Many(
+ 'ir.email.template-ir.action.report', 'template', 'report',
+ "Reports",
+ domain=[
+ ('model', '=', Eval('model_name')),
+ ],
+ depends=['model_name'])
+
+ model_name = fields.Function(
+ fields.Char("Model Name"), 'on_change_with_model_name')
+
+ @classmethod
+ def __setup__(cls):
+ super().__setup__()
+ for field in [
+ 'recipients',
+ 'recipients_secondary',
+ 'recipients_hidden',
+ ]:
+ field = getattr(cls, field)
+ field.domain.append(['OR',
+ ('relation', 'in', cls.email_models()),
+ [
+ ('model.model', 'in', cls.email_models()),
+ ('name', '=', 'id'),
+ ],
+ ])
+ cls.__rpc__.update({
+ 'get': RPC(instantiate=0),
+ 'get_default': RPC(),
+ })
+
+ @fields.depends('model')
+ def on_change_with_model_name(self, name=None):
+ if self.model:
+ return self.model.model
+
+ @classmethod
+ def validate(cls, templates):
+ super().validate(templates)
+ for template in templates:
+ template.check_subject()
+ template.check_body()
+
+ def check_subject(self):
+ if not self.subject:
+ return
+ try:
+ TextTemplate(self.subject)
+ except Exception as exception:
+ raise EmailTemplateError(
+ gettext('ir.msg_email_template_invalid_subject',
+ template=self.rec_name,
+ exception=exception)) from exception
+
+ def check_body(self):
+ if not self.body:
+ return
+ try:
+ TextTemplate(self.body)
+ except Exception as exception:
+ raise EmailTemplateError(
+ gettext('ir.msg_email_template_invalid_body',
+ template=self.rec_name,
+ exception=exception)) from exception
+
+ def get(self, record):
+ pool = Pool()
+ Model = pool.get(self.model.model)
+ record = Model(int(record))
+
+ values = {}
+ for field, key in [
+ ('recipients', 'to'),
+ ('recipients_secondary', 'cc'),
+ ('recipients_hidden', 'bcc'),
+ ]:
+ field = getattr(self, field)
+ if not field:
+ continue
+ try:
+ if field.name == 'id':
+ value = record
+ else:
+ value = getattr(record, field.name, None)
+ if value:
+ values[key] = self.get_addresses(value)
+ except AccessError:
+ continue
+
+ if self.subject:
+ try:
+ values['subject'] = (TextTemplate(self.subject)
+ .generate(record=record)
+ .render())
+ except AccessError:
+ pass
+ if self.body:
+ try:
+ values['body'] = (TextTemplate(self.body)
+ .generate(record=record)
+ .render())
+ except AccessError:
+ pass
+ if self.reports:
+ values['reports'] = [r.id for r in self.reports]
+ return values
+
+ @classmethod
+ def _get_default_exclude(cls, record):
+ return ['create_uid', 'write_uid']
+
+ @classmethod
+ def get_default(cls, model, record):
+ pool = Pool()
+ Field = pool.get('ir.model.field')
+ Model = pool.get(model)
+ record = Model(int(record))
+ values = {}
+
+ fields = Field.search([
+ ('model.model', '=', model),
+ ('name', 'not in', cls._get_default_exclude(record)),
+ ['OR',
+ ('relation', 'in', cls.email_models()),
+ [
+ ('model.model', 'in', cls.email_models()),
+ ('name', '=', 'id'),
+ ],
+ ],
+ ])
+ values['to'] = addresses = []
+ for field in fields:
+ try:
+ if field.name == 'id':
+ value = record
+ else:
+ value = getattr(record, field.name)
+ addresses += cls.get_addresses(value)
+ except AccessError:
+ pass
+
+ try:
+ values['subject'] = '%s: %s' % (
+ Model.__names__()['model'], record.rec_name)
+ except AccessError:
+ pass
+ return values
+
+ @classmethod
+ def email_models(cls):
+ return ['res.user']
+
+ @classmethod
+ def get_addresses(cls, value):
+ if isinstance(value, (list, tuple)):
+ addresses = (cls._get_address(v) for v in value)
+ else:
+ addresses = [cls._get_address(value)]
+ return [
+ _formataddr((name, email))
+ for name, email in filter(None, addresses)
+ if email]
+
+ @classmethod
+ def _get_address(cls, record):
+ pool = Pool()
+ User = pool.get('res.user')
+ if isinstance(record, User) and record.email:
+ return (record.name, record.email)
+
+ @classmethod
+ def get_languages(cls, value):
+ pool = Pool()
+ Configuration = pool.get('ir.configuration')
+ Lang = pool.get('ir.lang')
+ if isinstance(value, (list, tuple)):
+ languagues = {cls._get_language(v) for v in value}
+ else:
+ languagues = {cls._get_language(value)}
+ languagues = list(filter(None, languagues))
+ if not languagues:
+ return Lang.search([
+ ('code', '=', Configuration.get_language()),
+ ], limit=1)
+ return languagues
+
+ @classmethod
+ def _get_language(cls, record):
+ pool = Pool()
+ User = pool.get('res.user')
+ if isinstance(record, User) and record.language:
+ return record.language
+
+
+class EmailTemplate_Report(ModelSQL):
+ "Email Template - Report"
+ __name__ = 'ir.email.template-ir.action.report'
+
+ template = fields.Many2One(
+ 'ir.email.template', "Template", required=True, ondelete='CASCADE')
+ report = fields.Many2One(
+ 'ir.action.report', "Report", required=True, ondelete='CASCADE')
diff --git a/trytond/ir/message.xml b/trytond/ir/message.xml
index 2407881b2a..3e7899c51f 100644
--- a/trytond/ir/message.xml
+++ b/trytond/ir/message.xml
@@ -311,13 +311,17 @@ this repository contains the full copyright notices and license terms. -->
Notes
-
You are not allowed to execute wizard "%(wizard)s".
You are not allowed to execute wizard "%(wizard)s" on "%(model)s".
-
+
+ Invalid subject in e-mail template "%(template)s" with exception "%(exception)s"
+
+
+ Invalid body in e-mail template "%(template)s" with exception "%(exception)s"
+
diff --git a/trytond/ir/tryton.cfg b/trytond/ir/tryton.cfg
index 45e2e0454d..acadb72d70 100644
--- a/trytond/ir/tryton.cfg
+++ b/trytond/ir/tryton.cfg
@@ -20,3 +20,4 @@ xml:
calendar_.xml
message.xml
queue.xml
+ email.xml
diff --git a/trytond/ir/view/action_report_form.xml b/trytond/ir/view/action_report_form.xml
index 6123dffcbc..0acb0a3bb6 100644
--- a/trytond/ir/view/action_report_form.xml
+++ b/trytond/ir/view/action_report_form.xml
@@ -34,8 +34,6 @@ this repository contains the full copyright notices and license terms. -->
-
-
diff --git a/trytond/ir/view/action_wizard_form.xml b/trytond/ir/view/action_wizard_form.xml
index 5f6b3319df..8ac9714b7a 100644
--- a/trytond/ir/view/action_wizard_form.xml
+++ b/trytond/ir/view/action_wizard_form.xml
@@ -15,8 +15,6 @@ this repository contains the full copyright notices and license terms. -->
-
-
diff --git a/trytond/ir/view/email_form.xml b/trytond/ir/view/email_form.xml
new file mode 100644
index 0000000000..b60307d7fa
--- /dev/null
+++ b/trytond/ir/view/email_form.xml
@@ -0,0 +1,19 @@
+
+
+
diff --git a/trytond/ir/view/email_list.xml b/trytond/ir/view/email_list.xml
new file mode 100644
index 0000000000..d3398818ef
--- /dev/null
+++ b/trytond/ir/view/email_list.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/trytond/ir/view/email_template_form.xml b/trytond/ir/view/email_template_form.xml
new file mode 100644
index 0000000000..5514f81cca
--- /dev/null
+++ b/trytond/ir/view/email_template_form.xml
@@ -0,0 +1,20 @@
+
+
+
diff --git a/trytond/ir/view/email_template_list.xml b/trytond/ir/view/email_template_list.xml
new file mode 100644
index 0000000000..4cf12b2929
--- /dev/null
+++ b/trytond/ir/view/email_template_list.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/trytond/model/modelview.py b/trytond/model/modelview.py
index 1e8f92b8d5..b3d027250a 100644
--- a/trytond/model/modelview.py
+++ b/trytond/model/modelview.py
@@ -377,6 +377,7 @@ class ModelView(Model):
pool = Pool()
Action = pool.get('ir.action.keyword')
Export = pool.get('ir.export')
+ Email = pool.get('ir.email.template')
key = cls.__name__
result = cls._view_toolbar_get_cache.get(key)
if result:
@@ -387,11 +388,15 @@ class ModelView(Model):
exports = Export.search_read(
[('resource', '=', cls.__name__)],
fields_names=['name', 'export_fields.name'])
+ emails = Email.search_read(
+ [('model.model', '=', cls.__name__)],
+ fields_names=['name'])
result = {
'print': prints,
'action': actions,
'relate': relates,
'exports': exports,
+ 'emails': emails,
}
cls._view_toolbar_get_cache.set(key, result)
return result
diff --git a/trytond/res/ir.xml b/trytond/res/ir.xml
index da94f38f6e..98c630cf5f 100644
--- a/trytond/res/ir.xml
+++ b/trytond/res/ir.xml
@@ -1034,5 +1034,20 @@ this repository contains the full copyright notices and license terms. -->
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/trytond/res/view/user_form.xml b/trytond/res/view/user_form.xml
index 2139d6f82c..dd2ac46a91 100644
--- a/trytond/res/view/user_form.xml
+++ b/trytond/res/view/user_form.xml
@@ -16,7 +16,7 @@ this repository contains the full copyright notices and license terms. -->
-
+
diff --git a/trytond/res/view/user_form_preferences.xml b/trytond/res/view/user_form_preferences.xml
index b339bed4d5..7680d72517 100644
--- a/trytond/res/view/user_form_preferences.xml
+++ b/trytond/res/view/user_form_preferences.xml
@@ -11,7 +11,7 @@ this repository contains the full copyright notices and license terms. -->
-
+
diff --git a/trytond/tests/test_ir.py b/trytond/tests/test_ir.py
index f51db83a15..c35466cd3c 100644
--- a/trytond/tests/test_ir.py
+++ b/trytond/tests/test_ir.py
@@ -3,7 +3,9 @@
from dateutil.relativedelta import relativedelta
import datetime
import unittest
+from unittest.mock import patch, ANY
+from trytond.config import config
from trytond.pool import Pool
from trytond.transaction import Transaction
from .test_tryton import ModuleTestCase, with_transaction
@@ -101,6 +103,122 @@ class IrTestCase(ModuleTestCase):
self.assertEqual(admin_id, admin.id)
+ @with_transaction()
+ def test_email_send(self):
+ "Test sending email"
+ pool = Pool()
+ Email = pool.get('ir.email')
+ Report = pool.get('ir.action.report')
+ Attachment = pool.get('ir.attachment')
+
+ report = Report(
+ name="Test Email",
+ model='res.user',
+ report_name='tests.email_send',
+ report_content=b'report',
+ template_extension='txt',
+ )
+ report.save()
+
+ with patch('trytond.ir.email_.sendmail_transactional') as sendmail:
+ email = Email.send(
+ to='"John Doe" , Jane ',
+ cc='User ',
+ bcc='me@example.com',
+ subject="Email subject",
+ body='Hello
',
+ attachments=[('file.txt', b'data')],
+ record=('res.user', 1),
+ reports=[report.id])
+
+ attachments = Attachment.search([
+ ('resource', '=', str(email)),
+ ])
+
+ addresses = [
+ 'john@example.com',
+ 'jane@example.com',
+ 'user@example.com',
+ 'me@example.com']
+ sendmail.assert_called_once_with(
+ config.get('email', 'from'), addresses, ANY, datamanager=ANY)
+ self.assertEqual(
+ email.recipients,
+ '"John Doe" , Jane ')
+ self.assertEqual(email.recipients_secondary, 'User ')
+ self.assertEqual(email.recipients_hidden, 'me@example.com')
+ self.assertEqual(
+ [a.address for a in email.addresses],
+ addresses)
+ self.assertEqual(email.subject, "Email subject")
+ self.assertEqual(email.body, 'Hello
')
+ self.assertEqual(len(attachments), 2)
+ self.assertEqual(
+ {a.name for a in attachments}, {'file.txt', 'Test Email.txt'})
+ self.assertEqual(
+ {a.data for a in attachments}, {b'data', b'report'})
+
+ @with_transaction()
+ def test_email_template_get(self):
+ "Test email template get"
+ pool = Pool()
+ Template = pool.get('ir.email.template')
+ IrModel = pool.get('ir.model')
+ IrModelField = pool.get('ir.model.field')
+ User = pool.get('res.user')
+
+ admin = User(1)
+ admin.email = 'admin@example.com'
+ admin.save()
+ model, = IrModel.search([('model', '=', 'res.user')])
+ field, = IrModelField.search([
+ ('model', '=', model.id),
+ ('name', '=', 'id'),
+ ])
+
+ template = Template(
+ model=model,
+ name="Test",
+ recipients=field,
+ subject="Subject: ${record.login}",
+ body="Hello, ${record.name}
")
+ template.save()
+
+ values = template.get(admin)
+
+ self.assertEqual(
+ values, {
+ 'to': ['Administrator '],
+ 'subject': "Subject: admin",
+ 'body': 'Hello, Administrator
',
+ })
+
+ @with_transaction()
+ def test_email_template_get_default(self):
+ "Test email template get default"
+ pool = Pool()
+ Template = pool.get('ir.email.template')
+ IrModel = pool.get('ir.model')
+ IrModelField = pool.get('ir.model.field')
+ User = pool.get('res.user')
+
+ admin = User(1)
+ admin.email = 'admin@example.com'
+ admin.save()
+ model, = IrModel.search([('model', '=', 'res.user')])
+ field, = IrModelField.search([
+ ('model', '=', model.id),
+ ('name', '=', 'id'),
+ ])
+
+ values = Template.get_default(User.__name__, admin.id)
+
+ self.assertEqual(
+ values, {
+ 'to': ['Administrator '],
+ 'subject': "User: Administrator",
+ })
+
def suite():
return unittest.TestLoader().loadTestsFromTestCase(IrTestCase)