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:
@@ -7,6 +7,24 @@ from app.models import LeaveBalance, WorkEntry
|
|||||||
|
|
||||||
|
|
||||||
def compute_leave_used(year: int) -> dict[str, int]:
|
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)
|
start = date(year, 1, 1)
|
||||||
end = date(year, 12, 31)
|
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:
|
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))
|
balance = db.session.scalar(sa.select(LeaveBalance).where(LeaveBalance.year == year))
|
||||||
if balance is None:
|
if balance is None:
|
||||||
balance = LeaveBalance(year=year)
|
balance = LeaveBalance(year=year)
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
def minutes_to_str(minutes: int) -> str:
|
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 ""
|
sign = "-" if minutes < 0 else ""
|
||||||
minutes = abs(minutes)
|
minutes = abs(minutes)
|
||||||
return f"{sign}{minutes // 60}h{minutes % 60:02d}"
|
return f"{sign}{minutes // 60}h{minutes % 60:02d}"
|
||||||
@@ -18,10 +27,34 @@ _REFERENCE_MINUTES = {
|
|||||||
|
|
||||||
|
|
||||||
def work_minutes_reference(day_type: str) -> int:
|
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)
|
return _REFERENCE_MINUTES.get(day_type, 465)
|
||||||
|
|
||||||
|
|
||||||
def week_balance_minutes(actual_minutes: int, reference_minutes: int) -> int:
|
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
|
return actual_minutes - reference_minutes
|
||||||
|
|
||||||
|
|
||||||
@@ -30,8 +63,18 @@ import statistics as _stats
|
|||||||
|
|
||||||
def monthly_stats(entries: list) -> dict:
|
def monthly_stats(entries: list) -> dict:
|
||||||
"""
|
"""
|
||||||
Calcule médiane journalière et médiane hebdomadaire (semaines ISO)
|
Calcule la médiane journalière et la médiane hebdomadaire (semaines ISO) pour un groupe d'entrées.
|
||||||
pour un groupe d'entrées. Les absences (total_minutes=0) sont incluses.
|
|
||||||
|
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:
|
if not entries:
|
||||||
return {"median_daily_min": 0, "median_weekly_min": 0}
|
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]:
|
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] = {}
|
counts: dict[str, int] = {}
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
counts[entry.day_type] = counts.get(entry.day_type, 0) + 1
|
counts[entry.day_type] = counts.get(entry.day_type, 0) + 1
|
||||||
|
|||||||
@@ -4,9 +4,22 @@ def compute_km_for_entry(
|
|||||||
motor_vehicle_id: str | None = None,
|
motor_vehicle_id: str | None = None,
|
||||||
) -> dict[str, int]:
|
) -> dict[str, int]:
|
||||||
"""
|
"""
|
||||||
Retourne un dict {vehicle_id: km} pour un profil de trajet donné.
|
Calcule les distances parcourues par véhicule pour une entrée de journal donnée.
|
||||||
La clé générique 'moteur' est remplacée par motor_vehicle_id si fourni.
|
|
||||||
Retourne {} si pas de profil (TT, CONGE, etc.).
|
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:
|
if not journey_profile_id:
|
||||||
return {}
|
return {}
|
||||||
@@ -23,7 +36,18 @@ def compute_km_for_entry(
|
|||||||
|
|
||||||
|
|
||||||
def compute_co2_grams(km_by_vehicle: dict[str, int], vehicles: dict) -> float:
|
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
|
total = 0.0
|
||||||
for vehicle_id, km in km_by_vehicle.items():
|
for vehicle_id, km in km_by_vehicle.items():
|
||||||
vehicle = vehicles.get(vehicle_id, {})
|
vehicle = vehicles.get(vehicle_id, {})
|
||||||
@@ -36,9 +60,22 @@ def compute_frais_reels(
|
|||||||
total_km_moteur: float, tranches: list[dict], electric: bool = False
|
total_km_moteur: float, tranches: list[dict], electric: bool = False
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
Calcule les frais réels fiscaux selon le barème kilométrique.
|
Calcule le montant des frais réels déductibles selon le barème kilométrique fiscal.
|
||||||
km_max = 0 signifie "pas de limite" (dernière tranche).
|
|
||||||
electric=True applique la majoration de 20 % pour véhicules électriques.
|
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:
|
if not tranches or total_km_moteur <= 0:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|||||||
@@ -7,6 +7,18 @@ from app import db
|
|||||||
|
|
||||||
|
|
||||||
class WorkEntry(db.Model):
|
class WorkEntry(db.Model):
|
||||||
|
"""
|
||||||
|
Représente une entrée de journal de travail pour une journée unique.
|
||||||
|
|
||||||
|
Cette classe stocke les informations relatives à une journée de travail,
|
||||||
|
notamment la date, le type de journée (WORK, TT, GARDE, ASTREINTE, etc.),
|
||||||
|
les profils de trajet domicile-travail, le véhicule utilisé, un commentaire
|
||||||
|
et les plages horaires associées.
|
||||||
|
|
||||||
|
Invariants :
|
||||||
|
- La date est unique (une seule entrée par jour).
|
||||||
|
"""
|
||||||
|
|
||||||
__tablename__ = "work_entries"
|
__tablename__ = "work_entries"
|
||||||
|
|
||||||
id: so.Mapped[int] = so.mapped_column(primary_key=True)
|
id: so.Mapped[int] = so.mapped_column(primary_key=True)
|
||||||
@@ -25,6 +37,17 @@ class WorkEntry(db.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def total_minutes(self) -> int:
|
def total_minutes(self) -> int:
|
||||||
|
"""
|
||||||
|
Calcule la durée totale travaillée dans la journée en minutes.
|
||||||
|
|
||||||
|
Cette méthode somme la durée de toutes les plages horaires (`TimeSlot`)
|
||||||
|
associées à cette entrée. Elle gère le passage de minuit : si l'heure de fin
|
||||||
|
d'une plage est inférieure ou égale à son heure de début, la plage est
|
||||||
|
considérée comme se terminant le lendemain (ajout de 24 heures).
|
||||||
|
|
||||||
|
Retour :
|
||||||
|
int : La durée totale en minutes.
|
||||||
|
"""
|
||||||
total = 0
|
total = 0
|
||||||
for slot in self.time_slots:
|
for slot in self.time_slots:
|
||||||
start = slot.start_time.hour * 60 + slot.start_time.minute
|
start = slot.start_time.hour * 60 + slot.start_time.minute
|
||||||
@@ -35,11 +58,24 @@ class WorkEntry(db.Model):
|
|||||||
return total
|
return total
|
||||||
|
|
||||||
def total_hours_str(self) -> str:
|
def total_hours_str(self) -> str:
|
||||||
|
"""
|
||||||
|
Retourne la durée totale travaillée sous forme de chaîne formatée (ex: "7h45").
|
||||||
|
|
||||||
|
Retour :
|
||||||
|
str : La durée formatée au format "HhMM".
|
||||||
|
"""
|
||||||
minutes = self.total_minutes()
|
minutes = self.total_minutes()
|
||||||
return f"{minutes // 60}h{minutes % 60:02d}"
|
return f"{minutes // 60}h{minutes % 60:02d}"
|
||||||
|
|
||||||
|
|
||||||
class TimeSlot(db.Model):
|
class TimeSlot(db.Model):
|
||||||
|
"""
|
||||||
|
Représente une plage horaire de travail au sein d'une journée.
|
||||||
|
|
||||||
|
Chaque plage possède une heure de début et une heure de fin. Elle est rattachée
|
||||||
|
à une entrée de journal (`WorkEntry`).
|
||||||
|
"""
|
||||||
|
|
||||||
__tablename__ = "time_slots"
|
__tablename__ = "time_slots"
|
||||||
|
|
||||||
id: so.Mapped[int] = so.mapped_column(primary_key=True)
|
id: so.Mapped[int] = so.mapped_column(primary_key=True)
|
||||||
@@ -51,6 +87,13 @@ class TimeSlot(db.Model):
|
|||||||
|
|
||||||
|
|
||||||
class LeaveBalance(db.Model):
|
class LeaveBalance(db.Model):
|
||||||
|
"""
|
||||||
|
Représente le solde annuel des congés et RTT pour une année donnée.
|
||||||
|
|
||||||
|
Stocke les quotas initiaux/totaux de congés payés et de RTT alloués pour l'année.
|
||||||
|
Par défaut, un utilisateur bénéficie de 28 jours de congés et 19 jours de RTT.
|
||||||
|
"""
|
||||||
|
|
||||||
__tablename__ = "leave_balance"
|
__tablename__ = "leave_balance"
|
||||||
|
|
||||||
id: so.Mapped[int] = so.mapped_column(primary_key=True)
|
id: so.Mapped[int] = so.mapped_column(primary_key=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user