Merge remote-tracking branch 'origin/main'

# Conflicts:
#	app/calcul.py
This commit is contained in:
Antoine
2026-08-06 17:18:00 +02:00
19 changed files with 2097 additions and 123 deletions

View File

@ -113,6 +113,8 @@ def test_compute_week_cible_repartie_sur_seule_demi_journee_restante(freeze_toda
assert vendredi["cible_matin"] is None # matin déjà pointé (entrée+sortie), plus rien à y faire
assert vendredi["cible_aprem"] == "18:18"
# La préconisation aprem inclut aussi l'heure d'arrivée (reprise par défaut 13:30)
assert vendredi["cible_aprem_entree"] == "13:30"
def test_compute_week_semaine_totalement_vide_compte_les_10_demi_journees(freeze_today):
@ -126,10 +128,11 @@ def test_compute_week_semaine_totalement_vide_compte_les_10_demi_journees(freeze
result = compute_week(pointages, [], HEURES_JOUR_MIN)
assert result["slots_restants"] == 10
assert result["reste_min"] == HEURES_JOUR_MIN * 5
# Rien n'est encore en retard puisque rien n'a encore été renseigné : le rythme
# normal (à répartir librement sur les 10 demi-journées) suffit, pas de cible imposée.
# Pas de retard (rien n'a encore été renseigné) : le rythme normal suffit, extra = 0.
# Mais chaque demi-journée reste propose son horaire visé (matin 09:00→12:00,
# aprem 13:30→18:18 avec le surplus nominal basculé depuis le matin saturé).
assert result["extra_min"] == 0
assert all(j["cible_matin"] is None and j["cible_aprem"] is None for j in result["jours"])
assert all(j["cible_matin"] == "12:00" and j["cible_aprem"] == "18:18" for j in result["jours"])
def test_compute_week_jour_futur_non_entame_recoit_une_cible_repartie(freeze_today):
@ -148,30 +151,34 @@ def test_compute_week_jour_futur_non_entame_recoit_une_cible_repartie(freeze_tod
assert lundi_suivant["cible_matin"] is not None or lundi_suivant["cible_aprem"] is not None
def test_compute_week_jour_futur_a_egalite_pas_de_cible(freeze_today):
"""Quand le rythme normal des demi-journées restantes suffit déjà, aucune cible n'est affichée."""
def test_compute_week_jour_futur_a_egalite_propose_le_rythme_normal(freeze_today):
"""Quand le rythme normal des demi-journées restantes suffit (pas de rattrapage),
les horaires visés du matin et de l'après-midi sont quand même proposés."""
dates_completes = ["2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16", "2026-07-17"]
pointages = _semaine_complete(dates_completes)
pointages.append(_pointage("2026-07-20")) # lundi suivant, rien pointé — mais la semaine est déjà à l'équilibre
result = compute_week(pointages, [], HEURES_JOUR_MIN)
lundi_suivant = result["jours"][5]
assert lundi_suivant["cible_matin"] is None
assert lundi_suivant["cible_aprem"] is None
assert result["extra_min"] == 0
assert lundi_suivant["cible_matin"] == "12:00"
assert lundi_suivant["cible_aprem"] == "18:18"
def test_compute_week_jour_courant_non_entame_rythme_normal_suffit(freeze_today):
def test_compute_week_jour_courant_non_entame_propose_le_rythme_normal(freeze_today):
""""Aujourd'hui" sans rien pointé n'est pas traité comme un jour futur figé : ses deux
demi-journées entrent dans la répartition. Ici le manque (468 min) correspond exactement
au nominal des deux demi-journées restantes : le rythme normal suffit, pas de cible."""
au nominal des deux demi-journées restantes : le rythme normal suffit, mais les horaires
visés matin et aprem restent proposés."""
dates_completes = ["2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16"]
pointages = _semaine_complete(dates_completes)
pointages.append(_pointage("2026-07-17"))
result = compute_week(pointages, [], HEURES_JOUR_MIN)
vendredi = result["jours"][4]
assert vendredi["cible_matin"] is None
assert vendredi["cible_aprem"] is None
assert result["extra_min"] == 0
assert vendredi["cible_matin"] == "12:00"
assert vendredi["cible_aprem"] == "18:18"
def test_compute_week_demi_journee_en_cours_pas_de_double_comptage(freeze_today):
@ -194,8 +201,9 @@ def test_compute_week_demi_journee_en_cours_pas_de_double_comptage(freeze_today)
result = compute_week(pointages, [], HEURES_JOUR_MIN)
vendredi = result["jours"][4]
assert result["extra_min"] == 60
# matin saturé au nominal (09:00→12:00), pas de cible ; aprem : 13:30 + 288 + 60 = 19:18
assert vendredi["cible_matin"] is None
# matin en cours : sa cible correspond au planning nominal (09:00→12:00), pas de rattrapage
# possible (saturé au plafond pause) ; aprem : 13:30 + 288 + 60 = 19:18
assert vendredi["cible_matin"] == "12:00"
assert vendredi["cible_aprem"] == "19:18"
@ -221,7 +229,9 @@ def test_compute_week_cible_matin_ne_mange_jamais_la_pause_de_midi(freeze_today)
# 120 min de retard + 54 min de nominal matin qui ne tiennent pas avant la pause
# (et ne peuvent pas basculer sur l'après-midi, en congé)
assert result["extra_min"] == 174
assert vendredi["cible_matin"] is None
# Le matin (seul créneau dispo, aprem en congé) est saturé à son plafond 09:00→12:00 :
# cible proposée au rythme normal, mais le rattrapage (174 min) ne tient pas.
assert vendredi["cible_matin"] == "12:00"
assert vendredi["cible_aprem"] is None # congé
@ -243,8 +253,8 @@ def test_compute_week_surplus_creneau_sature_reporte_sur_les_autres(freeze_today
result = compute_week(pointages, [], HEURES_JOUR_MIN)
vendredi = result["jours"][4]
assert result["extra_min"] == 80
# matin saturé au nominal, pas de cible ; aprem : 13:30 + 288 + 80 = 19:38
assert vendredi["cible_matin"] is None
# matin saturé au nominal (cible au rythme normal 09:00→12:00) ; aprem : 13:30 + 288 + 80 = 19:38
assert vendredi["cible_matin"] == "12:00"
assert vendredi["cible_aprem"] == "19:38"
@ -273,7 +283,7 @@ def test_compute_week_avance_reduit_les_departs_du_soir_restants(freeze_today):
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_matin"] == "13:00" # rythme normal, jamais ajusté par l'avance
assert jour["cible_aprem"] == "17:55"
@ -301,6 +311,7 @@ def test_compute_week_avance_ne_descend_jamais_sous_aprem_fin(freeze_today):
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.
Le matin garde son horaire nominal (rythme normal), non ajusté.
Matin 08:00→13:00 (capacité 300 > nominal 270). Vendredi en congé après-midi,
120 min de retard : le seul créneau ouvert (le matin) n'est pas ajustable, le
@ -323,14 +334,14 @@ def test_compute_week_cible_matin_jamais_ajustee_meme_avec_de_la_marge(freeze_to
result = compute_week(pointages, conges, heures_jour, plages)
vendredi = result["jours"][4]
assert result["extra_min"] == 120
assert vendredi["cible_matin"] is None
assert vendredi["cible_matin"] == "12:30" # rythme normal (08:00 + 270 nominal), pas de rattrapage
assert vendredi["cible_aprem"] is None # congé
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.
"""L'arrivée visée sert de début projeté du matin (pour le rythme normal affiché
et 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"}]
@ -344,7 +355,7 @@ def test_compute_week_arrivee_visee_n_etend_pas_le_rattrapage_au_matin(freeze_to
result = compute_week(pointages, conges, HEURES_JOUR_MIN, plages)
vendredi = result["jours"][4]
assert result["extra_min"] == 120
assert vendredi["cible_matin"] is None
assert vendredi["cible_matin"] == "11:54" # rythme normal (08:00 + 234 nominal), pas de rattrapage
def test_compute_week_depart_vise_plafond_souple(freeze_today):
@ -372,7 +383,7 @@ def test_compute_week_depart_vise_plafond_souple(freeze_today):
# vendredi aprem : 13:30 + 234 + (36 + 9) = 18:09 ; lundi aprem : 13:30 + 288 + 9 = 18:27
# (sans départ visé, lundi finirait à 18:45)
assert vendredi["cible_aprem"] == "18:09"
assert lundi["cible_matin"] is None
assert lundi["cible_matin"] == "12:00" # matin au rythme normal (pas de rattrapage pour ce créneau)
assert lundi["cible_aprem"] == "18:27"