feat(navigation): add prominent validation quickstart action
This commit is contained in:
parent
0bbcaba211
commit
9c6b184e39
28614 changed files with 4356173 additions and 23 deletions
757
validation-suite/backend/mercury/app/services/demo_data.py
Normal file
757
validation-suite/backend/mercury/app/services/demo_data.py
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue