[IMP][FIX]DOCX Report Generation : enhancements

* Fixes for assets ;
* Handle HTML fields with basic rendering ;
* Add Jinja filters ;
* Add JSON Loads for TPL usage (context).
This commit is contained in:
Fabien BOURGEOIS 2025-02-03 19:03:56 +01:00
parent 324810c3ff
commit 58cb82cdfa
2 changed files with 85 additions and 14 deletions

View File

@ -11,20 +11,20 @@
This is the beta version, bugs may be present. This is the beta version, bugs may be present.
""", """,
"author": "RYDLAB", "author": "RYDLAB, Yaltik",
"website": "https://rydlab.ru", "website": "https://rydlab.ru",
"category": "Technical", "category": "Technical",
"version": "16.0.2.0.0", "version": "16.0.2.1.0",
"license": "LGPL-3", "license": "LGPL-3",
"depends": ["base", "web", "custom_report_field", "report_monetary_helpers"], "depends": ["base", "web", "custom_report_field", "report_monetary_helpers"],
"external_dependencies": {"python": ["docxcompose", "docxtpl", "bs4"]}, "external_dependencies": {"python": ["docxcompose", "docxtpl", "beautifulsoup4"]},
"data": [ "data": [
"views/ir_actions_report_views.xml", "views/ir_actions_report_views.xml",
], ],
"assets": { "assets": {
"web.assets_backend": [ "web.assets_backend": [
"docx_report/static/src/css/mimetypes.css", "docx_report_generation/static/src/css/mimetypes.css",
"docx_report/static/src/js/action_manager_report.js", "docx_report_generation/static/src/js/action_manager_report.js",
], ],
}, },
"images": ["static/description/banner.jpg"], "images": ["static/description/banner.jpg"],

View File

@ -2,11 +2,20 @@ from base64 import b64decode
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from collections import OrderedDict from collections import OrderedDict
from io import BytesIO from io import BytesIO
from functools import partial
from re import findall
from json import loads
from logging import getLogger from logging import getLogger
from lxml import etree
from docx import Document from docx import Document
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import RGBColor
from docxcompose.composer import Composer from docxcompose.composer import Composer
from docxtpl import DocxTemplate from docxtpl import DocxTemplate
import jinja2
from requests import codes as codes_request, post as post_request from requests import codes as codes_request, post as post_request
from requests.exceptions import RequestException from requests.exceptions import RequestException
@ -14,6 +23,7 @@ from odoo import _, api, fields, models
from odoo.exceptions import AccessError, UserError from odoo.exceptions import AccessError, UserError
from odoo.http import request from odoo.http import request
from odoo.tools.safe_eval import safe_eval, time from odoo.tools.safe_eval import safe_eval, time
from odoo.tools.misc import format_date
try: try:
from odoo.addons.gotenberg.service.utils import ( from odoo.addons.gotenberg.service.utils import (
@ -419,24 +429,77 @@ class IrActionsReport(models.Model):
) )
record_to_render = values["docs"] record_to_render = values["docs"]
# No more HTML / Markup stripping
docs = { docs = {
key: record_to_render[key] key: record_to_render[key]
for key in record_to_render._fields.keys() for key in record_to_render._fields.keys()
if not isinstance(record_to_render[key], fields.Markup) # if not isinstance(record_to_render[key], fields.Markup)
} }
docs.update(
{
key: self._parse_markup(record_to_render[key])
for key in record_to_render._fields.keys()
if isinstance(record_to_render[key], fields.Markup)
}
)
values["docs"] = docs values["docs"] = docs
def _html_generate(field, tpl):
def _create_list(paragraph, list_type):
p = paragraph._p #access to xml paragraph element
pPr = p.get_or_add_pPr() #access paragraph properties
numPr = OxmlElement('w:numPr') #create number properties element
numId = OxmlElement('w:numId') #create numId element - sets bullet type
numId.set(qn('w:val'), list_type) #set list type/indentation
numPr.append(numId) #add bullet type to number properties list
pPr.append(numPr) #add number properties to paragraph
if isinstance(field, fields.Markup):
html_field = str(field).replace('<br>', '').replace('&nbsp;', ' ')
xml_tree = etree.fromstring('<root>%s</root>' % html_field)
md = tpl.new_subdoc()
for child in xml_tree.iter():
if child.tag in ('p', 'strong', 'em', 's', 'u', 'ul',
'ol', 'li', 'font', 'a'):
if child.tag == 'p':
p = md.add_paragraph(child.text)
elif child.tag == 'a':
p.add_run(child.text).style = 'Hyperlink'
elif child.tag == 'strong':
p.add_run(child.text).bold = True
p.add_run(' ')
elif child.tag == 'em':
p.add_run(child.text).italic = True
p.add_run(' ')
elif child.tag == 'u':
p.add_run(child.text).underline = True
p.add_run(' ')
elif child.tag == 's':
p.add_run(child.text).font.strike = True
p.add_run(' ')
elif child.tag == 'font' and 'style' in child.attrib:
if 'color' in child.get('style'):
rgb = [int(_v)
for _v in findall(r'\d+', child.get('style'))]
if len(rgb) == 3:
p.add_run(child.text).font.color.rgb = RGBColor(*rgb)
else:
p.add_run(child.text)
elif child.tag == 'ul':
list_type = '9' # 9/25/28 : •, 8 : -, 19 : v
elif child.tag == 'ol':
list_type = '30' # 27 : 1.2 sans indent, 30: 1.2
elif child.tag == 'li':
lp = md.add_paragraph(child.text, style='List Paragraph')
_create_list(lp, list_type)
elif child.tag == 'root':
continue
else: # Not handled, add text only
p.add_run(child.text)
return md
jinja_env = jinja2.Environment()
docx_content = BytesIO() docx_content = BytesIO()
with BytesIO(b64decode(template)) as template_file: with BytesIO(b64decode(template)) as template_file:
doc = DocxTemplate(template_file) doc = DocxTemplate(template_file)
doc.render(values) jinja_env.filters['htmlgen'] = partial(_html_generate, tpl=doc)
jinja_env.filters['datefmt'] = lambda dt: format_date(self.env, dt)
doc.render(values, jinja_env)
doc.save(docx_content) doc.save(docx_content)
docx_content.seek(0) docx_content.seek(0)
return docx_content return docx_content
@ -476,3 +539,11 @@ class IrActionsReport(models.Model):
_logger.exception(e) _logger.exception(e)
finally: finally:
return result return result
@api.model
def _get_rendering_context(self, report, docids, data):
data = super()._get_rendering_context(report, docids, data)
data.update({
'jloads': loads,
})
return data