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
86 lines
3.8 KiB
Python
86 lines
3.8 KiB
Python
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.'))
|