antragstracker/backend/src/tracker/core/chains.py

153 lines
5.4 KiB
Python
Raw Normal View History

"""Ketten-Builder: groups Vorlagen into chains based on Aktenzeichen-Suffix references."""
import sqlite3
from collections import defaultdict
from tracker.core.status import compute_status
def build_suffix_references(conn: sqlite3.Connection) -> int:
"""Create referenzen entries for Aktenzeichen-Suffix relations.
E.g. 0362/2025-1 references 0362/2025 via suffix relation.
Returns the number of new references created.
"""
cursor = conn.execute("""
INSERT OR IGNORE INTO referenzen (quelle_id, ziel_id, typ, konfidenz)
SELECT child.id, parent.id, 'suffix', 1.0
FROM vorlagen child
JOIN vorlagen parent ON child.aktenzeichen_basis = parent.aktenzeichen_basis
WHERE child.aktenzeichen_suffix IS NOT NULL
AND parent.aktenzeichen_suffix IS NULL
AND child.id != parent.id
""")
# Also link sequential suffixes: -2 -> -1, -3 -> -2, etc.
conn.execute("""
INSERT OR IGNORE INTO referenzen (quelle_id, ziel_id, typ, konfidenz)
SELECT later.id, earlier.id, 'suffix', 1.0
FROM vorlagen later
JOIN vorlagen earlier
ON later.aktenzeichen_basis = earlier.aktenzeichen_basis
AND later.aktenzeichen_suffix IS NOT NULL
AND earlier.aktenzeichen_suffix IS NOT NULL
AND CAST(REPLACE(later.aktenzeichen_suffix, '-', '') AS INTEGER)
= CAST(REPLACE(earlier.aktenzeichen_suffix, '-', '') AS INTEGER) + 1
WHERE later.id != earlier.id
""")
conn.commit()
return cursor.rowcount
def build_chains(conn: sqlite3.Connection) -> int:
"""Build ketten from Vorlagen that share the same aktenzeichen_basis.
A chain's Ursprung is the Vorlage without suffix (the original).
Chain members are ordered by suffix number.
Returns the number of chains created/updated.
"""
# Find all aktenzeichen_basis values that have at least one entry
# and where the base vorlage is an antrag, anfrage, or stellungnahme
rows = conn.execute("""
SELECT aktenzeichen_basis, COUNT(*) as cnt
FROM vorlagen
WHERE aktenzeichen_basis IS NOT NULL
GROUP BY aktenzeichen_basis
HAVING cnt >= 1
""").fetchall()
count = 0
for row in rows:
basis = row["aktenzeichen_basis"]
# Get all Vorlagen in this chain, ordered by suffix
members = conn.execute("""
SELECT id, aktenzeichen, aktenzeichen_suffix, typ, datum_eingang, betreff
FROM vorlagen
WHERE aktenzeichen_basis = ?
ORDER BY
CASE WHEN aktenzeichen_suffix IS NULL THEN 0
ELSE CAST(REPLACE(aktenzeichen_suffix, '-', '') AS INTEGER)
END
""", (basis,)).fetchall()
if not members:
continue
ursprung = members[0]
# Only create chains for antrag/anfrage types (the base should be one)
chain_typ = ursprung["typ"]
if chain_typ not in ("antrag", "anfrage"):
continue
# Compute status
status_info = compute_status(conn, ursprung["id"], chain_typ, members)
# Determine letzte_aktivitaet
dates = [m["datum_eingang"] for m in members if m["datum_eingang"]]
letzte_aktivitaet = max(dates) if dates else ursprung["datum_eingang"]
# Check if chain already exists
existing = conn.execute(
"SELECT id FROM ketten WHERE ursprung_id = ?", (ursprung["id"],)
).fetchone()
if existing:
kette_id = existing["id"]
conn.execute("""
UPDATE ketten
SET typ = ?, thema = ?, status = ?, status_seit = ?,
letzte_aktivitaet = ?, vertagungen_count = ?
WHERE id = ?
""", (
chain_typ,
ursprung["betreff"],
status_info["status"],
status_info.get("status_seit"),
letzte_aktivitaet,
status_info.get("vertagungen_count", 0),
kette_id,
))
conn.execute("DELETE FROM ketten_glieder WHERE kette_id = ?", (kette_id,))
else:
cursor = conn.execute("""
INSERT INTO ketten (ursprung_id, typ, thema, status, status_seit,
letzte_aktivitaet, vertagungen_count)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (
ursprung["id"],
chain_typ,
ursprung["betreff"],
status_info["status"],
status_info.get("status_seit"),
letzte_aktivitaet,
status_info.get("vertagungen_count", 0),
))
kette_id = cursor.lastrowid
# Insert chain members
for pos, member in enumerate(members):
rolle = _determine_rolle(member, pos)
conn.execute("""
INSERT OR REPLACE INTO ketten_glieder (kette_id, vorlage_id, position, rolle)
VALUES (?, ?, ?, ?)
""", (kette_id, member["id"], pos, rolle))
count += 1
conn.commit()
return count
def _determine_rolle(member: sqlite3.Row, position: int) -> str:
if position == 0:
return "ursprung"
typ = member["typ"]
if typ == "stellungnahme":
return "stellungnahme"
if typ == "bericht":
return "bericht"
if typ in ("antrag", "anfrage"):
return "aenderung"
return "ergaenzung"