diff --git a/report_py3o_fusion_server/README.rst b/report_py3o_fusion_server/README.rst new file mode 100644 index 000000000..90f5841e7 --- /dev/null +++ b/report_py3o_fusion_server/README.rst @@ -0,0 +1,127 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +========================================== +Py3o Report Engine - Fusion server support +========================================== + +This addons was written to let a fusion server handle format conversion instead of local libreoffice. + +Installation +============ + +Install several additional components and Python libs: + +* `Py3o Fusion server `_, +* `Py3o render server `_, +* a Java Runtime Environment (JRE), which can be OpenJDK, +* Libreoffice started in the background in headless mode, +* the Java driver for Libreoffice (Juno). + +It is also possible to use the Python driver for Libreoffice (PyUNO), but it is recommended to use the Java driver because it is more stable. + +The installation procedure below uses the Java driver. It has been successfully tested on Ubuntu 16.04 LTS ; if you use another OS, you may have to change a few details. + +Installation of py3o.fusion: + +.. code:: + + pip install py3o.fusion + pip install service-identity + +Installation of py3o.renderserver: + +.. code:: + + pip install py3o.renderserver + +Installation of Libreoffice and JRE on Debian/Ubuntu: + +.. code:: + + sudo apt-get install default-jre ure libreoffice-java-common libreoffice-writer + +You may have to install additionnal fonts. For example, to have the special unicode symbols for phone/fax/email in the PDF reports generated by Py3o, you should install the following package: + +.. code:: + + sudo apt-get install fonts-symbola + +At the end, with the dependencies, you should have the following py3o python libs: + +.. code:: + + % pip freeze | grep py3o + py3o.formats==0.3 + py3o.fusion==0.8.6 + py3o.renderclient==0.2 + py3o.renderers.juno==0.8 + py3o.renderserver==0.5.1 + py3o.template==0.9.11 + py3o.types==0.1.1 + +Start the Py3o Fusion server: + +.. code:: + + start-py3o-fusion --debug -s localhost + +Start the Py3o render server: + +.. code:: + + start-py3o-renderserver --java=/usr/lib/jvm/default-java/jre/lib/amd64/server/libjvm.so --ure=/usr/share --office=/usr/lib/libreoffice --driver=juno --sofficeport=8997 + +On the output of the Py3o render server, the first line looks like: + +.. code:: + + DEBUG:root:Starting JVM: /usr/lib/jvm/default-java/jre/lib/amd64/server/libjvm.so with options: -Djava.class.path=/usr/local/lib/python2.7/dist-packages/py3o/renderers/juno/py3oconverter.jar:/usr/share/java/juh.jar:/usr/share/java/jurt.jar:/usr/share/java/ridl.jar:/usr/share/java/unoloader.jar:/usr/share/java/java_uno.jar:/usr/lib/libreoffice/program/classes/unoil.jar -Xmx150M + +After **-Djava.class.path**, there is a list of Java libs with *.jar* extension ; check that each JAR file is really present on your filesystem. If one of the jar files is present in another directory, create a symlink that points to the real location of the file. If all the jar files are present on another directory, adapt the *--ure=* argument on the command line of Py3o render server. + +To check that the Py3o Fusion server is running fine, visit the URL http://:8765/form. On this web page, under the section *Target format*, make sure that you have a line *This server currently supports these formats: ods, odt, docx, doc, html, docbook, pdf, xls.*. + +Known issues / Roadmap +====================== + +* none yet + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Florent Aide (`XCG Consulting `_) +* Laurent Mignon , +* Alexis de Lattre , +* Guewen Baconnier +* Omar Castiñeira +* Holger Brunn + +Do not contact contributors directly about help with questions or problems concerning this addon, but use the `community mailing list `_ or the `appropriate specialized mailinglist `_ for help, and the bug tracker linked in `Bug Tracker`_ above for technical issues. + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/report_py3o_fusion_server/__init__.py b/report_py3o_fusion_server/__init__.py new file mode 100644 index 000000000..a3e818a49 --- /dev/null +++ b/report_py3o_fusion_server/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import models diff --git a/report_py3o_fusion_server/__manifest__.py b/report_py3o_fusion_server/__manifest__.py new file mode 100644 index 000000000..ba9a78b88 --- /dev/null +++ b/report_py3o_fusion_server/__manifest__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + 'name': 'Py3o Report Engine - Fusion server support', + 'summary': 'Let the fusion server handle format conversion.', + 'version': '10.0.1.0.0', + 'category': 'Reporting', + 'license': 'AGPL-3', + 'author': 'XCG Consulting,' + 'ACSONE SA/NV,' + 'Odoo Community Association (OCA)', + 'website': 'https://github.com/OCA/reporting-engine', + 'depends': ['report_py3o'], + 'external_dependencies': { + 'python': [ + 'py3o.template', + 'py3o.formats', + ], + }, + 'demo': [ + "demo/report_py3o.xml", + ], + 'data': [ + "views/ir_report.xml", + 'security/ir.model.access.csv', + 'views/py3o_server.xml', + ], + 'installable': True, +} diff --git a/report_py3o_fusion_server/demo/report_py3o.xml b/report_py3o_fusion_server/demo/report_py3o.xml new file mode 100644 index 000000000..ac4f194ce --- /dev/null +++ b/report_py3o_fusion_server/demo/report_py3o.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/report_py3o_fusion_server/models/__init__.py b/report_py3o_fusion_server/models/__init__.py new file mode 100644 index 000000000..78c726c4d --- /dev/null +++ b/report_py3o_fusion_server/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import ir_actions_report_xml +from . import py3o_report +from . import py3o_server diff --git a/report_py3o_fusion_server/models/ir_actions_report_xml.py b/report_py3o_fusion_server/models/ir_actions_report_xml.py new file mode 100644 index 000000000..d4fa0db6f --- /dev/null +++ b/report_py3o_fusion_server/models/ir_actions_report_xml.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# © 2013 XCG Consulting +# © 2017 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import logging +from openerp import _, api, fields, models +from odoo.exceptions import ValidationError + +logger = logging.getLogger(__name__) + +try: + from py3o.formats import Formats +except ImportError: + logger.debug('Cannot import py3o.formats') + + +class IrActionsReportXml(models.Model): + _inherit = 'ir.actions.report.xml' + + @api.multi + @api.constrains("py3o_is_local_fusion", "py3o_server_id", "py3o_filetype") + 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): + raise ValidationError(_( + "Can not use not native format in local fusion. " + "Please specify a Fusion Server")) + + py3o_is_local_fusion = fields.Boolean( + "Local Fusion", + help="Native formats will be processed without a server. " + "You must use this mode if you call methods on your model into " + "the template.", + default=True) + py3o_server_id = fields.Many2one( + "py3o.server", + "Fusion Server") diff --git a/report_py3o_fusion_server/models/py3o_report.py b/report_py3o_fusion_server/models/py3o_report.py new file mode 100644 index 000000000..dd0381f0e --- /dev/null +++ b/report_py3o_fusion_server/models/py3o_report.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# © 2013 XCG Consulting +# © 2016 ACSONE SA/NV +# © 2017 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import json +import logging +import os +import requests +import tempfile +from contextlib import closing +from openerp import _, api, models +from openerp.exceptions import UserError +from StringIO import StringIO + +logger = logging.getLogger(__name__) + +try: + from py3o.template import Template + from py3o.template.helpers import Py3oConvertor +except ImportError: + logger.debug('Cannot import py3o.template') + + +class Py3oReport(models.TransientModel): + _inherit = 'py3o.report' + + @api.multi + def _create_single_report(self, model_instance, data, save_in_attachment): + """ This function to generate our py3o report + """ + self.ensure_one() + report_xml = self.ir_actions_report_xml_id + filetype = report_xml.py3o_filetype + if report_xml.py3o_is_local_fusion: + result_path = super(Py3oReport, self)._create_single_report( + model_instance, data, save_in_attachment, + ) + with closing(open(result_path, 'r')) as out_stream: + tmpl_data = out_stream.read() + datadict = {} + else: + result_fd, result_path = tempfile.mkstemp( + suffix='.' + filetype, prefix='p3o.report.tmp.') + tmpl_data = self.get_template(model_instance) + + in_stream = StringIO(tmpl_data) + with closing(os.fdopen(result_fd, 'w+')) as out_stream: + template = Template(in_stream, out_stream, escape_false=True) + localcontext = self._get_parser_context(model_instance, data) + expressions = template.get_all_user_python_expression() + py_expression = template.convert_py3o_to_python_ast( + expressions) + convertor = Py3oConvertor() + data_struct = convertor(py_expression) + datadict = data_struct.render(localcontext) + + # Call py3o.server to render the template in the desired format + files = { + 'tmpl_file': tmpl_data, + } + fields = { + "targetformat": filetype, + "datadict": json.dumps(datadict), + "image_mapping": "{}", + "escape_false": "on", + } + if report_xml.py3o_is_local_fusion: + fields['skipfusion'] = '1' + r = requests.post( + report_xml.py3o_server_id.url, data=fields, files=files) + if r.status_code != 200: + # server says we have an issue... let's tell that to enduser + raise UserError( + _('Fusion server error %s') % r.text, + ) + + chunk_size = 1024 + with open(result_path, 'w+') as fd: + for chunk in r.iter_content(chunk_size): + fd.write(chunk) + if len(model_instance) == 1: + self._postprocess_report( + result_path, model_instance.id, save_in_attachment) + return result_path diff --git a/report_py3o_fusion_server/models/py3o_server.py b/report_py3o_fusion_server/models/py3o_server.py new file mode 100644 index 000000000..099d355c1 --- /dev/null +++ b/report_py3o_fusion_server/models/py3o_server.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Copyright 2013 XCG Consulting (http://odoo.consulting) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class Py3oServer(models.Model): + _name = 'py3o.server' + _rec_name = 'url' + + url = fields.Char( + "Py3o Fusion Server URL", required=True, + help="If your Py3o Fusion server is on the same machine and runs " + "on the default port, the URL is http://localhost:8765/form") + is_active = fields.Boolean("Active", default=True) diff --git a/report_py3o_fusion_server/security/ir.model.access.csv b/report_py3o_fusion_server/security/ir.model.access.csv new file mode 100644 index 000000000..8015edc9b --- /dev/null +++ b/report_py3o_fusion_server/security/ir.model.access.csv @@ -0,0 +1,3 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +access_py3o_server_admin,access_py3o_server_admin,model_py3o_server,base.group_no_one,1,1,1,1 +access_py3o_server_user,access_py3o_server_user,model_py3o_server,base.group_user,1,0,0,0 diff --git a/report_py3o_fusion_server/static/description/icon.png b/report_py3o_fusion_server/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/report_py3o_fusion_server/static/description/icon.png differ diff --git a/report_py3o_fusion_server/tests/__init__.py b/report_py3o_fusion_server/tests/__init__.py new file mode 100644 index 000000000..06cc0deea --- /dev/null +++ b/report_py3o_fusion_server/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Therp BV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import test_report_py3o_fusion_server 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 new file mode 100644 index 000000000..ebe1a92d4 --- /dev/null +++ b/report_py3o_fusion_server/tests/test_report_py3o_fusion_server.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Therp BV +# 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.tests import test_report_py3o + + +@mock.patch( + 'requests.post', mock.Mock( + return_value=mock.Mock( + status_code=200, + iter_content=mock.Mock(return_value=['test_result']), + ) + ) +) +class TestReportPy3oFusionServer(test_report_py3o.TestReportPy3o): + def setUp(self): + super(TestReportPy3oFusionServer, self).setUp() + py3o_server = self.env['py3o.server'].create({"url": "http://dummy"}) + # check the call to the fusion server + self.report.write({ + "py3o_server_id": py3o_server.id, + "py3o_filetype": 'pdf', + }) + + def test_no_local_fusion_without_fusion_server(self): + self.assertTrue(self.report.py3o_is_local_fusion) + with self.assertRaises(ValidationError) as e: + self.report.write({"py3o_server_id": None}) + self.assertEqual( + e.exception.name, + "Can not use not native format in local fusion. " + "Please specify a Fusion Server") + + def test_reports_no_local_fusion(self): + self.report.py3o_is_local_fusion = False + self.test_reports() diff --git a/report_py3o_fusion_server/views/ir_report.xml b/report_py3o_fusion_server/views/ir_report.xml new file mode 100644 index 000000000..35cba84f5 --- /dev/null +++ b/report_py3o_fusion_server/views/ir_report.xml @@ -0,0 +1,13 @@ + + + + ir.actions.report.xml + + + + + + + + + diff --git a/report_py3o_fusion_server/views/py3o_server.xml b/report_py3o_fusion_server/views/py3o_server.xml new file mode 100644 index 000000000..810e59181 --- /dev/null +++ b/report_py3o_fusion_server/views/py3o_server.xml @@ -0,0 +1,38 @@ + + + + + py3o.server.configuration.form.view + py3o.server + +
+ + + + +
+
+
+ + + py3o.server.configuration.tree.view + py3o.server + + + + + + + + + + Py3o Servers + py3o.server + tree,form + + + + +