feat: Phase 2 – Budget-Engine + Direktzugriff (http://IP:Port)
Budget: - Modelle + Migration 0003: budget_template_lines, planned_income_lines, budget_periods, budget_period_lines. - services/budget: Perioden-Grenzen (einstellbarer Starttag, Label nach Endmonat), Standard-Vorlage vs. Monats-Override (Snapshot-on-Customize), Soll/Ist inkl. budgetierter Sparen-Transfers, geplante Einnahmen (Soll/Ist). - routers/budgets: Vorlage, geplante Einnahmen (CRUD), Monats-Soll/Ist, Monats-Override, Zurücksetzen. pytest: Perioden, Soll/Ist, Override/Reset, Einnahmen. - Frontend: Budget-Seite (Monat + Standard-Budget), Fortschrittsbalken grün/rot, Monats-Navigation, editierbare Soll-Beträge; Sparen-Option im Transfer-Formular. Deployment-Fix (Nutzer-Anforderung: http://IP:Port muss funktionieren): - Dienst bindet an APP_BIND_HOST=0.0.0.0 (direkt im LAN erreichbar). - APP_COOKIE_SECURE=false als Standard (sonst kein Login über reines HTTP); für reinen HTTPS-Betrieb umstellbar. Doku + install.sh entsprechend angepasst. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -289,3 +289,91 @@ class ExchangeRate(Base):
|
||||
currency: Mapped[str] = mapped_column(String(3), primary_key=True)
|
||||
rate_per_eur: Mapped[float] = mapped_column(Numeric(18, 8), nullable=False)
|
||||
source: Mapped[str] = mapped_column(String(16), nullable=False, default="ecb")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 2: Budget (ein Standard-Budget als Vorlage + Monats-Overrides)
|
||||
# Planned-Beträge sind in der Hauptwährung des Nutzers.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class BudgetTemplateLine(Base):
|
||||
"""Standard-Budget je Kategorie (die editierbare Vorlage)."""
|
||||
|
||||
__tablename__ = "budget_template_lines"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
owner_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
category_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("categories.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
planned_cents: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("owner_id", "category_id", name="uq_tpl_owner_category"),
|
||||
)
|
||||
|
||||
|
||||
class PlannedIncomeLine(Base):
|
||||
"""Geplante Einnahme (z. B. Gehalt) mit optionaler Kategorie und Zielkonto."""
|
||||
|
||||
__tablename__ = "planned_income_lines"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
owner_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
label: Mapped[str] = mapped_column(String(120), nullable=False)
|
||||
category_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("categories.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
target_account_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("accounts.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
planned_cents: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
position: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
|
||||
|
||||
class BudgetPeriod(Base):
|
||||
"""Ein Budget-Monat. Existiert erst, sobald der Monat angepasst wurde."""
|
||||
|
||||
__tablename__ = "budget_periods"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
owner_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
label: Mapped[str] = mapped_column(String(7), nullable=False) # YYYY-MM (End-/Hauptmonat)
|
||||
customized: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||
|
||||
lines: Mapped[list[BudgetPeriodLine]] = relationship(
|
||||
back_populates="period", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("owner_id", "label", name="uq_period_owner_label"),
|
||||
)
|
||||
|
||||
|
||||
class BudgetPeriodLine(Base):
|
||||
"""Monatsspezifischer Budget-Wert (Snapshot-on-Customize)."""
|
||||
|
||||
__tablename__ = "budget_period_lines"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
budget_period_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("budget_periods.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
category_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("categories.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
planned_cents: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
|
||||
period: Mapped[BudgetPeriod] = relationship(back_populates="lines")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("budget_period_id", "category_id", name="uq_periodline_period_category"),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user