feat(navigation): add prominent validation quickstart action

This commit is contained in:
Schubert Ferenc 2026-07-11 18:02:53 +02:00
parent 0bbcaba211
commit 9c6b184e39
28614 changed files with 4356173 additions and 23 deletions

View file

@ -0,0 +1,757 @@
from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass
from datetime import date
from pathlib import Path
import shutil
from typing import Any
from dateutil.relativedelta import relativedelta
from sqlalchemy import delete, select
from sqlalchemy.orm import Session
from app.models.contact import Contact
from app.models.customer import Customer, CustomerType
from app.models.device import Device
from app.models.equipment import Equipment, EquipmentKind, EquipmentStatus
from app.models.location import Location
from app.models.validation import Validation, ValidationStatus
from app.modules.orion.service import OrionReportService
from app.modules.orion.template_service import ReportTemplateService
from app.services.validation_workflow import ValidationWorkflowService
DEMO_TAG = "DEMO_DATA"
DEMO_REPORT_PREFIX = "DEMO-VAL"
DEMO_DEVICE_PREFIX = "DEMO-DEV"
DEMO_EQUIPMENT_PREFIX = "DEMO-EQ"
DEMO_UPLOAD_ROOT = Path("/app/uploads/demo-data")
DEMO_REPORT_ROOT = Path("/app/reports")
@dataclass(frozen=True)
class DemoCustomerTemplate:
name: str
specialty: str
street: str
postal_code: str
city: str
phone: str
email: str
hygiene_officer: str
quality_manager: str
operator: str
locations: list[dict[str, Any]]
contacts: list[dict[str, Any]]
device: dict[str, Any]
CUSTOMER_TEMPLATES: list[DemoCustomerTemplate] = [
DemoCustomerTemplate(
name="Zahnzentrum Alster",
specialty="Zahnarzt",
street="Alsterufer 18",
postal_code="20354",
city="Hamburg",
phone="040 5550010",
email="kontakt@zahnzentrum-alster.schubamed.de",
hygiene_officer="Dr. Jana Peters",
quality_manager="M. Kruse",
operator="Dr. Jana Peters",
locations=[
{"name": "Hauptpraxis", "street": "Alsterufer 18", "postal_code": "20354", "city": "Hamburg", "room": "Steri 1"},
{"name": "OP-Bereich", "street": "Alsterufer 18", "postal_code": "20354", "city": "Hamburg", "room": "OP 2"},
],
contacts=[
{"full_name": "Dr. Jana Peters", "function": "Betreiberin", "email": "jana.peters@schubamed.de", "phone": "040 5550011"},
{"full_name": "M. Kruse", "function": "QM-Beauftragte", "email": "qm.zahnzentrum.alster@schubamed.de", "phone": "040 5550012"},
],
device={
"manufacturer": "Euronda SpA",
"model": "E10.7",
"serial_number": "SN-DEMO-001",
"year_built": 2023,
"commissioned_on": date(2023, 6, 14),
"chamber_volume_liters": 24,
"steam_generation": "Eigendampferzeugung",
"water_treatment": "Vollentsalzung",
"documentation": "CF-Karte und PDF-Protokoll",
"supplier": "schubamed Medizintechnik",
},
),
DemoCustomerTemplate(
name="Urologie Zentrum Elbe",
specialty="Urologie",
street="Mönckebergstr. 11",
postal_code="20095",
city="Hamburg",
phone="040 5550020",
email="kontakt@urologie-elbe.schubamed.de",
hygiene_officer="Dr. Felix Brandt",
quality_manager="S. Lange",
operator="Dr. Felix Brandt",
locations=[
{"name": "Praxis Mitte", "street": "Mönckebergstr. 11", "postal_code": "20095", "city": "Hamburg", "room": "Steri A"},
],
contacts=[
{"full_name": "Dr. Felix Brandt", "function": "Betreiber", "email": "felix.brandt@schubamed.de", "phone": "040 5550021"},
],
device={
"manufacturer": "Euronda SpA",
"model": "E9",
"serial_number": "SN-DEMO-002",
"year_built": 2022,
"commissioned_on": date(2022, 11, 2),
"chamber_volume_liters": 18,
"steam_generation": "Generator integriert",
"water_treatment": "Wasserkonditionierung",
"documentation": "Digitale Dokumentation",
"supplier": "schubamed Medizintechnik",
},
),
DemoCustomerTemplate(
name="Orthopädie am Park",
specialty="Orthopädie",
street="Königsallee 44",
postal_code="40212",
city="Düsseldorf",
phone="0211 5550030",
email="kontakt@orthopaedie-park.schubamed.de",
hygiene_officer="Dr. Lea Sommer",
quality_manager="A. Hoffmann",
operator="Dr. Lea Sommer",
locations=[
{"name": "Ambulanz", "street": "Königsallee 44", "postal_code": "40212", "city": "Düsseldorf", "room": "Steri 1"},
{"name": "Behandlungszentrum", "street": "Kaiserswerther Str. 99", "postal_code": "40474", "city": "Düsseldorf", "room": "Steri 2"},
],
contacts=[
{"full_name": "Dr. Lea Sommer", "function": "Betreiberin", "email": "lea.sommer@schubamed.de", "phone": "0211 5550031"},
{"full_name": "A. Hoffmann", "function": "Hygienebeauftragte", "email": "hygiene.orthopaedie@schubamed.de", "phone": "0211 5550032"},
],
device={
"manufacturer": "MELAG",
"model": "Vacuklav 41 B+",
"serial_number": "SN-DEMO-003",
"year_built": 2021,
"commissioned_on": date(2021, 9, 8),
"chamber_volume_liters": 22,
"steam_generation": "Dampferzeuger intern",
"water_treatment": "VE-Wasser",
"documentation": "CF-Karte, USB-Export",
"supplier": "schubamed Medizintechnik",
},
),
DemoCustomerTemplate(
name="Dermatologie Rhein",
specialty="Dermatologie",
street="Breite Str. 8",
postal_code="50667",
city="Köln",
phone="0221 5550040",
email="kontakt@dermatologie-rhein.schubamed.de",
hygiene_officer="Dr. Sophie Keller",
quality_manager="D. Neumann",
operator="Dr. Sophie Keller",
locations=[
{"name": "Hautpraxis", "street": "Breite Str. 8", "postal_code": "50667", "city": "Köln", "room": "Steri"},
],
contacts=[
{"full_name": "Dr. Sophie Keller", "function": "Betreiberin", "email": "sophie.keller@schubamed.de", "phone": "0221 5550041"},
{"full_name": "D. Neumann", "function": "QM", "email": "qm.dermatologie.rhein@schubamed.de", "phone": "0221 5550042"},
],
device={
"manufacturer": "MELAG",
"model": "Vacuklav 44 B+",
"serial_number": "SN-DEMO-004",
"year_built": 2020,
"commissioned_on": date(2020, 4, 16),
"chamber_volume_liters": 24,
"steam_generation": "Eigendampferzeugung",
"water_treatment": "Osmose",
"documentation": "Digitale Dokumentation und Ausdruck",
"supplier": "schubamed Medizintechnik",
},
),
DemoCustomerTemplate(
name="HNO Zentrum Forum",
specialty="HNO",
street="Theatinerstr. 19",
postal_code="80333",
city="München",
phone="089 5550050",
email="kontakt@hno-forum.schubamed.de",
hygiene_officer="Dr. Tom Berger",
quality_manager="R. Wagner",
operator="Dr. Tom Berger",
locations=[
{"name": "Forum Praxis", "street": "Theatinerstr. 19", "postal_code": "80333", "city": "München", "room": "Steri 1"},
],
contacts=[
{"full_name": "Dr. Tom Berger", "function": "Betreiber", "email": "tom.berger@schubamed.de", "phone": "089 5550051"},
],
device={
"manufacturer": "MELAG",
"model": "PrimeLine",
"serial_number": "SN-DEMO-005",
"year_built": 2024,
"commissioned_on": date(2024, 3, 1),
"chamber_volume_liters": 29,
"steam_generation": "Generator integriert",
"water_treatment": "VE-Wasser",
"documentation": "USB-Export",
"supplier": "schubamed Medizintechnik",
},
),
DemoCustomerTemplate(
name="Gynäkologie Marienhof",
specialty="Gynäkologie",
street="Marienplatz 3",
postal_code="86150",
city="Augsburg",
phone="0821 5550060",
email="kontakt@gyn-marienhof.schubamed.de",
hygiene_officer="Dr. Miriam Wolf",
quality_manager="K. Braun",
operator="Dr. Miriam Wolf",
locations=[
{"name": "Frauenpraxis", "street": "Marienplatz 3", "postal_code": "86150", "city": "Augsburg", "room": "Steri"},
{"name": "Ambulanz Süd", "street": "Bürgermeister-Fischer-Str. 12", "postal_code": "86150", "city": "Augsburg", "room": "Steri 2"},
],
contacts=[
{"full_name": "Dr. Miriam Wolf", "function": "Betreiberin", "email": "miriam.wolf@schubamed.de", "phone": "0821 5550061"},
],
device={
"manufacturer": "MELAG",
"model": "ProLine",
"serial_number": "SN-DEMO-006",
"year_built": 2019,
"commissioned_on": date(2019, 8, 23),
"chamber_volume_liters": 18,
"steam_generation": "Kompaktgenerator",
"water_treatment": "VE-Wasser",
"documentation": "CF-Card",
"supplier": "schubamed Medizintechnik",
},
),
DemoCustomerTemplate(
name="Chirurgie Ost",
specialty="Chirurgie",
street="Lindenstr. 7",
postal_code="04109",
city="Leipzig",
phone="0341 5550070",
email="kontakt@chirurgie-ost.schubamed.de",
hygiene_officer="Dr. Paul Richter",
quality_manager="F. Scholz",
operator="Dr. Paul Richter",
locations=[
{"name": "OP-Zentrum", "street": "Lindenstr. 7", "postal_code": "04109", "city": "Leipzig", "room": "Steri OP"},
],
contacts=[
{"full_name": "Dr. Paul Richter", "function": "Betreiber", "email": "paul.richter@schubamed.de", "phone": "0341 5550071"},
{"full_name": "F. Scholz", "function": "Hygiene", "email": "hygiene.chirurgie.ost@schubamed.de", "phone": "0341 5550072"},
],
device={
"manufacturer": "W&H",
"model": "Lara XL",
"serial_number": "SN-DEMO-007",
"year_built": 2023,
"commissioned_on": date(2023, 2, 15),
"chamber_volume_liters": 17,
"steam_generation": "Dampferzeuger intern",
"water_treatment": "Mikrofiltration",
"documentation": "USB und Ausdruck",
"supplier": "schubamed Medizintechnik",
},
),
DemoCustomerTemplate(
name="Allgemeinmedizin West",
specialty="Allgemeinmedizin",
street="Bergstr. 25",
postal_code="70173",
city="Stuttgart",
phone="0711 5550080",
email="kontakt@allgemeinmedizin-west.schubamed.de",
hygiene_officer="Dr. Nina Beck",
quality_manager="J. Hartmann",
operator="Dr. Nina Beck",
locations=[
{"name": "Hausarztpraxis", "street": "Bergstr. 25", "postal_code": "70173", "city": "Stuttgart", "room": "Steri"},
],
contacts=[
{"full_name": "Dr. Nina Beck", "function": "Betreiberin", "email": "nina.beck@schubamed.de", "phone": "0711 5550081"},
],
device={
"manufacturer": "W&H",
"model": "Lisa",
"serial_number": "SN-DEMO-008",
"year_built": 2022,
"commissioned_on": date(2022, 7, 12),
"chamber_volume_liters": 22,
"steam_generation": "Eigendampferzeugung",
"water_treatment": "VE-Wasser",
"documentation": "Digital und Papier",
"supplier": "schubamed Medizintechnik",
},
),
DemoCustomerTemplate(
name="Augenärzte am Ring",
specialty="Augenarzt",
street="Ringstr. 52",
postal_code="50672",
city="Köln",
phone="0221 5550090",
email="kontakt@augenaerzte-ring.schubamed.de",
hygiene_officer="Dr. Karin Otto",
quality_manager="M. Franke",
operator="Dr. Karin Otto",
locations=[
{"name": "Augenzentrum", "street": "Ringstr. 52", "postal_code": "50672", "city": "Köln", "room": "Steri 1"},
{"name": "Laserzentrum", "street": "Ringstr. 54", "postal_code": "50672", "city": "Köln", "room": "Steri 2"},
],
contacts=[
{"full_name": "Dr. Karin Otto", "function": "Betreiberin", "email": "karin.otto@schubamed.de", "phone": "0221 5550091"},
{"full_name": "M. Franke", "function": "QM", "email": "qm.augen@schubamed.de", "phone": "0221 5550092"},
],
device={
"manufacturer": "Mocom",
"model": "B Classic",
"serial_number": "SN-DEMO-009",
"year_built": 2021,
"commissioned_on": date(2021, 5, 20),
"chamber_volume_liters": 12,
"steam_generation": "Kompaktgenerator",
"water_treatment": "Vollentsalzung",
"documentation": "USB-Export",
"supplier": "schubamed Medizintechnik",
},
),
DemoCustomerTemplate(
name="MVZ Medikon",
specialty="MVZ",
street="Forum 9",
postal_code="90402",
city="Nürnberg",
phone="0911 5550100",
email="kontakt@mvz-medikon.schubamed.de",
hygiene_officer="Dr. Lara Stein",
quality_manager="C. Meier",
operator="Dr. Lara Stein",
locations=[
{"name": "Hauptstandort", "street": "Forum 9", "postal_code": "90402", "city": "Nürnberg", "room": "Steri A"},
{"name": "Nebenstandort", "street": "Forum 11", "postal_code": "90402", "city": "Nürnberg", "room": "Steri B"},
],
contacts=[
{"full_name": "Dr. Lara Stein", "function": "Betreiberin", "email": "lara.stein@schubamed.de", "phone": "0911 5550101"},
{"full_name": "C. Meier", "function": "Hygiene", "email": "hygiene.mvz@schubamed.de", "phone": "0911 5550102"},
],
device={
"manufacturer": "Kronos",
"model": "B23",
"serial_number": "SN-DEMO-010",
"year_built": 2024,
"commissioned_on": date(2024, 1, 11),
"chamber_volume_liters": 23,
"steam_generation": "Eigendampferzeugung",
"water_treatment": "VE-Wasser",
"documentation": "Hybrid",
"supplier": "schubamed Medizintechnik",
},
),
]
EQUIPMENT_TEMPLATES: list[dict[str, Any]] = [
{
"kind": EquipmentKind.temperature_logger,
"manufacturer": "EBRO",
"model": "EBI 11",
"serial_number": "DEMO-EBI11-001",
"calibrated_on": date(2025, 1, 17),
"calibration_due_on": date(2026, 1, 17),
"status": EquipmentStatus.green,
"notes": "Demo-Temperaturlogger",
},
{
"kind": EquipmentKind.temperature_logger,
"manufacturer": "EBRO",
"model": "EBI 11",
"serial_number": "DEMO-EBI11-002",
"calibrated_on": date(2025, 2, 2),
"calibration_due_on": date(2026, 2, 2),
"status": EquipmentStatus.green,
"notes": "Demo-Temperaturlogger",
},
{
"kind": EquipmentKind.pressure_logger,
"manufacturer": "EBRO",
"model": "Drucklogger",
"serial_number": "DEMO-PR-001",
"calibrated_on": date(2025, 1, 21),
"calibration_due_on": date(2026, 1, 21),
"status": EquipmentStatus.green,
"notes": "Demo-Drucklogger",
},
{
"kind": EquipmentKind.sensor,
"manufacturer": "Mettler Toledo",
"model": "Waage",
"serial_number": "DEMO-SCALE-001",
"calibrated_on": date(2024, 12, 12),
"calibration_due_on": date(2025, 12, 12),
"status": EquipmentStatus.yellow,
"notes": "Demo-Waage",
},
{
"kind": EquipmentKind.sensor,
"manufacturer": "Mettler Toledo",
"model": "Leitwertmessgerät",
"serial_number": "DEMO-COND-001",
"calibrated_on": date(2025, 3, 4),
"calibration_due_on": date(2026, 3, 4),
"status": EquipmentStatus.green,
"notes": "Demo-Leitwertmessgerät",
},
{
"kind": EquipmentKind.sensor,
"manufacturer": "Trotec",
"model": "Raumklimamessgerät",
"serial_number": "DEMO-CLIMATE-001",
"calibrated_on": date(2024, 11, 1),
"calibration_due_on": date(2025, 11, 1),
"status": EquipmentStatus.red,
"notes": "Demo-Raumklimamessgerät",
},
]
class DemoDataService:
def __init__(self, session: Session, upload_root: Path | None = None, report_root: Path | None = None) -> None:
self.session = session
self.upload_root = upload_root or DEMO_UPLOAD_ROOT.parent
self.report_root = report_root or DEMO_REPORT_ROOT
self.demo_upload_root = self.upload_root / "demo-data"
def run(
self,
*,
customers: int = 10,
devices: int = 10,
validations: int = 20,
reports: bool = False,
images: bool = False,
reset: bool = False,
) -> dict[str, int]:
if reset:
self.reset()
created_customers = self.ensure_customers(max(customers, 10))
created_equipment = self.ensure_equipment()
created_devices = self.ensure_devices(max(devices, 10), created_customers)
created_validations = self.ensure_validations(max(validations, 20), created_customers, created_devices, images=images)
self.session.commit()
if reports:
self._generate_reports(created_validations)
return {
"customers": len(created_customers),
"devices": len(created_devices),
"validations": len(created_validations),
"equipment": len(created_equipment),
}
def reset(self) -> None:
demo_validation_ids = self.session.scalars(
select(Validation.id).where(
Validation.report_number.like(f"{DEMO_REPORT_PREFIX}%")
| Validation.notes.like(f"%{DEMO_TAG}%")
)
).all()
if demo_validation_ids:
self.session.execute(delete(Validation).where(Validation.id.in_(demo_validation_ids)))
self.session.execute(
delete(Equipment).where(Equipment.serial_number.like("DEMO-%") | Equipment.notes.like(f"%{DEMO_TAG}%"))
)
self.session.execute(
delete(Device).where(Device.serial_number.like("SN-DEMO-%") | Device.notes.like(f"%{DEMO_TAG}%"))
)
self.session.execute(
delete(Contact).where(
Contact.email.like("demo-%@schubamed.de") | Contact.notes.like(f"%{DEMO_TAG}%")
)
)
self.session.execute(
delete(Customer).where(
Customer.email.like("demo-%@schubamed.de") | Customer.notes.like(f"%{DEMO_TAG}%")
)
)
self.session.commit()
if self.demo_upload_root.exists():
shutil.rmtree(self.demo_upload_root)
self.report_root.mkdir(parents=True, exist_ok=True)
for pdf in self.report_root.glob(f"{DEMO_REPORT_PREFIX}*.pdf"):
pdf.unlink()
def ensure_customers(self, count: int) -> list[Customer]:
ensured: list[Customer] = []
for index in range(count):
template = CUSTOMER_TEMPLATES[index % len(CUSTOMER_TEMPLATES)]
occurrence = index // len(CUSTOMER_TEMPLATES)
customer = self._ensure_customer(template, occurrence, index)
ensured.append(customer)
return ensured
def ensure_equipment(self) -> list[Equipment]:
items: list[Equipment] = []
for template in EQUIPMENT_TEMPLATES:
equipment = self.session.scalar(
select(Equipment).where(Equipment.serial_number == template["serial_number"])
)
if equipment is None:
equipment = Equipment(**template)
equipment.notes = f"{template['notes']} {DEMO_TAG}"
self.session.add(equipment)
self.session.flush()
items.append(equipment)
return items
def ensure_devices(self, count: int, customers: list[Customer]) -> list[Device]:
devices: list[Device] = []
all_locations = {customer.id: list(self.session.scalars(select(Location).where(Location.customer_id == customer.id))) for customer in customers}
for index in range(count):
customer = customers[index % len(customers)]
template = CUSTOMER_TEMPLATES[index % len(CUSTOMER_TEMPLATES)].device
occurrence = index // len(CUSTOMER_TEMPLATES)
serial_number = template["serial_number"] if occurrence == 0 else f"{template['serial_number']}-{occurrence + 1}"
device = self.session.scalar(select(Device).where(Device.serial_number == serial_number))
if device is None:
location_candidates = all_locations.get(customer.id, [])
location_id = location_candidates[0].id if location_candidates else None
device_payload = deepcopy(template)
device_payload["serial_number"] = serial_number
device_payload["customer_id"] = customer.id
device_payload["location_id"] = location_id
device = Device(**device_payload)
device.notes = f"{DEMO_TAG} {customer.name}"
self.session.add(device)
self.session.flush()
devices.append(device)
return devices
def ensure_validations(
self,
count: int,
customers: list[Customer],
devices: list[Device],
*,
images: bool = False,
) -> list[Validation]:
validations: list[Validation] = []
report_number_index = 1
for index in range(count):
template = CUSTOMER_TEMPLATES[index % len(CUSTOMER_TEMPLATES)]
customer = customers[index % len(customers)]
device = devices[index % len(devices)]
locations = list(self.session.scalars(select(Location).where(Location.customer_id == customer.id)))
contacts = list(self.session.scalars(select(Contact).where(Contact.customer_id == customer.id)))
location = locations[index % len(locations)] if locations else None
contact = contacts[index % len(contacts)] if contacts else None
report_number = f"{DEMO_REPORT_PREFIX}-{report_number_index:03d}"
report_number_index += 1
validation = self.session.scalar(select(Validation).where(Validation.report_number == report_number))
if validation is None:
validation = Validation(
report_number=report_number,
customer_id=customer.id,
location_id=location.id if location else None,
contact_id=contact.id if contact else None,
device_id=device.id,
validation_type="Erstvalidierung" if index % 2 == 0 else "Revalidierung",
performed_on=date.today() - relativedelta(months=6 + index),
examiner_name="Dr. Lara Stein",
operator_name=template.operator,
participants=f"{customer.name} - Team",
status=[
ValidationStatus.draft.value,
ValidationStatus.approved.value,
ValidationStatus.completed.value,
][index % 3],
result="bestanden" if index % 3 else "bestanden mit Auflagen",
revalidation_interval_months=24,
equipment_ids=[equipment.id for equipment in self.session.scalars(select(Equipment).limit(3))],
environment_conditions={
"room_temperature": 22 + (index % 3),
"humidity": 48 + (index % 5),
"test_time": f"{8 + (index % 3)}:30",
"checks": [
{"text": "Raumbedingungen stabil", "value": "yes", "comment": ""},
{"text": "Aufstellort frei zugänglich", "value": "yes", "comment": ""},
],
},
documentation_checklist=self._checklist("documentation_control", index),
performance_checklist=self._checklist("sterilizer_description", index),
programs=[
{"name": "Vakuumtest", "selected": True, "custom": False},
{"name": "Bowie-Dick / Leerkammerprofil", "selected": index % 2 == 0, "custom": False},
{"name": "134 C hohl verpackt", "selected": True, "custom": False},
],
loading_patterns=self._loading_patterns(index),
measurement_data=self._measurement_data(index),
drying={"start_weight": 12.5, "end_weight": 12.1, "difference": 0.4, "assessment": "in Ordnung", "comment": ""},
recommendations=[
{"number": 1, "text": "Routinekontrolle dokumentieren", "deadline": "6 Monate", "status": "offen"}
] if index % 4 == 0 else [],
attachments=self._attachments_for_validation(index, images=images),
notes=DEMO_TAG,
)
self.session.add(validation)
self.session.flush()
ValidationWorkflowService(self.session).apply_revalidation_date(validation)
validations.append(validation)
return validations
def _ensure_customer(self, template: DemoCustomerTemplate, occurrence: int, index: int) -> Customer:
email = template.email if occurrence == 0 else f"demo-{index + 1:02d}@schubamed.de"
customer = self.session.scalar(select(Customer).where(Customer.email == email))
if customer is None:
customer = Customer(
customer_type=CustomerType.practice,
name=template.name if occurrence == 0 else f"{template.name} {occurrence + 1}",
street=template.street,
postal_code=template.postal_code,
city=template.city,
phone=template.phone,
email=email,
hygiene_officer=template.hygiene_officer,
quality_manager=template.quality_manager,
notes=f"{DEMO_TAG} {template.specialty}",
)
self.session.add(customer)
self.session.flush()
for loc_index, location_payload in enumerate(template.locations, start=1):
location = self.session.scalar(
select(Location).where(
Location.customer_id == customer.id,
Location.name == location_payload["name"],
)
)
if location is None:
self.session.add(Location(customer_id=customer.id, **location_payload))
for contact_payload in template.contacts:
contact = self.session.scalar(
select(Contact).where(
Contact.customer_id == customer.id,
Contact.full_name == contact_payload["full_name"],
)
)
if contact is None:
self.session.add(
Contact(
customer_id=customer.id,
full_name=contact_payload["full_name"],
function=contact_payload["function"],
email=contact_payload["email"],
phone=contact_payload["phone"],
notes=DEMO_TAG,
)
)
self.session.flush()
return customer
def _checklist(self, key: str, index: int) -> list[dict[str, Any]]:
bundle = ReportTemplateService(self.session).ensure_default_template()
template = next((item for item in bundle.checklists if item.checklist_key == key), None)
if template is None:
return []
values = ["yes", "yes", "na"] if index % 2 == 0 else ["yes", "na", "yes"]
return [
{
"number": item["number"],
"text": item["text"],
"value": values[(item["number"] - 1) % len(values)],
"comment": "",
}
for item in template.items
]
def _loading_patterns(self, index: int) -> list[dict[str, Any]]:
return [
{
"run": 1,
"pattern": "Standard",
"description": f"Demo-Beladung {index + 1}",
"images": [],
},
{
"run": 2,
"pattern": "Standard",
"description": "",
"images": [],
},
{
"run": 3,
"pattern": "Standard",
"description": "",
"images": [],
},
]
def _measurement_data(self, index: int) -> list[dict[str, Any]]:
base_temperature = 132 + (index % 3)
return [
{
"test_run": "Vakuumtest",
"start_time": "08:00",
"end_time": "08:08",
"duration": "00:08",
"leak_rate": "0.4",
"result": "bestanden",
},
{
"test_run": "Testlauf 1",
"start_time": "09:00",
"end_time": "09:31",
"duration": "00:31",
"minimum_temperature": base_temperature,
"maximum_temperature": base_temperature + 2,
"temperature_band": "2.0",
"holding_time": "3:00",
"pressure": "2.1",
"result": "bestanden",
},
]
def _attachments_for_validation(self, index: int, *, images: bool) -> list[dict[str, Any]]:
if not images or index >= 5:
return []
image_dir = self.demo_upload_root / "images"
image_dir.mkdir(parents=True, exist_ok=True)
image_path = image_dir / f"demo-figure-{index + 1}.svg"
if not image_path.exists():
image_path.write_text(
"\n".join(
[
'<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800" viewBox="0 0 1200 800">',
'<rect width="1200" height="800" fill="#f6f7f2"/>',
'<rect x="60" y="60" width="1080" height="680" rx="18" fill="#ffffff" stroke="#bfc5bd" stroke-width="3"/>',
'<text x="100" y="150" font-family="Arial, sans-serif" font-size="44" fill="#2f3b35">SCHUBAMED Demo-Bild</text>',
f'<text x="100" y="230" font-family="Arial, sans-serif" font-size="32" fill="#4d5b54">Validierung {index + 1}</text>',
"</svg>",
]
),
encoding="utf-8",
)
return [
{
"category": "Beladung",
"filename": image_path.name,
"content_type": "image/svg+xml",
"description": f"Demo-Abbildung {index + 1}",
"order": 1,
"storage_path": str(image_path),
}
]
def _generate_reports(self, validations: list[Validation]) -> None:
self.report_root.mkdir(parents=True, exist_ok=True)
ReportTemplateService(self.session).ensure_default_template()
for validation in validations:
OrionReportService(self.session, self.report_root).render_pdf(validation.id)