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:
@@ -0,0 +1,85 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from finance.models import Account, Budget, FinancialYear, YearlyIncome, YearlyBudgetItem
|
||||
|
||||
User = get_user_model()
|
||||
TARGET_YEAR = 2026
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Migrate existing revenue accounts and budgets into FinancialYear 2026. Idempotent — safe to run multiple times.'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='Preview what would be created without writing to the database.',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
dry_run = options['dry_run']
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING('DRY RUN — no changes will be saved.\n'))
|
||||
|
||||
total_incomes = 0
|
||||
total_budgets = 0
|
||||
|
||||
for user in User.objects.all():
|
||||
revenue_accounts = Account.objects.filter(user=user, account_type='revenue', active=True)
|
||||
budgets = Budget.objects.filter(account__user=user, active=True)
|
||||
|
||||
if not revenue_accounts.exists() and not budgets.exists():
|
||||
continue
|
||||
|
||||
self.stdout.write(f'\nUser: {user.email}')
|
||||
|
||||
if dry_run:
|
||||
fy = FinancialYear.objects.filter(user=user, year=TARGET_YEAR).first()
|
||||
if fy:
|
||||
self.stdout.write(f' FinancialYear {TARGET_YEAR} already exists (id={fy.pk})')
|
||||
else:
|
||||
self.stdout.write(f' Would create FinancialYear {TARGET_YEAR}')
|
||||
else:
|
||||
fy, fy_created = FinancialYear.objects.get_or_create(user=user, year=TARGET_YEAR)
|
||||
if fy_created:
|
||||
self.stdout.write(f' Created FinancialYear {TARGET_YEAR} (id={fy.pk})')
|
||||
else:
|
||||
self.stdout.write(f' FinancialYear {TARGET_YEAR} exists (id={fy.pk})')
|
||||
|
||||
for account in revenue_accounts:
|
||||
label = f'YearlyIncome "{account.name}" CHF {account.balance}'
|
||||
if dry_run:
|
||||
exists = fy and YearlyIncome.objects.filter(financial_year=fy, name=account.name).exists()
|
||||
self.stdout.write(f' {"SKIP (exists)" if exists else "Would create"}: {label}')
|
||||
else:
|
||||
_, created = YearlyIncome.objects.get_or_create(
|
||||
financial_year=fy,
|
||||
name=account.name,
|
||||
defaults={'amount': account.balance, 'member': user, 'active': True},
|
||||
)
|
||||
self.stdout.write(f' {"Created" if created else "Skipped (exists)"}: {label}')
|
||||
if created:
|
||||
total_incomes += 1
|
||||
|
||||
for budget in budgets:
|
||||
label = f'YearlyBudgetItem "{budget.name}" CHF {budget.amount}'
|
||||
if dry_run:
|
||||
exists = fy and YearlyBudgetItem.objects.filter(financial_year=fy, name=budget.name).exists()
|
||||
self.stdout.write(f' {"SKIP (exists)" if exists else "Would create"}: {label}')
|
||||
else:
|
||||
_, created = YearlyBudgetItem.objects.get_or_create(
|
||||
financial_year=fy,
|
||||
name=budget.name,
|
||||
defaults={'amount': budget.amount, 'active': budget.active},
|
||||
)
|
||||
self.stdout.write(f' {"Created" if created else "Skipped (exists)"}: {label}')
|
||||
if created:
|
||||
total_budgets += 1
|
||||
|
||||
if not dry_run:
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f'\nDone. Created {total_incomes} income(s) and {total_budgets} budget item(s).'
|
||||
))
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING('\nDry run complete. Re-run without --dry-run to apply.'))
|
||||
Reference in New Issue
Block a user