Files
GostGenerator/web/backend/app/services/pdf_export.py
T
2026-09-02 12:48:03 +03:00

183 lines
5.9 KiB
Python

"""Simplified GOST A4 PDF export with frame and table."""
from __future__ import annotations
from io import BytesIO
from pathlib import Path
from typing import Any
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen import canvas
from app.core.config import get_settings
_font_registered = False
FONT_NAME = "GOST_A"
def _ensure_font() -> str:
global _font_registered
if not _font_registered:
path = Path(get_settings().font_path)
if path.exists():
pdfmetrics.registerFont(TTFont(FONT_NAME, str(path)))
_font_registered = True
return FONT_NAME
return "Helvetica"
return FONT_NAME if Path(get_settings().font_path).exists() else "Helvetica"
DOC_TITLES = {
"perechen": "Перечень элементов",
"specification_pcb": "Спецификация",
"specification": "Спецификация",
"vedomost": "Ведомость покупных изделий",
}
COLUMNS = {
"perechen": [
("position", 30 * mm, "Поз."),
("designation", 100 * mm, "Наименование"),
("quantity", 15 * mm, "Кол."),
("note", 35 * mm, "Прим."),
],
"specification_pcb": [
("format", 12 * mm, "Форм."),
("zone", 10 * mm, "Зона"),
("position", 10 * mm, "Поз."),
("designation", 40 * mm, "Обозн."),
("name", 55 * mm, "Наименование"),
("quantity", 12 * mm, "Кол."),
("note", 30 * mm, "Прим."),
],
"specification": [
("format", 12 * mm, "Форм."),
("zone", 10 * mm, "Зона"),
("position", 10 * mm, "Поз."),
("designation", 40 * mm, "Обозн."),
("name", 55 * mm, "Наименование"),
("quantity", 12 * mm, "Кол."),
("note", 30 * mm, "Прим."),
],
"vedomost": [
("name", 40 * mm, "Наименование"),
("product_code", 20 * mm, "Код"),
("document_code", 25 * mm, "Док."),
("supplier", 20 * mm, "Пост."),
("where_used", 20 * mm, "Куда"),
("quantity_per_item", 12 * mm, "На изд."),
("total_quantity", 12 * mm, "Всего"),
("note", 20 * mm, "Прим."),
],
}
ROWS_FIRST = {"perechen": 25, "specification_pcb": 26, "specification": 27, "vedomost": 24}
ROWS_OTHER = {"perechen": 32, "specification_pcb": 32, "specification": 33, "vedomost": 29}
def _draw_frame(c: canvas.Canvas, w: float, h: float, page: int, total: int, inscriptions: dict[int, str], doc_type: str):
font = _ensure_font()
margin_left = 20 * mm
margin_right = 5 * mm
margin_top = 5 * mm
margin_bottom = 5 * mm
c.setLineWidth(0.8)
c.rect(margin_left, margin_bottom, w - margin_left - margin_right, h - margin_top - margin_bottom)
# Title block (simplified bottom-right)
block_h = 55 * mm if page == 1 else 15 * mm
block_w = 185 * mm
x0 = w - margin_right - block_w
y0 = margin_bottom
c.setLineWidth(0.5)
c.rect(x0, y0, block_w, block_h)
c.setFont(font, 8)
name = inscriptions.get(1, "")
designation = inscriptions.get(2, "")
org = inscriptions.get(9, "")
title = DOC_TITLES.get(doc_type, "")
c.drawString(x0 + 2 * mm, y0 + block_h - 6 * mm, f"{title}")
c.drawString(x0 + 2 * mm, y0 + block_h - 12 * mm, designation[:60])
c.drawString(x0 + 2 * mm, y0 + block_h - 18 * mm, name[:60])
if page == 1:
c.drawString(x0 + 2 * mm, y0 + 8 * mm, org[:40])
c.drawString(x0 + 2 * mm, y0 + 3 * mm, f"Разраб. {inscriptions.get(111, '')}")
c.drawString(x0 + 50 * mm, y0 + 3 * mm, f"Пров. {inscriptions.get(112, '')}")
c.drawRightString(x0 + block_w - 2 * mm, y0 + 3 * mm, f"Лист {page}/{total}")
def _chunk_rows(rows: list[dict[str, Any]], first: int, other: int) -> list[list[dict[str, Any]]]:
if not rows:
return [[]]
pages: list[list[dict[str, Any]]] = []
i = 0
limit = first
while i < len(rows):
pages.append(rows[i : i + limit])
i += limit
limit = other
return pages
def export_pdf(
table_type: str,
rows: list[dict[str, Any]],
inscriptions: dict[int, str] | None = None,
) -> bytes:
if table_type == "simple_list":
raise ValueError("PDF export is not available for simple_list")
inscriptions = inscriptions or {}
font = _ensure_font()
cols = COLUMNS[table_type]
first = ROWS_FIRST[table_type]
other = ROWS_OTHER[table_type]
pages = _chunk_rows(rows, first, other)
total = max(len(pages), 1)
buf = BytesIO()
c = canvas.Canvas(buf, pagesize=A4)
w, h = A4
for page_idx, page_rows in enumerate(pages, start=1):
_draw_frame(c, w, h, page_idx, total, inscriptions, table_type)
# table area
left = 20 * mm
top = h - 10 * mm
row_h = 6 * mm
header_y = top - 8 * mm
# column headers
x = left + 2 * mm
c.setFont(font, 7)
for field, width, label in cols:
c.drawString(x, header_y, label)
x += width
c.line(left, header_y - 2 * mm, left + sum(w for _, w, _ in cols) + 4 * mm, header_y - 2 * mm)
y = header_y - row_h
for row in page_rows:
if y < 65 * mm and page_idx == 1:
break
if y < 25 * mm:
break
x = left + 2 * mm
style_size = 8 if row.get("is_header") else 7
c.setFont(font, style_size)
for field, width, _ in cols:
text = "" if row.get("is_empty") else str(row.get(field, "") or "")
# truncate to fit roughly
max_chars = max(int(width / mm), 1)
c.drawString(x, y, text[: max_chars + 5])
x += width
y -= row_h
c.showPage()
c.save()
return buf.getvalue()