fix: 43 verwaiste Suffix-Vorlagen in Ketten zusammengeführt
- fix_orphan_chains.py: Findet Suffix-Dokumente (z.B. 0010/2025-1) ohne Kette - Matcht automatisch mit Basis-Vorlage (0010/2025) → erstellt Kette - 43 neue Ketten, 5 bestehende erweitert, 3 übersprungen (Basis fehlt) - Status-Engine auf alle neuen Ketten angewendet - Beispiel: Rüggeweg 0010/2025 + 0010/2025-1 → Kette 6313
This commit is contained in:
parent
77371fafc9
commit
401cd3acb0
187
scripts/fix_orphan_chains.py
Normal file
187
scripts/fix_orphan_chains.py
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fix orphaned suffix documents: match to base and create/extend chains.
|
||||||
|
|
||||||
|
Finds suffix documents (e.g. 0010/2025-1) not in any chain,
|
||||||
|
looks up their base document (0010/2025), and creates or extends chains.
|
||||||
|
"""
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def fix(db_path: str):
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
conn.execute("PRAGMA journal_mode = WAL")
|
||||||
|
conn.execute("PRAGMA foreign_keys = ON")
|
||||||
|
|
||||||
|
# Find suffix documents not in any chain
|
||||||
|
orphans = conn.execute("""
|
||||||
|
SELECT v.id, v.aktenzeichen, v.aktenzeichen_basis, v.aktenzeichen_suffix,
|
||||||
|
v.typ, v.datum_eingang
|
||||||
|
FROM vorlagen v
|
||||||
|
WHERE v.aktenzeichen_suffix IS NOT NULL
|
||||||
|
AND v.id NOT IN (SELECT vorlage_id FROM ketten_glieder)
|
||||||
|
ORDER BY v.aktenzeichen_basis, v.aktenzeichen_suffix
|
||||||
|
""").fetchall()
|
||||||
|
|
||||||
|
print(f"🔍 {len(orphans)} Suffix-Vorlagen ohne Kette gefunden")
|
||||||
|
|
||||||
|
created = 0
|
||||||
|
extended = 0
|
||||||
|
skipped = 0
|
||||||
|
|
||||||
|
for orphan in orphans:
|
||||||
|
basis_az = orphan["aktenzeichen_basis"]
|
||||||
|
|
||||||
|
# Find base document
|
||||||
|
basis = conn.execute(
|
||||||
|
"SELECT id, aktenzeichen, typ, betreff, datum_eingang FROM vorlagen "
|
||||||
|
"WHERE aktenzeichen_basis = ? AND (aktenzeichen_suffix IS NULL OR aktenzeichen_suffix = '')",
|
||||||
|
(basis_az,)
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if not basis:
|
||||||
|
# No base document found - skip
|
||||||
|
print(f" ⚠️ {orphan['aktenzeichen']}: Basis {basis_az} nicht gefunden — übersprungen")
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Is the base already in a chain?
|
||||||
|
existing_chain = conn.execute(
|
||||||
|
"SELECT kette_id FROM ketten_glieder WHERE vorlage_id = ?",
|
||||||
|
(basis["id"],)
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if existing_chain:
|
||||||
|
# Extend existing chain
|
||||||
|
kette_id = existing_chain["kette_id"]
|
||||||
|
max_pos = conn.execute(
|
||||||
|
"SELECT MAX(position) as mp FROM ketten_glieder WHERE kette_id = ?",
|
||||||
|
(kette_id,)
|
||||||
|
).fetchone()["mp"] or 0
|
||||||
|
|
||||||
|
# Check suffix isn't already in this chain
|
||||||
|
already = conn.execute(
|
||||||
|
"SELECT 1 FROM ketten_glieder WHERE kette_id = ? AND vorlage_id = ?",
|
||||||
|
(kette_id, orphan["id"])
|
||||||
|
).fetchone()
|
||||||
|
if already:
|
||||||
|
continue
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO ketten_glieder (kette_id, vorlage_id, position, rolle) VALUES (?, ?, ?, ?)",
|
||||||
|
(kette_id, orphan["id"], max_pos + 1, _rolle(orphan["typ"]))
|
||||||
|
)
|
||||||
|
extended += 1
|
||||||
|
print(f" ➕ {orphan['aktenzeichen']} → Kette {kette_id} (Position {max_pos + 1})")
|
||||||
|
else:
|
||||||
|
# Create new chain with base + suffix
|
||||||
|
# Determine chain type from base
|
||||||
|
typ = _chain_type(basis["typ"])
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO ketten (ursprung_id, typ, thema, status, begruendung) VALUES (?, ?, ?, ?, ?)",
|
||||||
|
(basis["id"], typ, basis["betreff"], "in_beratung",
|
||||||
|
f"Automatisch erstellt: {basis['aktenzeichen']} + Suffix-Dokument(e)")
|
||||||
|
)
|
||||||
|
kette_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
|
||||||
|
|
||||||
|
# Add base as position 0
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO ketten_glieder (kette_id, vorlage_id, position, rolle) VALUES (?, ?, ?, ?)",
|
||||||
|
(kette_id, basis["id"], 0, "ursprung")
|
||||||
|
)
|
||||||
|
# Add suffix as position 1
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO ketten_glieder (kette_id, vorlage_id, position, rolle) VALUES (?, ?, ?, ?)",
|
||||||
|
(kette_id, orphan["id"], 1, _rolle(orphan["typ"]))
|
||||||
|
)
|
||||||
|
created += 1
|
||||||
|
print(f" 🆕 Kette {kette_id}: {basis['aktenzeichen']} + {orphan['aktenzeichen']}")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# Also find base documents whose suffixes ARE in chains but the base itself isn't
|
||||||
|
base_orphans = conn.execute("""
|
||||||
|
SELECT v.id, v.aktenzeichen, v.aktenzeichen_basis
|
||||||
|
FROM vorlagen v
|
||||||
|
WHERE v.aktenzeichen_suffix IS NULL
|
||||||
|
AND v.aktenzeichen_basis IN (
|
||||||
|
SELECT DISTINCT v2.aktenzeichen_basis
|
||||||
|
FROM vorlagen v2
|
||||||
|
JOIN ketten_glieder kg ON kg.vorlage_id = v2.id
|
||||||
|
WHERE v2.aktenzeichen_suffix IS NOT NULL
|
||||||
|
)
|
||||||
|
AND v.id NOT IN (SELECT vorlage_id FROM ketten_glieder)
|
||||||
|
""").fetchall()
|
||||||
|
|
||||||
|
base_added = 0
|
||||||
|
for bv in base_orphans:
|
||||||
|
# Find which chain the suffix is in
|
||||||
|
chain = conn.execute("""
|
||||||
|
SELECT kg.kette_id FROM ketten_glieder kg
|
||||||
|
JOIN vorlagen v ON kg.vorlage_id = v.id
|
||||||
|
WHERE v.aktenzeichen_basis = ? AND v.aktenzeichen_suffix IS NOT NULL
|
||||||
|
LIMIT 1
|
||||||
|
""", (bv["aktenzeichen_basis"],)).fetchone()
|
||||||
|
|
||||||
|
if chain:
|
||||||
|
already = conn.execute(
|
||||||
|
"SELECT 1 FROM ketten_glieder WHERE kette_id = ? AND vorlage_id = ?",
|
||||||
|
(chain["kette_id"], bv["id"])
|
||||||
|
).fetchone()
|
||||||
|
if not already:
|
||||||
|
# Insert base at position 0, shift others up
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE ketten_glieder SET position = position + 1 WHERE kette_id = ?",
|
||||||
|
(chain["kette_id"],)
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO ketten_glieder (kette_id, vorlage_id, position, rolle) VALUES (?, ?, 0, 'ursprung')",
|
||||||
|
(chain["kette_id"], bv["id"])
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE ketten SET ursprung_id = ? WHERE id = ?",
|
||||||
|
(bv["id"], chain["kette_id"])
|
||||||
|
)
|
||||||
|
base_added += 1
|
||||||
|
print(f" 📎 Basis {bv['aktenzeichen']} → Kette {chain['kette_id']} (Position 0)")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
print(f"\n✅ Fertig:")
|
||||||
|
print(f" {created} neue Ketten erstellt")
|
||||||
|
print(f" {extended} Ketten erweitert")
|
||||||
|
print(f" {base_added} Basis-Dokumente nachgetragen")
|
||||||
|
print(f" {skipped} übersprungen (Basis nicht gefunden)")
|
||||||
|
|
||||||
|
# Run status engine on new chains
|
||||||
|
new_chain_count = created + extended + base_added
|
||||||
|
if new_chain_count > 0:
|
||||||
|
print(f"\n🔄 Status-Engine für betroffene Ketten wird NICHT automatisch ausgeführt.")
|
||||||
|
print(f" Bitte manuell: python -c \"from tracker.core.status import update_all_chains; ...\"")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _rolle(typ):
|
||||||
|
if typ in ("stellungnahme", "bericht"):
|
||||||
|
return "antwort"
|
||||||
|
return "folge"
|
||||||
|
|
||||||
|
|
||||||
|
def _chain_type(typ):
|
||||||
|
if typ == "antrag":
|
||||||
|
return "antrag"
|
||||||
|
elif typ == "anfrage":
|
||||||
|
return "anfrage"
|
||||||
|
return "sonstig"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
db = sys.argv[1] if len(sys.argv) > 1 else str(
|
||||||
|
Path(__file__).resolve().parents[1] / "data" / "tracker.db"
|
||||||
|
)
|
||||||
|
print(f"🔧 Orphan-Chain-Fix: {db}")
|
||||||
|
fix(db)
|
||||||
Loading…
Reference in New Issue
Block a user