| |
| import json |
| import sys |
| import logging |
| from pathlib import Path |
| from typing import Dict, Any |
|
|
| log = logging.getLogger(__name__) |
|
|
| def load_json(file_path: str) -> Dict[str, Any]: |
| """Load JSON with helpful error messages.""" |
| try: |
| with open(file_path, 'r', encoding='utf-8') as f: |
| return json.load(f) |
| except FileNotFoundError: |
| log.error(f"IFI JSON not found: {file_path}") |
| raise |
| except json.JSONDecodeError as e: |
| log.error(f"IFI JSON corrupt ({file_path}): {e}") |
| raise |
|
|
|
|
| def calculate_ifi( |
| gross_assets: float, |
| debts: float = 0.0, |
| primary_residence_value: float = 0.0, |
| json_file: str | None = None, |
| ) -> dict: |
| """ |
| IFI 2025 – progressive brackets + 30 % primary-residence abatement. |
| """ |
|
|
| |
|
|
| if json_file is None: |
| json_file = getattr(sys.modules[__name__], "_JSON_PATH", None) |
|
|
| if json_file is None: |
| |
| json_file = str(Path(__file__).parent / "ifi_taux.json") |
| log.debug(f"IFI JSON fallback → {json_file}") |
|
|
| log.info(f"IFI calc: gross={gross_assets:,.0f}€, debt={debts:,.0f}€, primary={primary_residence_value:,.0f}€") |
|
|
|
|
| |
| |
| data = load_json(json_file) |
| if not data: |
| return { |
| "tax": 0.0, |
| "net_taxable": 0.0, |
| "abattement": 0.0, |
| "abattement_applied": False, |
| "note": "Impossible de charger les taux IFI." |
| } |
|
|
|
|
| |
|
|
| net_assets = gross_assets - debts |
| abatement = 0.0 |
| abatement_applied = primary_residence_value > 0 |
|
|
| if abatement_applied: |
| abatement = primary_residence_value * data["abattement_primary_residence"] |
| net_assets -= abatement |
|
|
| net_assets = max(0.0, net_assets) |
|
|
| result = { |
| "tax": 0.0, |
| "net_taxable": round(net_assets, 2), |
| "abattement": round(abatement, 2), |
| "abattement_applied": abatement_applied, |
| "note": "" |
| } |
|
|
| |
| |
| |
| if net_assets <= data["threshold"]: |
| result["note"] = ( |
| f"Patrimoine net {net_assets:,.0f} € < seuil {data['threshold']:,.0f} € → pas d'IFI. " |
| f"{'Abattement 30 % appliqué.' if abatement_applied else 'Aucun abattement.'}" |
| ) |
| return result |
|
|
| |
| |
| |
| tax = 0.0 |
| remaining = net_assets |
| prev_max = 0.0 |
|
|
| for bracket in data["brackets"]: |
| current_max = bracket["max"] if bracket["max"] is not None else float("inf") |
| taxable = min(remaining, current_max - prev_max) |
| if taxable > 0: |
| tax += taxable * bracket["rate"] |
| remaining -= taxable |
| prev_max = current_max |
| if remaining <= 0: |
| break |
|
|
| result["tax"] = round(tax, 2) |
| result["note"] = ( |
| f"IFI calculé sur {net_assets:,.0f} € net. " |
| f"{'Abattement 30 % appliqué.' if abatement_applied else 'Aucun abattement.'} " |
| f"Taux progressifs 2025." |
| ) |
| log.info(f"IFI result → tax={result['tax']:,}€") |
| return result |