diff --git a/.isort.cfg b/.isort.cfg index 5751c40dd..e1f92ba4a 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -9,4 +9,4 @@ line_length=88 known_odoo=odoo known_odoo_addons=odoo.addons sections=FUTURE,STDLIB,THIRDPARTY,ODOO,ODOO_ADDONS,FIRSTPARTY,LOCALFOLDER -known_third_party= +known_third_party=PyPDF2,mock,openerp,pkg_resources,requests,werkzeug diff --git a/report_py3o/__manifest__.py b/report_py3o/__manifest__.py index 9b8a08331..4b45c018a 100644 --- a/report_py3o/__manifest__.py +++ b/report_py3o/__manifest__.py @@ -1,29 +1,23 @@ # Copyright 2013 XCG Consulting (http://odoo.consulting) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { - 'name': 'Py3o Report Engine', - 'summary': 'Reporting engine based on Libreoffice (ODT -> ODT, ' - 'ODT -> PDF, ODT -> DOC, ODT -> DOCX, ODS -> ODS, etc.)', - 'version': '12.0.2.0.2', - 'category': 'Reporting', - 'license': 'AGPL-3', - 'author': 'XCG Consulting,' - 'ACSONE SA/NV,' - 'Odoo Community Association (OCA)', - 'website': 'http://odoo.consulting/', - 'depends': ['web'], - 'external_dependencies': { - 'python': ['py3o.template', - 'py3o.formats', - 'PyPDF2'] - }, - 'data': [ - 'security/ir.model.access.csv', - 'views/menu.xml', - 'views/py3o_template.xml', - 'views/ir_actions_report.xml', - 'views/report_py3o.xml', - 'demo/report_py3o.xml', + "name": "Py3o Report Engine", + "summary": "Reporting engine based on Libreoffice (ODT -> ODT, " + "ODT -> PDF, ODT -> DOC, ODT -> DOCX, ODS -> ODS, etc.)", + "version": "12.0.2.0.2", + "category": "Reporting", + "license": "AGPL-3", + "author": "XCG Consulting," "ACSONE SA/NV," "Odoo Community Association (OCA)", + "website": "http://odoo.consulting/", + "depends": ["web"], + "external_dependencies": {"python": ["py3o.template", "py3o.formats", "PyPDF2"]}, + "data": [ + "security/ir.model.access.csv", + "views/menu.xml", + "views/py3o_template.xml", + "views/ir_actions_report.xml", + "views/report_py3o.xml", + "demo/report_py3o.xml", ], - 'installable': True, + "installable": True, } diff --git a/report_py3o/controllers/main.py b/report_py3o/controllers/main.py index 4b19c576d..ac4decaf6 100644 --- a/report_py3o/controllers/main.py +++ b/report_py3o/controllers/main.py @@ -2,58 +2,57 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) import json import mimetypes + from werkzeug import exceptions, url_decode -from odoo.http import route, request +from odoo.http import request, route +from odoo.tools import html_escape from odoo.addons.web.controllers import main -from odoo.addons.web.controllers.main import ( - _serialize_exception, - content_disposition -) -from odoo.tools import html_escape +from odoo.addons.web.controllers.main import _serialize_exception, content_disposition class ReportController(main.ReportController): - @route() def report_routes(self, reportname, docids=None, converter=None, **data): - if converter != 'py3o': + if converter != "py3o": return super(ReportController, self).report_routes( - reportname=reportname, docids=docids, converter=converter, - **data) + reportname=reportname, docids=docids, converter=converter, **data + ) context = dict(request.env.context) if docids: - docids = [int(i) for i in docids.split(',')] - if data.get('options'): - data.update(json.loads(data.pop('options'))) - if data.get('context'): + docids = [int(i) for i in docids.split(",")] + if data.get("options"): + data.update(json.loads(data.pop("options"))) + if data.get("context"): # Ignore 'lang' here, because the context in data is the # one from the webclient *but* if the user explicitely wants to # change the lang, this mechanism overwrites it. - data['context'] = json.loads(data['context']) - if data['context'].get('lang'): - del data['context']['lang'] - context.update(data['context']) + data["context"] = json.loads(data["context"]) + if data["context"].get("lang"): + del data["context"]["lang"] + context.update(data["context"]) - ir_action = request.env['ir.actions.report'] + ir_action = request.env["ir.actions.report"] action_py3o_report = ir_action.get_from_report_name( - reportname, "py3o").with_context(context) + reportname, "py3o" + ).with_context(context) if not action_py3o_report: raise exceptions.HTTPException( - description='Py3o action report not found for report_name ' - '%s' % reportname) + description="Py3o action report not found for report_name " + "%s" % reportname + ) res, filetype = action_py3o_report.render(docids, data) - filename = action_py3o_report.gen_report_download_filename( - docids, data) + filename = action_py3o_report.gen_report_download_filename(docids, data) if not filename.endswith(filetype): filename = "{}.{}".format(filename, filetype) content_type = mimetypes.guess_type("x." + filetype)[0] - http_headers = [('Content-Type', content_type), - ('Content-Length', len(res)), - ('Content-Disposition', content_disposition(filename)) - ] + http_headers = [ + ("Content-Type", content_type), + ("Content-Length", len(res)), + ("Content-Disposition", content_disposition(filename)), + ] return request.make_response(res, headers=http_headers) @route() @@ -67,31 +66,29 @@ class ReportController(main.ReportController): """ requestcontent = json.loads(data) url, report_type = requestcontent[0], requestcontent[1] - if 'py3o' not in report_type: + if "py3o" not in report_type: return super(ReportController, self).report_download(data, token) try: - reportname = url.split('/report/py3o/')[1].split('?')[0] + reportname = url.split("/report/py3o/")[1].split("?")[0] docids = None - if '/' in reportname: - reportname, docids = reportname.split('/') + if "/" in reportname: + reportname, docids = reportname.split("/") if docids: # Generic report: response = self.report_routes( - reportname, docids=docids, converter='py3o') + reportname, docids=docids, converter="py3o" + ) else: # Particular report: # decoding the args represented in JSON - data = list(url_decode(url.split('?')[1]).items()) + data = list(url_decode(url.split("?")[1]).items()) response = self.report_routes( - reportname, converter='py3o', **dict(data)) - response.set_cookie('fileToken', token) + reportname, converter="py3o", **dict(data) + ) + response.set_cookie("fileToken", token) return response except Exception as e: se = _serialize_exception(e) - error = { - 'code': 200, - 'message': "Odoo Server Error", - 'data': se - } + error = {"code": 200, "message": "Odoo Server Error", "data": se} return request.make_response(html_escape(json.dumps(error))) diff --git a/report_py3o/demo/report_py3o.xml b/report_py3o/demo/report_py3o.xml index 6d8941531..240960ead 100644 --- a/report_py3o/demo/report_py3o.xml +++ b/report_py3o/demo/report_py3o.xml @@ -16,5 +16,5 @@ report - + diff --git a/report_py3o/models/_py3o_parser_context.py b/report_py3o/models/_py3o_parser_context.py index c5b559516..370435150 100644 --- a/report_py3o/models/_py3o_parser_context.py +++ b/report_py3o/models/_py3o_parser_context.py @@ -2,24 +2,27 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) import html -import time import logging - +import time from base64 import b64decode -from odoo.tools import misc, mail + +from odoo.tools import mail, misc logger = logging.getLogger(__name__) try: from genshi.core import Markup except ImportError: - logger.debug('Cannot import py3o.template') + logger.debug("Cannot import py3o.template") def format_multiline_value(value): if value: - return Markup(html.escape(value).replace('\n', ''). - replace('\t', '')) + return Markup( + html.escape(value) + .replace("\n", "") + .replace("\t", "") + ) return "" @@ -32,38 +35,52 @@ class Py3oParserContext(object): self._env = env self.localcontext = { - 'user': self._env.user, - 'lang': self._env.lang, + "user": self._env.user, + "lang": self._env.lang, # Odoo default format methods - 'o_format_lang': self._format_lang, + "o_format_lang": self._format_lang, # prefixes with o_ to avoid nameclash with default method provided # by py3o.template - 'o_format_date': self._format_date, + "o_format_date": self._format_date, # give access to the time lib - 'time': time, + "time": time, # keeps methods from report_sxw to ease migration - 'display_address': display_address, - 'formatLang': self._old_format_lang, - 'format_multiline_value': format_multiline_value, - 'html_sanitize': mail.html2plaintext, - 'b64decode': b64decode, + "display_address": display_address, + "formatLang": self._old_format_lang, + "format_multiline_value": format_multiline_value, + "html_sanitize": mail.html2plaintext, + "b64decode": b64decode, } - def _format_lang(self, value, lang_code=False, digits=None, grouping=True, - monetary=False, dp=False, currency_obj=False, - no_break_space=True): + def _format_lang( + self, + value, + lang_code=False, + digits=None, + grouping=True, + monetary=False, + dp=False, + currency_obj=False, + no_break_space=True, + ): env = self._env if lang_code: context = dict(env.context, lang=lang_code) env = env(context=context) formatted_value = misc.formatLang( - env, value, digits=digits, grouping=grouping, - monetary=monetary, dp=dp, currency_obj=currency_obj) + env, + value, + digits=digits, + grouping=grouping, + monetary=monetary, + dp=dp, + currency_obj=currency_obj, + ) if currency_obj and currency_obj.symbol and no_break_space: parts = [] - if currency_obj.position == 'after': + if currency_obj.position == "after": parts = formatted_value.rsplit(" ", 1) - elif currency_obj and currency_obj.position == 'before': + elif currency_obj and currency_obj.position == "before": parts = formatted_value.split(" ", 1) if parts: formatted_value = "\N{NO-BREAK SPACE}".join(parts) @@ -71,11 +88,20 @@ class Py3oParserContext(object): def _format_date(self, value, lang_code=False, date_format=False): return misc.format_date( - self._env, value, lang_code=lang_code, date_format=date_format) + self._env, value, lang_code=lang_code, date_format=date_format + ) - def _old_format_lang(self, value, digits=None, date=False, date_time=False, - grouping=True, monetary=False, dp=False, - currency_obj=False): + def _old_format_lang( + self, + value, + digits=None, + date=False, + date_time=False, + grouping=True, + monetary=False, + dp=False, + currency_obj=False, + ): """ :param value: The value to format :param digits: Number of digits to display by default @@ -95,8 +121,13 @@ class Py3oParserContext(object): """ if not date and not date_time: return self._format_lang( - value, digits=digits, grouping=grouping, - monetary=monetary, dp=dp, currency_obj=currency_obj, - no_break_space=True) + value, + digits=digits, + grouping=grouping, + monetary=monetary, + dp=dp, + currency_obj=currency_obj, + no_break_space=True, + ) return self._format_date(self._env, value) diff --git a/report_py3o/models/ir_actions_report.py b/report_py3o/models/ir_actions_report.py index 1e60a373c..670455570 100644 --- a/report_py3o/models/ir_actions_report.py +++ b/report_py3o/models/ir_actions_report.py @@ -3,18 +3,18 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging import time -from odoo import api, fields, models, _ + +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: from py3o.formats import Formats except ImportError: - logger.debug('Cannot import py3o.formats') + logger.debug("Cannot import py3o.formats") PY3O_CONVERSION_COMMAND_PARAMETER = "py3o.conversion_command" @@ -25,15 +25,16 @@ class IrActionsReport(models.Model): The list is configurable in the configuration tab, see py3o_template.py """ - _inherit = 'ir.actions.report' + _inherit = "ir.actions.report" @api.multi @api.constrains("py3o_filetype", "report_type") def _check_py3o_filetype(self): for report in self: if report.report_type == "py3o" and not report.py3o_filetype: - raise ValidationError(_( - "Field 'Output Format' is required for Py3O report")) + raise ValidationError( + _("Field 'Output Format' is required for Py3O report") + ) @api.model def _get_py3o_filetypes(self): @@ -47,21 +48,15 @@ class IrActionsReport(models.Model): selections.append((name, description)) return selections - report_type = fields.Selection( - selection_add=[("py3o", "py3o")] - ) + report_type = fields.Selection(selection_add=[("py3o", "py3o")]) py3o_filetype = fields.Selection( - selection="_get_py3o_filetypes", - string="Output Format") - is_py3o_native_format = fields.Boolean( - compute='_compute_is_py3o_native_format' + selection="_get_py3o_filetypes", string="Output Format" ) - py3o_template_id = fields.Many2one( - 'py3o.template', - "Template") + is_py3o_native_format = fields.Boolean(compute="_compute_is_py3o_native_format") + py3o_template_id = fields.Many2one("py3o.template", "Template") module = fields.Char( - "Module", - help="The implementer module that provides this report") + "Module", help="The implementer module that provides this report" + ) py3o_template_fallback = fields.Char( "Fallback", size=128, @@ -69,24 +64,25 @@ class IrActionsReport(models.Model): "If the user does not provide a template this will be used " "it should be a relative path to root of YOUR module " "or an absolute path on your server." - )) - report_type = fields.Selection(selection_add=[('py3o', "Py3o")]) + ), + ) + report_type = fields.Selection(selection_add=[("py3o", "Py3o")]) py3o_multi_in_one = fields.Boolean( - string='Multiple Records in a Single Report', + string="Multiple Records in a Single Report", help="If you execute a report on several records, " "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.") + "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" - ) + string="Path to the libreoffice runtime", compute="_compute_lo_bin_path" + ) is_py3o_report_not_available = fields.Boolean( - compute='_compute_py3o_report_not_available' - ) + compute="_compute_py3o_report_not_available" + ) msg_py3o_report_not_available = fields.Char( - compute='_compute_py3o_report_not_available' - ) + compute="_compute_py3o_report_not_available" + ) @api.model def _register_hook(self): @@ -106,8 +102,10 @@ class IrActionsReport(models.Model): @api.model def _get_lo_bin(self): - lo_bin = self.env['ir.config_parameter'].sudo().get_param( - PY3O_CONVERSION_COMMAND_PARAMETER, 'libreoffice', + lo_bin = ( + self.env["ir.config_parameter"] + .sudo() + .get_param(PY3O_CONVERSION_COMMAND_PARAMETER, "libreoffice") ) try: lo_bin = find_in_path(lo_bin) @@ -118,12 +116,12 @@ class IrActionsReport(models.Model): @api.depends("report_type", "py3o_filetype") @api.multi def _compute_is_py3o_native_format(self): - format = Formats() + fmt = 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 + rec.is_py3o_native_format = fmt.get_format(filetype).native @api.multi def _compute_lo_bin_path(self): @@ -139,21 +137,24 @@ class IrActionsReport(models.Model): 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 + 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): return self.search( - [("report_name", "=", report_name), - ("report_type", "=", report_type)]) + [("report_name", "=", report_name), ("report_type", "=", report_type)] + ) @api.multi def render_py3o(self, res_ids, data): @@ -161,10 +162,13 @@ class IrActionsReport(models.Model): if self.report_type != "py3o": raise RuntimeError( "py3o rendition is only available on py3o report.\n" - "(current: '{}', expected 'py3o'".format(self.report_type)) - return self.env['py3o.report'].create({ - 'ir_actions_report_id': self.id - }).create_report(res_ids, data) + "(current: '{}', expected 'py3o'".format(self.report_type) + ) + return ( + self.env["py3o.report"] + .create({"ir_actions_report_id": self.id}) + .create_report(res_ids, data) + ) @api.multi def gen_report_download_filename(self, res_ids, data): @@ -174,9 +178,8 @@ class IrActionsReport(models.Model): report = self.get_from_report_name(self.report_name, self.report_type) if report.print_report_name and not len(res_ids) > 1: obj = self.env[self.model].browse(res_ids) - return safe_eval(report.print_report_name, - {'object': obj, 'time': time}) - return "%s.%s" % (self.name, self.py3o_filetype) + return safe_eval(report.print_report_name, {"object": obj, "time": time}) + return "{}.{}".format(self.name, self.py3o_filetype) @api.multi def _get_attachments(self, res_ids): diff --git a/report_py3o/models/py3o_report.py b/report_py3o/models/py3o_report.py index 7777f7bb1..b7b673faf 100644 --- a/report_py3o/models/py3o_report.py +++ b/report_py3o/models/py3o_report.py @@ -2,19 +2,20 @@ # Copyright 2016 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) import base64 -from base64 import b64decode -from io import BytesIO import logging import os -from contextlib import closing import subprocess - -import pkg_resources import sys import tempfile -from zipfile import ZipFile, ZIP_DEFLATED +from base64 import b64decode +from contextlib import closing +from io import BytesIO +from zipfile import ZIP_DEFLATED, ZipFile + +import pkg_resources + +from odoo import _, api, fields, models, tools -from odoo import api, fields, models, tools, _ from ._py3o_parser_context import Py3oParserContext logger = logging.getLogger(__name__) @@ -23,15 +24,15 @@ try: from py3o.template import Template from py3o import formats except ImportError: - logger.debug('Cannot import py3o.template') + logger.debug("Cannot import py3o.template") try: from py3o.formats import Formats, UnkownFormatException except ImportError: - logger.debug('Cannot import py3o.formats') + logger.debug("Cannot import py3o.formats") try: from PyPDF2 import PdfFileWriter, PdfFileReader except ImportError: - logger.debug('Cannot import PyPDF2') + logger.debug("Cannot import PyPDF2") _extender_functions = {} @@ -59,12 +60,13 @@ def py3o_report_extender(report_xml_id=None): def fct1(fct): _extender_functions.setdefault(report_xml_id, []).append(fct) return fct + return fct1 @py3o_report_extender() def default_extend(report_xml, context): - context['report_xml'] = report_xml + context["report_xml"] = report_xml class Py3oReport(models.TransientModel): @@ -72,8 +74,7 @@ class Py3oReport(models.TransientModel): _description = "Report Py30" ir_actions_report_id = fields.Many2one( - comodel_name="ir.actions.report", - required=True + comodel_name="ir.actions.report", required=True ) @api.multi @@ -81,18 +82,22 @@ class Py3oReport(models.TransientModel): """ Check if the path is a trusted path for py3o templates. """ real_path = os.path.realpath(path) - root_path = tools.config.get_misc('report_py3o', 'root_tmpl_path') + root_path = tools.config.get_misc("report_py3o", "root_tmpl_path") if not root_path: logger.warning( "You must provide a root template path into odoo.cfg to be " "able to use py3o template configured with an absolute path " - "%s", real_path) + "%s", + real_path, + ) return False is_valid = real_path.startswith(root_path + os.path.sep) if not is_valid: logger.warning( - "Py3o template path is not valid. %s is not a child of root " - "path %s", real_path, root_path) + "Py3o template path is not valid. %s is not a child of root " "path %s", + real_path, + root_path, + ) return is_valid @api.multi @@ -101,16 +106,14 @@ class Py3oReport(models.TransientModel): """ if filename and os.path.isfile(filename): fname, ext = os.path.splitext(filename) - ext = ext.replace('.', '') + ext = ext.replace(".", "") try: fformat = Formats().get_format(ext) if fformat and fformat.native: return True except UnkownFormatException: - logger.warning("Invalid py3o template %s", filename, - exc_info=1) - logger.warning( - '%s is not a valid Py3o template filename', filename) + logger.warning("Invalid py3o template %s", filename, exc_info=1) + logger.warning("%s is not a valid Py3o template filename", filename) return False @api.multi @@ -125,13 +128,12 @@ class Py3oReport(models.TransientModel): if report_xml.module: # if the default is defined flbk_filename = pkg_resources.resource_filename( - "odoo.addons.%s" % report_xml.module, - tmpl_name, + "odoo.addons.%s" % report_xml.module, tmpl_name ) elif self._is_valid_template_path(tmpl_name): flbk_filename = os.path.realpath(tmpl_name) if self._is_valid_template_filename(flbk_filename): - with open(flbk_filename, 'rb') as tmpl: + with open(flbk_filename, "rb") as tmpl: return tmpl.read() return None @@ -163,19 +165,14 @@ class Py3oReport(models.TransientModel): report_xml = self.ir_actions_report_id if report_xml.py3o_template_id.py3o_template_data: # if a user gave a report template - tmpl_data = b64decode( - report_xml.py3o_template_id.py3o_template_data - ) + tmpl_data = b64decode(report_xml.py3o_template_id.py3o_template_data) else: tmpl_data = self._get_template_fallback(model_instance) if tmpl_data is None: # if for any reason the template is not found - raise TemplateNotFound( - _('No template found. Aborting.'), - sys.exc_info(), - ) + raise TemplateNotFound(_("No template found. Aborting."), sys.exc_info()) return tmpl_data @@ -194,23 +191,20 @@ class Py3oReport(models.TransientModel): def _get_parser_context(self, model_instance, data): report_xml = self.ir_actions_report_id context = Py3oParserContext(self.env).localcontext - context.update( - report_xml._get_rendering_context(model_instance.ids, data) - ) - context['objects'] = model_instance + context.update(report_xml._get_rendering_context(model_instance.ids, data)) + context["objects"] = model_instance self._extend_parser_context(context, report_xml) return context @api.multi def _postprocess_report(self, model_instance, result_path): if len(model_instance) == 1 and self.ir_actions_report_id.attachment: - with open(result_path, 'rb') as f: + with open(result_path, "rb") as f: # we do all the generation process using files to avoid memory # consumption... # ... but odoo wants the whole data in memory anyways :) buffer = BytesIO(f.read()) - self.ir_actions_report_id.postprocess_pdf_report( - model_instance, buffer) + self.ir_actions_report_id.postprocess_pdf_report(model_instance, buffer) return result_path @api.multi @@ -219,23 +213,22 @@ class Py3oReport(models.TransientModel): """ self.ensure_one() result_fd, result_path = tempfile.mkstemp( - suffix='.ods', prefix='p3o.report.tmp.') + suffix=".ods", prefix="p3o.report.tmp." + ) tmpl_data = self.get_template(model_instance) in_stream = BytesIO(tmpl_data) - with closing(os.fdopen(result_fd, 'wb+')) as out_stream: + with closing(os.fdopen(result_fd, "wb+")) as out_stream: template = Template(in_stream, out_stream, escape_false=True) localcontext = self._get_parser_context(model_instance, data) template.render(localcontext) out_stream.seek(0) tmpl_data = out_stream.read() - if self.env.context.get('report_py3o_skip_conversion'): + if self.env.context.get("report_py3o_skip_conversion"): return result_path - result_path = self._convert_single_report( - result_path, model_instance, data - ) + result_path = self._convert_single_report(result_path, model_instance, data) return self._postprocess_report(model_instance, result_path) @@ -243,21 +236,19 @@ class Py3oReport(models.TransientModel): def _convert_single_report(self, result_path, model_instance, data): """Run a command to convert to our target format""" if not self.ir_actions_report_id.is_py3o_native_format: - command = self._convert_single_report_cmd( - result_path, model_instance, data, - ) - logger.debug('Running command %s', command) - output = subprocess.check_output( - command, cwd=os.path.dirname(result_path), - ) - logger.debug('Output was %s', output) + command = self._convert_single_report_cmd(result_path, model_instance, data) + logger.debug("Running command %s", command) + output = subprocess.check_output(command, cwd=os.path.dirname(result_path)) + logger.debug("Output was %s", output) self._cleanup_tempfiles([result_path]) result_path, result_filename = os.path.split(result_path) result_path = os.path.join( - result_path, '%s.%s' % ( + result_path, + "%s.%s" + % ( os.path.splitext(result_filename)[0], - self.ir_actions_report_id.py3o_filetype - ) + self.ir_actions_report_id.py3o_filetype, + ), ) return result_path @@ -267,43 +258,42 @@ class Py3oReport(models.TransientModel): lo_bin = self.ir_actions_report_id.lo_bin_path if not lo_bin: raise RuntimeError( - _("Libreoffice runtime not available. " - "Please contact your administrator.") + _( + "Libreoffice runtime not available. " + "Please contact your administrator." + ) ) return [ lo_bin, - '--headless', - '--convert-to', + "--headless", + "--convert-to", self.ir_actions_report_id.py3o_filetype, result_path, ] @api.multi - def _get_or_create_single_report(self, model_instance, data, - existing_reports_attachment): + def _get_or_create_single_report( + self, model_instance, data, existing_reports_attachment + ): self.ensure_one() - attachment = existing_reports_attachment.get( - model_instance.id) + attachment = existing_reports_attachment.get(model_instance.id) if attachment and self.ir_actions_report_id.attachment_use: content = base64.decodestring(attachment.datas) - report_file = tempfile.mktemp( - "." + self.ir_actions_report_id.py3o_filetype) + report_file = tempfile.mktemp("." + self.ir_actions_report_id.py3o_filetype) with open(report_file, "wb") as f: f.write(content) return report_file - return self._create_single_report( - model_instance, data) + return self._create_single_report(model_instance, data) @api.multi def _zip_results(self, reports_path): self.ensure_one() zfname_prefix = self.ir_actions_report_id.name - result_path = tempfile.mktemp(suffix="zip", prefix='py3o-zip-result') - with ZipFile(result_path, 'w', ZIP_DEFLATED) as zf: + result_path = tempfile.mktemp(suffix="zip", prefix="py3o-zip-result") + with ZipFile(result_path, "w", ZIP_DEFLATED) as zf: cpt = 0 for report in reports_path: - fname = "%s_%d.%s" % ( - zfname_prefix, cpt, report.split('.')[-1]) + fname = "%s_%d.%s" % (zfname_prefix, cpt, report.split(".")[-1]) zf.write(report, fname) cpt += 1 @@ -321,8 +311,9 @@ class Py3oReport(models.TransientModel): reader = PdfFileReader(path) writer.appendPagesFromReader(reader) merged_file_fd, merged_file_path = tempfile.mkstemp( - suffix='.pdf', prefix='report.merged.tmp.') - with closing(os.fdopen(merged_file_fd, 'wb')) as merged_file: + suffix=".pdf", prefix="report.merged.tmp." + ) + with closing(os.fdopen(merged_file_fd, "wb")) as merged_file: writer.write(merged_file) return merged_file_path @@ -337,7 +328,7 @@ class Py3oReport(models.TransientModel): if filetype == formats.FORMAT_PDF: return self._merge_pdf(reports_path), formats.FORMAT_PDF else: - return self._zip_results(reports_path), 'zip' + return self._zip_results(reports_path), "zip" @api.model def _cleanup_tempfiles(self, temporary_files): @@ -346,29 +337,26 @@ class Py3oReport(models.TransientModel): try: os.unlink(temporary_file) except (OSError, IOError): - logger.error( - 'Error when trying to remove file %s' % temporary_file) + logger.error("Error when trying to remove file %s" % temporary_file) @api.multi def create_report(self, res_ids, data): """ Override this function to handle our py3o report """ - model_instances = self.env[self.ir_actions_report_id.model].browse( - res_ids) + model_instances = self.env[self.ir_actions_report_id.model].browse(res_ids) reports_path = [] - if ( - len(res_ids) > 1 and - self.ir_actions_report_id.py3o_multi_in_one): - reports_path.append( - self._create_single_report( - model_instances, data)) + if len(res_ids) > 1 and self.ir_actions_report_id.py3o_multi_in_one: + reports_path.append(self._create_single_report(model_instances, data)) else: - existing_reports_attachment = \ - self.ir_actions_report_id._get_attachments(res_ids) + existing_reports_attachment = self.ir_actions_report_id._get_attachments( + res_ids + ) for model_instance in model_instances: reports_path.append( self._get_or_create_single_report( - model_instance, data, existing_reports_attachment)) + model_instance, data, existing_reports_attachment + ) + ) result_path, filetype = self._merge_results(reports_path) reports_path.append(result_path) @@ -378,7 +366,7 @@ class Py3oReport(models.TransientModel): # consumption... # ... but odoo wants the whole data in memory anyways :) - with open(result_path, 'r+b') as fd: + with open(result_path, "r+b") as fd: res = fd.read() self._cleanup_tempfiles(set(reports_path)) return res, filetype diff --git a/report_py3o/models/py3o_template.py b/report_py3o/models/py3o_template.py index e2a3632b5..1f9373322 100644 --- a/report_py3o/models/py3o_template.py +++ b/report_py3o/models/py3o_template.py @@ -4,20 +4,21 @@ from odoo import fields, models class Py3oTemplate(models.Model): - _name = 'py3o.template' - _description = 'Py3o template' + _name = "py3o.template" + _description = "Py3o template" name = fields.Char(required=True) py3o_template_data = fields.Binary("LibreOffice Template") filetype = fields.Selection( selection=[ - ('odt', "ODF Text Document"), - ('ods', "ODF Spreadsheet"), - ('odp', "ODF Presentation"), - ('fodt', "ODF Text Document (Flat)"), - ('fods', "ODF Spreadsheet (Flat)"), - ('fodp', "ODF Presentation (Flat)"), + ("odt", "ODF Text Document"), + ("ods", "ODF Spreadsheet"), + ("odp", "ODF Presentation"), + ("fodt", "ODF Text Document (Flat)"), + ("fods", "ODF Spreadsheet (Flat)"), + ("fodp", "ODF Presentation (Flat)"), ], string="LibreOffice Template File Type", required=True, - default='odt') + default="odt", + ) diff --git a/report_py3o/tests/test_report_py3o.py b/report_py3o/tests/test_report_py3o.py index 3c41f599a..c2e02b47e 100644 --- a/report_py3o/tests/test_report_py3o.py +++ b/report_py3o/tests/test_report_py3o.py @@ -2,39 +2,40 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).). import base64 -from base64 import b64decode -import mock +import logging import os -import pkg_resources import shutil import tempfile +from base64 import b64decode, b64encode from contextlib import contextmanager -from odoo import tools -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 +import mock +import pkg_resources from PyPDF2 import PdfFileWriter from PyPDF2.pdf import PageObject -import logging + +from odoo import tools +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase + +from odoo.addons.base.tests.test_mimetypes import PNG + +from ..models._py3o_parser_context import format_multiline_value +from ..models.ir_actions_report import PY3O_CONVERSION_COMMAND_PARAMETER +from ..models.py3o_report import TemplateNotFound logger = logging.getLogger(__name__) try: from genshi.core import Markup except ImportError: - logger.debug('Cannot import genshi.core') + logger.debug("Cannot import genshi.core") @contextmanager def temporary_copy(path): filname, ext = os.path.splitext(path) - tmp_filename = tempfile.mktemp(suffix='.' + ext) + tmp_filename = tempfile.mktemp(suffix="." + ext) try: shutil.copy2(path, tmp_filename) yield tmp_filename @@ -43,36 +44,35 @@ def temporary_copy(path): class TestReportPy3o(TransactionCase): - def setUp(self): super(TestReportPy3o, self).setUp() self.env.user.image = PNG self.report = self.env.ref("report_py3o.res_users_report_py3o") - self.py3o_report = self.env['py3o.report'].create({ - 'ir_actions_report_id': self.report.id}) + self.py3o_report = self.env["py3o.report"].create( + {"ir_actions_report_id": self.report.id} + ) def test_required_py3_filetype(self): self.assertEqual(self.report.report_type, "py3o") with self.assertRaises(ValidationError) as e: self.report.py3o_filetype = False self.assertEqual( - e.exception.name, - "Field 'Output Format' is required for Py3O report") + e.exception.name, "Field 'Output Format' is required for Py3O report" + ) - def _render_patched(self, result_text='test result', call_count=1): - py3o_report = self.env['py3o.report'] - py3o_report_obj = py3o_report.create({ - "ir_actions_report_id": self.report.id - }) + def _render_patched(self, result_text="test result", call_count=1): + py3o_report = self.env["py3o.report"] + py3o_report_obj = py3o_report.create({"ir_actions_report_id": self.report.id}) with mock.patch.object( - py3o_report.__class__, '_create_single_report') as patched_pdf: - result = tempfile.mktemp('.txt') - with open(result, 'w') as fp: + py3o_report.__class__, "_create_single_report" + ) as patched_pdf: + result = tempfile.mktemp(".txt") + with open(result, "w") as fp: fp.write(result_text) - patched_pdf.side_effect = lambda record, data:\ - py3o_report_obj._postprocess_report( - record, result - ) or result + patched_pdf.side_effect = ( + lambda record, data: py3o_report_obj._postprocess_report(record, result) + or result + ) # test the call the the create method inside our custom parser self.report.render(self.env.user.ids) self.assertEqual(call_count, patched_pdf.call_count) @@ -85,35 +85,35 @@ class TestReportPy3o(TransactionCase): def test_reports_merge_zip(self): self.report.py3o_filetype = "odt" - users = self.env['res.users'].search([]) + users = self.env["res.users"].search([]) self.assertTrue(len(users) > 0) - py3o_report = self.env['py3o.report'] + py3o_report = self.env["py3o.report"] _zip_results = self.py3o_report._zip_results with mock.patch.object( - py3o_report.__class__, '_zip_results') as patched_zip_results: + py3o_report.__class__, "_zip_results" + ) as patched_zip_results: patched_zip_results.side_effect = _zip_results content, filetype = self.report.render(users.ids) self.assertEqual(1, patched_zip_results.call_count) - self.assertEqual(filetype, 'zip') + self.assertEqual(filetype, "zip") def test_reports_merge_pdf(self): reports_path = [] - for i in range(0, 3): - result = tempfile.mktemp('.txt') + for _i in range(0, 3): + result = tempfile.mktemp(".txt") writer = PdfFileWriter() writer.addPage(PageObject.createBlankPage(width=100, height=100)) - with open(result, 'wb') as fp: + with open(result, "wb") as fp: writer.write(fp) reports_path.append(result) res = self.py3o_report._merge_pdf(reports_path) self.assertTrue(res) def test_report_load_from_attachment(self): - self.report.write({"attachment_use": True, - "attachment": "'my_saved_report'"}) - attachments = self.env['ir.attachment'].search([]) + self.report.write({"attachment_use": True, "attachment": "'my_saved_report'"}) + attachments = self.env["ir.attachment"].search([]) self._render_patched() - new_attachments = self.env['ir.attachment'].search([]) + new_attachments = self.env["ir.attachment"].search([]) created_attachement = new_attachments - attachments self.assertEqual(1, len(created_attachement)) content = b64decode(created_attachement.datas) @@ -123,7 +123,7 @@ class TestReportPy3o(TransactionCase): # generated document created_attachement.datas = base64.encodestring(b"new content") res = self.report.render(self.env.user.ids) - self.assertEqual((b'new content', self.report.py3o_filetype), res) + self.assertEqual((b"new content", self.report.py3o_filetype), res) def test_report_post_process(self): """ @@ -131,24 +131,24 @@ class TestReportPy3o(TransactionCase): generated report into an ir.attachment if requested. """ self.report.attachment = "object.name + '.txt'" - ir_attachment = self.env['ir.attachment'] - attachements = ir_attachment.search([(1, '=', 1)]) + ir_attachment = self.env["ir.attachment"] + attachements = ir_attachment.search([(1, "=", 1)]) self._render_patched() - attachements = ir_attachment.search([(1, '=', 1)]) - attachements + attachements = ir_attachment.search([(1, "=", 1)]) - attachements self.assertEqual(1, len(attachements.ids)) - self.assertEqual(self.env.user.name + '.txt', attachements.name) + self.assertEqual(self.env.user.name + ".txt", attachements.name) self.assertEqual(self.env.user._name, attachements.res_model) self.assertEqual(self.env.user.id, attachements.res_id) - self.assertEqual(b'test result', b64decode(attachements.datas)) + self.assertEqual(b"test result", b64decode(attachements.datas)) - @tools.misc.mute_logger('odoo.addons.report_py3o.models.py3o_report') + @tools.misc.mute_logger("odoo.addons.report_py3o.models.py3o_report") def test_report_template_configs(self): # the demo template is specified with a relative path in in the module # path tmpl_name = self.report.py3o_template_fallback flbk_filename = pkg_resources.resource_filename( - "odoo.addons.%s" % self.report.module, - tmpl_name) + "odoo.addons.%s" % self.report.module, tmpl_name + ) self.assertTrue(os.path.exists(flbk_filename)) res = self.report.render(self.env.user.ids) self.assertTrue(res) @@ -164,61 +164,63 @@ class TestReportPy3o(TransactionCase): self.report.render(self.env.user.ids) with temporary_copy(flbk_filename) as tmp_filename: self.report.py3o_template_fallback = tmp_filename - tools.config.misc['report_py3o'] = { - 'root_tmpl_path': os.path.dirname(tmp_filename)} + tools.config.misc["report_py3o"] = { + "root_tmpl_path": os.path.dirname(tmp_filename) + } res = self.report.render(self.env.user.ids) self.assertTrue(res) # the tempalte can also be provided as a binary field self.report.py3o_template_fallback = False - with open(flbk_filename, 'rb') as tmpl_file: + with open(flbk_filename, "rb") as tmpl_file: tmpl_data = b64encode(tmpl_file.read()) - py3o_template = self.env['py3o.template'].create({ - 'name': 'test_template', - 'py3o_template_data': tmpl_data, - 'filetype': 'odt'}) + py3o_template = self.env["py3o.template"].create( + { + "name": "test_template", + "py3o_template_data": tmpl_data, + "filetype": "odt", + } + ) self.report.py3o_template_id = py3o_template self.report.py3o_template_fallback = flbk_filename res = self.report.render(self.env.user.ids) self.assertTrue(res) - @tools.misc.mute_logger('odoo.addons.report_py3o.models.py3o_report') + @tools.misc.mute_logger("odoo.addons.report_py3o.models.py3o_report") def test_report_template_fallback_validity(self): tmpl_name = self.report.py3o_template_fallback flbk_filename = pkg_resources.resource_filename( - "odoo.addons.%s" % self.report.module, - tmpl_name) + "odoo.addons.%s" % self.report.module, tmpl_name + ) # an exising file in a native format is a valid template if it's - self.assertTrue(self.py3o_report._get_template_from_path( - tmpl_name)) + self.assertTrue(self.py3o_report._get_template_from_path(tmpl_name)) self.report.module = None # a directory is not a valid template.. - self.assertFalse(self.py3o_report._get_template_from_path('/etc/')) - self.assertFalse(self.py3o_report._get_template_from_path('.')) + self.assertFalse(self.py3o_report._get_template_from_path("/etc/")) + self.assertFalse(self.py3o_report._get_template_from_path(".")) # an vaild template outside the root_tmpl_path is not a valid template # path # located in trusted directory self.report.py3o_template_fallback = flbk_filename - self.assertFalse(self.py3o_report._get_template_from_path( - flbk_filename)) + self.assertFalse(self.py3o_report._get_template_from_path(flbk_filename)) with temporary_copy(flbk_filename) as tmp_filename: - self.assertTrue(self.py3o_report._get_template_from_path( - tmp_filename)) + self.assertTrue(self.py3o_report._get_template_from_path(tmp_filename)) # check security - self.assertFalse(self.py3o_report._get_template_from_path( - 'rm -rf . & %s' % flbk_filename)) + self.assertFalse( + self.py3o_report._get_template_from_path("rm -rf . & %s" % flbk_filename) + ) # a file in a non native LibreOffice format is not a valid template - with tempfile.NamedTemporaryFile(suffix='.toto')as f: - self.assertFalse(self.py3o_report._get_template_from_path( - f.name)) + with tempfile.NamedTemporaryFile(suffix=".toto") as f: + self.assertFalse(self.py3o_report._get_template_from_path(f.name)) # non exising files are not valid template - self.assertFalse(self.py3o_report._get_template_from_path( - '/etc/test.odt')) + self.assertFalse(self.py3o_report._get_template_from_path("/etc/test.odt")) def test_escape_html_characters_format_multiline_value(self): - self.assertEqual(Markup('<>&test;'), - format_multiline_value('<>\n&test;')) + 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 @@ -229,8 +231,9 @@ class TestReportPy3o(TransactionCase): 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.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 @@ -249,8 +252,9 @@ class TestReportPy3o(TransactionCase): 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.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) diff --git a/report_py3o_fusion_server/__manifest__.py b/report_py3o_fusion_server/__manifest__.py index 18b5126d2..805362dc6 100644 --- a/report_py3o_fusion_server/__manifest__.py +++ b/report_py3o_fusion_server/__manifest__.py @@ -1,31 +1,21 @@ # 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': '12.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", - "demo/py3o_pdf_options.xml", - ], - 'data': [ + "name": "Py3o Report Engine - Fusion server support", + "summary": "Let the fusion server handle format conversion.", + "version": "12.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", "demo/py3o_pdf_options.xml"], + "data": [ "views/ir_actions_report.xml", - 'security/ir.model.access.csv', - 'views/py3o_server.xml', - 'views/py3o_pdf_options.xml', + "security/ir.model.access.csv", + "views/py3o_server.xml", + "views/py3o_pdf_options.xml", ], - 'installable': True, + "installable": True, } diff --git a/report_py3o_fusion_server/models/ir_actions_report.py b/report_py3o_fusion_server/models/ir_actions_report.py index 6c8927d85..bda8982ea 100644 --- a/report_py3o_fusion_server/models/ir_actions_report.py +++ b/report_py3o_fusion_server/models/ir_actions_report.py @@ -2,14 +2,16 @@ # © 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__) class IrActionsReport(models.Model): - _inherit = 'ir.actions.report' + _inherit = "ir.actions.report" @api.multi @api.constrains("py3o_is_local_fusion", "py3o_server_id") @@ -17,40 +19,52 @@ class IrActionsReport(models.Model): for report in self: if report.report_type != "py3o": continue - if (not report.py3o_is_local_fusion and not report.py3o_server_id): - raise ValidationError(_( - "You can not use remote fusion without Fusion server. " - "Please specify a Fusion Server")) + if not report.py3o_is_local_fusion and not report.py3o_server_id: + raise ValidationError( + _( + "You can not use remote fusion without Fusion server. " + "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") + "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") pdf_options_id = fields.Many2one( - 'py3o.pdf.options', string='PDF Options', ondelete='restrict', + "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.") + "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.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): + 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 + 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/models/py3o_pdf_options.py b/report_py3o_fusion_server/models/py3o_pdf_options.py index d292fa18e..7a6399e09 100644 --- a/report_py3o_fusion_server/models/py3o_pdf_options.py +++ b/report_py3o_fusion_server/models/py3o_pdf_options.py @@ -2,223 +2,283 @@ # @author: Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, fields, models, _ -from odoo.exceptions import ValidationError import logging + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + logger = logging.getLogger(__name__) class Py3oPdfOptions(models.Model): - _name = 'py3o.pdf.options' - _description = 'Define PDF export options for Libreoffice' + _name = "py3o.pdf.options" + _description = "Define PDF export options for Libreoffice" name = fields.Char(required=True) # GENERAL TAB # UseLosslessCompression (bool) - image_compression = fields.Selection([ - ('lossless', 'Lossless Compression'), - ('jpeg', 'JPEG Compression'), - ], string='Image Compression', default='jpeg') + image_compression = fields.Selection( + [("lossless", "Lossless Compression"), ("jpeg", "JPEG Compression")], + string="Image Compression", + default="jpeg", + ) # Quality (int) image_jpeg_quality = fields.Integer( - string='Image JPEG Quality', default=90, - help="Enter a percentage between 0 and 100.") + string="Image JPEG Quality", + default=90, + help="Enter a percentage between 0 and 100.", + ) # ReduceImageResolution (bool) and MaxImageResolution (int) - image_reduce_resolution = fields.Selection([ - ('none', 'Disable'), - ('75', '75 DPI'), - ('150', '150 DPI'), - ('300', '300 DPI'), - ('600', '600 DPI'), - ('1200', '1200 DPI'), - ], string='Reduce Image Resolution', default='300') - watermark = fields.Boolean('Sign With Watermark') + image_reduce_resolution = fields.Selection( + [ + ("none", "Disable"), + ("75", "75 DPI"), + ("150", "150 DPI"), + ("300", "300 DPI"), + ("600", "600 DPI"), + ("1200", "1200 DPI"), + ], + string="Reduce Image Resolution", + default="300", + ) + watermark = fields.Boolean("Sign With Watermark") # Watermark (string) - watermark_text = fields.Char('WaterMark Text') + watermark_text = fields.Char("WaterMark Text") # UseTaggedPDF (bool) - tagged_pdf = fields.Boolean('Tagged PDF (add document structure)') + tagged_pdf = fields.Boolean("Tagged PDF (add document structure)") # SelectPdfVersion (int) # 0 = PDF 1.4 (default selection). # 1 = PDF/A-1 (ISO 19005-1:2005) pdfa = fields.Boolean( - 'Archive PDF/A-1a (ISO 19005-1)', + "Archive PDF/A-1a (ISO 19005-1)", help="If you enable this option, you will not be able to " - "password-protect the document or apply other security settings.") + "password-protect the document or apply other security settings.", + ) # ExportFormFields (bool) - pdf_form = fields.Boolean('Create PDF Form', default=True) + pdf_form = fields.Boolean("Create PDF Form", default=True) # FormsType (int) - pdf_form_format = fields.Selection([ - ('0', 'FDF'), - ('1', 'PDF'), - ('2', 'HTML'), - ('3', 'XML'), - ], string='Submit Format', default='0') + pdf_form_format = fields.Selection( + [("0", "FDF"), ("1", "PDF"), ("2", "HTML"), ("3", "XML")], + string="Submit Format", + default="0", + ) # AllowDuplicateFieldNames (bool) - pdf_form_allow_duplicate = fields.Boolean('Allow Duplicate Field Names') + pdf_form_allow_duplicate = fields.Boolean("Allow Duplicate Field Names") # ExportBookmarks (bool) - export_bookmarks = fields.Boolean('Export Bookmarks', default=True) + export_bookmarks = fields.Boolean("Export Bookmarks", default=True) # ExportPlaceholders (bool) - export_placeholders = fields.Boolean('Export Placeholders', default=True) + export_placeholders = fields.Boolean("Export Placeholders", default=True) # ExportNotes (bool) - export_comments = fields.Boolean('Export Comments') + export_comments = fields.Boolean("Export Comments") # ExportHiddenSlides (bool) ?? - export_hidden_slides = fields.Boolean( - 'Export Automatically Insered Blank Pages') + export_hidden_slides = fields.Boolean("Export Automatically Insered Blank Pages") # Doesn't make sense to have the option "View PDF after export" ! :) # INITIAL VIEW TAB # InitialView (int) - initial_view = fields.Selection([ - ('0', 'Page Only'), - ('1', 'Bookmarks and Page'), - ('2', 'Thumbnails and Page'), - ], string='Panes', default='0') + initial_view = fields.Selection( + [("0", "Page Only"), ("1", "Bookmarks and Page"), ("2", "Thumbnails and Page")], + string="Panes", + default="0", + ) # InitialPage (int) - initial_page = fields.Integer(string='Initial Page', default=1) + initial_page = fields.Integer(string="Initial Page", default=1) # Magnification (int) - magnification = fields.Selection([ - ('0', 'Default'), - ('1', 'Fit in Window'), - ('2', 'Fit Width'), - ('3', 'Fit Visible'), - ('4', 'Zoom'), - ], string='Magnification', default='0') + magnification = fields.Selection( + [ + ("0", "Default"), + ("1", "Fit in Window"), + ("2", "Fit Width"), + ("3", "Fit Visible"), + ("4", "Zoom"), + ], + string="Magnification", + default="0", + ) # Zoom (int) zoom = fields.Integer( - string='Zoom Factor', default=100, - help='Possible values: from 50 to 1600') + string="Zoom Factor", default=100, help="Possible values: from 50 to 1600" + ) # PageLayout (int) - page_layout = fields.Selection([ - ('0', 'Default'), - ('1', 'Single Page'), - ('2', 'Continuous'), - ('3', 'Continuous Facing'), - ], string='Page Layout', default='0') + page_layout = fields.Selection( + [ + ("0", "Default"), + ("1", "Single Page"), + ("2", "Continuous"), + ("3", "Continuous Facing"), + ], + string="Page Layout", + default="0", + ) # USER INTERFACE TAB # ResizeWindowToInitialPage (bool) resize_windows_initial_page = fields.Boolean( - string='Resize Windows to Initial Page') + string="Resize Windows to Initial Page" + ) # CenterWindow (bool) - center_window = fields.Boolean(string='Center Window on Screen') + center_window = fields.Boolean(string="Center Window on Screen") # OpenInFullScreenMode (bool) - open_fullscreen = fields.Boolean(string='Open in Full Screen Mode') + open_fullscreen = fields.Boolean(string="Open in Full Screen Mode") # DisplayPDFDocumentTitle (bool) - display_document_title = fields.Boolean(string='Display Document Title') + display_document_title = fields.Boolean(string="Display Document Title") # HideViewerMenubar (bool) - hide_menubar = fields.Boolean(string='Hide Menubar') + hide_menubar = fields.Boolean(string="Hide Menubar") # HideViewerToolbar (bool) - hide_toolbar = fields.Boolean(string='Hide Toolbar') + hide_toolbar = fields.Boolean(string="Hide Toolbar") # HideViewerWindowControls (bool) - hide_window_controls = fields.Boolean(string='Hide Windows Controls') + hide_window_controls = fields.Boolean(string="Hide Windows Controls") # OpenBookmarkLevels (int) -1 = all (default) from 1 to 10 - open_bookmark_levels = fields.Selection([ - ('-1', 'All Levels'), - ('1', '1'), - ('2', '2'), - ('3', '3'), - ('4', '4'), - ('5', '5'), - ('6', '6'), - ('7', '7'), - ('8', '8'), - ('9', '9'), - ('10', '10'), - ], default='-1', string='Visible Bookmark Levels') + open_bookmark_levels = fields.Selection( + [ + ("-1", "All Levels"), + ("1", "1"), + ("2", "2"), + ("3", "3"), + ("4", "4"), + ("5", "5"), + ("6", "6"), + ("7", "7"), + ("8", "8"), + ("9", "9"), + ("10", "10"), + ], + default="-1", + string="Visible Bookmark Levels", + ) # LINKS TAB # ExportBookmarksToPDFDestination (bool) export_bookmarks_named_dest = fields.Boolean( - string='Export Bookmarks as Named Destinations') + string="Export Bookmarks as Named Destinations" + ) # ConvertOOoTargetToPDFTarget (bool) convert_doc_ref_to_pdf_target = fields.Boolean( - string='Convert Document References to PDF Targets') + string="Convert Document References to PDF Targets" + ) # ExportLinksRelativeFsys (bool) - export_filesystem_urls = fields.Boolean( - string='Export URLs Relative to Filesystem') + export_filesystem_urls = fields.Boolean(string="Export URLs Relative to Filesystem") # PDFViewSelection -> mnDefaultLinkAction (int) - cross_doc_link_action = fields.Selection([ - ('0', 'Default'), - ('1', 'Open with PDF Reader Application'), - ('2', 'Open with Internet Browser'), - ], string='Cross-document Links', default='0') + cross_doc_link_action = fields.Selection( + [ + ("0", "Default"), + ("1", "Open with PDF Reader Application"), + ("2", "Open with Internet Browser"), + ], + string="Cross-document Links", + default="0", + ) # SECURITY TAB # EncryptFile (bool) - encrypt = fields.Boolean('Encrypt') + encrypt = fields.Boolean("Encrypt") # DocumentOpenPassword (char) - document_password = fields.Char(string='Document Password') + document_password = fields.Char(string="Document Password") # RestrictPermissions (bool) - restrict_permissions = fields.Boolean('Restrict Permissions') + restrict_permissions = fields.Boolean("Restrict Permissions") # PermissionPassword (char) - permission_password = fields.Char(string='Permission Password') + permission_password = fields.Char(string="Permission Password") # TODO PreparedPasswords + PreparedPermissionPassword # I don't see those fields in the LO interface ! # But they are used in the LO code... # Printing (int) - printing = fields.Selection([ - ('0', 'Not Permitted'), - ('1', 'Low Resolution (150 dpi)'), - ('2', 'High Resolution'), - ], string='Printing', default='2') + printing = fields.Selection( + [ + ("0", "Not Permitted"), + ("1", "Low Resolution (150 dpi)"), + ("2", "High Resolution"), + ], + string="Printing", + default="2", + ) # Changes (int) - changes = fields.Selection([ - ('0', 'Not Permitted'), - ('1', 'Inserting, Deleting and Rotating Pages'), - ('2', 'Filling in Form Fields'), - ('3', 'Commenting, Filling in Form Fields'), - ('4', 'Any Except Extracting Pages'), - ], string='Changes', default='4') + changes = fields.Selection( + [ + ("0", "Not Permitted"), + ("1", "Inserting, Deleting and Rotating Pages"), + ("2", "Filling in Form Fields"), + ("3", "Commenting, Filling in Form Fields"), + ("4", "Any Except Extracting Pages"), + ], + string="Changes", + default="4", + ) # EnableCopyingOfContent (bool) content_copying_allowed = fields.Boolean( - string='Enable Copying of Content', default=True) + string="Enable Copying of Content", default=True + ) # EnableTextAccessForAccessibilityTools (bool) text_access_accessibility_tools_allowed = fields.Boolean( - string='Enable Text Access for Accessibility Tools', default=True) - # DIGITAL SIGNATURE TAB - # This will be possible but not easy - # Because the certificate parameter is a pointer to a certificate - # already registered in LO - # On Linux LO reuses the Mozilla certificate store (on Windows the - # one from Windows) - # But there seems to be some possibilities to send this certificate via API - # It seems you can add temporary certificates during runtime: - # https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1security_1_1XCertificateContainer.html - # Here is an API to retrieve the known certificates: - # https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1xml_1_1crypto_1_1XSecurityEnvironment.html - # Thanks to 'samuel_m' on libreoffice-dev IRC chan for pointing me to this + string="Enable Text Access for Accessibility Tools", default=True + ) + + """ + DIGITAL SIGNATURE TAB + This will be possible but not easy + Because the certificate parameter is a pointer to a certificate + already registered in LO + On Linux LO reuses the Mozilla certificate store (on Windows the + one from Windows) + But there seems to be some possibilities to send this certificate via API + It seems you can add temporary certificates during runtime: + https://api.libreoffice.org/docs/idl/ref/ + interfacecom_1_1sun_1_1star_1_1security_1_1XCertificateContainer.html + Here is an API to retrieve the known certificates: + https://api.libreoffice.org/docs/idl/ref/ + interfacecom_1_1sun_1_1star_1_1xml_1_1crypto_1_1XSecurityEnvironment.html + Thanks to 'samuel_m' on libreoffice-dev IRC chan for pointing me to this + """ @api.constrains( - 'image_jpeg_quality', 'initial_page', 'pdfa', - 'cross_doc_link_action', 'magnification', 'zoom') + "image_jpeg_quality", + "initial_page", + "pdfa", + "cross_doc_link_action", + "magnification", + "zoom", + ) def check_pdf_options(self): for opt in self: if opt.image_jpeg_quality > 100 or opt.image_jpeg_quality < 1: - raise ValidationError(_( - "The parameter Image JPEG Quality must be between 1 %%" - " and 100 %% (current value: %s %%)") - % opt.image_jpeg_quality) + raise ValidationError( + _( + "The parameter Image JPEG Quality must be between 1 %%" + " and 100 %% (current value: %s %%)" + ) + % opt.image_jpeg_quality + ) if opt.initial_page < 1: - raise ValidationError(_( - "The initial page parameter must be strictly positive " - "(current value: %d)") % opt.initial_page) - if opt.pdfa and opt.cross_doc_link_action == '1': - raise ValidationError(_( - "The PDF/A option is not compatible with " - "'Cross-document Links' = " - "'Open with PDF Reader Application'.")) - if opt.magnification == '4' and (opt.zoom < 50 or opt.zoom > 1600): - raise ValidationError(_( - "The value of the zoom factor must be between 50 and 1600 " - "(current value: %d)") % opt.zoom) + raise ValidationError( + _( + "The initial page parameter must be strictly positive " + "(current value: %d)" + ) + % opt.initial_page + ) + if opt.pdfa and opt.cross_doc_link_action == "1": + raise ValidationError( + _( + "The PDF/A option is not compatible with " + "'Cross-document Links' = " + "'Open with PDF Reader Application'." + ) + ) + if opt.magnification == "4" and (opt.zoom < 50 or opt.zoom > 1600): + raise ValidationError( + _( + "The value of the zoom factor must be between 50 and 1600 " + "(current value: %d)" + ) + % opt.zoom + ) - @api.onchange('encrypt') + @api.onchange("encrypt") def encrypt_change(self): if not self.encrypt: self.document_password = False - @api.onchange('restrict_permissions') + @api.onchange("restrict_permissions") def restrict_permissions_change(self): if not self.restrict_permissions: self.permission_password = False - @api.onchange('pdfa') + @api.onchange("pdfa") def pdfa_change(self): if self.pdfa: self.pdf_form = False @@ -229,87 +289,97 @@ class Py3oPdfOptions(models.Model): self.ensure_one() options = {} # GENERAL TAB - if self.image_compression == 'lossless': - options['UseLosslessCompression'] = True + if self.image_compression == "lossless": + options["UseLosslessCompression"] = True else: - options['UseLosslessCompression'] = False - options['Quality'] = self.image_jpeg_quality - if self.image_reduce_resolution != 'none': - options['ReduceImageResolution'] = True - options['MaxImageResolution'] = int(self.image_reduce_resolution) + options["UseLosslessCompression"] = False + options["Quality"] = self.image_jpeg_quality + if self.image_reduce_resolution != "none": + options["ReduceImageResolution"] = True + options["MaxImageResolution"] = int(self.image_reduce_resolution) else: - options['ReduceImageResolution'] = False + options["ReduceImageResolution"] = False if self.watermark and self.watermark_text: - options['Watermark'] = self.watermark_text + options["Watermark"] = self.watermark_text if self.pdfa: - options['SelectPdfVersion'] = 1 - options['UseTaggedPDF'] = self.tagged_pdf + options["SelectPdfVersion"] = 1 + options["UseTaggedPDF"] = self.tagged_pdf else: - options['SelectPdfVersion'] = 0 + options["SelectPdfVersion"] = 0 if self.pdf_form and self.pdf_form_format and not self.pdfa: - options['ExportFormFields'] = True - options['FormsType'] = int(self.pdf_form_format) - options['AllowDuplicateFieldNames'] = self.pdf_form_allow_duplicate + options["ExportFormFields"] = True + options["FormsType"] = int(self.pdf_form_format) + options["AllowDuplicateFieldNames"] = self.pdf_form_allow_duplicate else: - options['ExportFormFields'] = False + options["ExportFormFields"] = False - options.update({ - 'ExportBookmarks': self.export_bookmarks, - 'ExportPlaceholders': self.export_placeholders, - 'ExportNotes': self.export_comments, - 'ExportHiddenSlides': self.export_hidden_slides, - }) + options.update( + { + "ExportBookmarks": self.export_bookmarks, + "ExportPlaceholders": self.export_placeholders, + "ExportNotes": self.export_comments, + "ExportHiddenSlides": self.export_hidden_slides, + } + ) # INITIAL VIEW TAB - options.update({ - 'InitialView': int(self.initial_view), - 'InitialPage': self.initial_page, - 'Magnification': int(self.magnification), - 'PageLayout': int(self.page_layout), - }) + options.update( + { + "InitialView": int(self.initial_view), + "InitialPage": self.initial_page, + "Magnification": int(self.magnification), + "PageLayout": int(self.page_layout), + } + ) - if self.magnification == '4': - options['Zoom'] = self.zoom + if self.magnification == "4": + options["Zoom"] = self.zoom # USER INTERFACE TAB - options.update({ - 'ResizeWindowToInitialPage': self.resize_windows_initial_page, - 'CenterWindow': self.center_window, - 'OpenInFullScreenMode': self.open_fullscreen, - 'DisplayPDFDocumentTitle': self.display_document_title, - 'HideViewerMenubar': self.hide_menubar, - 'HideViewerToolbar': self.hide_toolbar, - 'HideViewerWindowControls': self.hide_window_controls, - }) + options.update( + { + "ResizeWindowToInitialPage": self.resize_windows_initial_page, + "CenterWindow": self.center_window, + "OpenInFullScreenMode": self.open_fullscreen, + "DisplayPDFDocumentTitle": self.display_document_title, + "HideViewerMenubar": self.hide_menubar, + "HideViewerToolbar": self.hide_toolbar, + "HideViewerWindowControls": self.hide_window_controls, + } + ) if self.open_bookmark_levels: - options['OpenBookmarkLevels'] = int(self.open_bookmark_levels) + options["OpenBookmarkLevels"] = int(self.open_bookmark_levels) # LINKS TAB - options.update({ - 'ExportBookmarksToPDFDestination': - self.export_bookmarks_named_dest, - 'ConvertOOoTargetToPDFTarget': self.convert_doc_ref_to_pdf_target, - 'ExportLinksRelativeFsys': self.export_filesystem_urls, - 'PDFViewSelection': int(self.cross_doc_link_action), - }) + options.update( + { + "ExportBookmarksToPDFDestination": self.export_bookmarks_named_dest, + "ConvertOOoTargetToPDFTarget": self.convert_doc_ref_to_pdf_target, + "ExportLinksRelativeFsys": self.export_filesystem_urls, + "PDFViewSelection": int(self.cross_doc_link_action), + } + ) # SECURITY TAB if not self.pdfa: if self.encrypt and self.document_password: - options['EncryptFile'] = True - options['DocumentOpenPassword'] = self.document_password + options["EncryptFile"] = True + options["DocumentOpenPassword"] = self.document_password if self.restrict_permissions and self.permission_password: - options.update({ - 'RestrictPermissions': True, - 'PermissionPassword': self.permission_password, - 'Printing': int(self.printing), - 'Changes': int(self.changes), - 'EnableCopyingOfContent': self.content_copying_allowed, - 'EnableTextAccessForAccessibilityTools': - self.text_access_accessibility_tools_allowed, - }) + # fmt: off + options.update( + { + "RestrictPermissions": True, + "PermissionPassword": self.permission_password, + "Printing": int(self.printing), + "Changes": int(self.changes), + "EnableCopyingOfContent": self.content_copying_allowed, + "EnableTextAccessForAccessibilityTools": + self.text_access_accessibility_tools_allowed, + } + ) + # fmt: on - logger.debug( - 'Py3o PDF options ID %s converted to %s', self.id, options) + logger.debug("Py3o PDF options ID %s converted to %s", self.id, options) return options diff --git a/report_py3o_fusion_server/models/py3o_report.py b/report_py3o_fusion_server/models/py3o_report.py index 318b7a8fa..6b264073f 100644 --- a/report_py3o_fusion_server/models/py3o_report.py +++ b/report_py3o_fusion_server/models/py3o_report.py @@ -5,13 +5,14 @@ import json import logging import os -import requests import tempfile -from datetime import datetime from contextlib import closing +from datetime import datetime +from io import BytesIO + +import requests from openerp import _, api, models from openerp.exceptions import UserError -from io import BytesIO logger = logging.getLogger(__name__) @@ -19,11 +20,11 @@ try: from py3o.template import Template from py3o.template.helpers import Py3oConvertor except ImportError: - logger.debug('Cannot import py3o.template') + logger.debug("Cannot import py3o.template") class Py3oReport(models.TransientModel): - _inherit = 'py3o.report' + _inherit = "py3o.report" @api.multi def _create_single_report(self, model_instance, data): @@ -33,40 +34,32 @@ class Py3oReport(models.TransientModel): report_xml = self.ir_actions_report_id filetype = report_xml.py3o_filetype if not report_xml.py3o_server_id: - return super(Py3oReport, self)._create_single_report( - model_instance, data, - ) + return super(Py3oReport, self)._create_single_report(model_instance, data) elif report_xml.py3o_is_local_fusion: result_path = super( - Py3oReport, self.with_context( - report_py3o_skip_conversion=True, - ) - )._create_single_report( - model_instance, data - ) - with closing(open(result_path, 'rb')) as out_stream: + Py3oReport, self.with_context(report_py3o_skip_conversion=True) + )._create_single_report(model_instance, data) + with closing(open(result_path, "rb")) as out_stream: tmpl_data = out_stream.read() datadict = {} else: result_fd, result_path = tempfile.mkstemp( - suffix='.' + filetype, prefix='p3o.report.tmp.') + suffix="." + filetype, prefix="p3o.report.tmp." + ) tmpl_data = self.get_template(model_instance) in_stream = BytesIO(tmpl_data) - with closing(os.fdopen(result_fd, 'wb+')) as out_stream: + with closing(os.fdopen(result_fd, "wb+")) 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) + 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, - } + files = {"tmpl_file": tmpl_data} fields = { "targetformat": filetype, "datadict": json.dumps(datadict), @@ -74,37 +67,41 @@ class Py3oReport(models.TransientModel): "escape_false": "on", } if report_xml.py3o_is_local_fusion: - fields['skipfusion'] = '1' + fields["skipfusion"] = "1" url = report_xml.py3o_server_id.url logger.info( - 'Connecting to %s to convert report %s to %s', - url, report_xml.report_name, filetype) - if filetype == 'pdf': - options = report_xml.pdf_options_id or\ - report_xml.py3o_server_id.pdf_options_id + "Connecting to %s to convert report %s to %s", + url, + report_xml.report_name, + filetype, + ) + if filetype == "pdf": + options = ( + report_xml.pdf_options_id or report_xml.py3o_server_id.pdf_options_id + ) if options: pdf_options_dict = options.odoo2libreoffice_options() - fields['pdf_options'] = json.dumps(pdf_options_dict) - logger.debug('PDF Export options: %s', pdf_options_dict) + fields["pdf_options"] = json.dumps(pdf_options_dict) + logger.debug("PDF Export options: %s", pdf_options_dict) start_chrono = datetime.now() r = requests.post(url, data=fields, files=files) if r.status_code != 200: # server says we have an issue... let's tell that to enduser - logger.error('Py3o fusion server error: %s', r.text) - raise UserError( - _('Fusion server error %s') % r.text, - ) + logger.error("Py3o fusion server error: %s", r.text) + raise UserError(_("Fusion server error %s") % r.text) chunk_size = 1024 - with open(result_path, 'w+b') as fd: + with open(result_path, "w+b") as fd: for chunk in r.iter_content(chunk_size): fd.write(chunk) end_chrono = datetime.now() convert_seconds = (end_chrono - start_chrono).total_seconds() logger.info( - 'Report %s converted to %s in %s seconds', - report_xml.report_name, filetype, convert_seconds) + "Report %s converted to %s in %s seconds", + report_xml.report_name, + filetype, + convert_seconds, + ) if len(model_instance) == 1: - self._postprocess_report( - model_instance, result_path) + self._postprocess_report(model_instance, result_path) return result_path diff --git a/report_py3o_fusion_server/models/py3o_server.py b/report_py3o_fusion_server/models/py3o_server.py index cab1ee2f8..d7dd210f2 100644 --- a/report_py3o_fusion_server/models/py3o_server.py +++ b/report_py3o_fusion_server/models/py3o_server.py @@ -4,16 +4,21 @@ from odoo import fields, models class Py3oServer(models.Model): - _name = 'py3o.server' - _description = 'Py3o server' - _rec_name = 'url' + _name = "py3o.server" + _description = "Py3o server" + _rec_name = "url" url = fields.Char( - "Py3o Fusion Server URL", required=True, + "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") + "on the default port, the URL is http://localhost:8765/form", + ) is_active = fields.Boolean("Active", default=True) pdf_options_id = fields.Many2one( - 'py3o.pdf.options', string='PDF Options', ondelete='restrict', + "py3o.pdf.options", + string="PDF Options", + ondelete="restrict", help="PDF options can be set per Py3o Server but also per report. " - "If both are defined, the options on the report are used.") + "If both are defined, the options on the report are used.", + ) 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 bf9debd07..407f7fee5 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 @@ -1,62 +1,55 @@ # 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.models.ir_actions_report import \ - PY3O_CONVERSION_COMMAND_PARAMETER + +from odoo.addons.report_py3o.models.ir_actions_report import ( + PY3O_CONVERSION_COMMAND_PARAMETER, +) from odoo.addons.report_py3o.tests import test_report_py3o @mock.patch( - 'requests.post', mock.Mock( + "requests.post", + mock.Mock( return_value=mock.Mock( - status_code=200, - iter_content=mock.Mock(return_value=[b'test_result']), + status_code=200, iter_content=mock.Mock(return_value=[b"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"}) + 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', - }) + self.report.write({"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, - }) + 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, - "py3o_is_local_fusion": False, - }) + self.report.write({"py3o_server_id": None, "py3o_is_local_fusion": False}) self.assertEqual( e.exception.name, "You can not use remote fusion without Fusion server. " - "Please specify a Fusion Server") + "Please specify a Fusion Server", + ) def test_reports_no_local_fusion(self): self.report.py3o_is_local_fusion = False self.test_reports() def test_odoo2libreoffice_options(self): - for options in self.env['py3o.pdf.options'].search([]): + for options in self.env["py3o.pdf.options"].search([]): options_dict = options.odoo2libreoffice_options() self.assertIsInstance(options_dict, dict) @@ -73,8 +66,9 @@ class TestReportPy3oFusionServer(test_report_py3o.TestReportPy3o): 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.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 @@ -91,8 +85,9 @@ class TestReportPy3oFusionServer(test_report_py3o.TestReportPy3o): 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.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)