Skip to content
Snippets Groups Projects
render_report.py 6.53 KiB
Newer Older
# Copyright 2020 XCG Consulting (http://odoo.consulting)
# License AGPL0.3 or later (http://www.gnu.org/licenses/agpl)

# Much of this is based on OCA's report_py3o module (11.0 branch).

import base64
import logging
import os
import tempfile
from contextlib import closing
from io import BytesIO
from zipfile import ZIP_DEFLATED, ZipFile

from odoo import _, api, fields, models
from odoo.exceptions import ValidationError

from odoo.addons.redner.utils import formats
from odoo.addons.redner.utils.formats import Formats

logger = logging.getLogger(__name__)


try:
oury.balde's avatar
oury.balde committed
    from PyPDF2 import PdfFileReader, PdfFileWriter
except ImportError:
    logger.debug("Cannot import PyPDF2")


class RednerReport(models.TransientModel):
    _name = "redner.report"
    _description = "Report Redner"

    ir_actions_report_id = fields.Many2one(
        comodel_name="ir.actions.report", required=True
    )

    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:
                # 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_redner_report(
                    model_instance, buffer
                )
        return result_path

    def _create_single_report(self, model_instance, data):
oury.balde's avatar
oury.balde committed
        """This function to generate our redner report"""

        self.ensure_one()
        report_xml = self.ir_actions_report_id
        report_file = tempfile.mktemp("." + report_xml.redner_filetype)

        data = report_xml.get_report_data(model_instance.id)
        metadata = report_xml.get_report_metadata()

        fformat = Formats().get_format(
            self.ir_actions_report_id.redner_filetype
        )

        try:
            res = report_xml.redner_tmpl_id.redner.templates.render(
                report_xml.redner_tmpl_id.redner_id,
                data,
                accept=fformat.mimetype,
                metadata=metadata,
            )
        except Exception as e:
            if isinstance(e, ValidationError):
                raise
            raise ValidationError(
                _(
                    "We received an unexpected error from redner server. "
                    "Please contact your administrator"
                )
            )

        content = base64.b64decode(res[0]["body"])
        with open(report_file, "wb") as f:
            f.write(content)

        self._postprocess_report(model_instance, report_file)
        return report_file

    def _get_or_create_single_report(
        self, model_instance, data, existing_reports_attachment
    ):
        self.ensure_one()
        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.redner_filetype
            )
            with open(report_file, "wb") as f:
                f.write(content)
            return report_file
        return self._create_single_report(model_instance, data)

    def _zip_results(self, reports_path):
        self.ensure_one()
        zfname_prefix = self.ir_actions_report_id.name
        result_path = tempfile.mktemp(suffix="zip", prefix="redner-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],
                )
                zf.write(report, fname)

                cpt += 1
        return result_path

    @api.model
    def _merge_pdf(self, reports_path):
oury.balde's avatar
oury.balde committed
        """Merge PDF files into one.
        :param reports_path: list of path of pdf files
        :returns: path of the merged pdf
        """
        writer = PdfFileWriter()
        for path in reports_path:
            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:
            writer.write(merged_file)
        return merged_file_path

    def _merge_results(self, reports_path):
        self.ensure_one()
        filetype = self.ir_actions_report_id.redner_filetype
        if not reports_path:
            return False, False
        if len(reports_path) == 1:
            return reports_path[0], filetype
        if filetype == formats.FORMAT_PDF:
            return self._merge_pdf(reports_path), formats.FORMAT_PDF
        else:
            return self._zip_results(reports_path), "zip"

    @api.model
    def _cleanup_tempfiles(self, temporary_files):
        # Manual cleanup of the temporary files
        for temporary_file in temporary_files:
            try:
                os.unlink(temporary_file)
            except (OSError, IOError):
                logger.error(
                    "Error when trying to remove file %s" % temporary_file
                )

    def create_report(self, res_ids, data):
        """Produce the report, return PDF data & file extension"""
oury.balde's avatar
oury.balde committed

        self.ensure_one()

        report_xml = self.ir_actions_report_id

        model_instances = self.env[report_xml.model].browse(res_ids)

        reports_path = []
        if len(res_ids) > 1 and report_xml.redner_multi_in_one:
            reports_path.append(
                self._create_single_report(model_instances, data)
            )
        else:
            existing_reports_attachment = report_xml._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
                    )
                )

        result_path, filetype = self._merge_results(reports_path)
        reports_path.append(result_path)

        # Here is a little joke about Odoo
        # we do all the generation process using files to avoid memory
        # consumption...
        # ... but odoo wants the whole data in memory anyways :)

        with open(result_path, "r+b") as fd:
            res = fd.read()
        self._cleanup_tempfiles(set(reports_path))
        return res, filetype