docx_report_generation/models/ir_actions_report.py

184 lines
6.3 KiB
Python

import io
from collections import OrderedDict
from jinja2 import Environment as Jinja2Environment
from logging import getLogger
from docxcompose.composer import Composer
from docx import Document
from docxtpl import DocxTemplate
from odoo import _, api, fields, models, SUPERUSER_ID
from odoo.exceptions import AccessError, UserError
from odoo.sql_db import TestCursor
from odoo.tools.safe_eval import safe_eval, time
from ..utils.num2words import num2words_, num2words_currency
_logger = getLogger(__name__)
class IrActionsReport(models.Model):
_inherit = "ir.actions.actions"
report_name = fields.Char(required=False)
report_type = fields.Selection(
selection_add=[("docx-docx", "DOCX")], ondelete="cascade"
)
report_docx_template = fields.Binary(
string="Report docx template",
)
def _render_docx_docx(self, res_ids=None, data=None):
if not data:
data = {}
data.setdefault("report_type", "docx")
# access the report details with sudo() but evaluation context as current user
self_sudo = self.sudo()
save_in_attachment = OrderedDict()
# Maps the streams in `save_in_attachment` back to the records they came from
stream_record = dict()
if res_ids:
Model = self.env[self_sudo.model]
record_ids = Model.browse(res_ids)
docx_record_ids = Model
if self_sudo.attachment:
for record_id in record_ids:
attachment = self_sudo.retrieve_attachment(record_id)
if attachment:
stream = self_sudo._retrieve_stream_from_attachment(attachment)
save_in_attachment[record_id.id] = stream
stream_record[stream] = record_id
if not self_sudo.attachment_use or not attachment:
docx_record_ids += record_id
else:
docx_record_ids = record_ids
res_ids = docx_record_ids.ids
if save_in_attachment and not res_ids:
_logger.info("The DOCS report has been generated from attachments.")
return self_sudo._post_docx(save_in_attachment), "docx"
template = self.report_docx_template
template_path = template._full_path(template.store_fname)
doc = DocxTemplate(template_path)
jinja_env = Jinja2Environment()
functions = {
"number2words": num2words_,
"currency2words": num2words_currency,
}
jinja_env.globals.update(**functions)
doc.render(data, jinja_env)
docx_content = io.BytesIO()
doc.save(docx_content)
docx_content.seek(0)
if res_ids:
_logger.info(
"The DOCS report has been generated for model: %s, records %s."
% (self_sudo.model, str(res_ids))
)
return (
self_sudo._post_docx(
save_in_attachment, docx_content=docx_content, res_ids=res_ids
),
"docx",
)
return docx_content, "docx"
def _post_docx(self, save_in_attachment, docx_content=None, res_ids=None):
def close_streams(streams):
for stream in streams:
try:
stream.close()
except Exception:
pass
if len(save_in_attachment) == 1 and not docx_content:
return list(save_in_attachment.values())[0].getvalue()
streams = []
if docx_content:
# Build a record_map mapping id -> record
record_map = {
r.id: r
for r in self.env[self.model].browse(
[res_id for res_id in res_ids if res_id]
)
}
# If no value in attachment or no record specified, only append the whole docx.
if not record_map or not self.attachment:
streams.append(docx_content)
else:
if len(res_ids) == 1:
# Only one record, so postprocess directly and append the whole docx.
if (
res_ids[0] in record_map
and not res_ids[0] in save_in_attachment
):
new_stream = self._postprocess_docx_report(
record_map[res_ids[0]], docx_content
)
# If the buffer has been modified, mark the old buffer to be closed as well.
if new_stream and new_stream != docx_content:
close_streams([docx_content])
docx_content = new_stream
streams.append(docx_content)
else:
streams.append(docx_content)
if self.attachment_use:
for stream in save_in_attachment.values():
streams.append(stream)
if len(streams) == 1:
result = streams[0].getvalue()
else:
try:
result = self._merge_docx(streams)
except Exception:
raise UserError(_("One of the documents, you try to merge is fallback"))
close_streams(streams)
return result
def _postprocess_docx_report(self, record, buffer):
attachment_name = safe_eval(self.attachment, {"object": record, "time": time})
if not attachment_name:
return None
attachment_vals = {
"name": attachment_name,
"raw": buffer.getvalue(),
"res_model": self.model,
"res_id": record.id,
"type": "binary",
}
try:
self.env["ir.attachment"].create(attachment_vals)
except AccessError:
_logger.info(
"Cannot save DOCX report %r as attachment", attachment_vals["name"]
)
else:
_logger.info(
"The DOCX document %s is now saved in the database",
attachment_vals["name"],
)
return buffer
def _merge_docx(self, streams):
writer = Document()
composer = Composer(writer)
for stream in streams:
reader = Document(stream)
composer.append(reader)
return composer.getvalue()