feat: Salden-Seite mit Stichtag und Monatsuebersicht
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,15 +3,16 @@ from decimal import Decimal
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.auth import COOKIE, session_valid
|
||||
from app.db import get_session
|
||||
from app.engine.loans import add_months
|
||||
from app.engine.recurrence import occurrences
|
||||
from app.models.tables import (Account, Category, PlannedItem,
|
||||
ProjectionResult, RecurringItem,
|
||||
ScenarioLoan, ScenarioModifier)
|
||||
ScenarioLoan, ScenarioModifier, Transaction)
|
||||
from app.routers.imports import list_imports as _list_imports
|
||||
from app.routers.imports import preview as _preview_import
|
||||
from app.routers.planning import list_loans as _list_loans
|
||||
@@ -84,6 +85,25 @@ def _opt_date(value: str | None) -> date | None:
|
||||
return None
|
||||
|
||||
|
||||
def _month_starts_desc(session: Session, today: date) -> list[date]:
|
||||
"""Liefert die Monatsersten von "heute" rueckwaerts bis zum Monat der
|
||||
fruehesten bestaetigten Buchung, absteigend sortiert (heute zuerst).
|
||||
Ohne bestaetigte Buchungen: leere Liste."""
|
||||
earliest = session.execute(
|
||||
select(func.min(Transaction.booking_date)).where(Transaction.status == "confirmed")
|
||||
).scalar_one_or_none()
|
||||
if earliest is None:
|
||||
return []
|
||||
start = date(earliest.year, earliest.month, 1)
|
||||
current = date(today.year, today.month, 1)
|
||||
months = []
|
||||
m = current
|
||||
while m >= start:
|
||||
months.append(m)
|
||||
m = add_months(m, -1)
|
||||
return months
|
||||
|
||||
|
||||
@router.get("/", dependencies=[Depends(gui_session)])
|
||||
def index(request: Request, session: Session = Depends(get_session)):
|
||||
accounts = session.execute(select(Account)).scalars().all()
|
||||
@@ -158,6 +178,50 @@ def buchungen(
|
||||
})
|
||||
|
||||
|
||||
@router.get("/salden", dependencies=[Depends(gui_session)])
|
||||
def salden_page(
|
||||
request: Request,
|
||||
stichtag: str | None = None,
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
# Lenient wie die Filter auf /buchungen: ein leerer oder kaputter
|
||||
# stichtag-Parameter fuehrt nie zu 422, sondern faellt auf "heute" zurueck.
|
||||
today = date.today()
|
||||
parsed_stichtag = _opt_date(stichtag) or today
|
||||
|
||||
accounts = session.execute(select(Account)).scalars().all()
|
||||
balances = [(a, account_balance(session, a, at=parsed_stichtag)) for a in accounts]
|
||||
total = sum((b for _, b in balances), Decimal("0"))
|
||||
|
||||
month_rows = []
|
||||
for month in _month_starts_desc(session, today):
|
||||
# Saldo am Monatsanfang = Stand am ENDE DES VORTAGS (letzter Tag des
|
||||
# Vormonats), NICHT account_balance(at=month) - sonst wuerden
|
||||
# Buchungen AM 1. bereits in den "Anfangs"-Saldo einfliessen und den
|
||||
# Ausweis verzerren. Siehe Fussnote im Template ("Stand: Ende des
|
||||
# Vortags").
|
||||
vortag = month - timedelta(days=1)
|
||||
values = [(a, account_balance(session, a, at=vortag)) for a in accounts]
|
||||
month_rows.append({
|
||||
"month": month,
|
||||
"values": values,
|
||||
"total": sum((v for _, v in values), Decimal("0")),
|
||||
})
|
||||
|
||||
accounts_without_anchor = [
|
||||
a for a in accounts if a.anchor_date is None or a.anchor_balance is None
|
||||
]
|
||||
|
||||
return templates.TemplateResponse(request, "salden.html", {
|
||||
"stichtag": parsed_stichtag,
|
||||
"balances": balances,
|
||||
"total": total,
|
||||
"accounts": accounts,
|
||||
"months": month_rows,
|
||||
"accounts_without_anchor": accounts_without_anchor,
|
||||
})
|
||||
|
||||
|
||||
@router.get("/import", dependencies=[Depends(gui_session)])
|
||||
def import_page(request: Request, session: Session = Depends(get_session)):
|
||||
return templates.TemplateResponse(request, "import.html", {
|
||||
|
||||
Reference in New Issue
Block a user