feat(validation): add editing versioning revalidation and aligned reports

This commit is contained in:
Schubert Ferenc 2026-07-11 10:26:18 +02:00
parent f73a24df13
commit 302e542fda
28 changed files with 2691 additions and 406 deletions

View file

@ -0,0 +1,37 @@
from app.modules.orion.components.base import ReportComponent
from app.modules.orion.components.chapters import (
AttachmentComponent,
ChecklistComponent,
CoverComponent,
CustomerComponent,
DeviceComponent,
DryingComponent,
EnvironmentComponent,
EquipmentComponent,
LoadingComponent,
MeasurementComponent,
ProgramComponent,
RecommendationComponent,
StaticTextComponent,
SummaryComponent,
TocComponent,
)
__all__ = [
"AttachmentComponent",
"ChecklistComponent",
"CoverComponent",
"CustomerComponent",
"DeviceComponent",
"DryingComponent",
"EnvironmentComponent",
"EquipmentComponent",
"LoadingComponent",
"MeasurementComponent",
"ProgramComponent",
"RecommendationComponent",
"StaticTextComponent",
"ReportComponent",
"SummaryComponent",
"TocComponent",
]

View file

@ -0,0 +1,15 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from app.modules.orion.context import ReportContext
class ReportComponent(ABC):
anchor: str
title: str
@abstractmethod
def render(self, context: ReportContext) -> str:
raise NotImplementedError

View file

@ -0,0 +1,325 @@
from __future__ import annotations
from app.modules.orion.components.base import ReportComponent
from app.modules.orion.context import ReportContext
from app.modules.orion.html import definition_list, paragraph, section, table, text, yes_no
class CoverComponent(ReportComponent):
anchor = "cover"
title = "Deckblatt"
def render(self, context: ReportContext) -> str:
validation = context.validation
rows = definition_list(
[
("Berichtsnummer", validation.report_number),
("Validierungsart", validation.validation_type),
("Projekt", validation.project),
("Pruefdatum", validation.performed_on),
("Pruefungsort", validation.test_location),
("Pruefer", validation.examiner_name),
("Gesamtergebnis", validation.result),
("Status", validation.status),
("Ansprechpartner", context.contact.full_name if context.contact else "nicht erfasst"),
("Mitwirkende Personen", validation.participants or "nicht erfasst"),
]
)
return (
"<section class=\"cover-page\" id=\"cover\">"
"<div class=\"cover-kicker\">Neutraler Logo-Platzhalter · Validation Suite</div>"
"<h1>Pruefbericht 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}"
"<div class=\"signature-grid\"><div>Unterschrift technische Validierung</div><div>Unterschrift Auftraggeber</div></div>"
"</section>"
)
class TocComponent(ReportComponent):
anchor = "toc"
title = "Inhaltsverzeichnis"
def __init__(self, components: list[ReportComponent]) -> None:
self.components = components
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"}
)
return section(self.anchor, self.title, f"<ol class=\"toc-list\">{links}</ol>")
class SummaryComponent(ReportComponent):
anchor = "summary"
title = "Zusammenfassendes Ergebnis der Validierung"
def render(self, context: ReportContext) -> str:
validation = context.validation
body = definition_list(
[
("Kunde", context.customer.name),
("Standort", context.location.name if context.location else None),
("Geraet", f"{context.device.manufacturer} {context.device.model}" if context.device else None),
("Pruefmittel", len(context.equipment)),
("Ergebnis", validation.result),
("Mitwirkende Personen", validation.participants),
("Hinweis auf naechste Leistungsbeurteilung", validation.next_validation_on or "nicht erfasst"),
]
)
return section(self.anchor, self.title, body)
class StaticTextComponent(ReportComponent):
def __init__(self, anchor: str, title: str, body: str = "nicht erfasst") -> None:
self.anchor = anchor
self.title = title
self.body = body
def render(self, context: ReportContext) -> str:
return section(self.anchor, self.title, f"<p>{text(self.body)}</p>")
class CustomerComponent(ReportComponent):
anchor = "customer"
title = "Kunde, Standort und Ansprechpartner"
def render(self, context: ReportContext) -> str:
customer = context.customer
location = context.location
contact = context.contact
body = "<h3>Kunde</h3>" + definition_list(
[
("Name", customer.name),
("Typ", customer.customer_type.value),
("Adresse", " ".join(filter(None, [customer.street, customer.postal_code, customer.city]))),
("Telefon", customer.phone),
("Mail", customer.email),
("Betreiber", context.validation.operator_name),
("QM", customer.quality_manager),
("Hygienebeauftragter", customer.hygiene_officer),
]
)
if location:
body += "<h3>Standort</h3>" + definition_list(
[
("Name", location.name),
("Adresse", " ".join(filter(None, [location.street, location.postal_code, location.city]))),
("Raum", location.room),
]
)
if contact:
body += "<h3>Ansprechpartner</h3>" + definition_list(
[
("Name", contact.full_name),
("Funktion", contact.function),
("Mail", contact.email),
("Telefon", contact.phone),
]
)
return section(self.anchor, self.title, body)
class DeviceComponent(ReportComponent):
anchor = "device"
title = "Geraet"
def render(self, context: ReportContext) -> str:
device = context.device
if device is None:
return section(self.anchor, self.title, "")
body = definition_list(
[
("Hersteller", device.manufacturer),
("Modell", device.model),
("Typ", device.device_type),
("Seriennummer", device.serial_number),
("Baujahr", device.year_built),
("Inbetriebnahme", device.commissioned_on),
("Kammervolumen", f"{device.chamber_volume_liters} Liter" if device.chamber_volume_liters else None),
("Dampferzeugung", device.steam_generation),
("Wasseraufbereitung", device.water_treatment),
("Dokumentation", device.documentation),
("Lieferant", device.supplier),
]
)
return section(self.anchor, self.title, body)
class EquipmentComponent(ReportComponent):
anchor = "equipment"
title = "Pruefmittel"
def render(self, context: ReportContext) -> str:
rows = [
[
item.kind.value,
item.manufacturer,
item.model,
item.serial_number,
item.calibrated_on,
item.calibration_due_on,
item.status.value,
]
for item in context.equipment
]
return section(
self.anchor,
self.title,
table(
["Art", "Hersteller", "Modell", "Seriennummer", "Kalibriert", "Gueltig bis", "Status"],
rows,
),
)
class EnvironmentComponent(ReportComponent):
anchor = "environment"
title = "Umgebungsbedingungen"
def render(self, context: ReportContext) -> str:
data = context.validation.environment_conditions or {}
body = definition_list(
[
("Raumtemperatur", data.get("room_temperature")),
("relative Luftfeuchtigkeit", data.get("humidity")),
("Pruefzeit", data.get("test_time")),
]
)
rows = [[item.get("text"), yes_no(item.get("value")), item.get("comment")] for item in data.get("checks", [])]
if rows:
body += table(["Pruefpunkt", "Bewertung", "Kommentar"], rows)
return section(self.anchor, self.title, body)
class ChecklistComponent(ReportComponent):
anchor = "checklists"
title = "Dokumentations- und Leistungschecklisten"
def render(self, context: ReportContext) -> str:
documentation = context.validation.documentation_checklist or []
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)
def _render_items(self, items: list[dict]) -> str:
rows = [
[item.get("number"), item.get("text"), yes_no(item.get("value")), item.get("comment")]
for item in items
]
return table(["Nr.", "Pruefpunkt", "Bewertung", "Kommentar"], rows)
class ProgramComponent(ReportComponent):
anchor = "programs"
title = "Programme"
def render(self, context: ReportContext) -> str:
programs = [item for item in (context.validation.programs or []) if item.get("selected")]
rows = [[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))
class LoadingComponent(ReportComponent):
anchor = "loading"
title = "Beladungsmuster"
def render(self, context: ReportContext) -> str:
rows = [
[item.get("run"), item.get("pattern"), item.get("description"), len(item.get("images", []))]
for item in context.validation.loading_patterns or []
]
return section(self.anchor, self.title, table(["Testlauf", "Beladungsmuster", "Beschreibung", "Bilder"], rows))
class MeasurementComponent(ReportComponent):
anchor = "measurements"
title = "Messdaten"
def render(self, context: ReportContext) -> str:
rows = [
[
item.get("name"),
item.get("start_time"),
item.get("end_time"),
item.get("duration"),
item.get("leak_rate"),
item.get("min_temperature"),
item.get("max_temperature"),
item.get("temperature_band"),
item.get("holding_time"),
item.get("pressure"),
item.get("result"),
]
for item in context.validation.measurement_data or []
]
body = table(
[
"Bereich",
"Start",
"Ende",
"Dauer",
"Leckrate",
"Min. Temp.",
"Max. Temp.",
"Band",
"Haltezeit",
"Druck",
"Ergebnis",
],
rows,
"compact",
)
winlog_rows = []
for item in context.validation.measurement_data or []:
for imported in item.get("imports", []):
winlog_rows.append([item.get("name"), imported.get("filename"), imported.get("content_type")])
if winlog_rows:
body += "<h3>Winlog-Dateien</h3>" + table(["Bereich", "Datei", "Typ"], winlog_rows)
return section(self.anchor, self.title, body)
class DryingComponent(ReportComponent):
anchor = "drying"
title = "Trocknung"
def render(self, context: ReportContext) -> str:
data = context.validation.drying or {}
body = definition_list(
[
("Startgewicht", data.get("start_weight")),
("Endgewicht", data.get("end_weight")),
("Differenz", data.get("difference")),
("Bewertung", data.get("rating")),
("Bemerkung", data.get("comment")),
]
)
return section(self.anchor, self.title, body)
class RecommendationComponent(ReportComponent):
anchor = "recommendations"
title = "Empfehlungen und Auflagen"
def render(self, context: ReportContext) -> str:
rows = [
[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))
class AttachmentComponent(ReportComponent):
anchor = "attachments"
title = "Bilder und Anlagen"
def render(self, context: ReportContext) -> str:
rows = [
[item.get("order"), item.get("category"), item.get("filename"), item.get("description")]
for item in sorted(context.validation.attachments or [], key=lambda row: row.get("order") or 0)
]
return section(self.anchor, self.title, table(["Reihenfolge", "Kategorie", "Datei", "Beschreibung"], rows))