"""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()