Materialstamm-Bereinigung 2026-09-04: 17 Dubletten zusammengelegt
Live-Datenbereinigung (172 -> 155 Materialien): Excel-Import-Tippfehler und Leerzeichen-Varianten zusammengelegt, Material 148 "Manometer SN 1308409" korrigiert (Name -> "Manometer", materialtyp -> geraet_sn, SN als erste geraet_instanz-Zeile). Skript und Entscheidungsprotokoll (inkl. bewusst unverändert gelassener Fälle) dokumentiert für Nachvollziehbarkeit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
"""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"
|
||||
]
|
||||
|
||||
# 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())
|
||||
Reference in New Issue
Block a user