doc: document models and business rules

Co-authored-by: OpenAI/GPT-5.6-Terra <vibecoder@antoineve.me>
Co-authored-by: Google/Gemini-3.5-Flash <vibecoder@antoineve.me>
Co-authored-by: DeepSeek/DeepSeek-v4-Pro <vibecoder@antoineve.me>
Co-authored-by: DeepSeek/DeepSeek-v4-Flash <vibecoder@antoineve.me>
This commit is contained in:
2026-08-13 10:58:49 +02:00
parent c7a0a77d1f
commit 2d85fffd8d
4 changed files with 177 additions and 10 deletions

View File

@@ -7,6 +7,24 @@ from app.models import LeaveBalance, WorkEntry
def compute_leave_used(year: int) -> dict[str, int]:
"""
Calcule le nombre de jours de congés et de RTT consommés pour une année donnée.
Cette fonction interroge la base de données pour compter le nombre d'entrées
de journal (`WorkEntry`) de type 'CONGE' et 'RTT' comprises entre le 1er janvier
et le 31 décembre de l'année spécifiée.
Accès DB :
- Lecture seule sur la table `work_entries`.
Paramètres :
year (int) : L'année pour laquelle calculer les jours consommés.
Retour :
dict[str, int] : Un dictionnaire contenant :
- "conges" (int) : Le nombre de jours de congés consommés.
- "rtt" (int) : Le nombre de jours de RTT consommés.
"""
start = date(year, 1, 1)
end = date(year, 12, 31)
@@ -34,6 +52,23 @@ def compute_leave_used(year: int) -> dict[str, int]:
def get_or_create_balance(year: int) -> LeaveBalance:
"""
Récupère le solde annuel des congés et RTT pour une année donnée, ou le crée s'il n'existe pas.
Si aucun solde n'existe pour l'année spécifiée, un nouvel enregistrement `LeaveBalance`
est créé avec les valeurs par défaut (28 jours de congés, 19 jours de RTT) et enregistré
en base de données.
Accès DB :
- Lecture sur la table `leave_balance`.
- Écriture (insertion et commit) si l'enregistrement n'existe pas.
Paramètres :
year (int) : L'année concernée.
Retour :
LeaveBalance : L'objet représentant le solde annuel pour l'année spécifiée.
"""
balance = db.session.scalar(sa.select(LeaveBalance).where(LeaveBalance.year == year))
if balance is None:
balance = LeaveBalance(year=year)

View File

@@ -1,4 +1,13 @@
def minutes_to_str(minutes: int) -> str:
"""
Convertit une durée en minutes en une chaîne formatée lisible (ex: "7h45" ou "-1h15").
Paramètres :
minutes (int) : Le nombre de minutes à convertir (peut être négatif).
Retour :
str : La chaîne formatée au format "[signe]HhMM".
"""
sign = "-" if minutes < 0 else ""
minutes = abs(minutes)
return f"{sign}{minutes // 60}h{minutes % 60:02d}"
@@ -18,10 +27,34 @@ _REFERENCE_MINUTES = {
def work_minutes_reference(day_type: str) -> int:
"""
Retourne la durée de travail de référence en minutes pour un type de journée donné.
Les durées de référence sont :
- WORK, TT, FORMATION : 7h45 (465 minutes)
- GARDE : 10h00 (600 minutes)
- ASTREINTE, RTT, CONGE, MALADE, FERIE : 0 minute
Paramètres :
day_type (str) : Le type de journée (ex: "WORK", "TT", "CONGE").
Retour :
int : La durée de référence en minutes. Par défaut 465 minutes si le type est inconnu.
"""
return _REFERENCE_MINUTES.get(day_type, 465)
def week_balance_minutes(actual_minutes: int, reference_minutes: int) -> int:
"""
Calcule l'écart (solde) entre les minutes réellement travaillées et les minutes de référence.
Paramètres :
actual_minutes (int) : Le total des minutes travaillées.
reference_minutes (int) : Le total des minutes de référence.
Retour :
int : L'écart en minutes (positif si heures supplémentaires, négatif si déficit).
"""
return actual_minutes - reference_minutes
@@ -30,8 +63,18 @@ import statistics as _stats
def monthly_stats(entries: list) -> dict:
"""
Calcule médiane journalière et médiane hebdomadaire (semaines ISO)
pour un groupe d'entrées. Les absences (total_minutes=0) sont incluses.
Calcule la médiane journalière et la médiane hebdomadaire (semaines ISO) pour un groupe d'entrées.
Les absences (durée de travail de 0 minute) sont incluses dans les calculs.
Les semaines sont regroupées selon le calendrier ISO (année, numéro de semaine).
Paramètres :
entries (list) : Une liste d'objets `WorkEntry`.
Retour :
dict : Un dictionnaire contenant :
- "median_daily_min" (int) : La médiane des minutes travaillées par jour.
- "median_weekly_min" (int) : La médiane des minutes travaillées par semaine ISO.
"""
if not entries:
return {"median_daily_min": 0, "median_weekly_min": 0}
@@ -50,7 +93,16 @@ def monthly_stats(entries: list) -> dict:
def count_day_types(entries: list) -> dict[str, int]:
"""Retourne un dict {day_type: count} pour une liste d'entrées, sans les zéros."""
"""
Comptabilise le nombre d'occurrences de chaque type de journée dans une liste d'entrées.
Paramètres :
entries (list) : Une liste d'objets `WorkEntry`.
Retour :
dict[str, int] : Un dictionnaire associant chaque type de journée présent à son nombre d'occurrences.
Les types de journées non représentés ne figurent pas dans le dictionnaire.
"""
counts: dict[str, int] = {}
for entry in entries:
counts[entry.day_type] = counts.get(entry.day_type, 0) + 1

View File

@@ -4,9 +4,22 @@ def compute_km_for_entry(
motor_vehicle_id: str | None = None,
) -> dict[str, int]:
"""
Retourne un dict {vehicle_id: km} pour un profil de trajet donné.
La clé générique 'moteur' est remplacée par motor_vehicle_id si fourni.
Retourne {} si pas de profil (TT, CONGE, etc.).
Calcule les distances parcourues par véhicule pour une entrée de journal donnée.
Cette fonction associe un profil de trajet à ses distances configurées. Si le profil
contient une clé générique 'moteur', celle-ci est remplacée par l'identifiant réel du
véhicule motorisé (`motor_vehicle_id`) s'il est fourni. Si aucun véhicule motorisé n'est
fourni, la distance associée à la clé 'moteur' est ignorée.
Paramètres :
journey_profile_id (str | None) : L'identifiant du profil de trajet (ex: "domicile_travail").
Si None, retourne un dictionnaire vide.
journeys (dict) : La configuration des trajets (généralement issue de config.toml).
motor_vehicle_id (str | None) : L'identifiant du véhicule motorisé utilisé (ex: "citadine").
Retour :
dict[str, int] : Un dictionnaire associant chaque identifiant de véhicule (ex: "velo", "citadine")
à la distance parcourue en kilomètres.
"""
if not journey_profile_id:
return {}
@@ -23,7 +36,18 @@ def compute_km_for_entry(
def compute_co2_grams(km_by_vehicle: dict[str, int], vehicles: dict) -> float:
"""Calcule le CO2 total en grammes pour un dict {vehicle_id: km}."""
"""
Calcule la quantité totale de CO2 émise en grammes pour un ensemble de distances parcourues.
Paramètres :
km_by_vehicle (dict[str, int]) : Un dictionnaire associant chaque identifiant de véhicule
à la distance parcourue en kilomètres.
vehicles (dict) : La configuration des véhicules (généralement issue de config.toml)
contenant le taux d'émission de CO2 par kilomètre (`co2_per_km`).
Retour :
float : La quantité totale de CO2 émise en grammes.
"""
total = 0.0
for vehicle_id, km in km_by_vehicle.items():
vehicle = vehicles.get(vehicle_id, {})
@@ -36,9 +60,22 @@ def compute_frais_reels(
total_km_moteur: float, tranches: list[dict], electric: bool = False
) -> float:
"""
Calcule les frais réels fiscaux selon le barème kilométrique.
km_max = 0 signifie "pas de limite" (dernière tranche).
electric=True applique la majoration de 20 % pour véhicules électriques.
Calcule le montant des frais réels déductibles selon le barème kilométrique fiscal.
Le calcul s'effectue tranche par tranche en fonction du kilométrage annuel total parcouru
avec un véhicule motorisé. Une tranche avec `km_max = 0` signifie l'absence de limite
supérieure et est conventionnellement la dernière tranche du barème.
Une majoration de 20 % est appliquée sur le montant final si le véhicule est électrique.
Paramètres :
total_km_moteur (float) : Le kilométrage annuel total parcouru avec le véhicule motorisé.
tranches (list[dict]) : La liste des tranches du barème kilométrique pour la puissance fiscale
du véhicule (ex: taux, forfait, km_max).
electric (bool) : Indique si le véhicule est électrique (applique une majoration de 20 % si True).
Retour :
float : Le montant total calculé des frais réels en euros.
"""
if not tranches or total_km_moteur <= 0:
return 0.0