feat: deutsche Betragsanzeige (eur-Filter) und deutsche GUI-Labels

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 11:23:26 +02:00
parent bd9df8ab8b
commit fe0e1b5e93
9 changed files with 138 additions and 35 deletions

30
finance/app/formats.py Normal file
View File

@@ -0,0 +1,30 @@
"""Deutsche Anzeige-Formate fuer die GUI (Ausbaustufe 5).
Bewusst ohne locale-Modul: Container-Locales sind nicht garantiert
installiert, und die Ausgabe hinge sonst vom Host ab. Reine
Decimal-Formatierung; die JSON-API bleibt Punkt-Dezimal.
"""
from decimal import Decimal, ROUND_HALF_UP
DE_LABELS = {
"monthly": "monatlich",
"quarterly": "vierteljährlich",
"yearly": "jährlich",
"annuity": "Annuität",
"bullet": "endfällig",
"percent": "Prozent",
"absolute": "Absolut",
"remove": "Entfällt",
"ende": "Ende",
}
def eur(value) -> str:
"""Decimal("-2474.5") -> "-2.474,50" (ohne Euro-Zeichen, das setzt das Template)."""
d = Decimal(str(value)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
s = f"{d:,.2f}" # US-Trenner: "-2,474.50"
return s.replace(",", "\x00").replace(".", ",").replace("\x00", ".")
def de_label(value: str) -> str:
return DE_LABELS.get(value, value)

View File

@@ -10,6 +10,7 @@ from app.auth import COOKIE, session_valid
from app.db import get_session from app.db import get_session
from app.engine.loans import add_months from app.engine.loans import add_months
from app.engine.recurrence import occurrences from app.engine.recurrence import occurrences
from app.formats import de_label, eur
from app.models.tables import (Account, Category, PlannedItem, from app.models.tables import (Account, Category, PlannedItem,
ProjectionResult, RecurringItem, ProjectionResult, RecurringItem,
ScenarioLoan, ScenarioModifier, Transaction) ScenarioLoan, ScenarioModifier, Transaction)
@@ -28,6 +29,8 @@ PAGE_SIZE = 50
templates = Jinja2Templates(directory="app/templates") templates = Jinja2Templates(directory="app/templates")
templates.env.globals["app_version"] = get_version() templates.env.globals["app_version"] = get_version()
templates.env.filters["eur"] = eur
templates.env.filters["de_label"] = de_label
router = APIRouter() router = APIRouter()

View File

@@ -8,7 +8,7 @@
<td>{{ tx.booking_date.strftime('%d.%m.%Y') }}</td> <td>{{ tx.booking_date.strftime('%d.%m.%Y') }}</td>
<td>{{ tx.purpose }}</td> <td>{{ tx.purpose }}</td>
<td>{{ tx.counterparty }}</td> <td>{{ tx.counterparty }}</td>
<td class="{{ 'neg' if tx.amount < 0 else '' }}">{{ '%.2f'|format(tx.amount) }} €</td> <td class="{{ 'neg' if tx.amount < 0 else '' }}">{{ tx.amount|eur }} €</td>
<td>{% if tx.is_duplicate %}<span class="badge badge-warning">Duplikat</span>{% endif %}</td> <td>{% if tx.is_duplicate %}<span class="badge badge-warning">Duplikat</span>{% endif %}</td>
</tr> </tr>
{% else %} {% else %}
@@ -25,7 +25,7 @@
{% else %} {% else %}
<span class="badge badge-error">Saldo-Differenz bitte prüfen</span> <span class="badge badge-error">Saldo-Differenz bitte prüfen</span>
{% endif %} {% endif %}
· Eröffnungssaldo: {{ '%.2f'|format(statement.opening_balance) if statement.opening_balance is not none else '' }} € · Eröffnungssaldo: {{ statement.opening_balance|eur if statement.opening_balance is not none else '' }} €
· Endsaldo: {{ '%.2f'|format(statement.closing_balance) if statement.closing_balance is not none else '' }} € · Endsaldo: {{ statement.closing_balance|eur if statement.closing_balance is not none else '' }} €
· Duplikate: {{ duplicates }} · Duplikate: {{ duplicates }}
</p> </p>

View File

@@ -11,7 +11,7 @@
{% endif %} {% endif %}
<div class="total-balance {{ 'neg' if total < 0 else '' }}"> <div class="total-balance {{ 'neg' if total < 0 else '' }}">
Gesamtsaldo: {{ '%.2f'|format(total) }} € Gesamtsaldo: {{ total|eur }} €
</div> </div>
<table> <table>
@@ -26,10 +26,10 @@
<tr> <tr>
<td class="account">{{ account.name }}</td> <td class="account">{{ account.name }}</td>
<td>{{ account.bank }}</td> <td>{{ account.bank }}</td>
<td class="{{ 'neg' if balance < 0 else '' }}">{{ '%.2f'|format(balance) }} €</td> <td class="{{ 'neg' if balance < 0 else '' }}">{{ balance|eur }} €</td>
<td> <td>
{% if account.anchor_date is not none and account.anchor_balance is not none %} {% if account.anchor_date is not none and account.anchor_balance is not none %}
Anker: {{ '%.2f'|format(account.anchor_balance) }} € am {{ account.anchor_date.strftime('%d.%m.%Y') }} Anker: {{ account.anchor_balance|eur }} € am {{ account.anchor_date.strftime('%d.%m.%Y') }}
{% else %} {% else %}
kein Anker kein Anker
{% endif %} {% endif %}
@@ -47,7 +47,7 @@
<input type="date" name="anchor_date" <input type="date" name="anchor_date"
value="{{ account.anchor_date.isoformat() if account.anchor_date is not none else '' }}" required> value="{{ account.anchor_date.isoformat() if account.anchor_date is not none else '' }}" required>
<input type="text" name="anchor_balance" placeholder="500.00" <input type="text" name="anchor_balance" placeholder="500.00"
value="{{ account.anchor_balance if account.anchor_balance is not none else '' }}" required> value="{{ account.anchor_balance|eur if account.anchor_balance is not none else '' }}" required>
<button type="submit">Anker setzen</button> <button type="submit">Anker setzen</button>
</form> </form>
</td> </td>
@@ -68,7 +68,7 @@
<tr> <tr>
<td>{{ item.date.strftime('%d.%m.%Y') }}</td> <td>{{ item.date.strftime('%d.%m.%Y') }}</td>
<td>{{ item.name }}</td> <td>{{ item.name }}</td>
<td class="{{ 'neg' if item.amount < 0 else '' }}">{{ '%.2f'|format(item.amount) }} €</td> <td class="{{ 'neg' if item.amount < 0 else '' }}">{{ item.amount|eur }} €</td>
</tr> </tr>
{% else %} {% else %}
<tr><td colspan="3">Keine anstehenden Posten.</td></tr> <tr><td colspan="3">Keine anstehenden Posten.</td></tr>

View File

@@ -13,8 +13,8 @@
{% for r in recurring %} {% for r in recurring %}
<tr> <tr>
<td>{{ r.name }}</td> <td>{{ r.name }}</td>
<td class="{{ 'neg' if r.amount < 0 else '' }}">{{ '%.2f'|format(r.amount) }} €</td> <td class="{{ 'neg' if r.amount < 0 else '' }}">{{ r.amount|eur }} €</td>
<td>{{ r.rhythm }}</td> <td>{{ r.rhythm|de_label }}</td>
<td>{{ r.due_day }}</td> <td>{{ r.due_day }}</td>
<td>{{ category_names.get(r.category_id, '') }}</td> <td>{{ category_names.get(r.category_id, '') }}</td>
<td> <td>
@@ -39,7 +39,7 @@
<label>Betrag <input type="text" name="amount" placeholder="-49.99" required></label> <label>Betrag <input type="text" name="amount" placeholder="-49.99" required></label>
<label>Rhythmus <label>Rhythmus
<select name="rhythm" data-type="str"> <select name="rhythm" data-type="str">
{% for rh in rhythms %}<option value="{{ rh }}">{{ rh }}</option>{% endfor %} {% for rh in rhythms %}<option value="{{ rh }}">{{ rh|de_label }}</option>{% endfor %}
</select> </select>
</label> </label>
<label>Fälligkeitstag <input type="number" name="due_day" data-type="int" min="1" max="31" required></label> <label>Fälligkeitstag <input type="number" name="due_day" data-type="int" min="1" max="31" required></label>
@@ -64,8 +64,8 @@
{% for s in suggestions %} {% for s in suggestions %}
<tr> <tr>
<td>{{ s.name }}</td> <td>{{ s.name }}</td>
<td class="{{ 'neg' if s.amount < 0 else '' }}">{{ '%.2f'|format(s.amount) }} €</td> <td class="{{ 'neg' if s.amount < 0 else '' }}">{{ s.amount|eur }} €</td>
<td>{{ s.rhythm }}</td> <td>{{ s.rhythm|de_label }}</td>
<td>{{ s.due_day }}</td> <td>{{ s.due_day }}</td>
<td> <td>
<form class="inline-form" hx-ext="json-form" hx-post="/api/recurring" hx-swap="none" <form class="inline-form" hx-ext="json-form" hx-post="/api/recurring" hx-swap="none"
@@ -98,7 +98,7 @@
{% for p in planned %} {% for p in planned %}
<tr> <tr>
<td>{{ p.name }}</td> <td>{{ p.name }}</td>
<td class="{{ 'neg' if p.amount < 0 else '' }}">{{ '%.2f'|format(p.amount) }} €</td> <td class="{{ 'neg' if p.amount < 0 else '' }}">{{ p.amount|eur }} €</td>
<td>{{ p.due.strftime('%d.%m.%Y') }}</td> <td>{{ p.due.strftime('%d.%m.%Y') }}</td>
<td>{{ category_names.get(p.category_id, '') }}</td> <td>{{ category_names.get(p.category_id, '') }}</td>
<td> <td>
@@ -143,11 +143,11 @@
{% for l in loans %} {% for l in loans %}
<tr> <tr>
<td>{{ l.name }}</td> <td>{{ l.name }}</td>
<td>{{ '%.2f'|format(l.principal) }} €</td> <td>{{ l.principal|eur }} €</td>
<td>{{ '%.2f'|format(l.annual_rate_pct) }} %</td> <td>{{ l.annual_rate_pct|eur }} %</td>
<td>{{ l.term_months }} Monate</td> <td>{{ l.term_months }} Monate</td>
<td>{{ l.payout_date.strftime('%d.%m.%Y') }}</td> <td>{{ l.payout_date.strftime('%d.%m.%Y') }}</td>
<td>{{ l.repayment_type }}</td> <td>{{ l.repayment_type|de_label }}</td>
<td> <td>
<form class="inline-form" hx-delete="/api/loans/{{ l.id }}" hx-swap="none" <form class="inline-form" hx-delete="/api/loans/{{ l.id }}" hx-swap="none"
hx-confirm="„{{ l.name }}“ wirklich löschen?" hx-confirm="„{{ l.name }}“ wirklich löschen?"
@@ -181,7 +181,7 @@
<label>Auszahlungsdatum <input type="date" name="payout_date" required></label> <label>Auszahlungsdatum <input type="date" name="payout_date" required></label>
<label>Tilgungsart <label>Tilgungsart
<select name="repayment_type" data-type="str"> <select name="repayment_type" data-type="str">
{% for rt in repayment_types %}<option value="{{ rt }}">{{ rt }}</option>{% endfor %} {% for rt in repayment_types %}<option value="{{ rt }}">{{ rt|de_label }}</option>{% endfor %}
</select> </select>
</label> </label>
<button type="submit">Anlegen</button> <button type="submit">Anlegen</button>
@@ -227,8 +227,8 @@
{% if m.target_type == "category" %}Kategorie: {{ category_names.get(m.target_id, m.target_id) }} {% if m.target_type == "category" %}Kategorie: {{ category_names.get(m.target_id, m.target_id) }}
{% else %}Posten: {{ recurring_names.get(m.target_id, m.target_id) }}{% endif %} {% else %}Posten: {{ recurring_names.get(m.target_id, m.target_id) }}{% endif %}
</td> </td>
<td>{{ m.kind }}</td> <td>{{ m.kind|de_label }}</td>
<td>{{ '%.2f'|format(m.value) }}</td> <td>{{ m.value|eur }}</td>
<td> <td>
<form class="inline-form" hx-delete="/api/scenarios/{{ sc.id }}/modifiers/{{ m.id }}" hx-swap="none" <form class="inline-form" hx-delete="/api/scenarios/{{ sc.id }}/modifiers/{{ m.id }}" hx-swap="none"
hx-on::after-request="if(event.detail.successful){window.location.reload()}"> hx-on::after-request="if(event.detail.successful){window.location.reload()}">
@@ -266,7 +266,7 @@
</span> </span>
<label>Art <label>Art
<select name="kind" data-type="str"> <select name="kind" data-type="str">
{% for k in modifier_kinds %}<option value="{{ k }}">{{ k }}</option>{% endfor %} {% for k in modifier_kinds %}<option value="{{ k }}">{{ k|de_label }}</option>{% endfor %}
</select> </select>
</label> </label>
<label>Wert <input type="text" name="value" value="0"></label> <label>Wert <input type="text" name="value" value="0"></label>
@@ -281,7 +281,7 @@
{% if row.result %} {% if row.result %}
<p> <p>
Tiefpunkt: {{ '%.2f'|format(row.result.low_point_balance) }} € am {{ row.result.low_point_date.strftime('%d.%m.%Y') }}<br> Tiefpunkt: {{ row.result.low_point_balance|eur }} € am {{ row.result.low_point_date.strftime('%d.%m.%Y') }}<br>
{% if row.result.below_zero_date %}Unterschreitet 0 € ab {{ row.result.below_zero_date.strftime('%d.%m.%Y') }}<br>{% endif %} {% if row.result.below_zero_date %}Unterschreitet 0 € ab {{ row.result.below_zero_date.strftime('%d.%m.%Y') }}<br>{% endif %}
{% if row.result.below_threshold_date %}Unterschreitet Warnschwelle ab {{ row.result.below_threshold_date.strftime('%d.%m.%Y') }}<br>{% endif %} {% if row.result.below_threshold_date %}Unterschreitet Warnschwelle ab {{ row.result.below_threshold_date.strftime('%d.%m.%Y') }}<br>{% endif %}
Kurven in <a href="http://{{ request.url.hostname or '127.0.0.1' }}:8097" target="_blank" rel="noopener">Grafana</a> ansehen. Kurven in <a href="http://{{ request.url.hostname or '127.0.0.1' }}:8097" target="_blank" rel="noopener">Grafana</a> ansehen.
@@ -343,6 +343,9 @@
var parts = iso.split('-'); var parts = iso.split('-');
return parts[2] + '.' + parts[1] + '.' + parts[0]; return parts[2] + '.' + parts[1] + '.' + parts[0];
} }
function fmtEur(n) {
return Number(n).toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2});
}
function loadLoanSchedule(loanId) { function loadLoanSchedule(loanId) {
if (loadedSchedules[loanId]) return; if (loadedSchedules[loanId]) return;
loadedSchedules[loanId] = true; loadedSchedules[loanId] = true;
@@ -353,10 +356,10 @@
+ '<th>Tilgung</th><th>Restschuld</th></tr></thead><tbody>'; + '<th>Tilgung</th><th>Restschuld</th></tr></thead><tbody>';
rows.forEach(function (r) { rows.forEach(function (r) {
html += '<tr><td>' + formatIsoDate(r.due) + '</td>' html += '<tr><td>' + formatIsoDate(r.due) + '</td>'
+ '<td>' + Number(r.payment).toFixed(2) + ' €</td>' + '<td>' + fmtEur(r.payment) + ' €</td>'
+ '<td>' + Number(r.interest).toFixed(2) + ' €</td>' + '<td>' + fmtEur(r.interest) + ' €</td>'
+ '<td>' + Number(r.principal).toFixed(2) + ' €</td>' + '<td>' + fmtEur(r.principal) + ' €</td>'
+ '<td>' + Number(r.remaining).toFixed(2) + ' €</td></tr>'; + '<td>' + fmtEur(r.remaining) + ' €</td></tr>';
}); });
html += '</tbody></table>'; html += '</tbody></table>';
document.getElementById('schedule-' + loanId).innerHTML = html; document.getElementById('schedule-' + loanId).innerHTML = html;

View File

@@ -17,14 +17,14 @@
{% for account, balance in balances %} {% for account, balance in balances %}
<tr> <tr>
<td class="account">{{ account.name }}</td> <td class="account">{{ account.name }}</td>
<td class="amount {{ 'neg' if balance < 0 else '' }}">{{ '%.2f'|format(balance) }} €</td> <td class="amount {{ 'neg' if balance < 0 else '' }}">{{ balance|eur }} €</td>
</tr> </tr>
{% else %} {% else %}
<tr><td colspan="2">Keine Konten angelegt.</td></tr> <tr><td colspan="2">Keine Konten angelegt.</td></tr>
{% endfor %} {% endfor %}
<tr class="total-row"> <tr class="total-row">
<td><strong>Gesamt</strong></td> <td><strong>Gesamt</strong></td>
<td class="amount {{ 'neg' if total < 0 else '' }}"><strong>{{ '%.2f'|format(total) }} €</strong></td> <td class="amount {{ 'neg' if total < 0 else '' }}"><strong>{{ total|eur }} €</strong></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -48,9 +48,9 @@
<tr data-month="{{ row['month'].isoformat() }}"> <tr data-month="{{ row['month'].isoformat() }}">
<td>{{ row['month'].strftime('%m.%Y') }}</td> <td>{{ row['month'].strftime('%m.%Y') }}</td>
{% for account, value in row['values'] %} {% for account, value in row['values'] %}
<td class="amount {{ 'neg' if value < 0 else '' }}">{{ '%.2f'|format(value) }} €</td> <td class="amount {{ 'neg' if value < 0 else '' }}">{{ value|eur }} €</td>
{% endfor %} {% endfor %}
<td class="amount {{ 'neg' if row['total'] < 0 else '' }}">{{ '%.2f'|format(row['total']) }} €</td> <td class="amount {{ 'neg' if row['total'] < 0 else '' }}">{{ row['total']|eur }} €</td>
</tr> </tr>
{% else %} {% else %}
<tr><td colspan="{{ accounts|length + 2 }}">Keine bestätigten Buchungen vorhanden.</td></tr> <tr><td colspan="{{ accounts|length + 2 }}">Keine bestätigten Buchungen vorhanden.</td></tr>

View File

@@ -53,7 +53,7 @@
<td class="account">{{ account_names.get(tx.account_id, tx.account_id) }}</td> <td class="account">{{ account_names.get(tx.account_id, tx.account_id) }}</td>
<td>{{ tx.purpose }}</td> <td>{{ tx.purpose }}</td>
<td>{{ tx.counterparty }}</td> <td>{{ tx.counterparty }}</td>
<td class="amount {{ 'neg' if tx.amount < 0 else '' }}">{{ '%.2f'|format(tx.amount) }}</td> <td class="amount {{ 'neg' if tx.amount < 0 else '' }}">{{ tx.amount|eur }}</td>
<td> <td>
<select name="category_id" data-type="int" hx-ext="json-form" <select name="category_id" data-type="int" hx-ext="json-form"
hx-patch="/api/transactions/{{ tx.id }}" hx-trigger="change" hx-swap="none"> hx-patch="/api/transactions/{{ tx.id }}" hx-trigger="change" hx-swap="none">

View File

@@ -0,0 +1,43 @@
from decimal import Decimal
from app.formats import DE_LABELS, de_label, eur
def test_eur_basisformat():
assert eur(Decimal("2474.00")) == "2.474,00"
def test_eur_negativ_mit_mehreren_tausenderpunkten():
assert eur(Decimal("-1234567.5")) == "-1.234.567,50"
def test_eur_null_und_kleinbetraege():
assert eur(Decimal("0")) == "0,00"
assert eur(Decimal("-0.5")) == "-0,50"
assert eur(Decimal("999.99")) == "999,99"
def test_eur_rundung_half_up():
assert eur(Decimal("1.005")) == "1,01"
assert eur(Decimal("-1.005")) == "-1,01"
def test_eur_akzeptiert_strings_und_int():
# Templates reichen teils DB-Werte als str/int durch (z.B. Suggestions).
assert eur("1234.5") == "1.234,50"
assert eur(7) == "7,00"
def test_de_label_bekannte_und_unbekannte_werte():
assert de_label("monthly") == "monatlich"
assert de_label("quarterly") == "vierteljährlich"
assert de_label("yearly") == "jährlich"
assert de_label("annuity") == "Annuität"
assert de_label("bullet") == "endfällig"
assert de_label("percent") == "Prozent"
assert de_label("absolute") == "Absolut"
assert de_label("remove") == "Entfällt"
assert de_label("ende") == "Ende"
assert de_label("weirdvalue") == "weirdvalue"
assert set(DE_LABELS) == {"monthly", "quarterly", "yearly", "annuity",
"bullet", "percent", "absolute", "remove", "ende"}

View File

@@ -1,7 +1,8 @@
from datetime import date, timedelta from datetime import date, timedelta
from decimal import Decimal from decimal import Decimal
from app.models.tables import Account from app.formats import eur
from app.models.tables import Account, RecurringItem
H = {"Authorization": "Bearer test-key"} H = {"Authorization": "Bearer test-key"}
@@ -34,7 +35,7 @@ def test_index_shows_account_anchor(client, db):
r = client.get("/") r = client.get("/")
assert r.status_code == 200 assert r.status_code == 200
assert "Anker: 321.00" in r.text assert "Anker: 321,00" in r.text
assert "30.06.2026" in r.text assert "30.06.2026" in r.text
assert "kein Anker" in r.text assert "kein Anker" in r.text
# Inline-Pflegeformular fuer den Anker muss je Konto vorhanden sein. # Inline-Pflegeformular fuer den Anker muss je Konto vorhanden sein.
@@ -42,6 +43,29 @@ def test_index_shows_account_anchor(client, db):
assert 'name="anchor_balance"' in r.text assert 'name="anchor_balance"' in r.text
def test_gui_zeigt_deutsche_betragsformate(client, db):
client.post("/login", data={"username": "admin", "password": "geheim"})
acc = Account(bank="dkb", iban="DE-FMT-1", name="Formatkonto", type="giro")
db.add(acc)
db.flush()
acc.anchor_date = date(2026, 6, 30)
acc.anchor_balance = Decimal("12345.60")
db.commit()
r = client.get("/")
assert "12.345,60" in r.text
assert "12345.60" not in r.text
def test_planung_zeigt_deutsche_labels(client, db):
client.post("/login", data={"username": "admin", "password": "geheim"})
db.add(RecurringItem(name="Miete-Label-Test", amount=Decimal("-600.00"),
rhythm="monthly", due_day=1))
db.commit()
r = client.get("/planung")
assert "monatlich" in r.text
assert "-600,00" in r.text
def test_import_page_has_dropzone_and_list(client): def test_import_page_has_dropzone_and_list(client):
client.post("/login", data={"username": "admin", "password": "geheim"}) client.post("/login", data={"username": "admin", "password": "geheim"})
r = client.get("/import") r = client.get("/import")
@@ -234,7 +258,7 @@ def test_salden_page_stichtag_and_month_overview(client, db):
expected_stichtag = Decimal("1000.00") + Decimal("111.11") + Decimal("22.22") expected_stichtag = Decimal("1000.00") + Decimal("111.11") + Decimal("22.22")
r = client.get(f"/salden?stichtag={stichtag.isoformat()}") r = client.get(f"/salden?stichtag={stichtag.isoformat()}")
assert r.status_code == 200 assert r.status_code == 200
assert f"{expected_stichtag:.2f}" in r.text assert eur(expected_stichtag) in r.text
# Monatsuebersicht: genau 3 Monatszeilen (m2, m1, m0). # Monatsuebersicht: genau 3 Monatszeilen (m2, m1, m0).
assert r.text.count('data-month="') == 3 assert r.text.count('data-month="') == 3
@@ -245,7 +269,7 @@ def test_salden_page_stichtag_and_month_overview(client, db):
# m2), d.h. NACH Buchung 1 (in m2), aber VOR Buchung 2 (die faellt erst # m2), d.h. NACH Buchung 1 (in m2), aber VOR Buchung 2 (die faellt erst
# in m1 selbst und darf den Monatsanfang nicht verfaelschen). # in m1 selbst und darf den Monatsanfang nicht verfaelschen).
expected_m1_start = Decimal("1000.00") + Decimal("111.11") expected_m1_start = Decimal("1000.00") + Decimal("111.11")
assert f"{expected_m1_start:.2f}" in r.text assert eur(expected_m1_start) in r.text
def test_salden_stichtag_garbage_falls_back_to_today(client): def test_salden_stichtag_garbage_falls_back_to_today(client):