feat: insurance section — overview, documents, analysis, KVG premium comparison
- Insurance overview page (/insurance): current policies table with type, provider, premium, franchise, coverage, and document links - Documents page: upload and manage insurance documents - Analysis page: coverage gap analysis per insurance type - Priminfo integration (/insurance/priminfo): KVG premium comparison by insurer, model (TAR/HMO/etc.), franchise level, and accident coverage via embedded Priminfo iframe (no public API available) - Backend: Insurance, PraemienEntry, PraemienPolice models with migrations - Sidebar: insurance nav group with flyout and dropdown - i18n: all keys in DE/EN/FR/IT
This commit is contained in:
+109
-1
@@ -223,4 +223,112 @@ class BackupCode(models.Model):
|
||||
indexes = [models.Index(fields=['user', 'used'])]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user} – backup {'used' if self.used else 'active'}"
|
||||
return f"{self.user} – backup {'used' if self.used else 'active'}"
|
||||
|
||||
|
||||
class Insurance(models.Model):
|
||||
INSURANCE_TYPES = [
|
||||
('kvg', 'Krankenkasse Grundversicherung (KVG)'),
|
||||
('kk_zusatz', 'KK-Zusatzversicherung'),
|
||||
('nbu', 'Nicht-Berufsunfallversicherung (NBU)'),
|
||||
('haftpflicht', 'Privathaftpflicht'),
|
||||
('hausrat', 'Hausrat'),
|
||||
('mfz', 'MFZ-Haftpflicht'),
|
||||
('rechtsschutz', 'Rechtsschutz'),
|
||||
('saule_3a', 'Säule 3a'),
|
||||
('leben', 'Lebensversicherung'),
|
||||
('reise', 'Reiseversicherung'),
|
||||
('other', 'Sonstiges'),
|
||||
]
|
||||
|
||||
PERIOD_CHOICES = [
|
||||
('monthly', 'Monatlich'),
|
||||
('quarterly', 'Vierteljährlich'),
|
||||
('semi_annual', 'Halbjährlich'),
|
||||
('annual', 'Jährlich'),
|
||||
]
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='insurances',
|
||||
)
|
||||
insurance_type = models.CharField(max_length=30, choices=INSURANCE_TYPES)
|
||||
insurer = models.CharField(max_length=200)
|
||||
policy_number = models.CharField(max_length=100, blank=True, default='')
|
||||
premium = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
premium_period = models.CharField(max_length=20, choices=PERIOD_CHOICES, default='monthly')
|
||||
coverage_amount = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
||||
deductible = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
||||
valid_from = models.DateField(null=True, blank=True)
|
||||
valid_until = models.DateField(null=True, blank=True)
|
||||
notes = models.TextField(blank=True, default='')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['insurance_type']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_insurance_type_display()} – {self.insurer}"
|
||||
|
||||
|
||||
class PraemienEntry(models.Model):
|
||||
"""
|
||||
Swiss health insurance average premium data from BAG / Priminfo.
|
||||
Populated via management command: python manage.py import_praemien [year]
|
||||
Source: https://www.priminfo.admin.ch/downloads/praemienregionen_{year}.xlsx
|
||||
"""
|
||||
plz = models.CharField(max_length=10, db_index=True)
|
||||
ort = models.CharField(max_length=200)
|
||||
kanton = models.CharField(max_length=2)
|
||||
region = models.PositiveSmallIntegerField() # Prämienregion 0, 1, 2, or 3
|
||||
bfs_nr = models.PositiveIntegerField(db_index=True)
|
||||
gemeinde = models.CharField(max_length=200)
|
||||
bezirk = models.CharField(max_length=200, blank=True, default='')
|
||||
avg_adult = models.DecimalField(max_digits=8, decimal_places=2)
|
||||
avg_young_adult = models.DecimalField(max_digits=8, decimal_places=2)
|
||||
avg_child = models.DecimalField(max_digits=8, decimal_places=2)
|
||||
data_year = models.PositiveSmallIntegerField(db_index=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ['plz', 'ort', 'data_year']
|
||||
ordering = ['kanton', 'ort']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.plz} {self.ort} ({self.kanton}) – Region {self.region} – {self.data_year}"
|
||||
|
||||
|
||||
class PraemienPolice(models.Model):
|
||||
"""
|
||||
Granular KVG premium data per insurer, canton, region, age class, model, franchise.
|
||||
Populated via management command: python manage.py import_praemien [year]
|
||||
Source: https://opendata.bagnet.ch (Prämien_CH.csv)
|
||||
~217k rows for a full year.
|
||||
"""
|
||||
versicherer_id = models.PositiveIntegerField(db_index=True)
|
||||
kanton = models.CharField(max_length=2)
|
||||
region = models.PositiveSmallIntegerField() # 0, 1, 2, 3
|
||||
altersklasse = models.CharField(max_length=10) # AKL-ERW / AKL-JUG / AKL-KIN
|
||||
unfalleinschluss = models.CharField(max_length=10) # MIT-UNF / OHN-UNF
|
||||
tariftyp = models.CharField(max_length=10) # TAR-BASE / TAR-HAM / TAR-HMO / TAR-DIV
|
||||
tarifbezeichnung = models.CharField(max_length=200)
|
||||
franchisestufe = models.CharField(max_length=10) # FRAST1 … FRAST7
|
||||
franchise_chf = models.PositiveSmallIntegerField() # e.g. 300, 500, 1000 …
|
||||
praemie = models.DecimalField(max_digits=8, decimal_places=2)
|
||||
data_year = models.PositiveSmallIntegerField(db_index=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = [
|
||||
'versicherer_id', 'kanton', 'region', 'altersklasse',
|
||||
'unfalleinschluss', 'tariftyp', 'franchisestufe', 'data_year',
|
||||
]
|
||||
indexes = [
|
||||
models.Index(fields=[
|
||||
'kanton', 'region', 'altersklasse',
|
||||
'unfalleinschluss', 'tariftyp', 'franchisestufe', 'data_year',
|
||||
]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return (f"V{self.versicherer_id} {self.kanton} R{self.region} "
|
||||
f"{self.altersklasse} {self.tariftyp} {self.franchisestufe} → {self.praemie} CHF")
|
||||
Reference in New Issue
Block a user