from django.db import models from django.conf import settings class Account(models.Model): # Typen basierend auf der Firefly III Logik ACCOUNT_TYPES = [ ('asset', 'Asset Account (Bank/Cash)'), ('expense', 'Expense Account (Laden/Empfänger)'), ('revenue', 'Revenue Account (Einnahmequelle)'), ] user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='accounts', null=True, ) name = models.CharField(max_length=100) account_type = models.CharField(max_length=20, choices=ACCOUNT_TYPES, default='asset') balance = models.DecimalField(max_digits=12, decimal_places=2, default=0.00) salary_months = models.IntegerField(default=12, choices=[(12, 12), (13, 13)]) active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return f"{self.name} ({self.get_account_type_display()})" class Transaction(models.Model): description = models.CharField(max_length=255) amount = models.DecimalField(max_digits=12, decimal_places=2) date = models.DateField() # Die Verknüpfung zu den Konten (Double-Entry Prinzip) source_account = models.ForeignKey( Account, on_delete=models.CASCADE, related_name='withdrawals' ) destination_account = models.ForeignKey( Account, on_delete=models.CASCADE, related_name='deposits' ) def __str__(self): return f"{self.date}: {self.description} ({self.amount}€)" class Budget(models.Model): MAIN_CATEGORY_CHOICES = [ ('fixed_expenses', 'Fixe Ausgaben'), ('mobile_internet', 'Mobile & Internet'), ('subscriptions', 'Abonnements'), ('leisure', 'Freizeit'), ('tax_reserves', 'Steuerrücklagen'), ('insurance', 'Versicherungen'), ('loans', 'Abzahlungen & Kredite'), ] name = models.CharField(max_length=100) amount = models.DecimalField(max_digits=12, decimal_places=2) main_category = models.CharField(max_length=50, choices=MAIN_CATEGORY_CHOICES, default='fixed_expenses') account = models.ForeignKey( Account, on_delete=models.CASCADE, related_name='budgets' ) active = models.BooleanField(default=True) def __str__(self): return f"{self.name} ({self.amount} CHF)" class Expense(models.Model): CATEGORY_CHOICES = [ ('groceries', 'Groceries'), ('dining', 'Dining & Restaurants'), ('transport', 'Transport'), ('health', 'Health & Medical'), ('clothing', 'Clothing'), ('electronics', 'Electronics'), ('household', 'Household'), ('entertainment', 'Entertainment'), ('travel', 'Travel'), ('other', 'Other'), ] name = models.CharField(max_length=255) amount = models.DecimalField(max_digits=12, decimal_places=2) date = models.DateField() category = models.CharField(max_length=50, choices=CATEGORY_CHOICES, default='other') account = models.ForeignKey( Account, on_delete=models.CASCADE, related_name='expenses' ) notes = models.TextField(blank=True, default='') due_date = models.DateField(blank=True, null=True) def __str__(self): return f"{self.date}: {self.name} ({self.amount} CHF)" CANTON_CHOICES = [ ('AG', 'Aargau'), ('AI', 'Appenzell Innerrhoden'), ('AR', 'Appenzell Ausserrhoden'), ('BE', 'Bern'), ('BL', 'Basel-Landschaft'), ('BS', 'Basel-Stadt'), ('FR', 'Fribourg'), ('GE', 'Geneva'), ('GL', 'Glarus'), ('GR', 'Graubünden'), ('JU', 'Jura'), ('LU', 'Lucerne'), ('NE', 'Neuchâtel'), ('NW', 'Nidwalden'), ('OW', 'Obwalden'), ('SG', 'St. Gallen'), ('SH', 'Schaffhausen'), ('SO', 'Solothurn'), ('SZ', 'Schwyz'), ('TG', 'Thurgau'), ('TI', 'Ticino'), ('UR', 'Uri'), ('VD', 'Vaud'), ('VS', 'Valais'), ('ZG', 'Zug'), ('ZH', 'Zürich'), ] class Deadline(models.Model): TYPE_CHOICES = [ ('tax', 'Tax'), ('insurance', 'Insurance'), ('invoice', 'Invoice'), ('personal', 'Personal'), ('other', 'Other'), ] user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='deadlines', ) title = models.CharField(max_length=200) date = models.DateField() type = models.CharField(max_length=20, choices=TYPE_CHOICES, default='other') notes = models.TextField(blank=True, default='') def __str__(self): return f"{self.date}: {self.title}" class Profile(models.Model): user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='profile', null=True, ) first_name = models.CharField(max_length=100, blank=True, default='') last_name = models.CharField(max_length=100, blank=True, default='') email = models.EmailField(blank=True, default='') avatar_color = models.CharField(max_length=7, default='#1A56DB') avatar_image = models.ImageField(upload_to='avatars/', blank=True, null=True) canton = models.CharField(max_length=2, choices=CANTON_CHOICES, default='ZH') language = models.CharField(max_length=2, choices=[('de','Deutsch'),('fr','Français'),('it','Italiano'),('en','English')], default='de') totp_secret = models.CharField(max_length=64, blank=True, default='') totp_enabled = models.BooleanField(default=False) totp_last_used_code = models.CharField(max_length=6, blank=True, default='') recovery_email = models.EmailField(blank=True, default='') recovery_code_hash = models.CharField(max_length=64, blank=True, default='') recovery_code_expires = models.DateTimeField(null=True, blank=True) notif_deadlines = models.BooleanField(default=True) notif_budget_alerts = models.BooleanField(default=True) notif_monthly_summary = models.BooleanField(default=False) savings_rate_goal = models.PositiveSmallIntegerField(default=20) email_verified = models.BooleanField(default=False) email_verify_token = models.CharField(max_length=64, blank=True, default='') email_verify_token_expires = models.DateTimeField(null=True, blank=True) password_reset_token_hash = models.CharField(max_length=64, blank=True, default='') password_reset_token_expires = models.DateTimeField(null=True, blank=True) def __str__(self): return f"{self.first_name} {self.last_name}".strip() or 'Profile' class UserSession(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='user_sessions', ) session_key = models.CharField(max_length=64, unique=True) refresh_jti = models.CharField(max_length=255, blank=True, default='') device_name = models.CharField(max_length=200, blank=True, default='') ip_address = models.GenericIPAddressField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) last_active_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-last_active_at'] def __str__(self): return f"{self.user} – {self.device_name}" class ReadEvent(models.Model): EVENT_TYPES = [ ('deadline', 'Deadline'), ('expense', 'Expense'), ] user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='read_events', ) event_type = models.CharField(max_length=20, choices=EVENT_TYPES) event_id = models.PositiveIntegerField() class Meta: unique_together = ['user', 'event_type', 'event_id'] def __str__(self): return f"{self.user} – {self.event_type} {self.event_id}" class BackupCode(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='backup_codes', ) code_hash = models.CharField(max_length=64) used = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) class Meta: indexes = [models.Index(fields=['user', 'used'])] def __str__(self): return f"{self.user} – backup {'used' if self.used else 'active'}" # ── FinancialYear ───────────────────────────────────────────────────────────── class Household(models.Model): name = models.CharField(max_length=100) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='created_households', ) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.name class HouseholdMembership(models.Model): STATUS_CHOICES = [ ('pending', 'Pending'), ('active', 'Active'), ('left', 'Left'), ] ROLE_CHOICES = [ ('member', 'Member'), ('admin', 'Admin'), ] household = models.ForeignKey(Household, on_delete=models.CASCADE, related_name='memberships') user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='household_memberships', ) invited_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name='sent_invitations', ) status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='pending') role = models.CharField(max_length=10, choices=ROLE_CHOICES, default='member') effective_from_year = models.PositiveSmallIntegerField(null=True, blank=True) effective_until_year = models.PositiveSmallIntegerField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ['household', 'user'] def __str__(self): return f"{self.user} in {self.household} ({self.status})" class PendingHouseholdInvite(models.Model): household = models.ForeignKey(Household, on_delete=models.CASCADE, related_name='pending_invites') invited_email = models.EmailField() invited_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name='sent_pending_invitations', ) effective_from_year = models.PositiveSmallIntegerField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ['household', 'invited_email'] def __str__(self): return f"Pending invite for {self.invited_email} to {self.household}" class FinancialYear(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='financial_years', null=True, blank=True, ) household = models.ForeignKey( Household, on_delete=models.CASCADE, related_name='financial_years', null=True, blank=True, ) year = models.PositiveSmallIntegerField() is_active = models.BooleanField(default=True) notes = models.TextField(blank=True, default='') created_at = models.DateTimeField(auto_now_add=True) class Meta: constraints = [ models.CheckConstraint( condition=( models.Q(user__isnull=False, household__isnull=True) | models.Q(user__isnull=True, household__isnull=False) ), name='financial_year_owner_exclusive', ), models.UniqueConstraint( fields=['user', 'year'], condition=models.Q(user__isnull=False), name='unique_personal_financial_year', ), models.UniqueConstraint( fields=['household', 'year'], condition=models.Q(household__isnull=False), name='unique_household_financial_year', ), ] def __str__(self): owner = self.user or self.household return f"{owner} — {self.year}" class YearlyIncome(models.Model): financial_year = models.ForeignKey(FinancialYear, on_delete=models.CASCADE, related_name='incomes') member = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name='yearly_incomes', ) name = models.CharField(max_length=100) amount = models.DecimalField(max_digits=12, decimal_places=2) active = models.BooleanField(default=True) notes = models.TextField(blank=True, default='') def __str__(self): return f"{self.name}: CHF {self.amount} ({self.financial_year.year})" class YearlyBudgetItem(models.Model): financial_year = models.ForeignKey(FinancialYear, on_delete=models.CASCADE, related_name='budget_items') name = models.CharField(max_length=100) amount = models.DecimalField(max_digits=12, decimal_places=2) active = models.BooleanField(default=True) notes = models.TextField(blank=True, default='') def __str__(self): return f"{self.name}: CHF {self.amount} ({self.financial_year.year})"