details: https://code.tryton.org/tryton/commit/79ee566ee0dc
branch: default
user: Cédric Krier <[email protected]>
date: Thu Dec 11 16:31:51 2025 +0100
description:
Support the conversion of MJML reports to HTML
Closes #12597
diffstat:
trytond/CHANGELOG | 1 +
trytond/doc/ref/report.rst | 11 +++++++++++
trytond/setup.py | 3 ++-
trytond/tox.ini | 1 +
trytond/trytond/report/__init__.py | 4 ++--
trytond/trytond/report/report.py | 26 ++++++++++++++++++++++++++
trytond/trytond/tests/report.mjml | 9 +++++++++
trytond/trytond/tests/report.py | 5 +++++
trytond/trytond/tests/report.xml | 8 ++++++++
trytond/trytond/tests/test_report.py | 18 ++++++++++++++++++
10 files changed, 83 insertions(+), 3 deletions(-)
diffs (206 lines):
diff -r 817136daa763 -r 79ee566ee0dc trytond/CHANGELOG
--- a/trytond/CHANGELOG Wed Feb 18 11:28:35 2026 +0100
+++ b/trytond/CHANGELOG Thu Dec 11 16:31:51 2025 +0100
@@ -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
diff -r 817136daa763 -r 79ee566ee0dc trytond/doc/ref/report.rst
--- a/trytond/doc/ref/report.rst Wed Feb 18 11:28:35 2026 +0100
+++ b/trytond/doc/ref/report.rst Thu Dec 11 16:31:51 2025 +0100
@@ -129,6 +129,10 @@
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 @@
: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.
diff -r 817136daa763 -r 79ee566ee0dc trytond/setup.py
--- a/trytond/setup.py Wed Feb 18 11:28:35 2026 +0100
+++ b/trytond/setup.py Thu Dec 11 16:31:51 2025 +0100
@@ -81,7 +81,7 @@
'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 @@
'Levenshtein': ['python-Levenshtein'],
'html2text': ['html2text'],
'weasyprint': ['weasyprint'],
+ 'mjml': ['mrml'],
'coroutine': ['gevent>=1.1'],
'image': ['pillow'],
'barcode': ['python-barcode[images]'],
diff -r 817136daa763 -r 79ee566ee0dc trytond/tox.ini
--- a/trytond/tox.ini Wed Feb 18 11:28:35 2026 +0100
+++ b/trytond/tox.ini Thu Dec 11 16:31:51 2025 +0100
@@ -8,6 +8,7 @@
barcode
qrcode
email-validation
+ mjml
commands =
coverage run --omit=*/tests/* -m xmlrunner discover -s trytond.tests
{posargs}
commands_post =
diff -r 817136daa763 -r 79ee566ee0dc trytond/trytond/report/__init__.py
--- a/trytond/trytond/report/__init__.py Wed Feb 18 11:28:35 2026 +0100
+++ b/trytond/trytond/report/__init__.py Thu Dec 11 16:31:51 2025 +0100
@@ -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]
diff -r 817136daa763 -r 79ee566ee0dc trytond/trytond/report/report.py
--- a/trytond/trytond/report/report.py Wed Feb 18 11:28:35 2026 +0100
+++ b/trytond/trytond/report/report.py Thu Dec 11 16:31:51 2025 +0100
@@ -130,6 +130,19 @@
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 @@
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 @@
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
diff -r 817136daa763 -r 79ee566ee0dc trytond/trytond/tests/report.mjml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/trytond/trytond/tests/report.mjml Thu Dec 11 16:31:51 2025 +0100
@@ -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>
diff -r 817136daa763 -r 79ee566ee0dc trytond/trytond/tests/report.py
--- a/trytond/trytond/tests/report.py Wed Feb 18 11:28:35 2026 +0100
+++ b/trytond/trytond/tests/report.py Thu Dec 11 16:31:51 2025 +0100
@@ -9,7 +9,12 @@
__name__ = 'test.test_report'
+class TestReportMJML(Report):
+ __name__ = 'test.test_report_mjml'
+
+
def register(module):
Pool.register(
TestReport,
+ TestReportMJML,
module=module, type_='report')
diff -r 817136daa763 -r 79ee566ee0dc trytond/trytond/tests/report.xml
--- a/trytond/trytond/tests/report.xml Wed Feb 18 11:28:35 2026 +0100
+++ b/trytond/trytond/tests/report.xml Thu Dec 11 16:31:51 2025 +0100
@@ -9,5 +9,13 @@
<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>
diff -r 817136daa763 -r 79ee566ee0dc trytond/trytond/tests/test_report.py
--- a/trytond/trytond/tests/test_report.py Wed Feb 18 11:28:35 2026 +0100
+++ b/trytond/trytond/tests/test_report.py Thu Dec 11 16:31:51 2025 +0100
@@ -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 @@
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"