diff --git a/finance/app/routers/gui.py b/finance/app/routers/gui.py index da64f84..6a4dedc 100644 --- a/finance/app/routers/gui.py +++ b/finance/app/routers/gui.py @@ -13,7 +13,8 @@ from app.engine.recurrence import occurrences from app.formats import de_label, eur from app.models.tables import (Account, Category, PlannedItem, ProjectionResult, RecurringItem, - ScenarioLoan, ScenarioModifier, Transaction) + ScenarioLoan, ScenarioModifier, + ScenarioPlannedItem, Transaction) from app.routers.imports import list_imports as _list_imports from app.routers.imports import preview as _preview_import from app.routers.planning import list_loans as _list_loans @@ -264,10 +265,15 @@ def _scenario_rows(session: Session, scenarios: list, loans: list) -> list[dict] modifiers = session.execute( select(ScenarioModifier).where(ScenarioModifier.scenario_id == sc.id) ).scalars().all() + planned_items = session.execute( + select(ScenarioPlannedItem) + .where(ScenarioPlannedItem.scenario_id == sc.id) + .order_by(ScenarioPlannedItem.due)).scalars().all() rows.append({ "scenario": sc, "assigned_loan_ids": assigned_loan_ids, "modifiers": modifiers, + "planned_items": planned_items, "result": session.get(ProjectionResult, sc.id), }) return rows @@ -296,5 +302,5 @@ def planung_page(request: Request, session: Session = Depends(get_session)): "scenario_rows": _scenario_rows(session, scenarios, loans), "rhythms": ["monthly", "quarterly", "yearly"], "repayment_types": ["annuity", "bullet"], - "modifier_kinds": ["percent", "absolute", "remove"], + "modifier_kinds": ["percent", "absolute", "remove", "ende"], }) diff --git a/finance/app/templates/hilfe.html b/finance/app/templates/hilfe.html index 7b874ed..649bbc5 100644 --- a/finance/app/templates/hilfe.html +++ b/finance/app/templates/hilfe.html @@ -70,6 +70,11 @@ von Krediten und Modifikatoren (Kategorie oder Posten prozentual/absolut kürzen oder ganz streichen). "Durchrechnen" liefert: tiefster Kontostand mit Datum, erstes Datum unter 0 und unter der Warnschwelle. + Varianten wie „Best Case“ entstehen über Modifikatoren: Art „Ende“ lässt + einen wiederkehrenden Posten (z. B. eine auslaufende Miete) innerhalb des + Szenarios zu einem Datum enden, ohne den Posten selbst zu ändern; „Prozent“/ + „Absolut“ kürzen Beträge; „Entfällt“ blendet sie ganz aus. Szenario-eigene + Einmalzahlungen (z. B. ein erwarteter Zufluss) zählen nur in ihrem Szenario.
  • Grafana: Kontostand-Verläufe, Monatsausgaben nach diff --git a/finance/app/templates/planning.html b/finance/app/templates/planning.html index 2f6f917..811d3f5 100644 --- a/finance/app/templates/planning.html +++ b/finance/app/templates/planning.html @@ -311,7 +311,10 @@ {% else %}Posten: {{ recurring_names.get(m.target_id, m.target_id) }}{% endif %} {{ m.kind|de_label }} - {{ m.value|eur }} + + {% if m.kind == 'ende' %}{{ m.end_date.strftime('%d.%m.%Y') if m.end_date else '–' }} + {% else %}{{ m.value|eur }}{% endif %} +
    @@ -348,15 +351,49 @@ +
    +
    + Einmalzahlungen in diesem Szenario + + + + {% for sp in row.planned_items %} + + + + + + + {% else %} + + {% endfor %} + +
    NameBetragFällig am
    {{ sp.name }}{{ sp.amount|eur }} €{{ sp.due.strftime('%d.%m.%Y') }} +
    + +
    +
    Keine szenario-eigenen Einmalzahlungen.
    +
    + + + + +
    +

    Zählt nur in diesem Szenario — unabhängig von „Einmalposten einschließen“.

    +
    +
    @@ -413,6 +450,13 @@ recSelect.disabled = isCategory; } + function onModKindChange(select) { + var form = select.closest('form'); + var isEnde = select.value === 'ende'; + form.querySelector('[name="end_date"]').disabled = !isEnde; + form.querySelector('[name="value"]').disabled = isEnde; + } + function toggleScenarioLoan(scenarioId, loanId, checked, checkbox) { fetch('/api/scenarios/' + scenarioId + '/loans/' + loanId, { method: checked ? 'POST' : 'DELETE' }) .then(function (resp) { diff --git a/finance/tests/test_gui.py b/finance/tests/test_gui.py index e003d46..08658d2 100644 --- a/finance/tests/test_gui.py +++ b/finance/tests/test_gui.py @@ -2,7 +2,8 @@ from datetime import date, timedelta from decimal import Decimal from app.formats import eur -from app.models.tables import Account, Loan, PlannedItem, RecurringItem, Scenario +from app.models.tables import (Account, Loan, PlannedItem, RecurringItem, + Scenario, ScenarioModifier, ScenarioPlannedItem) H = {"Authorization": "Bearer test-key"} @@ -336,3 +337,23 @@ def test_salden_shows_ohne_anker_footnote(client, db): r = client.get("/salden") assert r.status_code == 200 assert "ohne Anker" in r.text and "relative Werte" in r.text + + +def test_szenario_gui_ende_und_einmalzahlungen(client, db): + client.post("/login", data={"username": "admin", "password": "geheim"}) + sc = Scenario(name="GUI-Ende", description="") + db.add(sc) + db.flush() + db.add(ScenarioModifier(scenario_id=sc.id, target_type="recurring", + target_id=1, kind="ende", value=Decimal("0"), + end_date=date(2026, 8, 31))) + db.add(ScenarioPlannedItem(scenario_id=sc.id, name="Zufluss-GUI", + amount=Decimal("5000.00"), due=date(2026, 7, 30))) + db.commit() + r = client.get("/planung").text + assert 'value="ende"' in r # neue Art im Dropdown + assert 'name="end_date"' in r # Datumsfeld im Modifikator-Formular + assert "31.08.2026" in r # ende-Modifikator zeigt Datum + assert "Einmalzahlungen in diesem Szenario" in r + assert "Zufluss-GUI" in r and "5.000,00" in r and "30.07.2026" in r + assert f'hx-post="/api/scenarios/{sc.id}/planned"' in r