Damit der Altbestand zum neuen Verhalten passt: Jeder Uebertrag wird zu einer Ausgabe beim Quellkonto und einer Einnahme beim Zielkonto, beide mit der neutralen Kategorie. Die Salden bleiben dabei exakt gleich, nur die Art aendert sich. Ohne --anwenden nur ein Bericht; vor dem Schreiben wird die Datenbank kopiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
111 lines
4.4 KiB
Python
Executable File
111 lines
4.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""Teilt bestehende Überträge in zwei gewöhnliche Buchungen auf.
|
||
|
||
Certain verbindet Konten nicht mehr selbst. Damit die Salden trotzdem exakt so
|
||
bleiben wie bisher, wird jeder Übertrag zu genau dem, was in den Auszügen steht:
|
||
einer Ausgabe beim Quellkonto und einer Einnahme beim Zielkonto. Beide bekommen
|
||
die neutrale Kategorie, zählen also nicht in die Statistik.
|
||
|
||
Ohne ``--anwenden`` wird nur berichtet. Vor dem Schreiben legt das Skript eine
|
||
Kopie der Datenbank an.
|
||
|
||
Aufruf: teile_uebertraege.py [--anwenden] [db]
|
||
"""
|
||
import shutil
|
||
import sqlite3
|
||
import sys
|
||
|
||
argv = sys.argv[1:]
|
||
ANWENDEN = "--anwenden" in argv
|
||
rest = [x for x in argv if not x.startswith("--")]
|
||
DB = rest[0] if rest else "/var/lib/certain-finance/app.db"
|
||
|
||
con = sqlite3.connect(DB)
|
||
con.row_factory = sqlite3.Row
|
||
konten = {r["id"]: r for r in con.execute("SELECT id, name, currency FROM accounts")}
|
||
|
||
|
||
def geld(c):
|
||
return f"{(c or 0) / 100:,.2f}".replace(",", "'")
|
||
|
||
|
||
def neutrale_kategorie(owner_id: int) -> int | None:
|
||
"""Die Kategorie, die aus Auswertungen herausfällt („Kontoübertrag")."""
|
||
r = con.execute(
|
||
"SELECT id FROM categories WHERE owner_id = ? AND excluded_from_stats = 1 "
|
||
"ORDER BY (name LIKE '%bertrag%') DESC, id LIMIT 1", (owner_id,)).fetchone()
|
||
return r["id"] if r else None
|
||
|
||
|
||
def eur_cents(betrag: int, waehrung: str, tag: str, ersatz: int) -> int:
|
||
"""Betrag in EUR – für die Statistik. Ohne Kurs bleibt der bisherige Wert."""
|
||
if waehrung.upper() == "EUR":
|
||
return betrag
|
||
r = con.execute(
|
||
"SELECT rate_per_eur FROM exchange_rates WHERE currency = ? AND date <= ? "
|
||
"ORDER BY date DESC LIMIT 1", (waehrung.upper(), tag)).fetchone()
|
||
if not r or not r["rate_per_eur"]:
|
||
return ersatz
|
||
return round(betrag / float(r["rate_per_eur"]))
|
||
|
||
|
||
uebertraege = con.execute(
|
||
"SELECT * FROM transactions WHERE type = 'transfer' AND to_account_id IS NOT NULL"
|
||
).fetchall()
|
||
print(f"Datenbank: {DB}\n{len(uebertraege)} Überträge gefunden\n")
|
||
if not uebertraege:
|
||
raise SystemExit(0)
|
||
|
||
je_paar: dict[tuple[int, int], list] = {}
|
||
for t in uebertraege:
|
||
je_paar.setdefault((t["account_id"], t["to_account_id"]), []).append(t)
|
||
print(f"{'von':<24}{'nach':<24}{'Anz':>5}{'Summe':>13}")
|
||
print("-" * 66)
|
||
for (a, b), liste in sorted(je_paar.items(), key=lambda kv: -len(kv[1])):
|
||
na = konten[a]["name"] if a in konten else f"?{a}"
|
||
nb = konten[b]["name"] if b in konten else f"?{b}"
|
||
print(f"{na[:23]:<24}{nb[:23]:<24}{len(liste):>5}"
|
||
f"{geld(sum(x['amount_cents'] for x in liste)):>13}")
|
||
|
||
fehlende = {t["owner_id"] for t in uebertraege if neutrale_kategorie(t["owner_id"]) is None}
|
||
if fehlende:
|
||
print("\nACHTUNG: Es gibt keine Kategorie mit „nicht in Auswertungen einrechnen“.")
|
||
print("Lege eine an (z. B. „Kontoübertrag“) – sonst bleiben die Buchungen ohne Kategorie")
|
||
print("und tauchen in der Statistik auf.")
|
||
|
||
if not ANWENDEN:
|
||
print("\nNichts geändert (Probelauf). Mit --anwenden ausführen.")
|
||
raise SystemExit(0)
|
||
|
||
sicherung = DB + ".vor-aufteilen"
|
||
shutil.copy(DB, sicherung)
|
||
print(f"\nSicherung: {sicherung}")
|
||
|
||
neu = 0
|
||
for t in uebertraege:
|
||
kat = neutrale_kategorie(t["owner_id"])
|
||
ziel = konten.get(t["to_account_id"])
|
||
gutschrift = t["to_amount_cents"] if t["to_amount_cents"] is not None else t["amount_cents"]
|
||
|
||
# Abgang bleibt beim Quellkonto – als Ausgabe.
|
||
con.execute(
|
||
"UPDATE transactions SET type='expense', to_account_id=NULL, to_amount_cents=NULL, "
|
||
"category_id=COALESCE(category_id, ?) WHERE id = ?", (kat, t["id"]))
|
||
|
||
# Zugang wird eine eigene Einnahme beim Zielkonto.
|
||
if ziel is not None:
|
||
con.execute(
|
||
"INSERT INTO transactions (owner_id, type, date, time, account_id, amount_cents, "
|
||
"amount_eur_cents, category_id, payee, description, notes, import_batch_id, "
|
||
"created_at, updated_at) VALUES (?, 'income', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||
(t["owner_id"], t["date"], t["time"], ziel["id"], gutschrift,
|
||
eur_cents(gutschrift, ziel["currency"], t["date"], t["amount_eur_cents"]),
|
||
kat, t["payee"], t["description"], t["notes"], t["import_batch_id"],
|
||
t["created_at"], t["updated_at"]))
|
||
neu += 1
|
||
|
||
con.commit()
|
||
print(f"{len(uebertraege)} Überträge zu Ausgaben gemacht, {neu} Einnahmen ergänzt")
|
||
print("Die Salden bleiben unverändert. Dienst neu starten:")
|
||
print(" sudo systemctl restart certain-finance")
|