Public release from ruodoo-project: 19.0 - 2026-05-10 21:19:01 UTC
This commit is contained in:
1
pdf_report_from_docx/__init__.py
Normal file
1
pdf_report_from_docx/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import models
|
||||
17
pdf_report_from_docx/__manifest__.py
Normal file
17
pdf_report_from_docx/__manifest__.py
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "PDF Report from DOCX",
|
||||
"summary": "Generate PDF reports by converting DOCX via Gotenberg.",
|
||||
"version": "19.0.1.0.0",
|
||||
"category": "Technical",
|
||||
"author": "Custom",
|
||||
"license": "LGPL-3",
|
||||
"depends": [
|
||||
"base",
|
||||
"docx_report",
|
||||
],
|
||||
"external_dependencies": {
|
||||
"python": ["requests"],
|
||||
},
|
||||
"data": [],
|
||||
"installable": True,
|
||||
}
|
||||
13
pdf_report_from_docx/docker-compose.yml
Normal file
13
pdf_report_from_docx/docker-compose.yml
Normal file
@ -0,0 +1,13 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:8
|
||||
container_name: gotenberg
|
||||
ports:
|
||||
- "3000:3000"
|
||||
command:
|
||||
[
|
||||
"gotenberg",
|
||||
"--chromium-disable-javascript=true"
|
||||
]
|
||||
2
pdf_report_from_docx/models/__init__.py
Normal file
2
pdf_report_from_docx/models/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . import ir_actions_report
|
||||
from . import docx_template
|
||||
14
pdf_report_from_docx/models/docx_template.py
Normal file
14
pdf_report_from_docx/models/docx_template.py
Normal file
@ -0,0 +1,14 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class DocxTemplate(models.Model):
|
||||
_inherit = "docx.template"
|
||||
|
||||
docx_output_type = fields.Selection(
|
||||
selection_add=[
|
||||
("pdf", "PDF"),
|
||||
],
|
||||
ondelete={
|
||||
"pdf": "set default",
|
||||
},
|
||||
)
|
||||
217
pdf_report_from_docx/models/ir_actions_report.py
Normal file
217
pdf_report_from_docx/models/ir_actions_report.py
Normal file
@ -0,0 +1,217 @@
|
||||
from collections import OrderedDict
|
||||
from io import BytesIO
|
||||
from logging import getLogger
|
||||
|
||||
from requests import codes as codes_request, post as post_request, get as get_request
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from odoo import _, api, models, fields
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from odoo.tools.safe_eval import safe_eval, time
|
||||
|
||||
_logger = getLogger(__name__)
|
||||
|
||||
|
||||
class IrActionsReport(models.Model):
|
||||
_inherit = "ir.actions.report"
|
||||
|
||||
report_type = fields.Selection(
|
||||
selection_add=[("docx-pdf", "DOCX (PDF)")],
|
||||
ondelete={"docx-pdf": "cascade"},
|
||||
)
|
||||
|
||||
def _gotenberg_base_url(self):
|
||||
ICP = self.env["ir.config_parameter"].sudo()
|
||||
base_url = ICP.get_param("gotenberg.server.url", "http://localhost:3000")
|
||||
return base_url.rstrip("/")
|
||||
|
||||
def _gotenberg_convert_url(self):
|
||||
return f"{self._gotenberg_base_url()}/forms/libreoffice/convert"
|
||||
|
||||
def _gotenberg_health_url(self):
|
||||
return f"{self._gotenberg_base_url()}/health"
|
||||
|
||||
def _gotenberg_auth(self):
|
||||
ICP = self.env["ir.config_parameter"].sudo()
|
||||
username = ICP.get_param("gotenberg.server.username") or None
|
||||
password = ICP.get_param("gotenberg.server.password") or None
|
||||
if username and password:
|
||||
return (username, password)
|
||||
return None
|
||||
|
||||
def _gotenberg_check_installed(self):
|
||||
"""Проверяем, что сервис живой (GET /health)."""
|
||||
url = self._gotenberg_health_url()
|
||||
try:
|
||||
resp = get_request(url, timeout=3)
|
||||
if resp.status_code == codes_request.ok:
|
||||
return True
|
||||
_logger.warning("Gotenberg health check failed: %s - %s", resp.status_code, resp.text)
|
||||
except Exception as e:
|
||||
_logger.warning("Gotenberg health check error: %s", e)
|
||||
return False
|
||||
|
||||
@api.depends("report_type", "model")
|
||||
def _compute_report_name(self):
|
||||
"""Расширяем логику вычисления report_name для docx-pdf."""
|
||||
super()._compute_report_name()
|
||||
for record in self:
|
||||
if (
|
||||
record.report_type == "docx-pdf"
|
||||
and record.model
|
||||
and record.id
|
||||
):
|
||||
record.report_name = "%s-docx_report+%s" % (record.model, record.id)
|
||||
|
||||
def retrieve_attachment(self, record):
|
||||
"""
|
||||
Расширяем retrieve_attachment: для docx-pdf отбираем только PDF-вложения.
|
||||
"""
|
||||
result = super().retrieve_attachment(record)
|
||||
if result and self.report_type == "docx-pdf":
|
||||
result = result.filtered(lambda r: r.mimetype == "application/pdf") or None
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def _render_docx_pdf(self, res_ids=None, data=None):
|
||||
"""
|
||||
Подготовка данных, рендер DOCX, конвертация в PDF через Gotenberg
|
||||
и сохранение/возврат результата.
|
||||
"""
|
||||
if not data:
|
||||
data = {}
|
||||
data.setdefault("report_type", "pdf")
|
||||
|
||||
self_sudo = self.sudo()
|
||||
|
||||
save_in_attachment = OrderedDict()
|
||||
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 and self_sudo.attachment_use:
|
||||
stream = BytesIO(attachment.raw)
|
||||
save_in_attachment[record_id.id] = stream
|
||||
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:
|
||||
_logger.info("The PDF report has been generated from attachment.")
|
||||
return self_sudo._post_pdf(save_in_attachment), "pdf"
|
||||
|
||||
docx_content = self._render_docx(res_ids, data=data)
|
||||
|
||||
if not self._gotenberg_check_installed():
|
||||
raise UserError(
|
||||
_(
|
||||
"Gotenberg converting service not available. "
|
||||
"The PDF can not be created."
|
||||
)
|
||||
)
|
||||
|
||||
pdf_content = self._get_pdf_from_office(docx_content)
|
||||
|
||||
if not pdf_content:
|
||||
raise UserError(
|
||||
_(
|
||||
"Gotenberg converting service not available. "
|
||||
"The PDF can not be created."
|
||||
)
|
||||
)
|
||||
|
||||
if res_ids:
|
||||
return (
|
||||
self_sudo._post_pdf(
|
||||
save_in_attachment, pdf_content=pdf_content, res_ids=res_ids
|
||||
),
|
||||
"pdf",
|
||||
)
|
||||
|
||||
return pdf_content, "pdf"
|
||||
|
||||
def _post_pdf(self, save_in_attachment, pdf_content=None, res_ids=None):
|
||||
"""
|
||||
Добавляет PDF-файл в вложения (ir.attachment).
|
||||
|
||||
2 режима:
|
||||
- save_in_attachment и not res_ids: берём уже существующие streams и объединяем.
|
||||
- res_ids и pdf_content: создаём вложения на основе только что сгенерированного PDF.
|
||||
"""
|
||||
self_sudo = self.sudo()
|
||||
attachment_vals_list = []
|
||||
|
||||
if save_in_attachment:
|
||||
reports_data = list(save_in_attachment.values())
|
||||
if len(reports_data) == 1:
|
||||
return reports_data[0].getvalue()
|
||||
else:
|
||||
return self._merge_pdfs(reports_data)
|
||||
|
||||
for res_id in res_ids or []:
|
||||
record = self.env[self_sudo.model].browse(res_id)
|
||||
if not self_sudo.attachment:
|
||||
attachment_name = False
|
||||
else:
|
||||
attachment_name = safe_eval(
|
||||
self_sudo.attachment, {"object": record, "time": time}
|
||||
)
|
||||
if not attachment_name:
|
||||
continue
|
||||
attachment_vals_list.append(
|
||||
{
|
||||
"name": attachment_name,
|
||||
"raw": pdf_content,
|
||||
"res_model": self_sudo.model,
|
||||
"res_id": record.id,
|
||||
"type": "binary",
|
||||
}
|
||||
)
|
||||
if attachment_vals_list:
|
||||
attachment_names = ", ".join(x["name"] for x in attachment_vals_list)
|
||||
try:
|
||||
self.env["ir.attachment"].create(attachment_vals_list)
|
||||
except AccessError:
|
||||
_logger.info(
|
||||
"Cannot save PDF report %r attachments for user %r",
|
||||
attachment_names,
|
||||
self.env.user.display_name,
|
||||
)
|
||||
else:
|
||||
_logger.info(
|
||||
"The PDF documents %r are now saved in the database",
|
||||
attachment_names,
|
||||
)
|
||||
return pdf_content
|
||||
|
||||
def _get_pdf_from_office(self, content_stream):
|
||||
"""
|
||||
Конвертация DOCX в PDF через сервис Gotenberg.
|
||||
content_stream — BytesIO с docx.
|
||||
"""
|
||||
result = None
|
||||
url = self._gotenberg_convert_url()
|
||||
auth = self._gotenberg_auth()
|
||||
try:
|
||||
response = post_request(
|
||||
url,
|
||||
files={"files": ("converted_file.docx", content_stream.read())},
|
||||
auth=auth,
|
||||
)
|
||||
if response.status_code == codes_request.ok:
|
||||
result = response.content
|
||||
else:
|
||||
_logger.warning(
|
||||
"Gotenberg response: %s - %s",
|
||||
response.status_code,
|
||||
response.content,
|
||||
)
|
||||
except RequestException as e:
|
||||
_logger.exception(e)
|
||||
finally:
|
||||
return result
|
||||
Reference in New Issue
Block a user