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:
Dotty Dotter 2026-04-01 14:07:01 +02:00
parent 77371fafc9
commit 401cd3acb0

View 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)