[IMP] report_py3o, report_py3o_fusion_server: black, isort

This commit is contained in:
Laurent Mignon (ACSONE)
2019-11-19 14:36:37 +01:00
committed by Elmeri Niemelä
parent 1370625b24
commit 0bf0160d2d
8 changed files with 346 additions and 328 deletions

View File

@@ -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', '<text:line-break/>').
replace('\t', '<text:s/><text:s/><text:s/><text:s/>'))
return Markup(
html.escape(value)
.replace("\n", "<text:line-break/>")
.replace("\t", "<text:s/><text:s/><text:s/><text:s/>")
)
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)

View File

@@ -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):

View File

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

View File

@@ -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",
)