diff --git a/scripts/fix_orphan_chains.py b/scripts/fix_orphan_chains.py new file mode 100644 index 0000000..3e5f8eb --- /dev/null +++ b/scripts/fix_orphan_chains.py @@ -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)