diff --git a/app/calcul.py b/app/calcul.py index 9bbbdaa..1454d0b 100644 --- a/app/calcul.py +++ b/app/calcul.py @@ -220,6 +220,16 @@ def compute_week(pointages: list[dict], conges: list[dict], heures_jour_min: int adjs[i] -= take left -= take + def _en_cours(jour, half: str) -> bool: + """True si cette demi-journée est déjà commencée aujourd'hui (entrée pointée, + pas encore de sortie) : la cible ne peut alors pas retomber avant l'heure + actuelle, sinon on recommanderait un départ déjà passé.""" + return ( + jour["date"] == today + and bool(jour.get(f"{half}_entree")) + and not jour.get(f"{half}_sortie") + ) + # Le matin propose toujours son horaire nominal (jamais ajusté par le rattrapage # hebdomadaire) ; l'après-midi propose son horaire nominal éventuellement décalé # par l'écart (retard ou avance) réparti ci-dessus. @@ -229,12 +239,16 @@ def compute_week(pointages: list[dict], conges: list[dict], heures_jour_min: int # Pas de plancher matin_fin ici : le nominal est déjà borné par cap (≤ matin_fin # - debut), donc debut + nominal ne peut pas dépasser matin_fin de lui-même. sortie_min = s["debut"] + s["nominal"] + if _en_cours(s["jour"], "matin"): + sortie_min = max(sortie_min, now_min) entree = f"{s['debut'] // 60:02d}:{s['debut'] % 60:02d}" s["jour"]["cible_matin_entree"] = entree s["jour"]["cible_matin"] = f"{sortie_min // 60:02d}:{sortie_min % 60:02d}" for i, s in enumerate(aprem_slots): sortie_min = max(s["debut"] + s["nominal"] + adjs[i], aprem_fin_min) + if _en_cours(s["jour"], "aprem"): + sortie_min = max(sortie_min, now_min) entree = f"{s['debut'] // 60:02d}:{s['debut'] % 60:02d}" s["jour"]["cible_aprem_entree"] = entree s["jour"]["cible_aprem"] = f"{sortie_min // 60:02d}:{sortie_min % 60:02d}" diff --git a/app/tests/test_calcul.py b/app/tests/test_calcul.py index 303863f..64b3e8d 100644 --- a/app/tests/test_calcul.py +++ b/app/tests/test_calcul.py @@ -405,3 +405,36 @@ def test_compute_week_plages_personnalisees(freeze_today): vendredi = result["jours"][4] assert vendredi["cible_matin"] is None assert vendredi["cible_aprem"] == "19:00" + + +def test_compute_week_cible_en_cours_ne_recommande_jamais_un_depart_deja_passe(monkeypatch): + """Régression : quand l'après-midi est en cours et que la semaine a de l'avance, + la répartition de l'avance ne doit jamais faire retomber la cible du jour même + avant l'heure actuelle — on ne peut pas recommander un départ déjà passé. + + Lundi-mercredi en léger surplus, jeudi (aujourd'hui, gelé à 17:19) après-midi en + cours depuis 13:37 : le calcul nominal donnerait 17:17 (avant maintenant), la + cible doit être plafonnée à 17:19. + """ + import calcul as calcul_module + + class FrozenDatetime(calcul_module.datetime): + _frozen = calcul_module.datetime(2026, 8, 6, 17, 19) + + @classmethod + def today(cls): + return cls._frozen + + monkeypatch.setattr(calcul_module, "datetime", FrozenDatetime) + + plages = {"arrivee_visee": "08:30"} + pointages = [ + _pointage("2026-08-03", "08:12", "12:02", "13:23", "18:00"), # +39 + _pointage("2026-08-04", "08:19", "12:02", "13:01", "17:01"), # -5 + _pointage("2026-08-05", "08:15", "12:03", "12:55", "17:02"), # +7 + _pointage("2026-08-06", "08:25", "12:05", aprem_entree="13:37"), # en cours + _pointage("2026-08-07"), + ] + result = compute_week(pointages, [], HEURES_JOUR_MIN, plages) + jeudi = result["jours"][3] + assert jeudi["cible_aprem"] == "17:19"