#!/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)