"""Einmaliger Datenbereinigungslauf, 2026-09-04: Dubletten im Materialstamm (Excel-Import-Tippfehler/Leerzeichen-Varianten) zusammengelegt + Datentyp-Fehler korrigiert. Dokumentation: ergebnisse/material_bereinigung_log.md. Deployment-Regel (Prompt 19): dieses Skript wird hier nur als Code/Audit-Trail abgelegt, wurde bereits interaktiv gegen die Live-DB auf 192.168.1.238 ausgeführt (nicht erneut laufen lassen - Materialien sind bereits gemerged, ein zweiter Lauf würde ins Leere laufen bzw. bei neu angelegten Materialien mit identischen IDs falsche Treffer liefern). Aufruf, falls je auf einer anderen/älteren DB nachvollzogen werden muss (im aktivierten venv, DATABASE_URL passend zur damaligen ID-Vergabe): python -m scripts.material_bereinigung_2026-09-04 """ from __future__ import annotations import asyncio from sqlalchemy import text from app.db.session import engine # (keep_id, drop_id) - drop wird auf keep umgehängt und danach gelöscht. DUBLETTEN_PAARE = [ (36, 100), # "E 153" / "E153" (47, 95), # "Rettungsdecke" / "Rettungsdecke " (Leerzeichen) (79, 55), # "Verbandspäckchen klein" (Leerzeichen) (132, 70), # "Thomasholder Kind" / "Thomas Holder Kind" (144, 71), # "Samsplint" (Leerzeichen) (151, 76), # "Rollenpflaster" (Leerzeichen) - NICHT zu verwechseln mit der # Größenvariante 151 selbst, die bewusst NICHT weiter # aufgeteilt wurde (siehe Log). (146, 4), # "Stethoskop" / "Stetoskop" (Tippfehler) (112, 5), # "Pupillenleuchte" / "Puppillenleuchte" (Tippfehler) (20, 54), # "Aluderm Strips..." / "...Stripes..." (Tippfehler) (35, 159), # "Farbkodierte..." / "Farbcodierte..." Blockerspritze (37, 101), # "Infusionssystem" / "Infussionssystem" (Tippfehler) (150, 50), # "Verbandspäckchen Groß" / "...gross" (58, 98), # "Wundschnellverband..." / "Wundschnelölverband..." (Tippfehler) (168, 90), # "Handschuhe" / "Handschuh lose" (170, 9), # "Schlingentupfer" / "Schlinggazetupfer" (62, 81), # "Verbandtuch 60x80" / "Verbandstuch 160x80" (Tippfehler) (48, 149), # "Elastische Binde" / "Elastische Binden" (161, 102), # "Beatmungsmasken S" / "Beatmunsmaske" (Excel-Kontext: direkt # nach "Beatmungsbeutel Kind" - Nutzer bestätigt Größe S, # andere Rucksäcke führen Beatmungsmaske S/M/L einzeln) (53, 151), # "Rollenpflaster gross" / "Rollenpflaster" (Rettungsrucksack + # PAX SEG nutzten in der Original-Excel unspezifiziertes # "Rollenpflaster" ohne Größe - Nutzer-Entscheidung: Default # auf "gross", keine Evidenz für schmal/breit) (89, 91), # "Blutdruckmanschette" / "Blutdruckmanschette ALT" (2026-09-05 # Nachkontrolle) - Nutzer-Entscheidung: gleiches Material, # "ALT" war keine bewusste Sortenunterscheidung ] # Tabellen mit FK auf material.id (Stand 2026-09-04, siehe alembic-Migrationen). _EINFACHE_TABELLEN = ["kontrollposition", "fehlbestand", "nachfuellung"] _UNIQUE_TABELLEN = [ ("vorlagenposition", "vorlage_id"), ("objektposition", "objekt_id"), ] async def _merge(conn, keep: int, drop: int) -> None: for tabelle, gruppen_spalte in _UNIQUE_TABELLEN: # UNIQUE(gruppen_spalte, material_id): existiert die Zielkombination # schon, ist die Dublettenzeile ein echter Konflikt -> löschen statt # umzuhängen (sonst UniqueViolation). await conn.execute( text( f"DELETE FROM {tabelle} t_drop WHERE t_drop.material_id = :drop " f"AND EXISTS (SELECT 1 FROM {tabelle} t_keep " f"WHERE t_keep.{gruppen_spalte} = t_drop.{gruppen_spalte} AND t_keep.material_id = :keep)" ), {"drop": drop, "keep": keep}, ) await conn.execute( text(f"UPDATE {tabelle} SET material_id = :keep WHERE material_id = :drop"), {"keep": keep, "drop": drop}, ) for tabelle in _EINFACHE_TABELLEN: await conn.execute( text(f"UPDATE {tabelle} SET material_id = :keep WHERE material_id = :drop"), {"keep": keep, "drop": drop}, ) await conn.execute(text("DELETE FROM material WHERE id = :drop"), {"drop": drop}) async def main() -> None: async with engine.begin() as conn: for keep, drop in DUBLETTEN_PAARE: await _merge(conn, keep, drop) print(f"gemerged: {drop} -> {keep}") # Datentyp-Fehler: SN stand im Namen statt in einem Geräte-Feld. await conn.execute( text("UPDATE material SET name = 'Manometer', materialtyp = 'geraet_sn' WHERE id = 148") ) r = await conn.execute(text("SELECT id FROM objektposition WHERE material_id = 148")) for (pos_id,) in r.all(): await conn.execute( text( "INSERT INTO geraet_instanz (objektposition_id, seriennummer) " "VALUES (:pos_id, '1308409') " "ON CONFLICT (objektposition_id, seriennummer) DO NOTHING" ), {"pos_id": pos_id}, ) print("Material 148: 'Manometer SN 1308409' -> 'Manometer' (geraet_sn) + geraet_instanz SN 1308409") result = await conn.execute(text("SELECT count(*) FROM material")) print(f"\nMaterialien danach: {result.scalar_one()}") if __name__ == "__main__": asyncio.run(main())