From 876aa27ef219a7f57fafb2ee56061570645cc35b Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Fri, 1 Feb 2019 14:52:24 +0100 Subject: [PATCH] [IMP][FIX] py3o_report, py3o_report_fusion_server: Compute the availability of py3o report Before this change it was not possible to install modules declaring py3o report into a non native format without specifying a Fusion server once the module py3o_report_fusion_server was installed. With theses changes, we now take care of the availability of the libreoffice runtime to display/log a warning message when the report is in a non native runtime. --- report_py3o/models/ir_actions_report.py | 78 +++++++++++++++++++ report_py3o/models/py3o_report.py | 16 ++-- report_py3o/tests/test_report_py3o.py | 41 ++++++++++ report_py3o/views/ir_actions_report.xml | 11 ++- .../models/ir_actions_report.py | 32 +++++--- .../tests/test_report_py3o_fusion_server.py | 61 ++++++++++++++- 6 files changed, 220 insertions(+), 19 deletions(-) diff --git a/report_py3o/models/ir_actions_report.py b/report_py3o/models/ir_actions_report.py index 484b42a7..1aabd4c9 100644 --- a/report_py3o/models/ir_actions_report.py +++ b/report_py3o/models/ir_actions_report.py @@ -5,8 +5,10 @@ import logging import time from odoo import api, fields, models, _ from odoo.exceptions import ValidationError +from odoo.tools.misc import find_in_path from odoo.tools.safe_eval import safe_eval + logger = logging.getLogger(__name__) try: @@ -14,6 +16,8 @@ try: except ImportError: logger.debug('Cannot import py3o.formats') +PY3O_CONVERSION_COMMAND_PARAMETER = "py3o.conversion_command" + class IrActionsReport(models.Model): """ Inherit from ir.actions.report to allow customizing the template @@ -49,6 +53,9 @@ class IrActionsReport(models.Model): py3o_filetype = fields.Selection( selection="_get_py3o_filetypes", string="Output Format") + is_py3o_native_format = fields.Boolean( + compute='_compute_is_py3o_native_format' + ) py3o_template_id = fields.Many2one( 'py3o.template', "Template") @@ -70,6 +77,77 @@ class IrActionsReport(models.Model): "by default Odoo will generate a ZIP file that contains as many " "files as selected records. If you enable this option, Odoo will " "generate instead a single report for the selected records.") + lo_bin_path = fields.Char( + string="Path to the libreoffice runtime", + compute="_compute_lo_bin_path" + ) + is_py3o_report_not_available = fields.Boolean( + compute='_compute_py3o_report_not_available' + ) + msg_py3o_report_not_available = fields.Char( + compute='_compute_py3o_report_not_available' + ) + + @api.model + def _register_hook(self): + self._validate_reports() + + @api.model + def _validate_reports(self): + """Check if the existing py3o reports should work with the current + installation. + + This method log a warning message into the logs for each report + that should not work. + """ + for report in self.search([("report_type", "=", "py3o")]): + if report.is_py3o_report_not_available: + logger.warning(report.msg_py3o_report_not_available) + + @api.model + def _get_lo_bin(self): + lo_bin = self.env['ir.config_parameter'].get_param( + PY3O_CONVERSION_COMMAND_PARAMETER, 'libreoffice', + ) + try: + lo_bin = find_in_path(lo_bin) + except IOError: + lo_bin = None + return lo_bin + + @api.depends("report_type", "py3o_filetype") + @api.multi + def _compute_is_py3o_native_format(self): + format = Formats() + for rec in self: + if not rec.report_type == "py3o": + continue + filetype = rec.py3o_filetype + rec.is_py3o_native_format = format.get_format(filetype).native + + @api.multi + def _compute_lo_bin_path(self): + lo_bin = self._get_lo_bin() + for rec in self: + rec.lo_bin_path = lo_bin + + @api.depends("lo_bin_path", "is_py3o_native_format", "report_type") + @api.multi + def _compute_py3o_report_not_available(self): + for rec in self: + if not rec.report_type == "py3o": + continue + if not rec.is_py3o_native_format and not rec.lo_bin_path: + rec.is_py3o_report_not_available = True + rec.msg_py3o_report_not_available = _( + "The libreoffice runtime is required to genereate the " + "py3o report '%s' but is not found into the bin path. You " + "must install the libreoffice runtime on the server. If " + "the runtime is already installed and is not found by " + "Odoo, you can provide the full path to the runtime by " + "setting the key 'py3o.conversion_command' into the " + "configuration parameters." + ) % rec.name @api.model def get_from_report_name(self, report_name, report_type): diff --git a/report_py3o/models/py3o_report.py b/report_py3o/models/py3o_report.py index ab3f3cdb..7777f7bb 100644 --- a/report_py3o/models/py3o_report.py +++ b/report_py3o/models/py3o_report.py @@ -242,8 +242,7 @@ class Py3oReport(models.TransientModel): @api.multi def _convert_single_report(self, result_path, model_instance, data): """Run a command to convert to our target format""" - filetype = self.ir_actions_report_id.py3o_filetype - if not Formats().get_format(filetype).native: + if not self.ir_actions_report_id.is_py3o_native_format: command = self._convert_single_report_cmd( result_path, model_instance, data, ) @@ -256,7 +255,8 @@ class Py3oReport(models.TransientModel): result_path, result_filename = os.path.split(result_path) result_path = os.path.join( result_path, '%s.%s' % ( - os.path.splitext(result_filename)[0], filetype + os.path.splitext(result_filename)[0], + self.ir_actions_report_id.py3o_filetype ) ) return result_path @@ -264,10 +264,14 @@ class Py3oReport(models.TransientModel): @api.multi def _convert_single_report_cmd(self, result_path, model_instance, data): """Return a command list suitable for use in subprocess.call""" + lo_bin = self.ir_actions_report_id.lo_bin_path + if not lo_bin: + raise RuntimeError( + _("Libreoffice runtime not available. " + "Please contact your administrator.") + ) return [ - self.env['ir.config_parameter'].get_param( - 'py3o.conversion_command', 'libreoffice', - ), + lo_bin, '--headless', '--convert-to', self.ir_actions_report_id.py3o_filetype, diff --git a/report_py3o/tests/test_report_py3o.py b/report_py3o/tests/test_report_py3o.py index b6f73f3f..3c41f599 100644 --- a/report_py3o/tests/test_report_py3o.py +++ b/report_py3o/tests/test_report_py3o.py @@ -15,6 +15,7 @@ from odoo.tests.common import TransactionCase from odoo.exceptions import ValidationError from odoo.addons.base.tests.test_mimetypes import PNG +from ..models.ir_actions_report import PY3O_CONVERSION_COMMAND_PARAMETER from ..models.py3o_report import TemplateNotFound from ..models._py3o_parser_context import format_multiline_value from base64 import b64encode @@ -83,6 +84,7 @@ class TestReportPy3o(TransactionCase): self.assertTrue(res) def test_reports_merge_zip(self): + self.report.py3o_filetype = "odt" users = self.env['res.users'].search([]) self.assertTrue(len(users) > 0) py3o_report = self.env['py3o.report'] @@ -217,3 +219,42 @@ class TestReportPy3o(TransactionCase): def test_escape_html_characters_format_multiline_value(self): self.assertEqual(Markup('<>&test;'), format_multiline_value('<>\n&test;')) + + def test_py3o_report_availability(self): + # This test could fails if libreoffice is not available on the server + self.report.py3o_filetype = "odt" + self.assertTrue(self.report.lo_bin_path) + self.assertTrue(self.report.is_py3o_native_format) + self.assertFalse(self.report.is_py3o_report_not_available) + self.assertFalse(self.report.msg_py3o_report_not_available) + + # specify a wrong lo bin path + self.env['ir.config_parameter'].set_param( + PY3O_CONVERSION_COMMAND_PARAMETER, "/wrong_path") + self.report.refresh() + # no bin path available but the report is still available since + # the output is into native format + self.assertFalse(self.report.lo_bin_path) + self.assertFalse(self.report.is_py3o_report_not_available) + self.assertFalse(self.report.msg_py3o_report_not_available) + res = self.report.render(self.env.user.ids) + self.assertTrue(res) + + # The report should become unavailable for an non native output format + self.report.py3o_filetype = "pdf" + self.assertFalse(self.report.is_py3o_native_format) + self.assertTrue(self.report.is_py3o_report_not_available) + self.assertTrue(self.report.msg_py3o_report_not_available) + with self.assertRaises(RuntimeError): + self.report.render(self.env.user.ids) + + # if we reset the wrong path, everything should work + self.env['ir.config_parameter'].set_param( + PY3O_CONVERSION_COMMAND_PARAMETER, "libreoffice") + self.report.refresh() + self.assertTrue(self.report.lo_bin_path) + self.assertFalse(self.report.is_py3o_native_format) + self.assertFalse(self.report.is_py3o_report_not_available) + self.assertFalse(self.report.msg_py3o_report_not_available) + res = self.report.render(self.env.user.ids) + self.assertTrue(res) diff --git a/report_py3o/views/ir_actions_report.xml b/report_py3o/views/ir_actions_report.xml index d4c90aac..eae06332 100644 --- a/report_py3o/views/ir_actions_report.xml +++ b/report_py3o/views/ir_actions_report.xml @@ -8,12 +8,21 @@ ir.actions.report - + + + + + diff --git a/report_py3o_fusion_server/models/ir_actions_report.py b/report_py3o_fusion_server/models/ir_actions_report.py index f95a1b09..6c8927d8 100644 --- a/report_py3o_fusion_server/models/ir_actions_report.py +++ b/report_py3o_fusion_server/models/ir_actions_report.py @@ -7,26 +7,19 @@ from odoo.exceptions import ValidationError logger = logging.getLogger(__name__) -try: - from py3o.formats import Formats -except ImportError: - logger.debug('Cannot import py3o.formats') - class IrActionsReport(models.Model): _inherit = 'ir.actions.report' @api.multi - @api.constrains("py3o_is_local_fusion", "py3o_server_id", "py3o_filetype") + @api.constrains("py3o_is_local_fusion", "py3o_server_id") def _check_py3o_server_id(self): for report in self: if report.report_type != "py3o": continue - is_native = Formats().get_format(report.py3o_filetype).native - if ((not is_native or not report.py3o_is_local_fusion) and - not report.py3o_server_id): + if (not report.py3o_is_local_fusion and not report.py3o_server_id): raise ValidationError(_( - "Can not use not native format in local fusion. " + "You can not use remote fusion without Fusion server. " "Please specify a Fusion Server")) py3o_is_local_fusion = fields.Boolean( @@ -42,3 +35,22 @@ class IrActionsReport(models.Model): 'py3o.pdf.options', string='PDF Options', ondelete='restrict', help="PDF options can be set per report, but also per Py3o Server. " "If both are defined, the options on the report are used.") + + @api.depends("lo_bin_path", "is_py3o_native_format", "report_type", + "py3o_server_id") + @api.multi + def _compute_py3o_report_not_available(self): + for rec in self: + if not rec.report_type == "py3o": + continue + if (not rec.is_py3o_native_format and + not rec.lo_bin_path and not rec.py3o_server_id): + rec.is_py3o_report_not_available = True + rec.msg_py3o_report_not_available = _( + "A fusion server or a libreoffice runtime are required " + "to genereate the py3o report '%s'. If the libreoffice" + "runtime is already installed and is not found by " + "Odoo, you can provide the full path to the runtime by " + "setting the key 'py3o.conversion_command' into the " + "configuration parameters." + ) % rec.name diff --git a/report_py3o_fusion_server/tests/test_report_py3o_fusion_server.py b/report_py3o_fusion_server/tests/test_report_py3o_fusion_server.py index 773aae5f..bf9debd0 100644 --- a/report_py3o_fusion_server/tests/test_report_py3o_fusion_server.py +++ b/report_py3o_fusion_server/tests/test_report_py3o_fusion_server.py @@ -2,6 +2,8 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import mock from odoo.exceptions import ValidationError +from odoo.addons.report_py3o.models.ir_actions_report import \ + PY3O_CONVERSION_COMMAND_PARAMETER from odoo.addons.report_py3o.tests import test_report_py3o @@ -22,14 +24,31 @@ class TestReportPy3oFusionServer(test_report_py3o.TestReportPy3o): "py3o_server_id": py3o_server.id, "py3o_filetype": 'pdf', }) + self.py3o_server = py3o_server def test_no_local_fusion_without_fusion_server(self): self.assertTrue(self.report.py3o_is_local_fusion) + # Fusion server is only required if not local... + self.report.write({ + "py3o_server_id": None, + "py3o_is_local_fusion": True, + }) + self.report.write({ + "py3o_server_id": self.py3o_server.id, + "py3o_is_local_fusion": True, + }) + self.report.write({ + "py3o_server_id": self.py3o_server.id, + "py3o_is_local_fusion": False, + }) with self.assertRaises(ValidationError) as e: - self.report.write({"py3o_server_id": None}) + self.report.write({ + "py3o_server_id": None, + "py3o_is_local_fusion": False, + }) self.assertEqual( e.exception.name, - "Can not use not native format in local fusion. " + "You can not use remote fusion without Fusion server. " "Please specify a Fusion Server") def test_reports_no_local_fusion(self): @@ -40,3 +59,41 @@ class TestReportPy3oFusionServer(test_report_py3o.TestReportPy3o): for options in self.env['py3o.pdf.options'].search([]): options_dict = options.odoo2libreoffice_options() self.assertIsInstance(options_dict, dict) + + def test_py3o_report_availability(self): + # if the report is not into a native format, we must have at least + # a libreoffice runtime or a fusion server. Otherwise the report is + # not usable and will fail at rutime. + # This test could fails if libreoffice is not available on the server + self.report.py3o_filetype = "odt" + self.assertTrue(self.report.lo_bin_path) + self.assertTrue(self.report.py3o_server_id) + self.assertTrue(self.report.is_py3o_native_format) + self.assertFalse(self.report.is_py3o_report_not_available) + self.assertFalse(self.report.msg_py3o_report_not_available) + + # specify a wrong lo bin path and a non native format. + self.env['ir.config_parameter'].set_param( + PY3O_CONVERSION_COMMAND_PARAMETER, "/wrong_path") + self.report.py3o_filetype = "pdf" + self.report.refresh() + # no native and no bin path, everything is still OK since a fusion + # server is specified. + self.assertFalse(self.report.lo_bin_path) + self.assertTrue(self.report.py3o_server_id) + self.assertFalse(self.report.is_py3o_native_format) + self.assertFalse(self.report.is_py3o_report_not_available) + self.assertFalse(self.report.msg_py3o_report_not_available) + + # if we remove the fusion server, the report becomes unavailable + self.report.py3o_server_id = False + self.assertTrue(self.report.is_py3o_report_not_available) + self.assertTrue(self.report.msg_py3o_report_not_available) + + # if we set a libreffice runtime, the report is available again + self.env['ir.config_parameter'].set_param( + PY3O_CONVERSION_COMMAND_PARAMETER, "libreoffice") + self.report.refresh() + self.assertTrue(self.report.lo_bin_path) + self.assertFalse(self.report.is_py3o_report_not_available) + self.assertFalse(self.report.msg_py3o_report_not_available)