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