Répartit avance/retard sur les départs du soir, plus jamais sur le matin

Le rattrapage hebdomadaire ne portait que sur le retard et pouvait
s'étaler sur le matin quand il avait de la marge. Objectif demandé :
pouvoir planifier les horaires à l'avance pour arriver à 0 en fin de
semaine, en ajustant uniquement les heures de départ du soir - dans
un sens (retard) comme dans l'autre (avance, plafonnée par aprem_fin).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Antoine
2026-08-06 17:12:33 +02:00
parent d3c991f8d6
commit b9a75fffea
3 changed files with 117 additions and 44 deletions

View File

@ -248,14 +248,63 @@ def test_compute_week_surplus_creneau_sature_reporte_sur_les_autres(freeze_today
assert vendredi["cible_aprem"] == "19:38"
def test_compute_week_cible_matin_possible_quand_la_plage_matin_a_de_la_marge(freeze_today):
"""Avec des plages personnalisées où la matinée obligatoire est plus longue que
le nominal, une cible matin peut être proposée — mais jamais au-delà de
matin_fin, pour que la pause (matin_fin → reprise) reste libre.
def test_compute_week_avance_reduit_les_departs_du_soir_restants(freeze_today):
"""Symétrique du rattrapage : une avance (travaillé plus que dû) doit réduire
les heures de départ du soir des jours restants, pas seulement rester
invisible — pour permettre de planifier un solde à 0 en fin de semaine.
Plages personnalisées sans report matin→aprem (capacité matin 240 = nominal).
Lundi +20 min d'avance, reste de la semaine vide : les 4 départs restants
sont chacun avancés de 5 min (18:00 nominal → 17:55).
"""
plages = {
"matin_debut": "09:00", "matin_fin": "13:00",
"aprem_debut": "14:00", "aprem_fin": "17:00",
"pause_dejeuner_fin": "14:00",
}
heures_jour = 480 # 240 matin + 240 aprem
pointages = [
_pointage("2026-07-13", "09:00", "13:00", "14:00", "18:20"), # +20 min
_pointage("2026-07-14"),
_pointage("2026-07-15"),
_pointage("2026-07-16"),
_pointage("2026-07-17"),
]
result = compute_week(pointages, [], heures_jour, plages)
assert result["extra_min"] == 0 # extra_min ne compte que le retard, pas l'avance
for jour in result["jours"][1:]:
assert jour["cible_matin"] is None
assert jour["cible_aprem"] == "17:55"
def test_compute_week_avance_ne_descend_jamais_sous_aprem_fin(freeze_today):
"""L'avance ne peut pas faire descendre un départ sous le plancher aprem_fin
(présence minimale obligatoire) : au-delà de la marge disponible, l'avance
restante n'est simplement pas placée (elle reste visible dans le solde).
234 min nominal aprem (13:30 + 234 = 17:24), plancher 17:00 : seulement 24 min
de marge par jour. Avec 200 min d'avance sur un seul jour ouvert, la cible
plafonne à 17:00 au lieu de descendre à 17:24 - 200.
"""
pointages = [
_pointage("2026-07-13", "09:00", "12:24", "13:30", "20:34"), # +200 min
_pointage("2026-07-14", "09:00", "12:24", "13:30", "17:54"),
_pointage("2026-07-15", "09:00", "12:24", "13:30", "17:54"),
_pointage("2026-07-16", "09:00", "12:24", "13:30", "17:54"),
_pointage("2026-07-17"),
]
result = compute_week(pointages, [], HEURES_JOUR_MIN)
vendredi = result["jours"][4]
assert vendredi["cible_aprem"] == "17:00"
def test_compute_week_cible_matin_jamais_ajustee_meme_avec_de_la_marge(freeze_today):
"""Le rattrapage hebdomadaire ne porte jamais sur le matin, même quand la
matinée obligatoire est plus longue que le nominal et aurait de la marge.
Matin 08:00→13:00 (capacité 300 > nominal 270). Vendredi en congé après-midi,
120 min de retard : le matin reçoit +30 max (plafond 13:00), le reste n'est
pas plaçable. La pause 13:00→14:00 reste intacte.
120 min de retard : le seul créneau ouvert (le matin) n'est pas ajustable, le
retard n'est pas plaçable ailleurs (l'après-midi est en congé).
"""
plages = {
"matin_debut": "08:00", "matin_fin": "13:00",
@ -274,15 +323,14 @@ def test_compute_week_cible_matin_possible_quand_la_plage_matin_a_de_la_marge(fr
result = compute_week(pointages, conges, heures_jour, plages)
vendredi = result["jours"][4]
assert result["extra_min"] == 120
# 08:00 + 270 nominal + 30 (saturé au plafond matin_fin) = 13:00
assert vendredi["cible_matin"] == "13:00"
assert vendredi["cible_matin"] is None
assert vendredi["cible_aprem"] is None # congé
def test_compute_week_arrivee_visee_etend_le_creneau_matin(freeze_today):
"""L'arrivée visée sert de début projeté du matin : en visant 08:00, le créneau
matin gagne de la capacité (08:00→12:00 = 240 ≥ 234 nominal) et peut recevoir
du rattrapage — toujours sans dépasser matin_fin.
def test_compute_week_arrivee_visee_n_etend_pas_le_rattrapage_au_matin(freeze_today):
"""L'arrivée visée sert de début projeté du matin (pour le calcul du surplus
basculant vers l'après-midi), mais n'ouvre jamais le matin au rattrapage
hebdomadaire — celui-ci reste réservé aux départs du soir.
"""
plages = {"arrivee_visee": "08:00"}
conges = [{"date": "2026-07-17", "type": "aprem"}]
@ -296,8 +344,7 @@ def test_compute_week_arrivee_visee_etend_le_creneau_matin(freeze_today):
result = compute_week(pointages, conges, HEURES_JOUR_MIN, plages)
vendredi = result["jours"][4]
assert result["extra_min"] == 120
# 08:00 + 234 nominal + 6 (saturé au plafond matin_fin) = 12:00
assert vendredi["cible_matin"] == "12:00"
assert vendredi["cible_matin"] is None
def test_compute_week_depart_vise_plafond_souple(freeze_today):