fix: neutrale Kategorien gelten für Ausgabe UND Einnahme

Kategorien erben ihre Art von der Gruppe, und die ist entweder Ausgabe oder
Einnahme. Für einen Kontoübertrag passt beides nicht: Auf dem einen Konto geht
Geld raus, auf dem anderen kommt es rein – man musste also zwei Kategorien in
zwei Gruppen anlegen.

Als „nicht in Auswertungen einrechnen" markierte Kategorien werden jetzt in
beiden Richtungen angeboten (Buchungsformular und Import-Prüfansicht), und ihre
Schlagwörter greifen unabhängig vom Vorzeichen. Eine Kategorie „Kontoübertrag"
in einer Ausgabe-Gruppe genügt damit für beide Seiten.

Test für beide Richtungen; 94 grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-21 14:09:54 +02:00
co-authored by Claude Opus 4.8
parent 7674e47f43
commit af865a5c7b
5 changed files with 69 additions and 9 deletions
+11 -5
View File
@@ -222,7 +222,7 @@ def _match_rule(
purpose: str,
amount: int | None,
allowed_kind: CategoryKind | None,
cat_kind: dict[int, CategoryKind],
cat_kind: dict[int, CategoryKind | None],
) -> CategorizationRule | None:
"""Erste passende Regel (Muster + optionale Betragsbedingung). Kategorie-Regeln
greifen nur, wenn die Kategorie-Art (Einnahme/Ausgabe) zur Buchung passt;
@@ -241,7 +241,10 @@ def _match_rule(
continue
if not rule.category_id:
continue
if allowed_kind is not None and cat_kind.get(rule.category_id) != allowed_kind:
# Neutrale Kategorien („Kontoübertrag") passen in beide Richtungen: Geld
# geht auf dem einen Konto raus und kommt auf dem anderen rein.
art = cat_kind.get(rule.category_id)
if allowed_kind is not None and art is not None and art != allowed_kind:
continue
if _pattern_hit(rule, p, z):
return rule
@@ -410,9 +413,12 @@ def detect_rows(db: Session, user: User, account: Account, candidates: list[dict
rules = db.query(CategorizationRule).filter(CategorizationRule.owner_id == user.id).all()
rules.sort(key=_rule_sort_key, reverse=True) # längstes/​manuell zuerst
cat_kind: dict[int, CategoryKind] = {
cid: kind
for cid, kind in db.query(Category.id, Category.type).filter(Category.owner_id == user.id)
# None = neutral: zählt für Ausgabe UND Einnahme (siehe _match_rule).
cat_kind: dict[int, CategoryKind | None] = {
cid: (None if neutral else kind)
for cid, kind, neutral in db.query(
Category.id, Category.type, Category.excluded_from_stats
).filter(Category.owner_id == user.id)
}
# Duplikate als Multiset zählen: eine Zeile gilt nur dann als Duplikat, wenn
# bereits GENAU SO VIELE identische Buchungen in der DB stehen. So werden echte,