Public release from ruodoo-project: 19.0 - 2026-05-10 21:19:01 UTC

This commit is contained in:
CI Publish Bot
2026-05-10 21:19:11 +00:00
commit cbf9e6e6d6
1213 changed files with 183945 additions and 0 deletions

View File

@ -0,0 +1 @@
from . import models

View 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,
}

View 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"
]

View File

@ -0,0 +1,2 @@
from . import ir_actions_report
from . import docx_template

View 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",
},
)

View 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