fix(orion): correct report numbering toc and first page
This commit is contained in:
parent
503b343070
commit
47f54d3461
7 changed files with 283 additions and 137 deletions
|
|
@ -27,6 +27,22 @@ def render_template_text(content: str, context: ReportContext) -> str:
|
|||
return rendered
|
||||
|
||||
|
||||
class NumberedComponent(ReportComponent):
|
||||
anchor = ""
|
||||
title = ""
|
||||
|
||||
def __init__(self, anchor: str | None = None, title: str | None = None, number: str | None = None) -> None:
|
||||
if anchor is not None:
|
||||
self.anchor = anchor
|
||||
if title is not None:
|
||||
self.title = title
|
||||
self.number = number
|
||||
|
||||
@property
|
||||
def bookmark_label(self) -> str:
|
||||
return f"{self.number} {self.title}" if self.number else self.title
|
||||
|
||||
|
||||
class CoverComponent(ReportComponent):
|
||||
anchor = "cover"
|
||||
title = "Deckblatt"
|
||||
|
|
@ -63,7 +79,7 @@ class CoverComponent(ReportComponent):
|
|||
'<div class="company-address">SCHUBAMED<br>Validation Suite<br>Medizintechnik und Validierung</div>'
|
||||
f'<img class="cover-logo" src="{logo_uri}" alt="SCHUBAMED Validation Suite">'
|
||||
"</div>"
|
||||
"<h1>PRUEFBERICHT ZUR VALIDIERUNG</h1>"
|
||||
"<h1>PRÜFBERICHT ZUR VALIDIERUNG</h1>"
|
||||
'<p class="cover-subtitle">Funktions- und Leistungsqualifikation Klein-Sterilisator</p>'
|
||||
f'<p class="cover-subtitle">{text(context.customer.name)}</p>'
|
||||
f"{rows}"
|
||||
|
|
@ -76,17 +92,27 @@ class TocComponent(ReportComponent):
|
|||
anchor = "toc"
|
||||
title = "Inhaltsverzeichnis"
|
||||
|
||||
def __init__(self, components: list[ReportComponent]) -> None:
|
||||
self.components = components
|
||||
def __init__(self, sections: list[dict]) -> None:
|
||||
self.sections = sections
|
||||
|
||||
def render(self, context: ReportContext) -> str:
|
||||
links = "".join(
|
||||
f'<li><a href="#{component.anchor}">{text(component.title)}</a></li>'
|
||||
for component in self.components
|
||||
if component.anchor not in {"cover", "toc"}
|
||||
self._toc_item(item)
|
||||
for item in self.sections
|
||||
if item.get("toc")
|
||||
)
|
||||
return section(self.anchor, self.title, f'<ol class="toc-list">{links}</ol>')
|
||||
|
||||
def _toc_item(self, item: dict) -> str:
|
||||
number = item.get("number")
|
||||
title = item.get("title")
|
||||
label = f"{number} {title}" if number else str(title)
|
||||
depth = str(number).count(".") + 1 if number else 2
|
||||
return (
|
||||
f'<li class="toc-level-{depth}"><a href="#{text(item.get("key"))}">'
|
||||
f'<span class="toc-label">{text(label)}</span></a></li>'
|
||||
)
|
||||
|
||||
|
||||
class SummaryComponent(ReportComponent):
|
||||
anchor = "summary"
|
||||
|
|
@ -121,17 +147,29 @@ class SummaryComponent(ReportComponent):
|
|||
|
||||
|
||||
class StaticTextComponent(ReportComponent):
|
||||
def __init__(self, anchor: str, title: str, body: str = "nicht erfasst", block_key: str | None = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
anchor: str,
|
||||
title: str,
|
||||
body: str = "nicht erfasst",
|
||||
block_key: str | None = None,
|
||||
number: str | None = None,
|
||||
) -> None:
|
||||
self.anchor = anchor
|
||||
self.title = title
|
||||
self.body = body
|
||||
self.block_key = block_key
|
||||
self.number = number
|
||||
|
||||
@property
|
||||
def bookmark_label(self) -> str:
|
||||
return f"{self.number} {self.title}" if self.number else self.title
|
||||
|
||||
def render(self, context: ReportContext) -> str:
|
||||
if self.block_key and self.block_key in context.text_blocks:
|
||||
content = render_template_text(context.text_blocks[self.block_key].content, context)
|
||||
return section(self.anchor, self.title, f"<p>{paragraph(content)}</p>")
|
||||
return section(self.anchor, self.title, f"<p>{paragraph(self.body)}</p>")
|
||||
return section(self.anchor, self.title, f"<p>{paragraph(content)}</p>", self.bookmark_label)
|
||||
return section(self.anchor, self.title, f"<p>{paragraph(self.body)}</p>", self.bookmark_label)
|
||||
|
||||
|
||||
class CustomerComponent(ReportComponent):
|
||||
|
|
@ -182,14 +220,14 @@ class CustomerComponent(ReportComponent):
|
|||
return section(self.anchor, self.title, body)
|
||||
|
||||
|
||||
class DeviceComponent(ReportComponent):
|
||||
class DeviceComponent(NumberedComponent):
|
||||
anchor = "device"
|
||||
title = "Geraet"
|
||||
|
||||
def render(self, context: ReportContext) -> str:
|
||||
device = context.device
|
||||
if device is None:
|
||||
return section(self.anchor, self.title, "")
|
||||
return section(self.anchor, self.title, "<p>nicht erfasst</p>", self.bookmark_label)
|
||||
body = definition_list(
|
||||
[
|
||||
("Hersteller", device.manufacturer),
|
||||
|
|
@ -212,10 +250,10 @@ class DeviceComponent(ReportComponent):
|
|||
("Lieferant", device.supplier),
|
||||
]
|
||||
)
|
||||
return section(self.anchor, self.title, body)
|
||||
return section(self.anchor, self.title, body, self.bookmark_label)
|
||||
|
||||
|
||||
class EquipmentComponent(ReportComponent):
|
||||
class EquipmentComponent(NumberedComponent):
|
||||
anchor = "equipment"
|
||||
title = "Pruefmittel"
|
||||
|
||||
|
|
@ -247,10 +285,11 @@ class EquipmentComponent(ReportComponent):
|
|||
],
|
||||
rows,
|
||||
),
|
||||
self.bookmark_label,
|
||||
)
|
||||
|
||||
|
||||
class EnvironmentComponent(ReportComponent):
|
||||
class EnvironmentComponent(NumberedComponent):
|
||||
anchor = "environment"
|
||||
title = "Umgebungsbedingungen"
|
||||
|
||||
|
|
@ -269,10 +308,10 @@ class EnvironmentComponent(ReportComponent):
|
|||
]
|
||||
if rows:
|
||||
body += table(["Pruefpunkt", "Bewertung", "Kommentar"], rows)
|
||||
return section(self.anchor, self.title, body)
|
||||
return section(self.anchor, self.title, body, self.bookmark_label)
|
||||
|
||||
|
||||
class ChecklistComponent(ReportComponent):
|
||||
class ChecklistComponent(NumberedComponent):
|
||||
anchor = "checklists"
|
||||
title = "Dokumentations- und Leistungschecklisten"
|
||||
|
||||
|
|
@ -286,7 +325,7 @@ class ChecklistComponent(ReportComponent):
|
|||
performance = context.validation.performance_checklist or []
|
||||
body = "<h3>Dokumentation</h3>" + self._render_items(documentation)
|
||||
body += "<h3>Leistung</h3>" + self._render_items(performance)
|
||||
return section(self.anchor, self.title, body)
|
||||
return section(self.anchor, self.title, body, self.bookmark_label)
|
||||
|
||||
def _render_items(self, items: list[dict]) -> str:
|
||||
rows = [
|
||||
|
|
@ -296,7 +335,7 @@ class ChecklistComponent(ReportComponent):
|
|||
return table(["Nr.", "Pruefpunkt", "Bewertung", "Kommentar"], rows)
|
||||
|
||||
|
||||
class ProgramComponent(ReportComponent):
|
||||
class ProgramComponent(NumberedComponent):
|
||||
anchor = "programs"
|
||||
title = "Programme"
|
||||
|
||||
|
|
@ -306,10 +345,10 @@ class ProgramComponent(ReportComponent):
|
|||
[index + 1, item.get("name"), "Eigenes Programm" if item.get("custom") else "Standard"]
|
||||
for index, item in enumerate(programs)
|
||||
]
|
||||
return section(self.anchor, self.title, table(["Nr.", "Programm", "Typ"], rows))
|
||||
return section(self.anchor, self.title, table(["Nr.", "Programm", "Typ"], rows), self.bookmark_label)
|
||||
|
||||
|
||||
class LoadingComponent(ReportComponent):
|
||||
class LoadingComponent(NumberedComponent):
|
||||
anchor = "loading"
|
||||
title = "Beladungsmuster"
|
||||
|
||||
|
|
@ -327,10 +366,11 @@ class LoadingComponent(ReportComponent):
|
|||
self.anchor,
|
||||
self.title,
|
||||
table(["Testlauf", "Beladungsmuster", "Beschreibung", "Bilder"], rows),
|
||||
self.bookmark_label,
|
||||
)
|
||||
|
||||
|
||||
class MeasurementComponent(ReportComponent):
|
||||
class MeasurementComponent(NumberedComponent):
|
||||
anchor = "measurements"
|
||||
title = "Messdaten"
|
||||
|
||||
|
|
@ -351,6 +391,7 @@ class MeasurementComponent(ReportComponent):
|
|||
self.anchor,
|
||||
self.title,
|
||||
table(["Testlauf", "Messwert", "Wert", "Einheit", "Quelle", "Sicherheit"], rows, "compact"),
|
||||
self.bookmark_label,
|
||||
)
|
||||
rows = [
|
||||
[
|
||||
|
|
@ -394,10 +435,10 @@ class MeasurementComponent(ReportComponent):
|
|||
if winlog_rows:
|
||||
body += "<h3>Winlog-Dateien</h3>" + table(["Bereich", "Datei", "Typ"], winlog_rows)
|
||||
body += "<p>Messdaten noch nicht bestätigt.</p>"
|
||||
return section(self.anchor, self.title, body)
|
||||
return section(self.anchor, self.title, body, self.bookmark_label)
|
||||
|
||||
|
||||
class DryingComponent(ReportComponent):
|
||||
class DryingComponent(NumberedComponent):
|
||||
anchor = "drying"
|
||||
title = "Trocknung"
|
||||
|
||||
|
|
@ -412,10 +453,10 @@ class DryingComponent(ReportComponent):
|
|||
("Bemerkung", data.get("comment")),
|
||||
]
|
||||
)
|
||||
return section(self.anchor, self.title, body)
|
||||
return section(self.anchor, self.title, body, self.bookmark_label)
|
||||
|
||||
|
||||
class RecommendationComponent(ReportComponent):
|
||||
class RecommendationComponent(NumberedComponent):
|
||||
anchor = "recommendations"
|
||||
title = "Empfehlungen und Auflagen"
|
||||
|
||||
|
|
@ -424,10 +465,10 @@ class RecommendationComponent(ReportComponent):
|
|||
[item.get("number"), item.get("text"), item.get("deadline"), item.get("status")]
|
||||
for item in context.validation.recommendations or []
|
||||
]
|
||||
return section(self.anchor, self.title, table(["Nr.", "Text", "Frist", "Status"], rows))
|
||||
return section(self.anchor, self.title, table(["Nr.", "Text", "Frist", "Status"], rows), self.bookmark_label)
|
||||
|
||||
|
||||
class AttachmentComponent(ReportComponent):
|
||||
class AttachmentComponent(NumberedComponent):
|
||||
anchor = "attachments"
|
||||
title = "Bilder und Anlagen"
|
||||
|
||||
|
|
@ -456,7 +497,7 @@ class AttachmentComponent(ReportComponent):
|
|||
body = table(["Reihenfolge", "Kategorie", "Datei", "Beschreibung"], rows)
|
||||
if figures:
|
||||
body += '<div class="figure-grid">' + "".join(figures) + "</div>"
|
||||
return section(self.anchor, self.title, body)
|
||||
return section(self.anchor, self.title, body, self.bookmark_label)
|
||||
|
||||
def _image_source(self, item: dict) -> str | None:
|
||||
content_type = str(item.get("content_type") or "")
|
||||
|
|
|
|||
|
|
@ -41,5 +41,12 @@ def table(headers: list[str], rows: list[list[Any]], css_class: str = "") -> str
|
|||
return f"<table class=\"data-table {css_class}\"><thead><tr>{head}</tr></thead><tbody>{body}</tbody></table>"
|
||||
|
||||
|
||||
def section(chapter_id: str, title: str, body: str) -> str:
|
||||
return f"<section class=\"chapter\" id=\"{text(chapter_id)}\"><h2>{text(title)}</h2>{body}</section>"
|
||||
def section(chapter_id: str, title: str, body: str, bookmark_label: str | None = None) -> str:
|
||||
bookmark_attr = (
|
||||
f' data-bookmark-label="{text(bookmark_label)}"' if bookmark_label is not None else ""
|
||||
)
|
||||
heading = bookmark_label or title
|
||||
return (
|
||||
f'<section class="chapter" id="{text(chapter_id)}">'
|
||||
f'<h2 class="chapter-title"{bookmark_attr}>{text(heading)}</h2>{body}</section>'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from app.modules.orion.components import (
|
|||
)
|
||||
from app.modules.orion.context import OrionContextBuilder, ReportContext
|
||||
from app.modules.orion.templates.report import render_document
|
||||
from app.modules.orion.template_service import REPORT_SECTIONS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -52,51 +53,35 @@ class OrionReportService:
|
|||
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]
|
||||
chapters = [self._component_for_section(item) for item in REPORT_SECTIONS]
|
||||
return [CoverComponent(), SummaryComponent(), TocComponent(REPORT_SECTIONS), CustomerComponent(), *chapters]
|
||||
|
||||
def _component_for_section(self, item: dict) -> ReportComponent:
|
||||
component = item.get("component")
|
||||
key = str(item["key"])
|
||||
title = str(item["title"])
|
||||
number = item.get("number")
|
||||
if component == "device":
|
||||
return DeviceComponent(key, title, number)
|
||||
if component == "checklist":
|
||||
return ChecklistComponent(key, title, number)
|
||||
if component == "environment":
|
||||
return EnvironmentComponent(key, title, number)
|
||||
if component == "programs":
|
||||
return ProgramComponent(key, title, number)
|
||||
if component == "loading":
|
||||
return LoadingComponent(key, title, number)
|
||||
if component == "equipment":
|
||||
return EquipmentComponent(key, title, number)
|
||||
if component == "measurements":
|
||||
return MeasurementComponent(key, title, number)
|
||||
if component == "drying":
|
||||
return DryingComponent(key, title, number)
|
||||
if component == "recommendations":
|
||||
return RecommendationComponent(key, title, number)
|
||||
if component == "attachments":
|
||||
return AttachmentComponent(key, title, number)
|
||||
return StaticTextComponent(key, title, block_key=item.get("block_key"), number=number)
|
||||
|
||||
def _append_pdf_attachments(self, context: ReportContext, output_path: Path) -> None:
|
||||
pdf_paths = []
|
||||
|
|
|
|||
|
|
@ -19,48 +19,50 @@ TEMPLATE_NAME = "Erstvalidierung Klein-Sterilisator"
|
|||
TEMPLATE_VERSION = "1.0"
|
||||
REFERENCE_PATH = "docs/reference/reports/Erstvalidierung_Dr.Durmaz_Steri_12-25.docx"
|
||||
|
||||
SECTION_DEFINITIONS: list[tuple[str, str | None, str, bool]] = [
|
||||
("bq", "1", "Funktionsqualifikation (BQ)", True),
|
||||
("bq_goal", "1.1", "Anlass und Ziel der Prüfung", False),
|
||||
("legal", "1.2", "Gesetzliche Grundlagen", False),
|
||||
("device", "1.3", "Angaben zum Gerät", False),
|
||||
("performance", "1.4", "Leistungsüberprüfung", False),
|
||||
("performance_checklist", "1.5", "Checkliste Leistungsanforderung", False),
|
||||
("documentation", "1.6", "Dokumentation / Kontrolle", False),
|
||||
("work_instructions", "1.7", "Arbeitsanweisungen", False),
|
||||
("environment", "1.8", "Umgebungsbedingungen", False),
|
||||
("batch_control", "1.9", "Chargenkontrolle", False),
|
||||
("programs", "1.10", "Beschreibung der verwendeten Programme", False),
|
||||
("loading", "1.11", "Beladungsbeschreibungen", False),
|
||||
("reference_load", "1.12", "Referenzbeladung Sterilisator", False),
|
||||
("equipment", "2", "Eingesetzte Prüfmittel", True),
|
||||
("measurement_devices", "2.1", "Beschreibung der Messgeräte", False),
|
||||
("thermo_equipment", "2.2", "Prüfmittel zur thermoelektrischen Untersuchung", False),
|
||||
("configuration", "2.3", "Prüfkonfiguration", False),
|
||||
("lq", "3", "Leistungsqualifikation (LQ)", True),
|
||||
("thermo_tests", "3.1", "Leistungsbeurteilung – Thermoelektrische Prüfungen", False),
|
||||
("vacuum_test", "3.1.1", "Vakuumtest", False),
|
||||
("run_1", "3.2", "Standardbeladung – Testlauf 1", False),
|
||||
("test_1", "3.2.1", "Test 1", False),
|
||||
("run_2", "3.3", "Standardbeladung – Testlauf 2", False),
|
||||
("test_2", "3.3.1", "Test 2", False),
|
||||
("run_3", "3.4", "Standardbeladung – Testlauf 3", False),
|
||||
("test_3", "3.4.1", "Test 3", False),
|
||||
("results", "4", "Ergebnisse der Validierung", True),
|
||||
("results_vacuum", "4.1", "Vakuumtest", False),
|
||||
("results_runs", "4.1.2", "Testläufe 1 bis 3", False),
|
||||
("drying", None, "Nachweis der Trocknungseigenschaften", False),
|
||||
("recommendations", "4.2", "Empfehlungen und Auflagen", False),
|
||||
("attachments", "5", "Anhang", True),
|
||||
("winlog", "5.1", "Winlog-Auswertungen", False),
|
||||
("release_docs", "5.2", "Freigabedokumentation / Routineprüfungen", False),
|
||||
("indicators", "5.3", "Nachweis der umgeschlagenen Indikatoren", False),
|
||||
("cycles", "6", "Programmabläufe / Zyklen", True),
|
||||
("practice_certificates", "6.1", "Zertifikate Praxis", False),
|
||||
("risk", "7", "Risikoeinstufung nach RKI", True),
|
||||
("closing_talk", "7.1", "Abschlussgespräch", False),
|
||||
("certificates", "8", "Zertifikate", True),
|
||||
("calibration_certificates", "9", "Werkskalibrierzertifikate Sensoren", True),
|
||||
REPORT_SECTIONS: list[dict[str, Any]] = [
|
||||
{"key": "functional_qualification", "number": "1", "title": "Funktionsqualifikation (BQ)", "toc": True, "component": "static", "block_key": None},
|
||||
{"key": "purpose", "number": "1.1", "title": "Anlass und Ziel der Prüfung", "toc": True, "component": "static", "block_key": "bq_goal"},
|
||||
{"key": "legal", "number": "1.2", "title": "Gesetzliche Grundlagen", "toc": True, "component": "static", "block_key": "legal"},
|
||||
{"key": "device", "number": "1.3", "title": "Angaben zum Gerät", "toc": True, "component": "device"},
|
||||
{"key": "performance", "number": "1.4", "title": "Leistungsüberprüfung", "toc": True, "component": "static", "block_key": "performance"},
|
||||
{"key": "performance_checklist", "number": "1.5", "title": "Checkliste Leistungsanforderung", "toc": True, "component": "checklist"},
|
||||
{"key": "documentation", "number": "1.6", "title": "Dokumentation / Kontrolle", "toc": True, "component": "static"},
|
||||
{"key": "work_instructions", "number": "1.7", "title": "Arbeitsanweisungen", "toc": True, "component": "static"},
|
||||
{"key": "environment", "number": "1.8", "title": "Umgebungsbedingungen", "toc": True, "component": "environment"},
|
||||
{"key": "batch_control", "number": "1.9", "title": "Chargenkontrolle", "toc": True, "component": "static"},
|
||||
{"key": "programs", "number": "1.10", "title": "Beschreibung der verwendeten Programme", "toc": True, "component": "programs"},
|
||||
{"key": "loading", "number": "1.11", "title": "Beladungsbeschreibungen", "toc": True, "component": "loading"},
|
||||
{"key": "reference_load", "number": "1.12", "title": "Referenzbeladung Sterilisator bei Validierung", "toc": True, "component": "static"},
|
||||
{"key": "equipment_root", "number": "2", "title": "Eingesetzte Prüfmittel", "toc": True, "component": "static"},
|
||||
{"key": "measurement_devices", "number": "2.1", "title": "Beschreibung der Messgeräte", "toc": True, "component": "equipment"},
|
||||
{"key": "thermo_equipment", "number": "2.2", "title": "Prüfmittel zur thermoelektrischen Untersuchung", "toc": True, "component": "static"},
|
||||
{"key": "configuration", "number": "2.3", "title": "Prüfkonfiguration", "toc": True, "component": "static"},
|
||||
{"key": "lq", "number": "3", "title": "Leistungsqualifikation (LQ)", "toc": True, "component": "static"},
|
||||
{"key": "thermo_tests", "number": "3.1", "title": "Leistungsbeurteilung – Thermoelektrische Prüfungen", "toc": True, "component": "static"},
|
||||
{"key": "vacuum_test", "number": "3.1.1", "title": "Vakuumtest", "toc": True, "component": "measurements"},
|
||||
{"key": "run_1", "number": "3.2", "title": "Standardbeladung (1. Durchlauf)", "toc": True, "component": "static"},
|
||||
{"key": "test_1", "number": "3.2.1", "title": "Test 1", "toc": True, "component": "static"},
|
||||
{"key": "run_2", "number": "3.3", "title": "Standardbeladung (2. Durchlauf)", "toc": True, "component": "static"},
|
||||
{"key": "test_2", "number": "3.3.1", "title": "Test 2", "toc": True, "component": "static"},
|
||||
{"key": "run_3", "number": "3.4", "title": "Standardbeladung (3. Durchlauf)", "toc": True, "component": "static"},
|
||||
{"key": "test_3", "number": "3.4.1", "title": "Test 3", "toc": True, "component": "static"},
|
||||
{"key": "results", "number": "4", "title": "Ergebnisse der Validierung", "toc": True, "component": "static"},
|
||||
{"key": "results_vacuum", "number": "4.1", "title": "Vakuumtest", "toc": True, "component": "static"},
|
||||
{"key": "results_runs", "number": "4.1.2", "title": "Testläufe 1 bis 3 Programm 134 °C B", "toc": True, "component": "static"},
|
||||
{"key": "drying", "number": None, "title": "Nachweis der Trocknungseigenschaften Testläufe 1 bis 3", "toc": True, "component": "drying"},
|
||||
{"key": "recommendations", "number": "4.2", "title": "Empfehlungen und Auflagen", "toc": True, "component": "recommendations"},
|
||||
{"key": "appendix", "number": "5", "title": "Anhang", "toc": True, "component": "static"},
|
||||
{"key": "winlog", "number": "5.1", "title": "Vakuumtest / Winlog-Auswertungen", "toc": True, "component": "attachments"},
|
||||
{"key": "bd_empty_chamber", "number": None, "title": "Testlauf Leerkammerprofil / Bowie-Dick", "toc": True, "component": "static"},
|
||||
{"key": "batch_release_docs", "number": None, "title": "Chargen- und Freigabedokumentation", "toc": True, "component": "static"},
|
||||
{"key": "release_docs", "number": "5.2", "title": "Freigabedokumentation / Routineprüfungen", "toc": True, "component": "static"},
|
||||
{"key": "indicators", "number": "5.3", "title": "Nachweis der umgeschlagenen Indikatoren", "toc": True, "component": "static"},
|
||||
{"key": "cycles", "number": "6", "title": "Programmabläufe / Zyklen des Sterilisators", "toc": True, "component": "static"},
|
||||
{"key": "practice_certificates", "number": "6.1", "title": "Zertifikate Praxis", "toc": True, "component": "static"},
|
||||
{"key": "risk", "number": "7", "title": "Risikoeinstufung der Praxis nach RKI", "toc": True, "component": "static"},
|
||||
{"key": "closing_talk", "number": "7.1", "title": "Abschlussgespräch zur Validierung", "toc": True, "component": "static"},
|
||||
{"key": "certificates", "number": "8", "title": "Zertifikate", "toc": True, "component": "static"},
|
||||
{"key": "calibration_certificates", "number": "9", "title": "Werkskalibrierzertifikate Sensoren", "toc": True, "component": "static"},
|
||||
]
|
||||
|
||||
TEXT_BLOCK_HEADINGS = {
|
||||
|
|
@ -252,15 +254,15 @@ class ReportTemplateService:
|
|||
def _create_children(self, template: ReportTemplate) -> None:
|
||||
paragraphs = self._read_docx_paragraphs()
|
||||
blocks = self._extract_blocks(paragraphs)
|
||||
for index, (section_key, number, title, page_break) in enumerate(SECTION_DEFINITIONS, start=1):
|
||||
for index, section in enumerate(REPORT_SECTIONS, start=1):
|
||||
self.session.add(
|
||||
ReportSection(
|
||||
template_id=template.id,
|
||||
section_key=section_key,
|
||||
number=number,
|
||||
title=title,
|
||||
section_key=str(section["key"]),
|
||||
number=section["number"],
|
||||
title=str(section["title"]),
|
||||
order_index=index,
|
||||
page_break_before=page_break,
|
||||
page_break_before=section["number"] in {"1", "2", "3", "4", "5", "6", "7", "8", "9"},
|
||||
active=True,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
@page {
|
||||
size: A4;
|
||||
margin: 34mm 18mm 24mm 18mm;
|
||||
@top-left { content: element(report-header); }
|
||||
@bottom-left { content: element(report-footer); }
|
||||
}
|
||||
|
||||
@page:first {
|
||||
@page cover {
|
||||
margin: 20mm 18mm 20mm 18mm;
|
||||
@top-left { content: ""; }
|
||||
@bottom-left { content: ""; }
|
||||
}
|
||||
|
||||
@page report {
|
||||
margin: 34mm 18mm 24mm 18mm;
|
||||
@top-left { content: element(report-header); }
|
||||
@bottom-left { content: element(report-footer); }
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
@ -27,6 +30,7 @@ body {
|
|||
}
|
||||
|
||||
.report-meta {
|
||||
display: none;
|
||||
string-set: report-number attr(data-report-number), report-version attr(data-report-version), report-date attr(data-report-date);
|
||||
}
|
||||
|
||||
|
|
@ -130,11 +134,16 @@ body {
|
|||
}
|
||||
|
||||
.cover-page {
|
||||
page: cover;
|
||||
min-height: 245mm;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
page-break-after: always;
|
||||
break-after: page;
|
||||
}
|
||||
|
||||
.report-content {
|
||||
page: report;
|
||||
}
|
||||
|
||||
.cover-top {
|
||||
|
|
@ -207,6 +216,12 @@ h2 {
|
|||
line-height: 1.15;
|
||||
margin: 0 0 7mm 0;
|
||||
padding-bottom: 4mm;
|
||||
bookmark-level: none;
|
||||
}
|
||||
|
||||
h2[data-bookmark-label] {
|
||||
bookmark-label: attr(data-bookmark-label);
|
||||
bookmark-level: 1;
|
||||
}
|
||||
|
||||
.chapter + .chapter {
|
||||
|
|
@ -286,9 +301,26 @@ dd {
|
|||
|
||||
.toc-list a {
|
||||
color: #2E3B40;
|
||||
display: grid;
|
||||
gap: 4mm;
|
||||
grid-template-columns: 1fr 14mm;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.toc-level-2 {
|
||||
padding-left: 6mm;
|
||||
}
|
||||
|
||||
.toc-level-3,
|
||||
.toc-level-4 {
|
||||
padding-left: 12mm;
|
||||
}
|
||||
|
||||
.toc-list a::after {
|
||||
content: target-counter(attr(href), page);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.figure-grid {
|
||||
display: grid;
|
||||
gap: 8mm;
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ def render_document(context: ReportContext, chapters: list[str]) -> str:
|
|||
css = (Path(__file__).resolve().parent / "report.css").read_text(encoding="utf-8")
|
||||
logo_uri = schubamed_logo_uri()
|
||||
title = f"Validierungsbericht {context.validation.report_number}"
|
||||
chapter_markup = "\n".join(chapters)
|
||||
cover_markup = chapters[0] if chapters else ""
|
||||
content_markup = "\n".join(chapters[1:])
|
||||
return f"""<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
|
|
@ -49,9 +50,12 @@ def render_document(context: ReportContext, chapters: list[str]) -> str:
|
|||
</head>
|
||||
<body>
|
||||
<article class="report-document">
|
||||
{cover_markup}
|
||||
<section class="report-content">
|
||||
<div class="report-meta" data-report-number="{text(context.validation.report_number)}" data-report-version="{text(context.validation.version)}" data-report-date="{text(context.validation.updated_at)}"></div>
|
||||
{render_report_chrome(context, logo_uri)}
|
||||
{chapter_markup}
|
||||
{content_markup}
|
||||
</section>
|
||||
</article>
|
||||
</body>
|
||||
</html>"""
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -16,7 +17,7 @@ from app.services.validation_workflow import ValidationWorkflowService
|
|||
from app.schemas.domain import ValidationCreate
|
||||
from app.modules.orion.service import OrionReportService
|
||||
from app.modules.orion.assets import SCHUBAMED_LOGO_PATH, schubamed_logo_uri
|
||||
from app.modules.orion.template_service import ReportTemplateService
|
||||
from app.modules.orion.template_service import REPORT_SECTIONS, ReportTemplateService
|
||||
from app.modules.helios.service import HeliosImportService
|
||||
|
||||
|
||||
|
|
@ -224,9 +225,9 @@ def test_orion_renders_reference_main_chapters(tmp_path):
|
|||
html = OrionReportService(db, tmp_path).render_html(validation.id)
|
||||
|
||||
assert "1 Funktionsqualifikation" in html
|
||||
assert "2.2 Prüfmittel" in html
|
||||
assert "2.2 Prüfmittel zur thermoelektrischen Untersuchung" in html
|
||||
assert "4 Ergebnisse der Validierung" in html
|
||||
assert "9. Werkskalibrierzertifikate Sensoren" in html
|
||||
assert "9 Werkskalibrierzertifikate Sensoren" in html
|
||||
|
||||
|
||||
def test_orion_renders_uploaded_images_as_real_images(tmp_path):
|
||||
|
|
@ -339,15 +340,89 @@ def test_orion_contains_required_reference_sections_and_three_runs(tmp_path):
|
|||
"Zusammenfassendes Ergebnis der Validierung",
|
||||
"1.1 Anlass und Ziel der Prüfung",
|
||||
"1.2 Gesetzliche Grundlagen",
|
||||
"3.2 Standardbeladung – Testlauf 1",
|
||||
"3.3 Standardbeladung – Testlauf 2",
|
||||
"3.4 Standardbeladung – Testlauf 3",
|
||||
"3.2 Standardbeladung (1. Durchlauf)",
|
||||
"3.3 Standardbeladung (2. Durchlauf)",
|
||||
"3.4 Standardbeladung (3. Durchlauf)",
|
||||
]:
|
||||
assert title in html
|
||||
assert "Neutraler Logo-Platzhalter" not in html
|
||||
assert "report-header-brand" in html
|
||||
|
||||
|
||||
def test_orion_toc_uses_central_section_numbers_and_titles(tmp_path):
|
||||
db = session()
|
||||
customer, location, device = seed(db)
|
||||
validation = valid_validation(customer, location, device)
|
||||
db.add(validation)
|
||||
db.flush()
|
||||
|
||||
html = OrionReportService(db, tmp_path).render_html(validation.id)
|
||||
|
||||
expected = [
|
||||
"1.3 Angaben zum Gerät",
|
||||
"1.8 Umgebungsbedingungen",
|
||||
"2.3 Prüfkonfiguration",
|
||||
"3.1.1 Vakuumtest",
|
||||
"4.2 Empfehlungen und Auflagen",
|
||||
"9 Werkskalibrierzertifikate Sensoren",
|
||||
]
|
||||
for label in expected:
|
||||
assert label in html
|
||||
assert "3.1.1 Leistungsqualifikation" not in html
|
||||
assert "4.2 Trocknung" not in html
|
||||
assert html.index("1 Funktionsqualifikation") < html.index("9 Werkskalibrierzertifikate")
|
||||
|
||||
|
||||
def test_orion_html_has_single_cover_before_report_content(tmp_path):
|
||||
db = session()
|
||||
customer, location, device = seed(db)
|
||||
validation = valid_validation(customer, location, device)
|
||||
db.add(validation)
|
||||
db.flush()
|
||||
|
||||
html = OrionReportService(db, tmp_path).render_html(validation.id)
|
||||
|
||||
assert html.count('class="cover-page"') == 1
|
||||
assert html.index('class="cover-page"') < html.index('class="report-content"')
|
||||
assert html.index('class="cover-page"') < html.index('class="report-meta"')
|
||||
assert "<h1>PRÜFBERICHT ZUR VALIDIERUNG</h1>" in html
|
||||
|
||||
|
||||
def test_orion_pdf_first_page_is_cover_not_blank(tmp_path):
|
||||
from pypdf import PdfReader
|
||||
|
||||
db = session()
|
||||
customer, location, device = seed(db)
|
||||
validation = valid_validation(customer, location, device)
|
||||
db.add(validation)
|
||||
db.flush()
|
||||
|
||||
pdf_path = OrionReportService(db, tmp_path).render_pdf(validation.id)
|
||||
first_page_text = PdfReader(str(pdf_path)).pages[0].extract_text() or ""
|
||||
normalized_text = re.sub(r"\s+", "", first_page_text.upper())
|
||||
|
||||
assert "PRÜFBERICHTZURVALIDIERUNG" in normalized_text
|
||||
assert "FUNKTIONS-UNDLEISTUNGSQUALIFIKATION" in normalized_text
|
||||
|
||||
|
||||
def test_orion_bookmark_labels_are_created_from_central_sections(tmp_path):
|
||||
db = session()
|
||||
customer, location, device = seed(db)
|
||||
validation = valid_validation(customer, location, device)
|
||||
db.add(validation)
|
||||
db.flush()
|
||||
|
||||
html = OrionReportService(db, tmp_path).render_html(validation.id)
|
||||
bookmark_count = html.count("data-bookmark-label=")
|
||||
toc_count = len([item for item in REPORT_SECTIONS if item.get("toc")])
|
||||
|
||||
assert bookmark_count == toc_count
|
||||
for item in REPORT_SECTIONS:
|
||||
number = item.get("number")
|
||||
label = f"{number} {item['title']}" if number else item["title"]
|
||||
assert f'data-bookmark-label="{label}"' in html
|
||||
|
||||
|
||||
def test_orion_appends_pdf_attachments(tmp_path):
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue