diff --git a/trytond/CHANGELOG b/trytond/CHANGELOG index 20555e072a..c77737ac1f 100644 --- a/trytond/CHANGELOG +++ b/trytond/CHANGELOG @@ -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 --git a/trytond/doc/ref/report.rst b/trytond/doc/ref/report.rst index 333abe1f20..a223763cf3 100644 --- a/trytond/doc/ref/report.rst +++ b/trytond/doc/ref/report.rst @@ -129,6 +129,10 @@ Class methods are: Email ===== +Email reports must have ``HTML`` as output format. +`MJML `_ 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. diff --git a/trytond/setup.py b/trytond/setup.py index 742674aa14..c0e16bb795 100755 --- a/trytond/setup.py +++ b/trytond/setup.py @@ -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]'], diff --git a/trytond/tox.ini b/trytond/tox.ini index a8ebea2070..448bbd0011 100644 --- a/trytond/tox.ini +++ b/trytond/tox.ini @@ -8,6 +8,7 @@ extras = barcode qrcode email-validation + mjml commands = coverage run --omit=*/tests/* -m xmlrunner discover -s trytond.tests {posargs} commands_post = diff --git a/trytond/trytond/report/__init__.py b/trytond/trytond/report/__init__.py index 3967dd8114..b29485fb47 100644 --- a/trytond/trytond/report/__init__.py +++ b/trytond/trytond/report/__init__.py @@ -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 --git a/trytond/trytond/report/report.py b/trytond/trytond/report/report.py index 136aa76ae0..a713931f88 100644 --- a/trytond/trytond/report/report.py +++ b/trytond/trytond/report/report.py @@ -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(' + + + + ${user.name} + + + + diff --git a/trytond/trytond/tests/report.py b/trytond/trytond/tests/report.py index 645c2ad87c..471ac510ed 100644 --- a/trytond/trytond/tests/report.py +++ b/trytond/trytond/tests/report.py @@ -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') diff --git a/trytond/trytond/tests/report.xml b/trytond/trytond/tests/report.xml index 227d9fe2ee..04068ba568 100644 --- a/trytond/trytond/tests/report.xml +++ b/trytond/trytond/tests/report.xml @@ -9,5 +9,13 @@ this repository contains the full copyright notices and license terms. --> tests/report.txt txt + + + Test Report MJML + test.test_report_mjml + tests/report.mjml + xml + html + diff --git a/trytond/trytond/tests/test_report.py b/trytond/trytond/tests/test_report.py index 4140da7387..2bb7ace205 100644 --- a/trytond/trytond/tests/test_report.py +++ b/trytond/trytond/tests/test_report.py @@ -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], '') + @with_transaction() def test_get_email_html(self): "Test get email"