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)