183 lines
5.9 KiB
Python
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()
|