fix: Eine berechnete Budget-Zeile ueberlebt jetzt das Kopieren
Beide Kopierwege uebernahmen nur planned_cents. Ein Eintrag der Betriebsart "berechnet" fiel dabei still auf "fest" zurueck - mit der Zahl, die zufaellig zuletzt in planned_cents stand. Der Bezug zu den Vertraegen war danach weg, ohne dass irgendwo stand, dass er weg ist. Vorlage -> Monat (copy_template_to_month) zieht jetzt nach, was ensure_customized nebenan laengst richtig macht. Monat -> Vorlage (copy_month_to_template) nimmt die Betriebsart mit, statt die gerade herauskommende Zahl einzufrieren: Eine berechnete Zeile soll auch in der Vorlage berechnet bleiben. Dabei aufgefallen: Eine berechnete Zeile gehoert auch dann in die Vorlage, wenn planned_cents null ist - ihre Zahl steht in den Vertraegen. Eine feste Null bleibt dagegen draussen, sonst fuellte sich die Vorlage mit Nullzeilen fuer jede je beruehrte Kategorie. Das war Backend-Ergaenzung D und damit die letzte offene. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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:
|
def copy_month_to_template(db: Session, user: User, label: str) -> None:
|
||||||
"""Setzt die Standard-Vorlage auf die wirksamen Soll-Werte dieses Monats
|
"""Setzt die Standard-Vorlage auf die Soll-Werte dieses Monats
|
||||||
(„diesen Monat als Standard festlegen"). Einnahmen bleiben unberührt."""
|
(„diesen Monat als Standard festlegen"). Einnahmen bleiben unberührt.
|
||||||
planned = effective_planned(db, user, label) # {category_id: cents}
|
|
||||||
|
Ü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()
|
db.query(BudgetTemplateLine).filter(BudgetTemplateLine.owner_id == user.id).delete()
|
||||||
for category_id, cents in planned.items():
|
for zeile in zeilen:
|
||||||
if cents > 0:
|
# Eine feste Null ist nichts Geplantes und gehört nicht in die Vorlage.
|
||||||
db.add(BudgetTemplateLine(owner_id=user.id, category_id=category_id, planned_cents=cents))
|
# 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()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
@@ -241,7 +255,13 @@ def copy_template_to_month(db: Session, user: User, label: str) -> BudgetPeriod:
|
|||||||
db.flush()
|
db.flush()
|
||||||
period.customized = True
|
period.customized = True
|
||||||
for tl in db.query(BudgetTemplateLine).filter(BudgetTemplateLine.owner_id == user.id):
|
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()
|
db.commit()
|
||||||
return period
|
return period
|
||||||
|
|
||||||
|
|||||||
@@ -399,3 +399,66 @@ def test_vorlage_nennt_betriebsart_und_zuschlag(admin):
|
|||||||
assert zeile["planned_cents"] == 12000
|
assert zeile["planned_cents"] == 12000
|
||||||
assert zeile["modus"] == "berechnet"
|
assert zeile["modus"] == "berechnet"
|
||||||
assert zeile["zuschlag_cents"] == 2500
|
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"])
|
||||||
|
|||||||
Reference in New Issue
Block a user