diff --git a/backend/app/services/budget.py b/backend/app/services/budget.py index cab666b..ba3f5ec 100644 --- a/backend/app/services/budget.py +++ b/backend/app/services/budget.py @@ -217,13 +217,27 @@ def reset_month(db: Session, user: User, label: str) -> None: def copy_month_to_template(db: Session, user: User, label: str) -> None: - """Setzt die Standard-Vorlage auf die wirksamen Soll-Werte dieses Monats - („diesen Monat als Standard festlegen"). Einnahmen bleiben unberührt.""" - planned = effective_planned(db, user, label) # {category_id: cents} + """Setzt die Standard-Vorlage auf die Soll-Werte dieses Monats + („diesen Monat als Standard festlegen"). Einnahmen bleiben unberührt. + + Übernommen wird die **Betriebsart**, nicht die gerade herauskommende Zahl: + Eine berechnete Zeile soll auch in der Vorlage berechnet bleiben. Vorher + fror dieser Weg den Monatswert als festen Betrag ein – der Bezug zu den + Verträgen war danach still weg, und niemand sah, wo er geblieben war. + """ + zeilen = list(_zeilen(db, user, label)) db.query(BudgetTemplateLine).filter(BudgetTemplateLine.owner_id == user.id).delete() - for category_id, cents in planned.items(): - if cents > 0: - db.add(BudgetTemplateLine(owner_id=user.id, category_id=category_id, planned_cents=cents)) + for zeile in zeilen: + # Eine feste Null ist nichts Geplantes und gehört nicht in die Vorlage. + # Eine berechnete Zeile dagegen schon: Ihre Zahl steht in den + # Verträgen, nicht in planned_cents. + if zeile.modus != "berechnet" and zeile.planned_cents <= 0: + continue + db.add(BudgetTemplateLine( + owner_id=user.id, category_id=zeile.category_id, + planned_cents=zeile.planned_cents, modus=zeile.modus, + zuschlag_cents=zeile.zuschlag_cents, + )) db.commit() @@ -241,7 +255,13 @@ def copy_template_to_month(db: Session, user: User, label: str) -> BudgetPeriod: db.flush() period.customized = True for tl in db.query(BudgetTemplateLine).filter(BudgetTemplateLine.owner_id == user.id): - period.lines.append(BudgetPeriodLine(category_id=tl.category_id, planned_cents=tl.planned_cents)) + period.lines.append(BudgetPeriodLine( + category_id=tl.category_id, planned_cents=tl.planned_cents, + # Wie in ``ensure_customized``: Ohne diese beiden fiele eine + # berechnete Zeile auf „fest" zurueck, mit der Zahl, die zufaellig + # zuletzt in planned_cents stand. + modus=tl.modus, zuschlag_cents=tl.zuschlag_cents, + )) db.commit() return period diff --git a/backend/tests/test_budget.py b/backend/tests/test_budget.py index e2ac360..6af40e3 100644 --- a/backend/tests/test_budget.py +++ b/backend/tests/test_budget.py @@ -399,3 +399,66 @@ def test_vorlage_nennt_betriebsart_und_zuschlag(admin): assert zeile["planned_cents"] == 12000 assert zeile["modus"] == "berechnet" assert zeile["zuschlag_cents"] == 2500 + + +def _frische_kategorie(admin, name): + import secrets + + gruppe = next(g for g in admin.get("/api/categories").json() + if g["kind"] == "expense") + return admin.post("/api/categories", json={ + "group_id": gruppe["id"], "name": f"{name} {secrets.token_hex(3)}"}).json()["id"] + + +def test_berechnete_zeile_ueberlebt_den_weg_in_den_monat(admin): + """Vorlage -> Monat darf die Betriebsart nicht verlieren. + + Fiel die Zeile auf "fest" zurueck, stand als Soll die Zahl, die zufaellig + zuletzt in planned_cents lag - und der Bezug zu den Vertraegen war weg, + ohne dass irgendwo stand, dass er weg ist. + """ + today = date.today() + label = f"{today.year:04d}-{today.month:02d}" + kat = _frische_kategorie(admin, "Berechnet-Monat") + + admin.put(f"/api/budget/template/category/{kat}", + params={"planned_cents": 0, "modus": "berechnet", "zuschlag_cents": 1500}) + + r = admin.post(f"/api/budget/{label}/from-template") + assert r.status_code == 200, r.text + + zeile = next(c for g in r.json()["groups"] for c in g["categories"] + if c["category_id"] == kat) + assert zeile["modus"] == "berechnet" + assert zeile["zuschlag_cents"] == 1500 + + +def test_berechnete_zeile_ueberlebt_den_weg_in_die_vorlage(admin): + """Monat -> Vorlage ebenso: "diesen Monat als Standard festlegen" soll die + Betriebsart uebernehmen, nicht die gerade herauskommende Zahl einfrieren.""" + today = date.today() + label = f"{today.year:04d}-{today.month:02d}" + kat = _frische_kategorie(admin, "Berechnet-Vorlage") + + admin.put(f"/api/budget/{label}/category/{kat}", + params={"planned_cents": 0, "modus": "berechnet", "zuschlag_cents": 2500}) + + r = admin.post(f"/api/budget/template/from-month/{label}") + assert r.status_code == 200, r.text + + zeile = next(l for l in r.json()["lines"] if l["category_id"] == kat) + assert zeile["modus"] == "berechnet" + assert zeile["zuschlag_cents"] == 2500 + + +def test_eine_feste_null_landet_nicht_in_der_vorlage(admin): + """Nichts Geplantes bleibt nichts Geplantes - sonst fuellte sich die + Vorlage mit Nullzeilen fuer jede je beruehrte Kategorie.""" + today = date.today() + label = f"{today.year:04d}-{today.month:02d}" + kat = _frische_kategorie(admin, "Nullzeile") + + admin.put(f"/api/budget/{label}/category/{kat}", params={"planned_cents": 0}) + r = admin.post(f"/api/budget/template/from-month/{label}") + + assert all(l["category_id"] != kat for l in r.json()["lines"])