from datetime import date def test_period_bounds_and_labels(): from app.services import budget # Gehalt am 25.: September-Budget läuft 25.08.–24.09. assert budget.period_bounds("2026-09", 25) == (date(2026, 8, 25), date(2026, 9, 24)) # Starttag 1: Kalendermonat. assert budget.period_bounds("2026-09", 1) == (date(2026, 9, 1), date(2026, 9, 30)) # Label-Zuordnung assert budget.period_of(date(2026, 8, 27), 25) == "2026-09" assert budget.period_of(date(2026, 9, 10), 25) == "2026-09" assert budget.period_of(date(2026, 9, 26), 25) == "2026-10" assert budget.period_of(date(2026, 9, 15), 1) == "2026-09" assert budget.shift_label("2026-09", 1) == "2026-10" assert budget.shift_label("2026-01", -1) == "2025-12" def _lebensmittel(admin): groups = admin.get("/api/categories").json() for g in groups: for c in g["categories"]: if c["name"] == "Lebensmittel": return c["id"] raise AssertionError("Lebensmittel-Kategorie fehlt") def _gehalt(admin): groups = admin.get("/api/categories").json() for g in groups: for c in g["categories"]: if c["name"] == "Gehalt": return c["id"] raise AssertionError("Gehalt-Kategorie fehlt") def test_budget_soll_ist_and_override(admin): # Starttag 1 -> aktueller Zeitraum = Kalendermonat, heutige Buchungen zählen. admin.put("/api/settings/budget_start_day", params={"value": "1"}) today = date.today() label = f"{today.year:04d}-{today.month:02d}" acc = admin.post("/api/accounts", json={"name": "Budgetkonto", "currency": "EUR"}).json() leb = _lebensmittel(admin) # Standard-Budget für Lebensmittel: 500,00 EUR. admin.put(f"/api/budget/template/category/{leb}", params={"planned_cents": 50000}) # Ausgabe 300,00 EUR heute. admin.post("/api/transactions", json={"type": "expense", "date": str(today), "account_id": acc["id"], "amount_cents": 30000, "category_id": leb}) v = admin.get("/api/budget/current").json() row = next(c for g in v["groups"] for c in g["categories"] if c["category_id"] == leb) assert row["planned_cents"] == 50000 assert row["actual_cents"] == 30000 assert row["over"] is False assert v["customized"] is False # Noch 300 EUR -> insgesamt 600 > 500 -> über Budget. admin.post("/api/transactions", json={"type": "expense", "date": str(today), "account_id": acc["id"], "amount_cents": 30000, "category_id": leb}) v = admin.get(f"/api/budget/{label}").json() row = next(c for g in v["groups"] for c in g["categories"] if c["category_id"] == leb) assert row["actual_cents"] == 60000 assert row["over"] is True # Monats-Override auf 700 EUR -> nicht mehr über Budget, Monat individualisiert. v = admin.put(f"/api/budget/{label}/category/{leb}", params={"planned_cents": 70000}).json() row = next(c for g in v["groups"] for c in g["categories"] if c["category_id"] == leb) assert row["planned_cents"] == 70000 assert row["over"] is False assert v["customized"] is True # Vorlage bleibt unverändert. tpl = admin.get("/api/budget/template").json() tpl_leb = next(l for l in tpl["lines"] if l["category_id"] == leb) assert tpl_leb["planned_cents"] == 50000 # Zurücksetzen -> wieder Vorlagenwert, nicht mehr individualisiert. v = admin.delete(f"/api/budget/{label}").json() row = next(c for g in v["groups"] for c in g["categories"] if c["category_id"] == leb) assert row["planned_cents"] == 50000 assert v["customized"] is False def test_planned_income(admin): admin.put("/api/settings/budget_start_day", params={"value": "1"}) today = date.today() acc = admin.post("/api/accounts", json={"name": "Lohnkonto", "currency": "EUR"}).json() gehalt = _gehalt(admin) r = admin.post("/api/budget/income", json={ "label": "Gehalt", "category_id": gehalt, "target_account_id": acc["id"], "planned_cents": 300000, }) assert r.status_code == 201 # Tatsächliche Einnahme 3000 EUR. admin.post("/api/transactions", json={"type": "income", "date": str(today), "account_id": acc["id"], "amount_cents": 300000, "category_id": gehalt}) v = admin.get("/api/budget/current").json() line = next(i for i in v["income"] if i["label"] == "Gehalt") assert line["planned_cents"] == 300000 assert line["received_cents"] == 300000 assert v["totals"]["planned_income"] >= 300000