Files
geo/backend/pdf_generator/generate_estimate_pdf.py
2026-02-04 00:11:19 +05:00

250 lines
8.4 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Генерация PDF сметы с корректной кириллицей.
Читает JSON со структурой сметы из stdin, пишет PDF в stdout.
Использование:
echo '<json>' | python generate_estimate_pdf.py > smeta.pdf
или вызов из Node.js с pipe.
"""
import json
import sys
import os
import html
from io import BytesIO
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import mm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak
def find_cyrillic_font():
"""Найти TTF с поддержкой кириллицы: fonts/ рядом со скриптом или Windows Arial."""
script_dir = os.path.dirname(os.path.abspath(__file__))
candidates_regular = [
os.path.join(script_dir, "fonts", "PTSans-Regular.ttf"),
os.path.join(script_dir, "fonts", "pt-sans.regular.ttf"),
os.path.join(script_dir, "fonts", "arial.ttf"),
"C:\\Windows\\Fonts\\arial.ttf",
"C:\\Windows\\Fonts\\times.ttf",
]
candidates_bold = [
os.path.join(script_dir, "fonts", "PTSans-Bold.ttf"),
os.path.join(script_dir, "fonts", "arialbd.ttf"),
"C:\\Windows\\Fonts\\arialbd.ttf",
]
regular = None
bold = None
for p in candidates_regular:
if os.path.isfile(p):
regular = p
break
for p in candidates_bold:
if os.path.isfile(p):
bold = p
break
fonts_dir = os.path.join(script_dir, "fonts")
if not regular and os.path.isdir(fonts_dir):
for f in os.listdir(fonts_dir):
if not f.lower().endswith(".ttf"):
continue
p = os.path.join(fonts_dir, f)
if "bold" in f.lower() or "700" in f or "bold" in p.lower():
bold = bold or p
else:
regular = regular or p
return regular, bold
def register_fonts():
regular, bold = find_cyrillic_font()
if not regular:
raise FileNotFoundError(
"Шрифт с кириллицей не найден. Положите TTF (например PTSans-Regular.ttf, Arial) в папку pdf_generator/fonts/"
)
pdfmetrics.registerFont(TTFont("Cyrillic", regular))
if bold and bold != regular:
pdfmetrics.registerFont(TTFont("CyrillicBold", bold))
else:
pdfmetrics.registerFont(TTFont("CyrillicBold", regular))
return "Cyrillic", "CyrillicBold"
def num_fmt(value):
try:
return f"{float(value):,.2f}".replace(",", " ").replace(".", ",")
except (TypeError, ValueError):
return str(value)
def currency_fmt(value):
try:
return f"{float(value):,.2f} руб.".replace(",", " ").replace(".", ",")
except (TypeError, ValueError):
return str(value)
def build_pdf(data, font_name, font_bold):
buffer = BytesIO()
doc = SimpleDocTemplate(
buffer,
pagesize=A4,
leftMargin=15 * mm,
rightMargin=15 * mm,
topMargin=15 * mm,
bottomMargin=15 * mm,
)
story = []
# Стили
style_center = ParagraphStyle(
name="Center",
fontName=font_bold,
fontSize=14,
alignment=1,
spaceAfter=6,
)
style_center_small = ParagraphStyle(
name="CenterSmall",
fontName=font_name,
fontSize=11,
alignment=1,
spaceAfter=4,
)
style_normal = ParagraphStyle(
name="Normal",
fontName=font_name,
fontSize=10,
spaceAfter=4,
)
style_cell = ParagraphStyle(
name="TableCell",
fontName=font_name,
fontSize=8,
leading=9,
leftIndent=0,
rightIndent=0,
wordWrap="CJK",
)
number = data.get("number", "")
direction_name = (data.get("direction") or {}).get("name", "")
object_name = data.get("objectName", "")
customer = data.get("customer", "")
executor = data.get("executor", "")
story.append(Paragraph(f"Исполнительная смета №{number}", style_center))
story.append(Paragraph(f"{direction_name} на объекте:", style_center_small))
story.append(Paragraph(f"«{object_name}»", style_center_small))
story.append(Spacer(1, 6 * mm))
story.append(Paragraph(f"Наименование организации Заказчика: «{customer}»", style_normal))
story.append(Paragraph(f"Наименование организации Исполнителя: «{executor}»", style_normal))
story.append(Spacer(1, 6 * mm))
# Таблица: позиции по разделам
col_widths = [25, 180, 90, 45, 45, 75]
headers = ["", "Наименование работ", "Обоснование", "Цена", "Объем", "Стоимость"]
section_names = {
"field": "Полевые работы",
"office": "Камеральные работы",
"laboratory": "Лабораторные работы",
"other": "",
}
items = data.get("items") or []
by_section = {}
for it in items:
sec = it.get("sectionType") or "other"
by_section.setdefault(sec, []).append(it)
for sec_key in ("field", "office", "laboratory", "other"):
sec_items = by_section.get(sec_key, [])
if not sec_items:
continue
title = section_names.get(sec_key)
if title:
story.append(Paragraph(title, ParagraphStyle(name="Sec", fontName=font_bold, fontSize=9, spaceAfter=2)))
table_data = [headers]
for it in sec_items:
raw_name = (it.get("workName") or "").strip()
safe_name = html.escape(raw_name)
wname_para = Paragraph(safe_name, style_cell)
just_text = (it.get("justification") or "")[:25]
table_data.append([
str(it.get("orderNumber", "")),
wname_para,
just_text,
num_fmt(it.get("basePrice")),
num_fmt(it.get("quantity")),
num_fmt(it.get("totalPrice")),
])
t = Table(table_data, colWidths=col_widths)
t.setStyle(TableStyle([
("FONTNAME", (0, 0), (-1, 0), font_bold),
("FONTSIZE", (0, 0), (-1, -1), 8),
("FONTNAME", (0, 1), (-1, -1), font_name),
("ALIGN", (0, 0), (0, -1), "CENTER"),
("ALIGN", (3, 0), (-1, -1), "RIGHT"),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("GRID", (0, 0), (-1, -1), 0.5, colors.black),
("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
]))
story.append(t)
story.append(Spacer(1, 4 * mm))
# Итоги
totals = data.get("totals") or []
if totals:
story.append(Spacer(1, 4 * mm))
for tot in totals:
label = tot.get("label", "")
desc = tot.get("description")
if desc:
label = f"{label} ({desc})"
val = currency_fmt(tot.get("resultValue"))
story.append(Paragraph(f"{label}: {val}", style_normal))
story.append(Spacer(1, 10 * mm))
story.append(Paragraph("Выполнил: _____________________", style_normal))
doc.build(story)
return buffer.getvalue()
def main():
try:
font_name, font_bold = register_fonts()
except FileNotFoundError as e:
sys.stderr.write(str(e) + "\n")
sys.exit(1)
try:
if len(sys.argv) >= 2:
with open(sys.argv[1], "r", encoding="utf-8") as f:
data = json.load(f)
else:
raw = sys.stdin.buffer.read()
if hasattr(raw, "decode"):
raw = raw.decode("utf-8")
data = json.loads(raw)
except Exception as e:
sys.stderr.write(f"JSON error: {e}\n")
sys.exit(2)
try:
pdf_bytes = build_pdf(data, font_name, font_bold)
except Exception as e:
sys.stderr.write(f"PDF error: {e}\n")
sys.exit(3)
sys.stdout.buffer.write(pdf_bytes)
if __name__ == "__main__":
main()