Files
WYGIWYH/app/apps/api/views/accounts.py
T
Herculino Trotta 359e5880cc feat(investments): add investment tracker
Investments are accounts flagged as investments; performance is derived from their transactions. Linked transfers are contributions and withdrawals (average cost, realized gains), unlinked transactions are gains or losses, and income comes from the accounts' income entities.

- Unit-priced (exchange currency) or value-tracked accounts
- Money-weighted return (XIRR), annualized after a year
- Totals converted along each currency's exchange chain, with currency effect
- Investments list and detail pages; buy/sell/income use the transfer and transaction forms
- GET /api/accounts/<id>/performance/
2026-11-05 11:07:32 -03:00

99 lines
3.4 KiB
Python

from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from apps.accounts.models import AccountGroup, Account
from apps.accounts.services import get_account_balance
from apps.api.permissions import SHARED_OBJECT_PERMISSIONS
from apps.api.serializers import (
AccountGroupSerializer,
AccountSerializer,
AccountBalanceSerializer,
InvestmentPerformanceSerializer,
)
from apps.investments.services import get_holding_performance
class AccountGroupViewSet(viewsets.ModelViewSet):
"""ViewSet for managing account groups."""
permission_classes = SHARED_OBJECT_PERMISSIONS
queryset = AccountGroup.objects.all()
serializer_class = AccountGroupSerializer
filterset_fields = {
"name": ["exact", "icontains"],
"owner": ["exact"],
}
search_fields = ["name"]
ordering_fields = "__all__"
ordering = ["id"]
def get_queryset(self):
return AccountGroup.objects.all()
@extend_schema_view(
balance=extend_schema(
summary="Get account balance",
description="Returns the current and projected balance for the account, along with currency data.",
responses={200: AccountBalanceSerializer},
),
performance=extend_schema(
summary="Get investment performance",
description="Returns quantity, cost basis, market value, realized/unrealized gains, income and total return, in the account's exchange currency (or its own currency when it has none).",
responses={200: InvestmentPerformanceSerializer},
),
)
class AccountViewSet(viewsets.ModelViewSet):
"""ViewSet for managing accounts."""
permission_classes = SHARED_OBJECT_PERMISSIONS
queryset = Account.objects.all()
serializer_class = AccountSerializer
filterset_fields = {
"name": ["exact", "icontains"],
"group": ["exact", "isnull"],
"currency": ["exact"],
"exchange_currency": ["exact", "isnull"],
"is_asset": ["exact"],
"is_investment": ["exact"],
"is_archived": ["exact"],
"owner": ["exact"],
}
search_fields = ["name"]
ordering_fields = "__all__"
ordering = ["id"]
def get_queryset(self):
return Account.objects.all().select_related(
"group", "currency", "exchange_currency"
)
@action(detail=True, methods=["get"], permission_classes=[IsAuthenticated])
def balance(self, request, pk=None):
"""Get current and projected balance for an account."""
account = self.get_object()
current_balance = get_account_balance(account, paid_only=True)
projected_balance = get_account_balance(account, paid_only=False)
serializer = AccountBalanceSerializer(
{
"current_balance": current_balance,
"projected_balance": projected_balance,
"currency": account.currency,
}
)
return Response(serializer.data)
@action(detail=True, methods=["get"], permission_classes=[IsAuthenticated])
def performance(self, request, pk=None):
"""Get investment performance (cost basis, value, returns) for an account."""
account = self.get_object()
return Response(
InvestmentPerformanceSerializer(get_holding_performance(account)).data
)