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:
@@ -3225,3 +3225,147 @@ Keine Commits in dieser Session.
|
|||||||
- backend/tests/test_objekte.py | 131 +++++++++++++++++++++++++++
|
- backend/tests/test_objekte.py | 131 +++++++++++++++++++++++++++
|
||||||
|
|
||||||
---
|
---
|
||||||
|
## 2026-09-04 20:54 – 20:55 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** backend
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
- 6934276 Karte 14 Schritt 2 (Admin-Teil) + Frontend für Anhänger-Kopplung
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 34 ++++
|
||||||
|
- backend/DEVLOG.md | 17 ++
|
||||||
|
- backend/app/api/v1/api.py | 2 +
|
||||||
|
- backend/app/api/v1/endpoints/geraet_instanz.py | 91 +++++++++++
|
||||||
|
- backend/app/api/v1/endpoints/objekte.py | 4 +
|
||||||
|
- backend/app/schemas/geraet_instanz.py | 31 ++++
|
||||||
|
- backend/app/schemas/objekt.py | 2 +
|
||||||
|
- backend/app/services/geraet_instanz.py | 130 +++++++++++++++
|
||||||
|
- backend/tests/test_geraet_instanz.py | 90 +++++++++++
|
||||||
|
- frontend/e2e/mockApi.ts | 3 +
|
||||||
|
- frontend/src/api/types.ts | 13 ++
|
||||||
|
- frontend/src/pages/admin/GeraeteInstanzenListe.tsx | 177 +++++++++++++++++++++
|
||||||
|
- frontend/src/pages/admin/ObjektPositionenPanel.tsx | 27 +++-
|
||||||
|
- frontend/src/pages/admin/ObjektSection.tsx | 71 +++++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-09-04 20:55 – 20:55 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** asb-material
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 34 ++++
|
||||||
|
- backend/DEVLOG.md | 17 ++
|
||||||
|
- backend/app/api/v1/api.py | 2 +
|
||||||
|
- backend/app/api/v1/endpoints/geraet_instanz.py | 91 +++++++++++
|
||||||
|
- backend/app/api/v1/endpoints/objekte.py | 4 +
|
||||||
|
- backend/app/schemas/geraet_instanz.py | 31 ++++
|
||||||
|
- backend/app/schemas/objekt.py | 2 +
|
||||||
|
- backend/app/services/geraet_instanz.py | 130 +++++++++++++++
|
||||||
|
- backend/tests/test_geraet_instanz.py | 90 +++++++++++
|
||||||
|
- frontend/e2e/mockApi.ts | 3 +
|
||||||
|
- frontend/src/api/types.ts | 13 ++
|
||||||
|
- frontend/src/pages/admin/GeraeteInstanzenListe.tsx | 177 +++++++++++++++++++++
|
||||||
|
- frontend/src/pages/admin/ObjektPositionenPanel.tsx | 27 +++-
|
||||||
|
- frontend/src/pages/admin/ObjektSection.tsx | 71 +++++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-09-04 20:59 – 20:59 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** asb-material
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 34 +++++++++++++
|
||||||
|
- backend/DEVLOG.md | 17 +++++++
|
||||||
|
- backend/app/api/v1/api.py | 2 +
|
||||||
|
- backend/app/api/v1/endpoints/geraet_instanz.py | 91 ++++++++++++++++++++++++++++++++++
|
||||||
|
- backend/app/api/v1/endpoints/objekte.py | 4 ++
|
||||||
|
- backend/app/schemas/geraet_instanz.py | 31 ++++++++++++
|
||||||
|
- backend/app/schemas/objekt.py | 2 +
|
||||||
|
- backend/app/services/geraet_instanz.py | 130 +++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
- backend/tests/test_geraet_instanz.py | 90 ++++++++++++++++++++++++++++++++++
|
||||||
|
- frontend/e2e/mockApi.ts | 3 ++
|
||||||
|
- frontend/src/api/types.ts | 13 +++++
|
||||||
|
- frontend/src/pages/admin/GeraeteInstanzenListe.tsx | 177 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
- frontend/src/pages/admin/ObjektPositionenPanel.tsx | 27 ++++++++---
|
||||||
|
- frontend/src/pages/admin/ObjektSection.tsx | 71 +++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-09-04 21:00 – 21:00 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** asb-material
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 34 +++++++++++++
|
||||||
|
- backend/DEVLOG.md | 17 +++++++
|
||||||
|
- backend/app/api/v1/api.py | 2 +
|
||||||
|
- backend/app/api/v1/endpoints/geraet_instanz.py | 91 ++++++++++++++++++++++++++++++++++
|
||||||
|
- backend/app/api/v1/endpoints/objekte.py | 4 ++
|
||||||
|
- backend/app/schemas/geraet_instanz.py | 31 ++++++++++++
|
||||||
|
- backend/app/schemas/objekt.py | 2 +
|
||||||
|
- backend/app/services/geraet_instanz.py | 130 +++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
- backend/tests/test_geraet_instanz.py | 90 ++++++++++++++++++++++++++++++++++
|
||||||
|
- frontend/e2e/mockApi.ts | 3 ++
|
||||||
|
- frontend/src/api/types.ts | 13 +++++
|
||||||
|
- frontend/src/pages/admin/GeraeteInstanzenListe.tsx | 177 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
- frontend/src/pages/admin/ObjektPositionenPanel.tsx | 27 ++++++++---
|
||||||
|
- frontend/src/pages/admin/ObjektSection.tsx | 71 +++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-09-04 21:00 – 21:09 (8m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** asb-material
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 34 +++++++++++++
|
||||||
|
- backend/DEVLOG.md | 17 +++++++
|
||||||
|
- backend/app/api/v1/api.py | 2 +
|
||||||
|
- backend/app/api/v1/endpoints/geraet_instanz.py | 91 ++++++++++++++++++++++++++++++++++
|
||||||
|
- backend/app/api/v1/endpoints/objekte.py | 4 ++
|
||||||
|
- backend/app/schemas/geraet_instanz.py | 31 ++++++++++++
|
||||||
|
- backend/app/schemas/objekt.py | 2 +
|
||||||
|
- backend/app/services/geraet_instanz.py | 130 +++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
- backend/tests/test_geraet_instanz.py | 90 ++++++++++++++++++++++++++++++++++
|
||||||
|
- frontend/e2e/mockApi.ts | 3 ++
|
||||||
|
- frontend/src/api/types.ts | 13 +++++
|
||||||
|
- frontend/src/pages/admin/GeraeteInstanzenListe.tsx | 177 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
- frontend/src/pages/admin/ObjektPositionenPanel.tsx | 27 ++++++++---
|
||||||
|
- frontend/src/pages/admin/ObjektSection.tsx | 71 +++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-09-04 21:10 – 21:22 (12m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** asb-material
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 34 +++++++++++++
|
||||||
|
- backend/DEVLOG.md | 17 +++++++
|
||||||
|
- backend/app/api/v1/api.py | 2 +
|
||||||
|
- backend/app/api/v1/endpoints/geraet_instanz.py | 91 ++++++++++++++++++++++++++++++++++
|
||||||
|
- backend/app/api/v1/endpoints/objekte.py | 4 ++
|
||||||
|
- backend/app/schemas/geraet_instanz.py | 31 ++++++++++++
|
||||||
|
- backend/app/schemas/objekt.py | 2 +
|
||||||
|
- backend/app/services/geraet_instanz.py | 130 +++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
- backend/tests/test_geraet_instanz.py | 90 ++++++++++++++++++++++++++++++++++
|
||||||
|
- frontend/e2e/mockApi.ts | 3 ++
|
||||||
|
- frontend/src/api/types.ts | 13 +++++
|
||||||
|
- frontend/src/pages/admin/GeraeteInstanzenListe.tsx | 177 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
- frontend/src/pages/admin/ObjektPositionenPanel.tsx | 27 ++++++++---
|
||||||
|
- frontend/src/pages/admin/ObjektSection.tsx | 71 +++++++++++++++++++++++++++
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Materialstamm-Bereinigung – Log
|
||||||
|
|
||||||
|
Laufender Log für Bereinigungsläufe gegen den Materialstamm (Dubletten aus dem
|
||||||
|
Excel-Import, Datentyp-Fehler). Skripte liegen unter
|
||||||
|
`backend/scripts/material_bereinigung_<datum>.py`, jeweils gegen die Live-DB
|
||||||
|
auf 192.168.1.238 ausgeführt (nicht Teil der Alembic-Migrationskette, da reine
|
||||||
|
Datenbereinigung, kein Schema-Wechsel).
|
||||||
|
|
||||||
|
## 2026-09-04
|
||||||
|
|
||||||
|
Ausgangslage: 172 Materialien, per Fuzzy-Matching (Normalisierung + `difflib`)
|
||||||
|
auf Dubletten/Tippfehler geprüft. Skript: `material_bereinigung_2026-09-04.py`.
|
||||||
|
|
||||||
|
### Zusammengelegt (17 Paare, 172 → 155 Materialien)
|
||||||
|
|
||||||
|
| Behalten | Entfernt | Grund |
|
||||||
|
|---|---|---|
|
||||||
|
| E 153 | E153 | Leerzeichen-Variante |
|
||||||
|
| Rettungsdecke | Rettungsdecke (Leerzeichen) | Leerzeichen-Variante |
|
||||||
|
| Verbandspäckchen klein | Verbandspäckchen klein (Leerzeichen) | Leerzeichen-Variante |
|
||||||
|
| Thomasholder Kind | Thomas Holder Kind | Schreibweise |
|
||||||
|
| Samsplint | Samsplint (Leerzeichen) | Leerzeichen-Variante |
|
||||||
|
| Rollenpflaster | Rollenpflaster (Leerzeichen) | Leerzeichen-Variante |
|
||||||
|
| Stethoskop | Stetoskop | Tippfehler |
|
||||||
|
| Pupillenleuchte | Puppillenleuchte | Tippfehler |
|
||||||
|
| Aluderm Strips 1,9x7,2 | Aluderm Stripes 1,9x7,2 | Tippfehler |
|
||||||
|
| Farbkodierte Blockerspritze | Farbcodierte Blockerspritze | Schreibvariante |
|
||||||
|
| Infusionssystem | Infussionssystem | Tippfehler |
|
||||||
|
| Verbandspäckchen Groß | Verbandspäckchen gross | Schreibvariante |
|
||||||
|
| Wundschnellverband 10x6 a 8 | Wundschnelölverband 10x6 | Tippfehler |
|
||||||
|
| Handschuhe | Handschuh lose | Nutzer-Entscheidung: gleiches Material |
|
||||||
|
| Schlingentupfer | Schlinggazetupfer | Nutzer-Entscheidung: gleiches Material |
|
||||||
|
| Verbandtuch 60x80 | Verbandstuch 160x80 | Nutzer-Entscheidung: Tippfehler ("1" zu viel) |
|
||||||
|
| Elastische Binde | Elastische Binden | Nutzer-Entscheidung: reiner Plural-Unterschied |
|
||||||
|
|
||||||
|
Ein echter Konflikt trat bei "Aluderm Strips/Stripes" auf: beide Varianten
|
||||||
|
waren bereits unabhängig in denselben Vorlagen (1, 4) bzw. demselben Objekt
|
||||||
|
(1) verwendet – die Dublettenzeile wurde dort gelöscht statt umgehängt (siehe
|
||||||
|
`_merge()`-Logik im Skript, UNIQUE-Constraint-Konfliktbehandlung).
|
||||||
|
|
||||||
|
### Datentyp-Fehler korrigiert
|
||||||
|
|
||||||
|
- **Material 148**: Name war `"Manometer SN 1308409"` (Seriennummer im
|
||||||
|
Namensfeld statt in einem Geräte-Feld), `materialtyp='ablauf_charge'`.
|
||||||
|
Korrigiert zu Name `"Manometer"`, `materialtyp='geraet_sn'`, SN `1308409`
|
||||||
|
als erste `geraet_instanz`-Zeile an der betroffenen Objektposition angelegt
|
||||||
|
(Karte 14).
|
||||||
|
|
||||||
|
### Bewusst NICHT verändert (Nutzer-Entscheidung)
|
||||||
|
|
||||||
|
- **151 "Rollenpflaster"** (ohne Größenangabe, neben 53 "gross", 106
|
||||||
|
"schmal", 107 "breit"): wird in 2 Vorlagen/Objekten (Rucksack PAX SEG,
|
||||||
|
Rettungsrucksack) genutzt, keine Größe aus dem Kontext ableitbar –
|
||||||
|
bleibt eigenständig, bis geklärt ist, welcher Größe die bestehenden
|
||||||
|
Verwendungen entsprechen.
|
||||||
|
- **102 "Beatmunsmaske"** (Tippfehler, neben 161/162/163 "Beatmungsmasken
|
||||||
|
S/M/L"): wird in Vorlage/Objekt/Kontroll-Historie genutzt (Rettungsrucksack
|
||||||
|
Standard), aber unklar welcher Größe die bestehende Verwendung entspricht –
|
||||||
|
Nutzer-Feedback: "Schreibfehler, müssen wir uns nochmal anschauen". Noch
|
||||||
|
offen, bei nächster Gelegenheit klären und nachziehen.
|
||||||
|
|
||||||
|
### Bewusst als eigenständig bestätigt (kein Fund, nur Ähnlichkeits-Rauschen)
|
||||||
|
|
||||||
|
Güdeltubus/Laryntubus Gr. 2–5, Flexüle-Farbvarianten, Spritzen-Größen (2er/5er/
|
||||||
|
10er/20er), Endotubus-Größen (3,0–8,0), "Größe 1/3/4/5" – alles legitime,
|
||||||
|
unterschiedliche Produktvarianten, keine Dubletten.
|
||||||
Reference in New Issue
Block a user