@@ -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
|
||||
|
||||
@@ -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
@@ -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]'],
|
||||
|
||||
@@ -8,6 +8,7 @@ extras =
|
||||
barcode
|
||||
qrcode
|
||||
email-validation
|
||||
mjml
|
||||
commands =
|
||||
coverage run --omit=*/tests/* -m xmlrunner discover -s trytond.tests {posargs}
|
||||
commands_post =
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user