Ajoute la section [home_assistant] au TOML et sa validation au chargement via get_home_assistant_config(). La section est facultative pour préserver la compatibilité avec les configurations existantes, mais si elle est présente elle doit être complète et référencer un type de journée, un trajet et un véhicule à moteur existants, sous peine de refuser le démarrage de l'application. La configuration validée est exposée dans app.config['HOME_ASSISTANT']. Co-authored-by: OpenAI/GPT-5.6-Luna-Pro <vibecoder@antoineve.me>
205 lines
7.9 KiB
Python
205 lines
7.9 KiB
Python
"""Module de chargement et d'accès à la configuration TOML de l'application.
|
|
|
|
Ce module sert d'interface entre l'application Flask et le fichier `config.toml`.
|
|
Toute la configuration des véhicules, des trajets et du barème kilométrique y est stockée
|
|
et chargée au démarrage dans `app.config["TOML"]`.
|
|
|
|
Contrat TOML :
|
|
1. Véhicules (`[vehicles]`) :
|
|
- Chaque véhicule possède un identifiant unique (clé).
|
|
- Attributs : `name` (nom d'affichage), `type` ("moteur" ou "velo"), `fuel` ("electric", "essence", etc.),
|
|
`co2_per_km` (émissions de CO2 en grammes par km), et optionnellement `cv` (puissance fiscale pour le barème kilométrique).
|
|
2. Trajets (`[journeys]`) :
|
|
- Profils de trajets prédéfinis (ex: "moteur_seul").
|
|
- Attributs : `name` (nom d'affichage), `distances` (dictionnaire associant un type de véhicule à une distance en km).
|
|
3. Barème kilométrique (`[bareme_kilometrique.YYYY]`) :
|
|
- Organisé par année (ex: "2026") puis par puissance fiscale (`cv_3`, `cv_4`, `cv_5`, `cv_6`, `cv_7plus`).
|
|
- Chaque catégorie contient une liste de `tranches` définissant les formules de calcul des frais réels.
|
|
- Une tranche possède : `km_max` (limite supérieure de la tranche, `km_max = 0` signifie "pas de limite supérieure"),
|
|
`taux` (coefficient multiplicateur par km) et `forfait` (montant forfaitaire à ajouter).
|
|
4. Types de jours sans trajet :
|
|
- Certains types de journées (Télétravail, Maladie, Congé, RTT, Férié) n'impliquent aucun déplacement physique.
|
|
"""
|
|
|
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
|
|
from flask import current_app
|
|
|
|
_HOME_ASSISTANT_REQUIRED_KEYS = {
|
|
"timezone",
|
|
"default_day_type",
|
|
"default_journey_profile_id",
|
|
"default_motor_vehicle_id",
|
|
}
|
|
_VALID_DAY_TYPES = {
|
|
"WORK",
|
|
"TT",
|
|
"GARDE",
|
|
"ASTREINTE",
|
|
"FORMATION",
|
|
"RTT",
|
|
"CONGE",
|
|
"MALADE",
|
|
"FERIE",
|
|
}
|
|
|
|
|
|
def get_vehicles():
|
|
"""Récupère l'ensemble des véhicules configurés dans le fichier TOML.
|
|
|
|
Retourne:
|
|
dict: Un dictionnaire des véhicules où la clé est l'identifiant du véhicule
|
|
et la valeur est un dictionnaire contenant ses propriétés (name, type, fuel, co2_per_km, cv).
|
|
Retourne un dictionnaire vide si aucune configuration n'est chargée.
|
|
"""
|
|
return current_app.config.get("TOML", {}).get("vehicles", {})
|
|
|
|
|
|
def get_motor_vehicles():
|
|
"""Filtre et retourne uniquement les véhicules de type 'moteur'.
|
|
|
|
Cette fonction exclut les véhicules alternatifs (comme les vélos) pour ne conserver
|
|
que ceux qui possèdent une puissance fiscale (CV) et sont éligibles au barème kilométrique.
|
|
|
|
Retourne:
|
|
dict: Un dictionnaire contenant uniquement les véhicules dont le type est 'moteur'.
|
|
"""
|
|
return {k: v for k, v in get_vehicles().items() if v.get("type") == "moteur"}
|
|
|
|
|
|
def get_journeys():
|
|
"""Récupère l'ensemble des profils de trajets configurés dans le fichier TOML.
|
|
|
|
Retourne:
|
|
dict: Un dictionnaire des trajets où la clé est l'identifiant du trajet
|
|
et la valeur est un dictionnaire contenant ses propriétés (name, distances).
|
|
Retourne un dictionnaire vide si aucune configuration n'est chargée.
|
|
"""
|
|
return current_app.config.get("TOML", {}).get("journeys", {})
|
|
|
|
|
|
def get_home_assistant_config() -> dict[str, str] | None:
|
|
"""Retourne la configuration Home Assistant, après validation.
|
|
|
|
L'absence de section désactive la future intégration et reste compatible avec
|
|
les anciens fichiers TOML. Une section présente doit en revanche être
|
|
complète et cohérente avec les véhicules, trajets et types de journées
|
|
connus de l'application.
|
|
"""
|
|
config = current_app.config.get("TOML", {}).get("home_assistant")
|
|
if config is None:
|
|
return None
|
|
if not isinstance(config, dict):
|
|
raise ValueError("Configuration [home_assistant] invalide : la section doit être une table")
|
|
|
|
missing = _HOME_ASSISTANT_REQUIRED_KEYS - config.keys()
|
|
if missing:
|
|
missing_keys = ", ".join(sorted(missing))
|
|
raise ValueError(
|
|
f"Configuration [home_assistant] incomplète : clé(s) manquante(s) {missing_keys}"
|
|
)
|
|
|
|
if any(
|
|
not isinstance(config[key], str) or not config[key] for key in _HOME_ASSISTANT_REQUIRED_KEYS
|
|
):
|
|
raise ValueError(
|
|
"Configuration [home_assistant] invalide : toutes les valeurs doivent être des chaînes non vides"
|
|
)
|
|
|
|
timezone = config["timezone"]
|
|
try:
|
|
ZoneInfo(timezone)
|
|
except (ZoneInfoNotFoundError, ValueError) as exc:
|
|
raise ValueError(
|
|
f"Configuration [home_assistant] invalide : fuseau horaire inconnu {timezone!r}"
|
|
) from exc
|
|
|
|
day_type = config["default_day_type"]
|
|
if day_type not in _VALID_DAY_TYPES:
|
|
raise ValueError(
|
|
f"Configuration [home_assistant] invalide : type de journée inconnu {day_type!r}"
|
|
)
|
|
|
|
journey_id = config["default_journey_profile_id"]
|
|
if journey_id not in get_journeys():
|
|
raise ValueError(f"Configuration [home_assistant] invalide : trajet inconnu {journey_id!r}")
|
|
|
|
vehicle_id = config["default_motor_vehicle_id"]
|
|
vehicle = get_vehicles().get(vehicle_id)
|
|
if vehicle is None:
|
|
raise ValueError(
|
|
f"Configuration [home_assistant] invalide : véhicule inconnu {vehicle_id!r}"
|
|
)
|
|
if vehicle.get("type") != "moteur":
|
|
raise ValueError(
|
|
f"Configuration [home_assistant] invalide : le véhicule {vehicle_id!r} n'est pas un véhicule moteur"
|
|
)
|
|
|
|
return {key: config[key] for key in _HOME_ASSISTANT_REQUIRED_KEYS}
|
|
|
|
|
|
def journey_has_motor(journey_profile_id: str | None) -> bool:
|
|
"""Vérifie si un profil de trajet donné inclut une distance pour véhicule à moteur.
|
|
|
|
Cette validation permet de déterminer si l'utilisateur doit sélectionner un véhicule
|
|
à moteur lors de la saisie d'une journée de travail avec ce trajet.
|
|
|
|
Paramètres:
|
|
journey_profile_id (str | None): L'identifiant du profil de trajet à vérifier.
|
|
|
|
Retourne:
|
|
bool: True si le trajet existe et définit une distance pour la clé 'moteur',
|
|
False sinon ou si l'identifiant est nul.
|
|
"""
|
|
if not journey_profile_id:
|
|
return False
|
|
journeys = get_journeys()
|
|
profile = journeys.get(journey_profile_id, {})
|
|
return "moteur" in profile.get("distances", {})
|
|
|
|
|
|
def get_bareme(year: int, cv: int) -> list[dict]:
|
|
"""Récupère les tranches du barème kilométrique pour une année et une puissance fiscale données.
|
|
|
|
Le barème kilométrique officiel est structuré en tranches de distances annuelles.
|
|
Cette fonction sélectionne la bonne catégorie de puissance fiscale (CV) :
|
|
- cv <= 3 -> 'cv_3'
|
|
- cv == 4 -> 'cv_4'
|
|
- cv == 5 -> 'cv_5'
|
|
- cv == 6 -> 'cv_6'
|
|
- cv >= 7 -> 'cv_7plus'
|
|
|
|
Paramètres:
|
|
year (int): L'année civile concernée par le calcul.
|
|
cv (int): La puissance fiscale du véhicule en chevaux fiscaux.
|
|
|
|
Retourne:
|
|
list[dict]: Une liste de dictionnaires représentant les tranches applicables.
|
|
Chaque tranche contient 'km_max' (0 si pas de limite), 'taux' et 'forfait'.
|
|
"""
|
|
bareme = current_app.config.get("TOML", {}).get("bareme_kilometrique", {})
|
|
year_data = bareme.get(str(year), {})
|
|
if cv <= 3:
|
|
key = "cv_3"
|
|
elif cv == 4:
|
|
key = "cv_4"
|
|
elif cv == 5:
|
|
key = "cv_5"
|
|
elif cv == 6:
|
|
key = "cv_6"
|
|
else:
|
|
key = "cv_7plus"
|
|
return year_data.get(key, {}).get("tranches", [])
|
|
|
|
|
|
def day_types_without_journey():
|
|
"""Retourne l'ensemble des types de jours qui n'impliquent aucun trajet physique.
|
|
|
|
Ces types de jours (Télétravail, Maladie, Congé, RTT, Férié) sont exemptés de la saisie
|
|
de trajets ou de véhicules, car le travail s'effectue à distance ou l'employé est absent.
|
|
|
|
Retourne:
|
|
set[str]: Un ensemble de codes de types de jours (ex: {"TT", "MALADE", ...}).
|
|
"""
|
|
return {"TT", "MALADE", "CONGE", "RTT", "FERIE"}
|