Validation_Suite/validation-suite/backend/mercury/app/services/reference_masterdata.py
2026-07-11 15:50:15 +02:00

380 lines
15 KiB
Python

from __future__ import annotations
from dataclasses import dataclass, field
from datetime import date
from pathlib import Path
import re
from typing import Any
from sqlalchemy import 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
@dataclass
class ImportResult:
created: list[str] = field(default_factory=list)
updated: list[str] = field(default_factory=list)
unchanged: list[str] = field(default_factory=list)
conflicts: list[str] = field(default_factory=list)
errors: list[str] = field(default_factory=list)
validation_id: str | None = None
def as_dict(self) -> dict[str, Any]:
return {
"created": self.created,
"updated": self.updated,
"unchanged": self.unchanged,
"conflicts": self.conflicts,
"errors": self.errors,
"validation_id": self.validation_id,
}
class ReferenceMasterdataImportService:
def __init__(self, session: Session) -> None:
self.session = session
def import_reference_docx(
self,
reference_path: Path,
*,
dry_run: bool = False,
update_existing: bool = False,
create_validation: bool = False,
) -> ImportResult:
if not reference_path.exists():
raise FileNotFoundError(reference_path)
result = ImportResult()
payload = self._payload()
customer = self._upsert_customer(payload["customer"], result, dry_run, update_existing)
location = self._upsert_location(customer, payload["location"], result, dry_run, update_existing)
self._upsert_contacts(customer, payload["contacts"], result, dry_run, update_existing)
device = self._upsert_device(customer, location, payload["device"], result, dry_run, update_existing)
self._upsert_equipment(payload["equipment"], result, dry_run, update_existing)
if create_validation:
validation = self._create_validation(customer, location, device, result, dry_run)
result.validation_id = validation.id if validation is not None else None
if not dry_run:
self.session.commit()
return result
def _payload(self) -> dict[str, Any]:
return {
"customer": {
"name": "Urologische Praxis Dr. Durmaz",
"customer_type": CustomerType.practice,
"specialty": "Urologie",
"display_name": "Urologie Dr. Durmaz",
},
"location": {
"name": "Praxis Nürnberg",
"street": "Wölckernstr. 5",
"postal_code": "90459",
"city": "Nürnberg",
"country": "Deutschland",
},
"contacts": [
{
"full_name": "Dr. Durmaz",
"function": "Verantwortlicher Betreiber",
"notes": "Arzt; QM-Mitverantwortlicher",
},
{
"full_name": "Frau Bal",
"function": "QM-Beauftragte",
"notes": "Hygienebeauftragte; Sachkunde A / Fachkenntnisse Aufbereitung und Freigabe von Medizinprodukten",
},
],
"device": {
"manufacturer": "Euronda SpA",
"model": "E10.7",
"device_type": "Dampf-Kleinsterilisator Klasse B",
"serial_number": "EXN250688",
"year_built": 2025,
"commissioned_on": date(2025, 12, 5),
"chamber_volume_liters": 23,
"steam_generation": "Eigendampferzeugung",
"water_treatment": "Wasserversorgung über Aquafilter Euronda",
"documentation": "interne CF-Card; Protokollausgabe am PC/Rechner möglich",
"supplier": "schubamed-Medizintechnik, 92421 Schwandorf",
"notes": "Sterilisationsverfahren: Konditionierung mit Wasserdampf; Konditionierung teilweise oberhalb und unterhalb des Umgebungsdruckes; Chargendokumentation über interne CF-Card.",
},
"equipment": [
{
"kind": EquipmentKind.temperature_logger,
"manufacturer": "Ebro",
"model": "EBI 11",
"serial_number": "15102807",
"calibrated_on": date(2025, 1, 17),
"status": EquipmentStatus.green,
"notes": "Bezeichnung T235; Messbereich 0 °C bis +150 °C",
},
{
"kind": EquipmentKind.temperature_logger,
"manufacturer": "Ebro",
"model": "EBI 11",
"serial_number": "15211066",
"calibrated_on": date(2025, 1, 17),
"status": EquipmentStatus.green,
"notes": "Bezeichnung T240; Messbereich 0 °C bis +150 °C",
},
{
"kind": EquipmentKind.temperature_logger,
"manufacturer": "Ebro",
"model": "EBI 11",
"serial_number": "15102538",
"calibrated_on": date(2025, 1, 17),
"status": EquipmentStatus.green,
"notes": "Bezeichnung T240; Messbereich 0 °C bis +150 °C",
},
{
"kind": EquipmentKind.temperature_logger,
"manufacturer": "Ebro",
"model": "EBI 11",
"serial_number": "15211067",
"calibrated_on": date(2025, 1, 17),
"status": EquipmentStatus.green,
"notes": "Bezeichnung T240; Messbereich 0 °C bis +150 °C",
},
{
"kind": EquipmentKind.temperature_logger,
"manufacturer": "Ebro",
"model": "EBI 11",
"serial_number": "15125738",
"calibrated_on": date(2025, 1, 17),
"status": EquipmentStatus.green,
"notes": "Bezeichnung T240; Quellenkonflikt: Referenz nennt auch 1525738, mehrfach belegt ist 15125738.",
},
{
"kind": EquipmentKind.pressure_logger,
"manufacturer": "Ebro",
"model": "EBI 11",
"serial_number": "P111",
"calibrated_on": date(2025, 1, 17),
"status": EquipmentStatus.green,
"notes": "Drucklogger",
},
],
}
def _normalize(self, value: str) -> str:
return re.sub(r"[^a-z0-9]+", "", value.lower())
def _upsert_customer(self, payload: dict[str, Any], result: ImportResult, dry_run: bool, update_existing: bool) -> Customer:
target_name = self._normalize(payload["name"])
customer = next(
(item for item in self.session.scalars(select(Customer)) if self._normalize(item.name) == target_name),
None,
)
if customer is None:
customer = Customer(customer_type=payload["customer_type"], name=payload["name"])
customer.notes = f"Fachrichtung: {payload['specialty']}\nAnzeigename: {payload['display_name']}"
if not dry_run:
self.session.add(customer)
self.session.flush()
result.created.append("customer")
return customer
if update_existing:
changed = False
if customer.notes != f"Fachrichtung: {payload['specialty']}\nAnzeigename: {payload['display_name']}":
customer.notes = f"Fachrichtung: {payload['specialty']}\nAnzeigename: {payload['display_name']}"
changed = True
if customer.customer_type != payload["customer_type"]:
customer.customer_type = payload["customer_type"]
changed = True
if changed:
result.updated.append("customer")
else:
result.unchanged.append("customer")
else:
result.unchanged.append("customer")
return customer
def _upsert_location(self, customer: Customer, payload: dict[str, Any], result: ImportResult, dry_run: bool, update_existing: bool) -> Location:
location = next(
(
item
for item in self.session.scalars(select(Location).where(Location.customer_id == customer.id))
if item.street == payload["street"]
and item.postal_code == payload["postal_code"]
and item.city == payload["city"]
),
None,
)
if location is None:
location = Location(
customer_id=customer.id,
name=payload["name"],
street=payload["street"],
postal_code=payload["postal_code"],
city=payload["city"],
)
if not dry_run:
self.session.add(location)
self.session.flush()
result.created.append("location")
return location
if update_existing:
location.name = payload["name"]
result.updated.append("location")
else:
result.unchanged.append("location")
return location
def _upsert_contacts(
self,
customer: Customer,
contacts: list[dict[str, Any]],
result: ImportResult,
dry_run: bool,
update_existing: bool,
) -> None:
for payload in contacts:
contact = self.session.scalar(
select(Contact).where(
Contact.customer_id == customer.id,
Contact.full_name == payload["full_name"],
)
)
notes = payload["notes"]
if contact is None:
contact = Contact(
customer_id=customer.id,
full_name=payload["full_name"],
function=payload["function"],
notes=notes,
)
if not dry_run:
self.session.add(contact)
self.session.flush()
result.created.append(f"contact:{payload['full_name']}")
continue
if update_existing:
contact.function = payload["function"]
contact.notes = notes
result.updated.append(f"contact:{payload['full_name']}")
else:
result.unchanged.append(f"contact:{payload['full_name']}")
def _upsert_device(
self,
customer: Customer,
location: Location,
payload: dict[str, Any],
result: ImportResult,
dry_run: bool,
update_existing: bool,
) -> Device:
device = self.session.scalar(select(Device).where(Device.serial_number == payload["serial_number"]))
if device is None:
device = Device(
customer_id=customer.id,
location_id=location.id,
manufacturer=payload["manufacturer"],
model=payload["model"],
device_type=payload["device_type"],
serial_number=payload["serial_number"],
year_built=payload["year_built"],
commissioned_on=payload["commissioned_on"],
chamber_volume_liters=payload["chamber_volume_liters"],
steam_generation=payload["steam_generation"],
water_treatment=payload["water_treatment"],
documentation=payload["documentation"],
supplier=payload["supplier"],
notes=payload["notes"],
)
if not dry_run:
self.session.add(device)
self.session.flush()
result.created.append("device")
return device
if update_existing:
for key, value in payload.items():
if hasattr(device, key):
setattr(device, key, value)
result.updated.append("device")
else:
result.unchanged.append("device")
return device
def _upsert_equipment(
self,
items: list[dict[str, Any]],
result: ImportResult,
dry_run: bool,
update_existing: bool,
) -> None:
for payload in items:
equipment = self.session.scalar(
select(Equipment).where(Equipment.serial_number == payload["serial_number"])
)
if equipment is None:
equipment = Equipment(
kind=payload["kind"],
manufacturer=payload["manufacturer"],
model=payload["model"],
serial_number=payload["serial_number"],
calibrated_on=payload["calibrated_on"],
status=payload["status"],
notes=payload["notes"],
)
if not dry_run:
self.session.add(equipment)
self.session.flush()
result.created.append(f"equipment:{payload['serial_number']}")
continue
if update_existing:
equipment.kind = payload["kind"]
equipment.manufacturer = payload["manufacturer"]
equipment.model = payload["model"]
equipment.calibrated_on = payload["calibrated_on"]
equipment.status = payload["status"]
equipment.notes = payload["notes"]
result.updated.append(f"equipment:{payload['serial_number']}")
else:
result.unchanged.append(f"equipment:{payload['serial_number']}")
def _create_validation(
self,
customer: Customer,
location: Location,
device: Device,
result: ImportResult,
dry_run: bool,
) -> Validation | None:
existing = self.session.scalar(
select(Validation).where(
Validation.customer_id == customer.id,
Validation.device_id == device.id,
Validation.validation_type == "Erstvalidierung",
)
)
if existing is not None:
result.unchanged.append("validation")
return existing
validation = Validation(
report_number="REF-IMPORT-1",
customer_id=customer.id,
location_id=location.id,
device_id=device.id,
validation_type="Erstvalidierung",
status=ValidationStatus.draft.value,
examiner_name="nicht erfasst",
)
if not dry_run:
self.session.add(validation)
self.session.flush()
result.created.append("validation")
return validation