128 lines
6.1 KiB
Python
128 lines
6.1 KiB
Python
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
import logging
|
||
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.modules.orion.assets import ORION_ASSET_DIR
|
||
from app.modules.orion.components import (
|
||
AttachmentComponent,
|
||
ChecklistComponent,
|
||
CoverComponent,
|
||
CustomerComponent,
|
||
DeviceComponent,
|
||
DryingComponent,
|
||
EnvironmentComponent,
|
||
EquipmentComponent,
|
||
LoadingComponent,
|
||
MeasurementComponent,
|
||
ProgramComponent,
|
||
RecommendationComponent,
|
||
ReportComponent,
|
||
StaticTextComponent,
|
||
SummaryComponent,
|
||
TocComponent,
|
||
)
|
||
from app.modules.orion.context import OrionContextBuilder, ReportContext
|
||
from app.modules.orion.templates.report import render_document
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class OrionReportService:
|
||
def __init__(self, session: Session, generated_dir: Path | None = None) -> None:
|
||
self.session = session
|
||
self.generated_dir = generated_dir or Path("/app/reports")
|
||
|
||
def render_html(self, validation_id: str) -> str:
|
||
context = OrionContextBuilder(self.session, self.generated_dir).build(validation_id)
|
||
components = self._components()
|
||
chapters = [component.render(context) for component in components]
|
||
return render_document(context, chapters)
|
||
|
||
def render_pdf(self, validation_id: str) -> Path:
|
||
from weasyprint import HTML
|
||
|
||
context = OrionContextBuilder(self.session, self.generated_dir).build(validation_id)
|
||
output_path = context.generated_dir / f"{context.validation.report_number}.pdf"
|
||
html = self.render_html(validation_id)
|
||
HTML(string=html, base_url=str(ORION_ASSET_DIR)).write_pdf(output_path)
|
||
self._append_pdf_attachments(context, output_path)
|
||
return output_path
|
||
|
||
def _components(self) -> list[ReportComponent]:
|
||
chapters: list[ReportComponent] = [
|
||
StaticTextComponent("bq", "1 Funktionsqualifikation (BQ)", "nicht erfasst"),
|
||
StaticTextComponent("bq-goal", "1.1 Anlass und Ziel der Prüfung", block_key="bq_goal"),
|
||
StaticTextComponent("legal", "1.2 Gesetzliche Grundlagen", block_key="legal"),
|
||
DeviceComponent(),
|
||
StaticTextComponent("performance", "1.4 Leistungsüberprüfung", block_key="performance"),
|
||
ChecklistComponent(),
|
||
StaticTextComponent("documentation", "1.6 Dokumentation / Kontrolle"),
|
||
StaticTextComponent("work-instructions", "1.7 Arbeitsanweisungen"),
|
||
EnvironmentComponent(),
|
||
StaticTextComponent("batch-control", "1.9 Chargenkontrolle"),
|
||
ProgramComponent(),
|
||
LoadingComponent(),
|
||
StaticTextComponent("reference-load", "1.12 Referenzbeladung Sterilisator"),
|
||
StaticTextComponent("equipment", "2 Eingesetzte Prüfmittel"),
|
||
EquipmentComponent(),
|
||
StaticTextComponent("equipment-thermo", "2.2 Prüfmittel zur thermoelektrischen Untersuchung"),
|
||
StaticTextComponent("configuration", "2.3 Prüfkonfiguration"),
|
||
StaticTextComponent("lq", "3 Leistungsqualifikation (LQ)"),
|
||
StaticTextComponent("thermo-tests", "3.1 Leistungsbeurteilung – Thermoelektrische Prüfungen"),
|
||
MeasurementComponent(),
|
||
StaticTextComponent("run-1", "3.2 Standardbeladung – Testlauf 1"),
|
||
StaticTextComponent("test-1", "3.2.1 Test 1"),
|
||
StaticTextComponent("run-2", "3.3 Standardbeladung – Testlauf 2"),
|
||
StaticTextComponent("test-2", "3.3.1 Test 2"),
|
||
StaticTextComponent("run-3", "3.4 Standardbeladung – Testlauf 3"),
|
||
StaticTextComponent("test-3", "3.4.1 Test 3"),
|
||
StaticTextComponent("results", "4 Ergebnisse der Validierung"),
|
||
StaticTextComponent("results-vacuum", "4.1 Vakuumtest"),
|
||
StaticTextComponent("results-runs", "4.1.2 Testläufe 1 bis 3"),
|
||
DryingComponent(),
|
||
RecommendationComponent(),
|
||
StaticTextComponent("appendix", "5 Anhang"),
|
||
AttachmentComponent(),
|
||
StaticTextComponent("winlog", "5.1 Winlog-Auswertungen"),
|
||
StaticTextComponent("release-docs", "5.2 Freigabedokumentation / Routineprüfungen"),
|
||
StaticTextComponent("indicators", "5.3 Nachweis der umgeschlagenen Indikatoren"),
|
||
StaticTextComponent("cycles", "6 Programmabläufe / Zyklen"),
|
||
StaticTextComponent("practice-certificates", "6.1 Zertifikate Praxis"),
|
||
StaticTextComponent("risk", "7 Risikoeinstufung nach RKI"),
|
||
StaticTextComponent("closing-talk", "7.1 Abschlussgespräch"),
|
||
StaticTextComponent("certificates", "8. Zertifikate"),
|
||
StaticTextComponent("calibration-certificates", "9. Werkskalibrierzertifikate Sensoren"),
|
||
]
|
||
return [CoverComponent(), SummaryComponent(), TocComponent(chapters), CustomerComponent(), *chapters]
|
||
|
||
def _append_pdf_attachments(self, context: ReportContext, output_path: Path) -> None:
|
||
pdf_paths = []
|
||
for item in context.validation.attachments or []:
|
||
filename = str(item.get("filename") or "")
|
||
content_type = str(item.get("content_type") or "")
|
||
if not filename.lower().endswith(".pdf") and content_type != "application/pdf":
|
||
continue
|
||
storage_path = item.get("storage_path")
|
||
if storage_path and Path(str(storage_path)).exists():
|
||
pdf_paths.append(Path(str(storage_path)))
|
||
if not pdf_paths:
|
||
return
|
||
try:
|
||
from pypdf import PdfReader, PdfWriter
|
||
|
||
writer = PdfWriter()
|
||
for page in PdfReader(str(output_path)).pages:
|
||
writer.add_page(page)
|
||
for pdf_path in pdf_paths:
|
||
try:
|
||
for page in PdfReader(str(pdf_path)).pages:
|
||
writer.add_page(page)
|
||
except Exception:
|
||
logger.exception("Could not append attachment PDF %s", pdf_path)
|
||
with output_path.open("wb") as handle:
|
||
writer.write(handle)
|
||
except Exception:
|
||
logger.exception("Could not merge Orion PDF attachments for validation %s", context.validation.id)
|