- 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
188 lines
6.8 KiB
Python
188 lines
6.8 KiB
Python
#!/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)
|