fe4aeb3034
- 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
166 lines
6.0 KiB
Python
166 lines
6.0 KiB
Python
from rest_framework import serializers
|
|
from django.contrib.auth import get_user_model
|
|
from .models import (
|
|
Account, Transaction, Budget, Expense, Profile, Deadline,
|
|
Household, HouseholdMembership, PendingHouseholdInvite,
|
|
FinancialYear, YearlyIncome, YearlyBudgetItem,
|
|
)
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
class AccountSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = Account
|
|
exclude = ['user']
|
|
|
|
|
|
class TransactionSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = Transaction
|
|
fields = '__all__'
|
|
|
|
def validate(self, data):
|
|
request = self.context.get('request')
|
|
if not request:
|
|
return data
|
|
user = request.user
|
|
source = data.get('source_account') or (self.instance.source_account if self.instance else None)
|
|
dest = data.get('destination_account') or (self.instance.destination_account if self.instance else None)
|
|
if source and source.user != user:
|
|
raise serializers.ValidationError('Source account does not belong to you.')
|
|
if dest and dest.user != user:
|
|
raise serializers.ValidationError('Destination account does not belong to you.')
|
|
return data
|
|
|
|
|
|
class BudgetSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = Budget
|
|
fields = '__all__'
|
|
|
|
|
|
class ExpenseSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = Expense
|
|
fields = '__all__'
|
|
|
|
|
|
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
|
|
exclude = ['user', 'totp_secret', 'email_verify_token', 'email_verify_token_expires', 'password_reset_token_hash', 'password_reset_token_expires']
|
|
|
|
|
|
class DeadlineSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = Deadline
|
|
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)
|
|
|
|
def validate_email(self, value):
|
|
if User.objects.filter(email=value).exists():
|
|
raise serializers.ValidationError('Email already registered.')
|
|
return value
|
|
|
|
def create(self, validated_data):
|
|
email = validated_data['email']
|
|
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
|