Law/app/services/invoice_pdf.py
2026-02-23 17:54:19 +03:00

84 lines
2.8 KiB
Python

from __future__ import annotations
from datetime import datetime
from typing import Any
import unicodedata
def _ascii_text(value: Any) -> str:
text = str(value or "")
normalized = unicodedata.normalize("NFKD", text)
return normalized.encode("ascii", "ignore").decode("ascii")
def _escape_pdf_text(value: str) -> str:
return value.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
def _build_content_stream(lines: list[str]) -> bytes:
safe_lines = [_escape_pdf_text(_ascii_text(line)) for line in lines]
if not safe_lines:
safe_lines = ["Invoice"]
parts = ["BT", "/F1 11 Tf", "50 800 Td"]
for index, line in enumerate(safe_lines):
if index == 0:
parts.append(f"({line}) Tj")
else:
parts.append("T*")
parts.append(f"({line}) Tj")
parts.append("ET")
return "\n".join(parts).encode("latin-1", errors="ignore")
def build_invoice_pdf_bytes(
*,
invoice_number: str,
amount: float,
currency: str,
status: str,
issued_at: datetime | None,
paid_at: datetime | None,
payer_display_name: str,
request_track_number: str,
issued_by_name: str | None,
requisites: dict[str, Any] | None,
) -> bytes:
lines = [
f"Invoice: {invoice_number}",
f"Request: {request_track_number}",
f"Payer: {payer_display_name}",
f"Amount: {amount:.2f} {currency}",
f"Status: {status}",
f"Issued at: {issued_at.isoformat() if issued_at else '-'}",
f"Paid at: {paid_at.isoformat() if paid_at else '-'}",
f"Issued by: {issued_by_name or '-'}",
"Requisites:",
]
req = dict(requisites or {})
if req:
for key in sorted(req.keys()):
lines.append(f"{key}: {req.get(key)}")
else:
lines.append("-")
stream = _build_content_stream(lines)
objects = [
b"1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n",
b"2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n",
b"3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >> endobj\n",
b"4 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj\n",
f"5 0 obj << /Length {len(stream)} >> stream\n".encode("latin-1") + stream + b"\nendstream endobj\n",
]
body = b"%PDF-1.4\n"
offsets = [0]
for obj in objects:
offsets.append(len(body))
body += obj
xref_offset = len(body)
body += f"xref\n0 {len(objects)+1}\n".encode("latin-1")
body += b"0000000000 65535 f \n"
for offset in offsets[1:]:
body += f"{offset:010d} 00000 n \n".encode("latin-1")
body += f"trailer << /Size {len(objects)+1} /Root 1 0 R >>\nstartxref\n{xref_offset}\n%%EOF\n".encode("latin-1")
return body