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