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 - + + + + +