Support the conversion of MJML reports to HTML

Closes #12597
This commit is contained in:
Cédric Krier
2025-12-11 16:31:51 +01:00
parent eeef31b221
commit b034e23e91
10 changed files with 83 additions and 3 deletions
+1
View File
@@ -1,3 +1,4 @@
* Support the conversion of MJML reports to HTML
* Allow filtering users to be notified by cron tasks
Version 7.8.0 - 2025-12-15
+11
View File
@@ -129,6 +129,10 @@ Class methods are:
Email
=====
Email reports must have ``HTML`` as output format.
`MJML <https://mjml.io/>`_ syntax can be used with the ``XML`` input format and
``HTML`` output format to generate responsive emails.
.. function:: get_email(report, record, languages)
Returns the :py:class:`~email.message.EmailMessage` and title using the
@@ -136,3 +140,10 @@ Email
:class:`~trytond.model.ModelStorage` record for each language.
.. note:: Order languages with the preferred last.
.. function:: mjml_to_html(content)
Converts ``MJML`` content to ``HTML``.
.. warning::
It may return the content unconverted if some dependencies are missing.
+2 -1
View File
@@ -81,7 +81,7 @@ setup(name=name,
'trytond.ir.ui': ['*.xml', '*.rng', '*.rnc'],
'trytond.res': [
'tryton.cfg', '*.xml', '*.html', 'view/*.xml', 'locale/*.po'],
'trytond.tests': ['tryton.cfg', '*.xml', '*.txt'],
'trytond.tests': ['tryton.cfg', '*.xml', '*.txt', '*.mjml'],
},
scripts=[
'bin/trytond',
@@ -154,6 +154,7 @@ setup(name=name,
'Levenshtein': ['python-Levenshtein'],
'html2text': ['html2text'],
'weasyprint': ['weasyprint'],
'mjml': ['mrml'],
'coroutine': ['gevent>=1.1'],
'image': ['pillow'],
'barcode': ['python-barcode[images]'],
+1
View File
@@ -8,6 +8,7 @@ extras =
barcode
qrcode
email-validation
mjml
commands =
coverage run --omit=*/tests/* -m xmlrunner discover -s trytond.tests {posargs}
commands_post =
+2 -2
View File
@@ -1,5 +1,5 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from .report import Report, get_email
from .report import Report, get_email, mjml_to_html
__all__ = [Report, get_email]
__all__ = [Report, get_email, mjml_to_html]
+26
View File
@@ -130,6 +130,19 @@ class TranslateFactory:
self.report_name, text, text_plural, n)
def _lazy_import(name):
attr_name = f'_mod_{name}'
if not hasattr(_lazy_import, attr_name):
try:
mod = __import__(name)
except ImportError:
mod = None
setattr(_lazy_import, attr_name, mod)
else:
mod = getattr(_lazy_import, attr_name)
return mod
class Report(URLMixin, PoolBase):
@classmethod
@@ -414,6 +427,13 @@ class Report(URLMixin, PoolBase):
and output_format == 'pdf'):
return output_format, weasyprint.HTML(string=data).write_pdf()
if (input_format == 'xml'
and output_format == 'html'
and isinstance(data, str)
and data.startswith('<mjml')):
if mrml := _lazy_import('mrml'):
return output_format, mrml.to_html(data).content
if input_format == output_format and output_format in MIMETYPES:
return output_format, data
@@ -637,3 +657,9 @@ def get_email(report, record, languages):
msg.add_header(
'Content-Language', ', '.join(l.code for l in languages))
return msg, title
def mjml_to_html(content):
if mrml := _lazy_import('mrml'):
content = mrml.to_html(content).content
return content
+9
View File
@@ -0,0 +1,9 @@
<mjml xmlns:py="http://genshi.edgewall.org/">
<mj-body>
<mj-section>
<mj-column>
${user.name}
</mj-column>
</mj-section>
</mj-body>
</mjml>
+5
View File
@@ -9,7 +9,12 @@ class TestReport(Report):
__name__ = 'test.test_report'
class TestReportMJML(Report):
__name__ = 'test.test_report_mjml'
def register(module):
Pool.register(
TestReport,
TestReportMJML,
module=module, type_='report')
+8
View File
@@ -9,5 +9,13 @@ this repository contains the full copyright notices and license terms. -->
<field name="report">tests/report.txt</field>
<field name="template_extension">txt</field>
</record>
<record model="ir.action.report" id="test_report_mjml">
<field name="name">Test Report MJML</field>
<field name="report_name">test.test_report_mjml</field>
<field name="report">tests/report.mjml</field>
<field name="template_extension">xml</field>
<field name="extension">html</field>
</record>
</data>
</tryton>
+18
View File
@@ -1,9 +1,15 @@
# 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 datetime
import unittest
from email.message import EmailMessage
from unittest.mock import Mock, patch
try:
import mrml
except ImportError:
mrml = None
from trytond.model.exceptions import AccessError
from trytond.pool import Pool
from trytond.report.report import Report, get_email
@@ -87,6 +93,18 @@ class ReportTestCase(TestCase):
with self.assertRaises(AccessError):
Report.execute([1], {'model': 'test.access'})
@unittest.skipUnless(mrml, "required mrml")
@with_transaction()
def test_convert_mjml_to_html(self):
"Test convert MJML to HTML"
pool = Pool()
Report = pool.get('test.test_report_mjml', type='report')
oext, content, _, _ = Report.execute([1], {})
self.assertEqual(oext, 'html')
self.assertEqual(content[:15], '<!doctype html>')
@with_transaction()
def test_get_email_html(self):
"Test get email"