380 lines
15 KiB
Python
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
|