diff --git a/finance/app/routers/gui.py b/finance/app/routers/gui.py
index 6a4dedc..7c79b2d 100644
--- a/finance/app/routers/gui.py
+++ b/finance/app/routers/gui.py
@@ -264,6 +264,7 @@ def _scenario_rows(session: Session, scenarios: list, loans: list) -> list[dict]
}
modifiers = session.execute(
select(ScenarioModifier).where(ScenarioModifier.scenario_id == sc.id)
+ .order_by(ScenarioModifier.id)
).scalars().all()
planned_items = session.execute(
select(ScenarioPlannedItem)
diff --git a/finance/app/templates/hilfe.html b/finance/app/templates/hilfe.html
index 649bbc5..12d0cde 100644
--- a/finance/app/templates/hilfe.html
+++ b/finance/app/templates/hilfe.html
@@ -70,11 +70,13 @@
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.
+ Varianten wie „Best Case“ pflegst du in der Tabelle „Einträge“ des
+ Szenarios: Art „Ende“ lässt einen wiederkehrenden Posten (z. B. eine
+ auslaufende Miete) zu einem Datum enden, ohne den Posten selbst zu
+ ändern; „Prozent“/„Absolut“ kürzen Beträge; „Entfällt“ blendet sie ganz
+ aus; „Einmalzahlung“ ergänzt einen einmaligen Zu- oder Abfluss, der nur
+ in diesem Szenario zählt. Jeder Eintrag ist über „Bearbeiten“ direkt
+ änderbar.
{% if m.kind in ('percent', 'absolute') %}{{ m.value|eur }}{% else %}–{% endif %}
+
{% if m.kind == 'ende' %}{{ m.end_date.strftime('%d.%m.%Y') if m.end_date else '–' }}{% else %}–{% endif %}
+
+
+
+
+
+
+
+
+
+
+ {% endfor %}
+ {% for sp in row.planned_items %}
+
+
{{ sp.name }}
+
Einmalzahlung
+
{{ sp.amount|eur }} €
+
{{ sp.due.strftime('%d.%m.%Y') }}
+
+
+
+
+
+
+
+
+
+
+ {% endfor %}
+ {% if not row.modifiers and not row.planned_items %}
+
Keine Einträge.
+ {% endif %}
+
+
-
-
-
- Einmalzahlungen in diesem Szenario
-
-
Name
Betrag
Fällig am
-
- {% for sp in row.planned_items %}
-
-
{{ sp.name }}
-
{{ sp.amount|eur }} €
-
{{ sp.due.strftime('%d.%m.%Y') }}
-
-
-
-
-
-
- {% else %}
-
Keine szenario-eigenen Einmalzahlungen.
- {% endfor %}
-
-
-
-
-
-
-
-
-
Zählt nur in diesem Szenario — unabhängig von „Einmalposten einschließen“.
-
+
+
+
+
+
+
+
+
@@ -450,11 +486,37 @@
recSelect.disabled = isCategory;
}
- function onModKindChange(select) {
+ function onEntryArtChange(select) {
+ // Umschaltmatrix "Neuer Eintrag"/Modifikator-Edit: Felder nur per
+ // disabled schalten (UX-Regel). Fehlt ein Feld (Edit-Formulare haben
+ // kein name/amount/due), wird es uebersprungen.
var form = select.closest('form');
- var isEnde = select.value === 'ende';
- form.querySelector('[name="end_date"]').disabled = !isEnde;
- form.querySelector('[name="value"]').disabled = isEnde;
+ var art = select.value;
+ var isMod = art !== 'einmal';
+ function setDisabled(name, disabled) {
+ var el = form.querySelector('[name="' + name + '"]');
+ if (el) { el.disabled = disabled; }
+ }
+ setDisabled('value', !(art === 'percent' || art === 'absolute'));
+ setDisabled('end_date', art !== 'ende');
+ setDisabled('name', isMod);
+ setDisabled('amount', isMod);
+ setDisabled('due', isMod);
+ var typeSelect = form.querySelector('[name="target_type"]');
+ if (typeSelect) {
+ typeSelect.disabled = !isMod;
+ var isCategory = typeSelect.value === 'category';
+ var catSelect = form.querySelector('.mod-target-category select');
+ var recSelect = form.querySelector('.mod-target-recurring select');
+ if (catSelect) { catSelect.disabled = !isMod || !isCategory; }
+ if (recSelect) { recSelect.disabled = !isMod || isCategory; }
+ }
+ // POST-Ziel nur beim Anlege-Formular wechseln (Edit-Formulare patchen fix).
+ if (form.hasAttribute('data-planned-url')) {
+ form.setAttribute('hx-post', isMod ? form.getAttribute('data-modifiers-url')
+ : form.getAttribute('data-planned-url'));
+ htmx.process(form);
+ }
}
function toggleScenarioLoan(scenarioId, loanId, checked, checkbox) {
diff --git a/finance/tests/test_gui.py b/finance/tests/test_gui.py
index 08658d2..76a2fcc 100644
--- a/finance/tests/test_gui.py
+++ b/finance/tests/test_gui.py
@@ -354,6 +354,36 @@ def test_szenario_gui_ende_und_einmalzahlungen(client, db):
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 "Einträge" in r
+ assert f'data-planned-url="/api/scenarios/{sc.id}/planned"' 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
+
+
+def test_szenario_eintraege_tabelle(client, db):
+ client.post("/login", data={"username": "admin", "password": "geheim"})
+ sc = Scenario(name="Eintraege-GUI", 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(ScenarioModifier(scenario_id=sc.id, target_type="category",
+ target_id=1, kind="percent", value=Decimal("25.00")))
+ db.add(ScenarioPlannedItem(scenario_id=sc.id, name="Zufluss-Eintrag",
+ amount=Decimal("5000.00"), due=date(2026, 7, 30)))
+ db.commit()
+ r = client.get("/planung").text
+ assert "Einträge" in r
+ # alte details-Abschnitte sind ersetzt
+ assert "Modifikatoren" not in r
+ assert "Einmalzahlungen in diesem Szenario" not in r
+ # gemischte Zeilen in EINER Tabelle
+ assert "31.08.2026" in r and "25,00" in r and "Zufluss-Eintrag" in r
+ assert ">Einmalzahlung<" in r
+ # Inline-Edit je Typ
+ assert f'hx-patch="/api/scenarios/{sc.id}/modifiers/' in r
+ assert f'hx-patch="/api/scenarios/{sc.id}/planned/' in r
+ # Anlege-Formular: Eintragsart inkl. Einmalzahlung + Endpunkt-Wechsel-Attribute
+ assert 'value="einmal"' in r
+ assert f'data-planned-url="/api/scenarios/{sc.id}/planned"' in r
+ assert "onEntryArtChange" in r and "onModKindChange" not in r