feat: Budget – mehrere Monate nebeneinander + Kopieren Monat<->Vorlage
- Umschalter 1·2·3: Monate nebeneinander (responsives Raster, auf schmalem
Schirm gestapelt); prev/next verschiebt das ganze Fenster
- Je Monat: „Als Standard festlegen" (Monat -> Vorlage) und „Standard
übernehmen" (Vorlage -> Monat, überschreibt), plus Zurücksetzen
- Backend: copy_month_to_template / copy_template_to_month + Endpunkte
POST /budget/template/from-month/{label} und /budget/{label}/from-template
- Test fürs Kopieren; globale .grow-Utility ergänzt
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -254,3 +254,26 @@ def reset_month(
|
||||
budget.parse_label(label)
|
||||
budget.reset_month(db, user, label)
|
||||
return _build_view(db, user, label)
|
||||
|
||||
|
||||
# --- Kopieren Monat <-> Vorlage --------------------------------------------
|
||||
|
||||
|
||||
@router.post("/template/from-month/{label}")
|
||||
def template_from_month(
|
||||
label: str, db: Session = Depends(get_db), user: User = Depends(get_current_user)
|
||||
) -> dict:
|
||||
"""„Diesen Monat als Standard festlegen": Monat -> Vorlage."""
|
||||
budget.parse_label(label)
|
||||
budget.copy_month_to_template(db, user, label)
|
||||
return get_template(db, user)
|
||||
|
||||
|
||||
@router.post("/{label}/from-template")
|
||||
def month_from_template(
|
||||
label: str, db: Session = Depends(get_db), user: User = Depends(get_current_user)
|
||||
) -> dict:
|
||||
"""„Standard in diesen Monat übernehmen": Vorlage -> Monat (überschreibt)."""
|
||||
budget.parse_label(label)
|
||||
budget.copy_template_to_month(db, user, label)
|
||||
return _build_view(db, user, label)
|
||||
|
||||
@@ -132,6 +132,36 @@ def reset_month(db: Session, user: User, label: str) -> None:
|
||||
db.commit()
|
||||
|
||||
|
||||
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}
|
||||
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))
|
||||
db.commit()
|
||||
|
||||
|
||||
def copy_template_to_month(db: Session, user: User, label: str) -> BudgetPeriod:
|
||||
"""Überschreibt den Monat mit der aktuellen Vorlage (auch wenn er schon
|
||||
angepasst war): vorhandene Monatszeilen weg, Vorlage frisch übernehmen."""
|
||||
period = _period(db, user, label)
|
||||
if period is None:
|
||||
period = BudgetPeriod(owner_id=user.id, label=label, customized=True)
|
||||
db.add(period)
|
||||
db.flush()
|
||||
else:
|
||||
for ln in list(period.lines):
|
||||
db.delete(ln)
|
||||
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))
|
||||
db.commit()
|
||||
return period
|
||||
|
||||
|
||||
# --- Ist-Werte -------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -83,6 +83,30 @@ def test_budget_soll_ist_and_override(admin):
|
||||
assert v["customized"] is False
|
||||
|
||||
|
||||
def test_budget_copy_month_and_template(admin):
|
||||
admin.put("/api/settings/budget_start_day", params={"value": "1"})
|
||||
today = date.today()
|
||||
label = f"{today.year:04d}-{today.month:02d}"
|
||||
leb = _lebensmittel(admin)
|
||||
|
||||
admin.put(f"/api/budget/template/category/{leb}", params={"planned_cents": 50000})
|
||||
# Monat individuell auf 700.
|
||||
admin.put(f"/api/budget/{label}/category/{leb}", params={"planned_cents": 70000})
|
||||
|
||||
# Monat -> Vorlage: Vorlage übernimmt den 700er Wert.
|
||||
r = admin.post(f"/api/budget/template/from-month/{label}")
|
||||
assert r.status_code == 200, r.text
|
||||
tpl = r.json()
|
||||
assert next(l for l in tpl["lines"] if l["category_id"] == leb)["planned_cents"] == 70000
|
||||
|
||||
# Vorlage auf 900 ändern, dann Vorlage -> Monat (überschreibt den angepassten Monat).
|
||||
admin.put(f"/api/budget/template/category/{leb}", params={"planned_cents": 90000})
|
||||
v = admin.post(f"/api/budget/{label}/from-template").json()
|
||||
row = next(c for g in v["groups"] for c in g["categories"] if c["category_id"] == leb)
|
||||
assert row["planned_cents"] == 90000
|
||||
assert v["customized"] is True
|
||||
|
||||
|
||||
def test_planned_income(admin):
|
||||
admin.put("/api/settings/budget_start_day", params={"value": "1"})
|
||||
today = date.today()
|
||||
|
||||
Reference in New Issue
Block a user