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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 & Garten: 300,00 € / Monat" in r.text and "Rasen" in r.text
|
assert "Haus & 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
|
||||||
|
|||||||
@@ -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"):
|
||||||
|
|||||||
Reference in New Issue
Block a user