fix: Aufteilung – Final-Review-Nachträge (Monat <1000, Übrige-Mitglieder, Auth-Test, Doku) (A13)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-29 16:56:57 +02:00
co-authored by Claude Opus 5.5
parent b32f28fff5
commit 987e28847c
6 changed files with 51 additions and 9 deletions
+1
View File
@@ -125,6 +125,7 @@ flowchart TD
E_scenario --> E_recurrence E_scenario --> E_recurrence
E_breakdown --> E_scenario E_breakdown --> E_scenario
E_breakdown --> E_recurrence E_breakdown --> E_recurrence
E_breakdown --> E_loans
P_registry --> P_vr P_registry --> P_vr
P_registry --> P_hvb P_registry --> P_hvb
+2 -2
View File
@@ -426,8 +426,8 @@ def _aufteilung_context(session: Session, today: date, monat: str | None,
if month is None: if month is None:
ctx["error"] = f"Ungültiger Monat „{monat}“ – bitte im Format JJJJ-MM wählen." ctx["error"] = f"Ungültiger Monat „{monat}“ – bitte im Format JJJJ-MM wählen."
return ctx return ctx
ctx["monat_wert"] = month.strftime("%Y-%m") ctx["monat_wert"] = f"{month.year:04d}-{month.month:02d}"
ctx["monat_anzeige"] = month.strftime("%m.%Y") ctx["monat_anzeige"] = f"{month.month:02d}.{month.year:04d}"
scenario = None scenario = None
if szenario: if szenario:
valid_id = re.fullmatch(r"[0-9]{1,9}", szenario) # ASCII, kein Integer-Überlauf valid_id = re.fullmatch(r"[0-9]{1,9}", szenario) # ASCII, kein Integer-Überlauf
+3 -2
View File
@@ -14,8 +14,8 @@ class="seg {{ seg.s.color }}{% if seg.q %} klickbar{% endif %}{% if seg.gewaehlt
<figcaption>{{ titel }}</figcaption> <figcaption>{{ titel }}</figcaption>
<svg class="torte-svg" viewBox="-1.05 -1.05 2.1 2.1" role="img" aria-label="{{ titel }}"> <svg class="torte-svg" viewBox="-1.05 -1.05 2.1 2.1" role="img" aria-label="{{ titel }}">
{% for seg in segmente %}{% set s = seg.s %} {% for seg in segmente %}{% set s = seg.s %}
{% if s.path %}<path d="{{ s.path }}" {{ seg_attrs(seg) }}><title>{{ s.label }}: {{ s.value|eur }} € ({{ s.share|pct }})</title></path> {% if s.path %}<path d="{{ s.path }}" {{ seg_attrs(seg) }}><title>{{ s.label }}{% if s.merged %} ({{ s.members|join(', ') }}){% endif %}: {{ s.value|eur }} € ({{ s.share|pct }})</title></path>
{% else %}<circle r="1" {{ seg_attrs(seg) }}><title>{{ s.label }}: {{ s.value|eur }} € ({{ s.share|pct }})</title></circle>{% endif %} {% else %}<circle r="1" {{ seg_attrs(seg) }}><title>{{ s.label }}{% if s.merged %} ({{ s.members|join(', ') }}){% endif %}: {{ s.value|eur }} € ({{ s.share|pct }})</title></circle>{% endif %}
{% endfor %} {% endfor %}
</svg> </svg>
<ul class="legende"> <ul class="legende">
@@ -23,6 +23,7 @@ class="seg {{ seg.s.color }}{% if seg.q %} klickbar{% endif %}{% if seg.gewaehlt
<li{% if seg.gewaehlt %} class="gewaehlt"{% endif %}><span class="swatch {{ s.color }}"></span> <li{% if seg.gewaehlt %} class="gewaehlt"{% endif %}><span class="swatch {{ s.color }}"></span>
{%- if seg.q %}<a href="/aufteilung?{{ seg.q }}" hx-get="/aufteilung/kategorie?{{ seg.q }}" hx-target="#aufteilung-kategorie" hx-push-url="/aufteilung?{{ seg.q }}">{{ s.label }}</a> {%- if seg.q %}<a href="/aufteilung?{{ seg.q }}" hx-get="/aufteilung/kategorie?{{ seg.q }}" hx-target="#aufteilung-kategorie" hx-push-url="/aufteilung?{{ seg.q }}">{{ s.label }}</a>
{%- else %}<span>{{ s.label }}</span>{% endif %} {%- else %}<span>{{ s.label }}</span>{% endif %}
{%- if s.merged %} <span class="legende-zusatz">({{ s.members|join(', ') }})</span>{% endif %}
<span class="legende-wert">{{ s.value|eur }} € · {{ s.share|pct }}{% if seg.zusatz %} · {{ seg.zusatz }}{% endif %}</span></li> <span class="legende-wert">{{ s.value|eur }} € · {{ s.share|pct }}{% if seg.zusatz %} · {{ seg.zusatz }}{% endif %}</span></li>
{% endfor %} {% endfor %}
</ul> </ul>
+5 -5
View File
@@ -95,18 +95,18 @@
<strong>Aufteilung:</strong> zeigt für einen Monat, welche monatlichen <strong>Aufteilung:</strong> zeigt für einen Monat, welche monatlichen
Kosten je Kategorie anfallen und wie viel der Einkünfte frei verfügbar Kosten je Kategorie anfallen und wie viel der Einkünfte frei verfügbar
bleibt. Grundlage ist immer die Planung (Fixposten, Einmalposten, bleibt. Grundlage ist immer die Planung (Fixposten, Einmalposten,
Kredite), nie die tatsächlichen Buchungen: „System-Plan" rechnet mit Kredite), nie die tatsächlichen Buchungen: „System-Plan“ rechnet mit
allen Posten und Krediten ohne Szenario-Änderungen und ist für jeden allen Posten und Krediten ohne Szenario-Änderungen und ist für jeden
Monat wählbar; ein Szenario (mit seinen Änderungen und Krediten) erst ab Monat wählbar; ein Szenario (mit seinen Änderungen und Krediten) erst ab
dem laufenden Monat. Fixposten werden auf den Monat umgerechnet dem laufenden Monat. Fixposten werden auf den Monat umgerechnet
(vierteljährlich ÷ 3, jährlich ÷ 12), Einmalposten und Kreditraten zählen (vierteljährlich ÷ 3, jährlich ÷ 12), Einmalposten und Kreditraten zählen
voll im Monat ihrer Fälligkeit, eine Kredit-Auszahlung zählt nicht als voll im Monat ihrer Fälligkeit, eine Kredit-Auszahlung zählt nicht als
Einkunft. Die Häkchen „Fixposten", „Einmalposten", „Kredite" gelten für Einkunft. Die Häkchen „Fixposten“, „Einmalposten“, „Kredite“ gelten für
Einnahmen und Ausgaben. Das Tortendiagramm zeigt die Einkünfte (Kosten je Einnahmen und Ausgaben. Das Tortendiagramm zeigt die Einkünfte (Kosten je
Kategorie plus „Frei verfügbar"); übersteigen die Kosten die Einkünfte, Kategorie plus „Frei verfügbar“); übersteigen die Kosten die Einkünfte,
zeigt es die Gesamtkosten. Kleine Kategorien (unter 3 %, bzw. ab der zeigt es die Gesamtkosten. Kleine Kategorien (unter 3 %, bzw. ab der
neunten) sind zu „Übrige" zusammengefasst – höchstens 8 benannte neunten) sind zu „Übrige“ zusammengefasst – höchstens 8 benannte
Kategorien plus „Übrige" und „Frei verfügbar". Ein Klick auf ein Segment oder Kategorien plus „Übrige“ und „Frei verfügbar“. Ein Klick auf ein Segment oder
einen Legendeneintrag zeigt darunter die Posten dieser Kategorie. einen Legendeneintrag zeigt darunter die Posten dieser Kategorie.
</li> </li>
</ul> </ul>
+34
View File
@@ -276,3 +276,37 @@ def test_drilldown_sonderzeichen(client, db):
assert "kategorie=Haus+%26+Garten" in t assert "kategorie=Haus+%26+Garten" in t
r = _drill(client, kategorie="Haus & Garten") r = _drill(client, kategorie="Haus & Garten")
assert "Haus &amp; Garten: 300,00 € / Monat" in r.text and "Rasen" in r.text assert "Haus &amp; Garten: 300,00 € / Monat" in r.text and "Rasen" in r.text
def test_monat_vor_jahr_1000_vierstellig(client, db):
_login(client)
eink, wohnen = Category(name="Einkommen"), Category(name="Wohnen")
db.add_all([eink, wohnen])
db.flush()
db.add_all([
RecurringItem(name="Gehalt", amount=D("3000.00"), rhythm="monthly", due_day=1, category_id=eink.id),
RecurringItem(name="Miete", amount=D("-1000.00"), rhythm="monthly", due_day=1, category_id=wohnen.id),
])
db.commit()
r = _get(client, monat="0001-01")
assert r.status_code == 200
assert 'value="0001-01"' in r.text
assert "01.0001" in r.text
assert "monat=0001-01" in r.text
def test_uebrige_mitglieder_in_legende_der_zweiten_torte(client, db):
_login(client)
eink, kat = Category(name="Einkommen"), Category(name="Vielfalt")
db.add_all([eink, kat])
db.flush()
db.add(RecurringItem(name="Gehalt", amount=D("3000.00"), rhythm="monthly", due_day=1, category_id=eink.id))
db.add(RecurringItem(name="Großposten", amount=D("-1000.00"), rhythm="monthly", due_day=1, category_id=kat.id))
for i in range(10):
db.add(RecurringItem(name=f"Kleinposten {i}", amount=D("-5.00"), rhythm="monthly",
due_day=1, category_id=kat.id))
db.commit()
t = _drill(client, kategorie="Vielfalt").text
legende = re.search(r'<ul class="legende">(.*?)</ul>', t, re.S).group(1)
assert 'class="legende-zusatz"' in legende
assert "Kleinposten 3" in legende
+6
View File
@@ -17,6 +17,12 @@ def test_pages_require_login(client):
assert r.headers["location"] == "/login" assert r.headers["location"] == "/login"
def test_aufteilung_fragment_erfordert_login(client):
r = client.get("/aufteilung/kategorie", follow_redirects=False)
assert r.status_code in (302, 303)
assert r.headers["location"] == "/login"
def test_pages_render_after_login(client): def test_pages_render_after_login(client):
client.post("/login", data={"username": "admin", "password": "geheim"}) client.post("/login", data={"username": "admin", "password": "geheim"})
for path in ("/", "/buchungen", "/import", "/salden", "/planung", "/szenarien", "/aufteilung", "/hilfe"): for path in ("/", "/buchungen", "/import", "/salden", "/planung", "/szenarien", "/aufteilung", "/hilfe"):