diff --git a/app/business/leave_calc.py b/app/business/leave_calc.py index 1a40863..3899a90 100644 --- a/app/business/leave_calc.py +++ b/app/business/leave_calc.py @@ -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) diff --git a/app/business/time_calc.py b/app/business/time_calc.py index 55f858f..c1f2fc7 100644 --- a/app/business/time_calc.py +++ b/app/business/time_calc.py @@ -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 diff --git a/app/business/travel_calc.py b/app/business/travel_calc.py index aaf0985..a50206d 100644 --- a/app/business/travel_calc.py +++ b/app/business/travel_calc.py @@ -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 diff --git a/app/models.py b/app/models.py index 02ab5b8..8554627 100644 --- a/app/models.py +++ b/app/models.py @@ -7,6 +7,18 @@ from app import db 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" id: so.Mapped[int] = so.mapped_column(primary_key=True) @@ -25,6 +37,17 @@ class WorkEntry(db.Model): ) 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 for slot in self.time_slots: start = slot.start_time.hour * 60 + slot.start_time.minute @@ -35,11 +58,24 @@ class WorkEntry(db.Model): return total 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() return f"{minutes // 60}h{minutes % 60:02d}" 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" id: so.Mapped[int] = so.mapped_column(primary_key=True) @@ -51,6 +87,13 @@ class TimeSlot(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" id: so.Mapped[int] = so.mapped_column(primary_key=True)