From 3cd6f17146ab144da1438908aa540645e6f92e5a Mon Sep 17 00:00:00 2001 From: Raimon Esteve Date: Mon, 11 Dec 2023 12:26:24 +0100 Subject: [PATCH] Execute reports by multiprocessing queue (timeout) (#19) * Execute reports by multiprocessing queue (timeout) #158319 * Raise UserError when queue was empty * Use subprocess and script to create report pdf | #158319 --------- Co-authored-by: Jared Esparza --- __init__.py | 10 +++---- engine.py | 16 ++++++++--- generator.py | 58 +++++++++++++++++++++++++++++++++++++++- generator_script.py | 44 ++++++++++++++++++++++++++++++ invoice/invoice.py | 2 +- message.xml | 10 +++++++ production/production.py | 2 +- purchase/purchase.py | 2 +- sale/sale.py | 2 +- stock/stock.py | 2 +- html.py => template.py | 0 html.xml => template.xml | 0 tryton.cfg | 2 +- 13 files changed, 134 insertions(+), 16 deletions(-) create mode 100644 generator_script.py rename html.py => template.py (100%) rename html.xml => template.xml (100%) diff --git a/__init__.py b/__init__.py index bcf7584..7754d15 100644 --- a/__init__.py +++ b/__init__.py @@ -5,7 +5,7 @@ from trytond.pool import Pool from trytond.report import Report from . import action from . import translation -from . import html +from . import template from . import engine from . import product from . import invoice @@ -20,10 +20,10 @@ def register(): Pool.register( action.ActionReport, action.HTMLTemplateTranslation, - html.Signature, - html.Template, - html.TemplateUsage, - html.ReportTemplate, + template.Signature, + template.Template, + template.TemplateUsage, + template.ReportTemplate, module=module, type_='model') Pool.register( translation.ReportTranslationSet, diff --git a/engine.py b/engine.py index 4283c09..cf45b76 100644 --- a/engine.py +++ b/engine.py @@ -424,9 +424,10 @@ class HTMLReportMixin: return pdf_data @classmethod - def execute(cls, ids, data): + def __execute(cls, ids, data, queue=None): cls.check_access() action, model = cls.get_action(data) + # in case is not jinja, call super() if action.template_extension != 'jinja': return super().execute(ids, data) @@ -485,7 +486,6 @@ class HTMLReportMixin: if action.html_copies and action.html_copies > 1: content = cls.merge_pdfs([content] * action.html_copies) - Printer = None try: Printer = Pool().get('printer') @@ -494,7 +494,15 @@ class HTMLReportMixin: if Printer: return Printer.send_report(oext, content, action_name, action) - return oext, content, cls.get_direct_print(action), filename + return oext, content, cls.get_direct_print(action), filename + + @classmethod + def execute(cls, ids, data): + cls.check_access() + action, model = cls.get_action(data) + if action.template_extension != 'jinja': + return super().execute(ids, data) + return cls.__execute(ids, data, queue=None) @classmethod def _execute_html_report(cls, records, data, action, side_margin=2, @@ -551,7 +559,7 @@ class HTMLReportMixin: last_footer_html=last_footer, side_margin=side_margin, extra_vertical_margin=extra_vertical_margin - ).render_html().write_pdf() + ).render_pdf() else: document = content return extension, document diff --git a/generator.py b/generator.py index 4a7f0f0..2d125df 100644 --- a/generator.py +++ b/generator.py @@ -1,5 +1,11 @@ from weasyprint import HTML, CSS - +from trytond.i18n import gettext +from trytond.exceptions import UserError +from trytond.transaction import Transaction +import os +import json +import tempfile +import subprocess class PdfGenerator: """ @@ -182,6 +188,35 @@ class PdfGenerator: return main_doc + def render_pdf(self): + context = Transaction().context + timeout_report = context.get('timeout_report', None) + + if timeout_report: + path = os.path.dirname(os.path.abspath(__file__)) + '/' + json_path = self.to_json_file() + process = subprocess.Popen(['python3', path+'generator_script.py', json_path], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + encoding='utf-8', errors='ignore') + out = None + try: + out, err = process.communicate(timeout=timeout_report) + except subprocess.TimeoutExpired: + process.kill() + out, err = process.communicate() + raise UserError(gettext('html_report.msg_error_timeout', seconds=timeout_report)) + finally: + os.remove(json_path) + + document = None + if out and os.path.exists(out.strip()): + with open(out.strip(), 'rb') as file: + document = file.read() + os.remove(out.strip()) + else: + document = self.render_html().write_pdf() + return document + @staticmethod def get_element(boxes, element): """ @@ -196,3 +231,24 @@ class PdfGenerator: box_children = PdfGenerator.get_element(box.all_children(), element) if box_children: return box_children + + def to_json_file(self): + """ + Write the PdfGenerator properties to a JSON file. + + Parameters: + - filepath: The path to the JSON file. + """ + data = { + "main_html": self.main_html, + "header_html": self.header_html, + "footer_html": self.footer_html, + "last_footer_html": self.last_footer_html, + "base_url": self.base_url, + "side_margin": self.side_margin, + "extra_vertical_margin": self.extra_vertical_margin + } + with tempfile.NamedTemporaryFile(mode='w', delete=False) as file: + filepath = file.name + json.dump(data, file) + return filepath diff --git a/generator_script.py b/generator_script.py new file mode 100644 index 0000000..fd901b0 --- /dev/null +++ b/generator_script.py @@ -0,0 +1,44 @@ +import sys +import json +import tempfile +from generator import PdfGenerator + + +def from_json_file(filepath): + """ + Read the generated JSON file and instantiate a PdfGenerator with the properties. + + Parameters: + - filepath: The path to the JSON file. + + Returns: + - An instance of PdfGenerator with the properties from the JSON file. + """ + with open(filepath, "r") as file: + data = json.load(file) + reuslt = PdfGenerator( + main_html=data["main_html"], + header_html=data["header_html"], + footer_html=data["footer_html"], + last_footer_html=data["last_footer_html"], + base_url=data["base_url"], + side_margin=data["side_margin"], + extra_vertical_margin=data["extra_vertical_margin"] + ) + return reuslt + +def main(argv): + json_filepath = argv[1] + pdf_generator = from_json_file(json_filepath) + result = pdf_generator.render_html().write_pdf() + with tempfile.NamedTemporaryFile(delete=False) as temp: + temp.write(result) + return temp.name + + +if __name__ == '__main__': + if len(sys.argv) < 2: + print("Debe proporcionar la ruta del archivo JSON como argumento.") + sys.exit(1) + print(main(sys.argv)) + sys.exit(0) \ No newline at end of file diff --git a/invoice/invoice.py b/invoice/invoice.py index 83af81b..b4b4797 100644 --- a/invoice/invoice.py +++ b/invoice/invoice.py @@ -1,7 +1,7 @@ from trytond.model import fields from trytond.pool import Pool, PoolMeta from trytond.pyson import Eval -from trytond.modules.html_report.html import HTMLPartyInfoMixin +from trytond.modules.html_report.template import HTMLPartyInfoMixin from trytond.modules.html_report.engine import HTMLReportMixin diff --git a/message.xml b/message.xml index fccbb7d..5dcb8f2 100644 --- a/message.xml +++ b/message.xml @@ -22,5 +22,15 @@ The nullslast filter only accept a list of tuples. + + The process was interrupted by %(seconds)s seconds timeout. + + + The queue (timeout) was empty and can not render the report. + + + There was an exception rendering the report: +%(result)s + diff --git a/production/production.py b/production/production.py index f7ab23f..41c7f05 100644 --- a/production/production.py +++ b/production/production.py @@ -1,7 +1,7 @@ from trytond.model import fields from trytond.pool import PoolMeta from trytond.pyson import Eval -from trytond.modules.html_report.html import HTMLPartyInfoMixin +from trytond.modules.html_report.template import HTMLPartyInfoMixin class Production(HTMLPartyInfoMixin, metaclass=PoolMeta): diff --git a/purchase/purchase.py b/purchase/purchase.py index b52125e..9427acc 100644 --- a/purchase/purchase.py +++ b/purchase/purchase.py @@ -1,6 +1,6 @@ from trytond.pool import PoolMeta from trytond.pyson import Eval -from trytond.modules.html_report.html import HTMLPartyInfoMixin +from trytond.modules.html_report.template import HTMLPartyInfoMixin class Purchase(HTMLPartyInfoMixin, metaclass=PoolMeta): diff --git a/sale/sale.py b/sale/sale.py index d586a4e..885d053 100644 --- a/sale/sale.py +++ b/sale/sale.py @@ -1,6 +1,6 @@ from trytond.pool import PoolMeta, Pool from trytond.pyson import Eval -from trytond.modules.html_report.html import HTMLPartyInfoMixin +from trytond.modules.html_report.template import HTMLPartyInfoMixin from trytond.modules.html_report.engine import HTMLReportMixin diff --git a/stock/stock.py b/stock/stock.py index 2a0d429..6f9c66b 100644 --- a/stock/stock.py +++ b/stock/stock.py @@ -1,7 +1,7 @@ from trytond.model import fields from trytond.pool import PoolMeta, Pool from trytond.pyson import Eval -from trytond.modules.html_report.html import HTMLPartyInfoMixin +from trytond.modules.html_report.template import HTMLPartyInfoMixin from trytond.modules.html_report.engine import HTMLReportMixin diff --git a/html.py b/template.py similarity index 100% rename from html.py rename to template.py diff --git a/html.xml b/template.xml similarity index 100% rename from html.xml rename to template.xml diff --git a/tryton.cfg b/tryton.cfg index ec46265..d6d8545 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -18,7 +18,7 @@ extras_depend: stock_valued production xml: - html.xml + template.xml action.xml message.xml templates/base.xml