feat: Konto-Saldo-Anker mit ankerbasierter Saldenrechnung
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,26 +1,53 @@
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.tables import Account, Statement, Transaction
|
||||
from app.models.tables import Account, Transaction
|
||||
|
||||
|
||||
def account_balance(session: Session, account: Account) -> Decimal:
|
||||
stmt = session.execute(
|
||||
select(Statement).where(Statement.account_id == account.id,
|
||||
Statement.status == "confirmed")
|
||||
.order_by(Statement.period_end.desc())).scalars().first()
|
||||
def _confirmed_sum(session: Session, account_id: int,
|
||||
after: date | None, upto: date | None) -> Decimal:
|
||||
"""Summe bestaetigter Buchungen fuer `account_id` mit `after < booking_date
|
||||
<= upto` (Raender werden nur gesetzt, wenn nicht None)."""
|
||||
q = select(func.coalesce(func.sum(Transaction.amount), 0)).where(
|
||||
Transaction.account_id == account.id, Transaction.status == "confirmed")
|
||||
if stmt is not None:
|
||||
q = q.where(Transaction.booking_date > stmt.period_end)
|
||||
base = stmt.closing_balance
|
||||
else:
|
||||
base = Decimal("0")
|
||||
return Decimal(base) + Decimal(session.execute(q).scalar_one())
|
||||
Transaction.account_id == account_id, Transaction.status == "confirmed")
|
||||
if after is not None:
|
||||
q = q.where(Transaction.booking_date > after)
|
||||
if upto is not None:
|
||||
q = q.where(Transaction.booking_date <= upto)
|
||||
return Decimal(session.execute(q).scalar_one())
|
||||
|
||||
|
||||
def total_balance(session: Session) -> Decimal:
|
||||
def account_balance(session: Session, account: Account, at: date | None = None) -> Decimal:
|
||||
"""Kontostand am Ende des Tages `at` (Default: heute).
|
||||
|
||||
Konto-Saldo-Anker (Ausbaustufe 3 Task 1) ersetzt die bisherige
|
||||
Statement-closing-Logik ersatzlos. Konvention: der Anker gilt PER
|
||||
TAGESENDE des Ankerdatums - er enthaelt bereits alle Buchungen bis
|
||||
einschliesslich diesem Tag.
|
||||
|
||||
Mit Anker: anchor_balance + Sum(tx: anchor_date < booking_date <= at)
|
||||
- Sum(tx: at < booking_date <= anchor_date)
|
||||
(jeweils nur eine der beiden Summen ist nicht-leer, je nachdem ob `at`
|
||||
vor oder nach dem Anker liegt; bei at == anchor_date sind beide leer.)
|
||||
|
||||
Ohne Anker: Sum(tx: booking_date <= at) - bisheriges Verhalten mit Basis 0.
|
||||
"""
|
||||
if at is None:
|
||||
at = date.today()
|
||||
|
||||
if account.anchor_date is not None and account.anchor_balance is not None:
|
||||
anchor_date = account.anchor_date
|
||||
anchor_balance = Decimal(account.anchor_balance)
|
||||
if at >= anchor_date:
|
||||
return anchor_balance + _confirmed_sum(session, account.id, anchor_date, at)
|
||||
return anchor_balance - _confirmed_sum(session, account.id, at, anchor_date)
|
||||
|
||||
return _confirmed_sum(session, account.id, None, at)
|
||||
|
||||
|
||||
def total_balance(session: Session, at: date | None = None) -> Decimal:
|
||||
accounts = session.execute(select(Account)).scalars().all()
|
||||
return sum((account_balance(session, a) for a in accounts), Decimal("0"))
|
||||
return sum((account_balance(session, a, at) for a in accounts), Decimal("0"))
|
||||
|
||||
Reference in New Issue
Block a user