feat: financial year planning — annual budgets, income tracking, household sharing
- Financial year page (/financial-year): year selector, 3 KPI cards (income, fixed costs, actual expenses), income and budget-items tabs with inline CRUD - Revenue accounts as income source: salary-months toggle (12/13) per account - Household support: create household, invite members by email (existing and new users via PendingHouseholdInvite), accept invitations, set roles - Combined household income view across all active members - FinancialYear, YearlyIncome, YearlyBudgetItem, Household, HouseholdMembership models with migrations; household invite email template - Management command to migrate existing accounts/budgets to financial years - FinancialYearService in Angular with full API integration - Dashboard updated: income/fixed-costs read from financial year data, year dropdown synced with available financial years - Sidebar: financial year nav item added - i18n: all keys in DE/EN/FR/IT
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
from rest_framework import serializers
|
||||
from django.contrib.auth import get_user_model
|
||||
from .models import Account, Transaction, Budget, Expense, Profile, Deadline
|
||||
from .models import (
|
||||
Account, Transaction, Budget, Expense, Profile, Deadline,
|
||||
Household, HouseholdMembership, PendingHouseholdInvite,
|
||||
FinancialYear, YearlyIncome, YearlyBudgetItem,
|
||||
)
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@@ -44,6 +48,10 @@ class ExpenseSerializer(serializers.ModelSerializer):
|
||||
|
||||
class ProfileSerializer(serializers.ModelSerializer):
|
||||
totp_enabled = serializers.BooleanField(read_only=True)
|
||||
email = serializers.SerializerMethodField()
|
||||
|
||||
def get_email(self, obj):
|
||||
return obj.email or (obj.user.email if obj.user else '')
|
||||
|
||||
class Meta:
|
||||
model = Profile
|
||||
@@ -56,6 +64,76 @@ class DeadlineSerializer(serializers.ModelSerializer):
|
||||
exclude = ['user']
|
||||
|
||||
|
||||
class HouseholdMembershipSerializer(serializers.ModelSerializer):
|
||||
user_email = serializers.EmailField(source='user.email', read_only=True)
|
||||
invited_by_email = serializers.EmailField(source='invited_by.email', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = HouseholdMembership
|
||||
fields = ['id', 'user', 'user_email', 'invited_by_email', 'status', 'role',
|
||||
'effective_from_year', 'effective_until_year', 'created_at']
|
||||
read_only_fields = ['id', 'user', 'user_email', 'invited_by_email', 'created_at']
|
||||
|
||||
|
||||
class PendingHouseholdInviteSerializer(serializers.ModelSerializer):
|
||||
invited_by_email = serializers.EmailField(source='invited_by.email', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PendingHouseholdInvite
|
||||
fields = ['id', 'invited_email', 'invited_by_email', 'effective_from_year', 'created_at']
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class HouseholdSerializer(serializers.ModelSerializer):
|
||||
memberships = HouseholdMembershipSerializer(many=True, read_only=True)
|
||||
pending_invites = PendingHouseholdInviteSerializer(many=True, read_only=True)
|
||||
created_by_email = serializers.EmailField(source='created_by.email', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Household
|
||||
fields = ['id', 'name', 'created_by_email', 'memberships', 'pending_invites', 'created_at']
|
||||
read_only_fields = ['id', 'created_by_email', 'memberships', 'pending_invites', 'created_at']
|
||||
|
||||
|
||||
class YearlyIncomeSerializer(serializers.ModelSerializer):
|
||||
member_email = serializers.EmailField(source='member.email', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = YearlyIncome
|
||||
fields = ['id', 'member', 'member_email', 'name', 'amount', 'active', 'notes']
|
||||
read_only_fields = ['id', 'member_email']
|
||||
|
||||
|
||||
class YearlyBudgetItemSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = YearlyBudgetItem
|
||||
fields = ['id', 'name', 'amount', 'active', 'notes']
|
||||
read_only_fields = ['id']
|
||||
|
||||
|
||||
class FinancialYearSerializer(serializers.ModelSerializer):
|
||||
incomes = YearlyIncomeSerializer(many=True, read_only=True)
|
||||
budget_items = YearlyBudgetItemSerializer(many=True, read_only=True)
|
||||
total_income = serializers.SerializerMethodField()
|
||||
total_fixed_costs = serializers.SerializerMethodField()
|
||||
owner_type = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = FinancialYear
|
||||
fields = ['id', 'year', 'is_active', 'notes', 'owner_type', 'household_id',
|
||||
'total_income', 'total_fixed_costs', 'incomes', 'budget_items', 'created_at']
|
||||
read_only_fields = ['id', 'created_at', 'owner_type', 'household_id', 'total_income', 'total_fixed_costs']
|
||||
|
||||
def get_total_income(self, obj):
|
||||
return sum(i.amount for i in obj.incomes.filter(active=True))
|
||||
|
||||
def get_total_fixed_costs(self, obj):
|
||||
return sum(b.amount for b in obj.budget_items.filter(active=True))
|
||||
|
||||
def get_owner_type(self, obj):
|
||||
return 'household' if obj.household_id else 'personal'
|
||||
|
||||
|
||||
class RegisterSerializer(serializers.Serializer):
|
||||
email = serializers.EmailField()
|
||||
password = serializers.CharField(min_length=8, write_only=True)
|
||||
@@ -67,8 +145,21 @@ class RegisterSerializer(serializers.Serializer):
|
||||
|
||||
def create(self, validated_data):
|
||||
email = validated_data['email']
|
||||
return User.objects.create_user(
|
||||
user = User.objects.create_user(
|
||||
username=email,
|
||||
email=email,
|
||||
password=validated_data['password'],
|
||||
)
|
||||
from .models import PendingHouseholdInvite, HouseholdMembership
|
||||
for invite in PendingHouseholdInvite.objects.filter(invited_email__iexact=email):
|
||||
HouseholdMembership.objects.get_or_create(
|
||||
household=invite.household,
|
||||
user=user,
|
||||
defaults={
|
||||
'invited_by': invite.invited_by,
|
||||
'status': 'pending',
|
||||
'effective_from_year': invite.effective_from_year,
|
||||
},
|
||||
)
|
||||
invite.delete()
|
||||
return user
|
||||
|
||||
Reference in New Issue
Block a user