"""Astreinte : événements, décompte par tranches de 15 min, repos de 11h et retour décalé.""" from calcul import ( compute_week, decompte_astreinte, heures_dues, heures_travaillees, hhmm_to_minutes, jour_astreinte_qualifie, exclusion_retour, nominaux_du_jour, fenetre_astreinte_jour, retour_apres_repos, DEFAULT_PLAGES, PAS_ASTREINTE_MIN, ASTREINTE_DEBUT_MIN, ) import models HEURES_JOUR_MIN = 468 # 7h48 PLAGES = dict(DEFAULT_PLAGES) def _pointage(date, matin_entree=None, matin_sortie=None, aprem_entree=None, aprem_sortie=None, astreinte_events=None): return { "date": date, "matin_entree": matin_entree, "matin_sortie": matin_sortie, "aprem_entree": aprem_entree, "aprem_sortie": aprem_sortie, "astreinte_events": astreinte_events, } # ── Qualification ───────────────────────────────────────────────────────────── def test_jour_astreinte_qualifie(): astreintes = ["2026-07-14"] assert jour_astreinte_qualifie("2026-07-14", astreintes) # jour d'astreinte assert jour_astreinte_qualifie("2026-07-15", astreintes) # lendemain assert not jour_astreinte_qualifie("2026-07-13", astreintes) assert not jour_astreinte_qualifie("2026-07-15", []) # ── Heures dues : exclusion avant l'heure de retour ─────────────────────────── def test_heures_dues_retour_avant_ouverture_sans_effet(): assert heures_dues("2026-07-13", [], HEURES_JOUR_MIN, PLAGES, hhmm_to_minutes("08:30")) == HEURES_JOUR_MIN def test_heures_dues_retour_midi_retire_la_presence_du_matin(): # 13:00 → plage matin 09:00-12:00 (180 min) retirée du décompte assert heures_dues("2026-07-13", [], HEURES_JOUR_MIN, PLAGES, hhmm_to_minutes("13:00")) == HEURES_JOUR_MIN - 180 def test_heures_dues_retour_apres_midi_retire_matin_et_partie_aprem(): # 15:00 → matin (180) + 14:00→15:00 (60) assert heures_dues("2026-07-13", [], HEURES_JOUR_MIN, PLAGES, hhmm_to_minutes("15:00")) == HEURES_JOUR_MIN - 240 def test_heures_dues_conge_matin_pas_de_double_exclusion(): conges = [{"date": "2026-07-13", "type": "matin"}] # base = demi-journée aprem (234) ; le matin déjà en congé n'est pas décompté deux fois assert heures_dues("2026-07-13", conges, HEURES_JOUR_MIN, PLAGES, hhmm_to_minutes("13:00")) == 234 assert heures_dues("2026-07-13", conges, HEURES_JOUR_MIN, PLAGES, hhmm_to_minutes("15:00")) == 234 - 60 def test_exclusion_retour_sans_retour(): assert exclusion_retour(set(), None, PLAGES) == (0, 0) # ── Heures travaillées : la présence avant le retour ne compte pas ──────────── def test_heures_travaillees_presence_avant_retour_non_comptee(): p = _pointage("2026-07-13", "09:00", "12:00", "14:00", "18:00") # matin 09:00→12:00 entièrement avant le retour 13:00 → 0 ; aprem 14:00→18:00 → 240 assert heures_travaillees(p, hhmm_to_minutes("13:00")) == 240 assert heures_travaillees(p) == 180 + 240 def test_heures_travaillees_entree_chevauchant_le_retour(): p = _pointage("2026-07-13", "08:00", "12:00") # entrée décalée à 10:30 → 90 min comptées assert heures_travaillees(p, hhmm_to_minutes("10:30")) == 90 # ── Retour au plus tôt après 11h de repos (depuis le dernier événement) ─────── def test_retour_fin_nocturne_sur_le_lendemain(): """Event 01:00→02:00 saisi sur le lendemain de l'astreinte → retour 13:00.""" p_by_date = {"2026-07-15": _pointage("2026-07-15", astreinte_events=[{"debut": "01:00", "fin": "02:00"}])} # 02:00 + 11h = 13:00 assert retour_apres_repos("2026-07-15", p_by_date, PLAGES, ["2026-07-14"]) == 13 * 60 def test_retour_activite_matinale_sur_le_lendemain_qualifie(): """Event 05:00→06:00 saisi sur le lendemain de l'astreinte : le matin du lendemain est dans la fenêtre 18:00→18:00, l'activité s'est terminée à 06:00 ce jour-là → repos jusqu'à 17:00 le même jour.""" p_by_date = {"2026-07-15": _pointage("2026-07-15", astreinte_events=[{"debut": "05:00", "fin": "06:00"}])} assert retour_apres_repos("2026-07-15", p_by_date, PLAGES, ["2026-07-14"]) == 17 * 60 def test_retour_intervention_avant_18h_sur_le_jour_as_ignoree(): """L'astreinte couvre 18:00 → 18:00 : une intervention du matin du jour déclaré (avant 18:00) n'est pas de l'astreinte et ne déclenche pas de repos.""" p_by_date = {"2026-07-14": _pointage("2026-07-14", astreinte_events=[{"debut": "10:00", "fin": "12:00"}])} assert retour_apres_repos("2026-07-14", p_by_date, PLAGES, ["2026-07-14"]) is None assert retour_apres_repos("2026-07-15", p_by_date, PLAGES, ["2026-07-14"]) is None def test_retour_intervention_finissant_avant_18h_la_veille_ignoree(): """Event 16:00→17:30 la veille déclarée AS : fin 17:30 arrondie 17:30, avant le début de la fenêtre (18:00) → pas de contrainte de retour.""" p_by_date = {"2026-07-14": _pointage("2026-07-14", astreinte_events=[{"debut": "16:00", "fin": "17:30"}])} assert retour_apres_repos("2026-07-15", p_by_date, PLAGES, ["2026-07-14"]) is None def test_retour_fin_du_soir_sur_le_jour_d_astreinte(): """Event 21:00→23:30 sur le jour d'astreinte → repos terminé 10:30 le lendemain.""" p_by_date = {"2026-07-14": _pointage("2026-07-14", astreinte_events=[{"debut": "21:00", "fin": "23:30"}])} # 23:30 + 11h = 10:30 le lendemain assert retour_apres_repos("2026-07-15", p_by_date, PLAGES, ["2026-07-14"]) == 10 * 60 + 30 def test_retour_fin_hors_quart_arrondie_au_quart_entame(): """Event 21:00→23:37 : le quart d'heure entamé est travaillé, fin comptée 23:45 → repos terminé 10:45 le lendemain.""" p_by_date = {"2026-07-14": _pointage("2026-07-14", astreinte_events=[{"debut": "21:00", "fin": "23:37"}])} assert retour_apres_repos("2026-07-15", p_by_date, PLAGES, ["2026-07-14"]) == 10 * 60 + 45 def test_retour_fin_nocturne_hors_quart_arrondie(): """Event 23:00→01:07 (à cheval sur minuit) : fin comptée 01:15 → retour 12:15.""" p_by_date = {"2026-07-14": _pointage("2026-07-14", astreinte_events=[{"debut": "23:00", "fin": "01:07"}])} assert retour_apres_repos("2026-07-15", p_by_date, PLAGES, ["2026-07-14"]) == 12 * 60 + 15 def test_retour_event_a_cheval_sur_minuit_saisi_la_veille(): """Event 23:00→01:00 (fin < début) saisi la veille : fin réelle 01:00 le jour même → 01:00 + 11h = 12:00.""" p_by_date = {"2026-07-14": _pointage("2026-07-14", astreinte_events=[{"debut": "23:00", "fin": "01:00"}])} assert retour_apres_repos("2026-07-15", p_by_date, PLAGES, ["2026-07-14"]) == 12 * 60 def test_retour_nuit_d_astreinte_finissant_au_matin(): """Event 23:00→06:00 (à cheval sur minuit) sur le jour d'astreinte : fin réelle 06:00 le lendemain → retour 17:00 le lendemain.""" p_by_date = {"2026-07-14": _pointage("2026-07-14", astreinte_events=[{"debut": "23:00", "fin": "06:00"}])} assert retour_apres_repos("2026-07-15", p_by_date, PLAGES, ["2026-07-14"]) == 17 * 60 def test_retour_la_plus_tardive_des_deux_fins_gagne(): """Fins veille 23:30 (retour 10:30) ET jour 02:00 (retour 13:00) : la plus tardive gagne.""" p_by_date = { "2026-07-14": _pointage("2026-07-14", astreinte_events=[{"debut": "21:00", "fin": "23:30"}]), "2026-07-15": _pointage("2026-07-15", astreinte_events=[{"debut": "01:00", "fin": "02:00"}]), } assert retour_apres_repos("2026-07-15", p_by_date, PLAGES, ["2026-07-14"]) == 13 * 60 def test_retour_event_sans_astreinte_ignore(): p_by_date = {"2026-07-14": _pointage("2026-07-14", astreinte_events=[{"debut": "21:00", "fin": "23:30"}])} assert retour_apres_repos("2026-07-15", p_by_date, PLAGES, []) is None def test_retour_fin_tot_ne_contraint_rien(): """Event 17:15→17:30 → repos terminé 04:30, avant l'ouverture : pas de contrainte.""" p_by_date = {"2026-07-14": _pointage("2026-07-14", astreinte_events=[{"debut": "17:15", "fin": "17:30"}])} assert retour_apres_repos("2026-07-15", p_by_date, PLAGES, ["2026-07-14"]) is None # ── Décompte astreinte : tranches de 15 min entamées, par règle ─────────────── def test_decompte_tranche_entamee_comptee_entiere(): """22:07→22:20 touche les tranches alignées 22:00 et 22:15 : comptées entières.""" d = decompte_astreinte("2026-07-13", [{"debut": "22:07", "fin": "22:20"}]) assert d["tranches"] == 2 # tranches 22:00 + 22:15 entamées assert d["total_min"] == 2 * PAS_ASTREINTE_MIN # 30 assert d["total_h"] == "0h30" assert d["categories"][0]["nom"] == "Nuit" # lundi 22:xx → Nuit ×1,5 assert d["total_pondere_min"] == 45 # 30 × 1,5 def test_decompte_arrondi_des_bornes(): """09:50→10:05 touche les tranches 09:45 et 10:00 : 2 tranches en Journée ×1.""" d = decompte_astreinte("2026-07-13", [{"debut": "09:50", "fin": "10:05"}]) assert d["tranches"] == 2 # tranches 09:45 + 10:00 assert d["total_min"] == 30 assert d["categories"][0]["nom"] == "Journée" # lundi en journée ×1 assert d["total_pondere_min"] == 30 def test_decompte_evenement_a_cheval_sur_minuit(): """Mardi 14/07 (férié), 23:00→01:00 : 8 tranches, tout en Nuit ×1,5.""" d = decompte_astreinte("2026-07-14", [{"debut": "23:00", "fin": "01:00"}]) assert d["tranches"] == 8 # 23:00→01:00 = 120 min = 8 × 15 assert d["total_min"] == 120 assert d["categories"][0]["nom"] == "Nuit" assert d["total_pondere_min"] == 180 # 120 × 1,5 def test_decompte_jour_ferie_en_journee(): """14 juillet : la journée compte en « Dimanche / férié » ×2.""" d = decompte_astreinte("2026-07-14", [{"debut": "10:00", "fin": "12:00"}]) assert d["total_min"] == 120 # 2h = 8 tranches assert d["categories"][0]["nom"] == "Dimanche / férié" assert d["total_pondere_min"] == 240 # 120 × 2 assert d["total_pondere_h"] == "4h00" def test_decompte_vendredi_vers_samedi_reste_en_nuit(): """23:00→02:00 le vendredi : 12 tranches, toutes en Nuit (même après minuit).""" d = decompte_astreinte("2026-07-17", [{"debut": "23:00", "fin": "02:00"}]) assert d["tranches"] == 12 # 180 min assert d["total_min"] == 180 assert [c["nom"] for c in d["categories"]] == ["Nuit"] assert d["total_pondere_min"] == 270 # 180 × 1,5 def test_decompte_chevauchement_dedouble(): """10:00→10:20 et 10:10→10:30 se recouvrent : les tranches communes ne comptent qu'une fois dans le total du jour, chaque event garde son propre détail.""" d = decompte_astreinte("2026-07-13", [ {"debut": "10:00", "fin": "10:20"}, {"debut": "10:10", "fin": "10:30"}, ]) assert len(d["events"]) == 2 assert d["events"][0]["tranches"] == 2 # tranches 10:00 + 10:15 assert d["events"][0]["min"] == 30 assert d["events"][1]["tranches"] == 2 # 10:10 aligné sur la tranche 10:00 assert d["events"][1]["min"] == 30 assert d["tranches"] == 2 # union dédoublonnée assert d["total_min"] == 30 assert d["categories"][0]["nom"] == "Journée" def test_decompte_event_incomplet_ignore(): """Un événement sans debut ou sans fin est ignoré.""" d = decompte_astreinte("2026-07-13", [{"debut": "22:00"}, {"fin": "23:00"}, {}]) assert d["tranches"] == 0 assert d["total_min"] == 0 assert d["events"] == [] assert d["categories"] == [] def test_decompte_regles_personnalisees(): """Une config custom remplace les règles par défaut (fenêtre à cheval sur minuit).""" facteurs = [{"nom": "Garde", "jours": ["semaine"], "debut": "18:00", "fin": "09:00", "facteur": 2}] d = decompte_astreinte("2026-07-13", [{"debut": "20:00", "fin": "21:00"}], facteurs) assert d["total_min"] == 60 assert d["categories"][0]["nom"] == "Garde" assert d["total_pondere_min"] == 120 # ── Nominaux ────────────────────────────────────────────────────────────────── def test_nominaux_exclusion_totale_du_matin(): m, a = nominaux_du_jour(set(), 360, 180, 0) assert (m, a) == (0, 180) def test_nominaux_sans_exclusion_reste_la_moitié(): m, a = nominaux_du_jour(set(), 468, 0, 0) assert (m, a) == (234, 234) # ── compute_week ────────────────────────────────────────────────────────────── def test_compute_week_decompte_absent_des_heures_de_semaine(freeze_today): """Lundi et jeudi complets, mardi astreinte avec event 23:00→01:00, mercredi vide : les heures d'astreinte n'entrent jamais dans total_travaille/total_du, seulement dans result["astreinte"].""" pointages = [ _pointage("2026-07-13", "09:00", "12:24", "13:30", "17:54"), _pointage("2026-07-14", astreinte_events=[{"debut": "23:00", "fin": "01:00"}]), _pointage("2026-07-15"), _pointage("2026-07-16", "09:00", "12:24", "13:30", "17:54"), _pointage("2026-07-17"), ] result = compute_week(pointages, [], HEURES_JOUR_MIN, PLAGES, ["2026-07-14"]) # seul le pointé compte : 2 × 468 = 15h36, astreinte exclue assert result["total_travaille"] == "15h36" ast = result["astreinte"] assert ast["actif"] is True assert ast["total_min"] == 120 # 23:00→01:00 = 8 tranches assert ast["total_h"] == "2h00" assert ast["total_pondere_min"] == 180 # Nuit ×1,5 assert ast["total_pondere_h"] == "3h00" assert ast["categories"][0]["nom"] == "Nuit" assert any(j["date"] == "2026-07-14" for j in ast["jours"]) mardi = result["jours"][1] assert mardi["astreinte_min"] == 120 assert mardi["astreinte_h"] == "2h00" assert mardi["astreinte_pondere_min"] == 180 def test_compute_week_fin_nocturne_decale_le_decompte_et_les_cibles(freeze_today): """Mardi astreinte, event 01:00→02:00 saisi sur le mercredi (lendemain) : retour mercredi 13:00, présence du matin retirée de la semaine, aucune cible matin, après-midi 13:30→18:18 (nominal 288 = 234 + report).""" pointages = [ _pointage("2026-07-13", "09:00", "12:24", "13:30", "17:54"), _pointage("2026-07-14", "09:00", "12:24", "13:30", "17:54"), _pointage("2026-07-15", astreinte_events=[{"debut": "01:00", "fin": "02:00"}]), _pointage("2026-07-16"), _pointage("2026-07-17"), ] result = compute_week(pointages, [], HEURES_JOUR_MIN, PLAGES, ["2026-07-14"]) mercredi = result["jours"][2] assert mercredi["du_min"] == 468 - 180 # matin 09:00→12:00 exclu par le retour 13:00 assert mercredi["retour"] == "13:00" # 02:00 + 11h assert mercredi["cible_matin"] is None assert mercredi["cible_aprem"] == "18:18" assert result["total_du"] == "36h00" # 39h - 3h de présence retirées def test_compute_week_nuit_finissant_au_matin(freeze_today): """Event 23:00→06:00 (à cheval sur minuit) saisi sur le mardi astreinte : le retour 17:00 s'applique au mercredi (matin et après-midi exclus jusqu'à 17:00).""" pointages = [ _pointage("2026-07-13", "09:00", "12:24", "13:30", "17:54"), _pointage("2026-07-14", "09:00", "12:24", "13:30", "17:54", astreinte_events=[{"debut": "23:00", "fin": "06:00"}]), _pointage("2026-07-15"), _pointage("2026-07-16"), _pointage("2026-07-17"), ] result = compute_week(pointages, [], HEURES_JOUR_MIN, PLAGES, ["2026-07-14"]) mercredi = result["jours"][2] assert mercredi["retour"] == "17:00" # matin (180) + 14:00→17:00 (180) exclus → dû = 108 assert mercredi["du_min"] == 468 - 360 assert mercredi["cible_matin"] is None # aprem démarre à 17:00, nominal 108 (54 matin + 54 aprem) → 18:48 assert mercredi["cible_aprem_entree"] == "17:00" assert mercredi["cible_aprem"] == "18:48" # décompte : 23:00→06:00 = 28 tranches = 7h en Nuit ×1,5 assert result["jours"][1]["astreinte_min"] == 420 assert result["astreinte"]["total_pondere_min"] == 630 def test_compute_week_event_ignore_hors_jour_qualifie(freeze_today): """Un event saisi un jour non qualifié (pas d'astreinte) ne change rien au décompte ni au décompte d'astreinte.""" pointages = [ _pointage("2026-07-13", "09:00", "12:24", "13:30", "17:54"), _pointage("2026-07-14", "09:00", "12:24", "13:30", "17:54", astreinte_events=[{"debut": "01:00", "fin": "02:00"}]), _pointage("2026-07-15"), _pointage("2026-07-16"), _pointage("2026-07-17"), ] result = compute_week(pointages, [], HEURES_JOUR_MIN, PLAGES, []) mardi = result["jours"][1] assert mardi["du_min"] == HEURES_JOUR_MIN assert mardi["retour_min"] is None assert mardi["astreinte_min"] == 0 assert result["astreinte"]["actif"] is False def test_compute_week_arrivee_visee_ne_precede_jamais_le_retour(freeze_today): """Event mardi 21:00→23:30 → retour mercredi 10:30 : arrivée cible du matin 10:30, sortie 12:00, et le nominal qui ne tient pas (234-90=144 → 54 manquantes) bascule sur l'après-midi du même jour.""" pointages = [ _pointage("2026-07-13", "09:00", "12:24", "13:30", "17:54"), _pointage("2026-07-14", "09:00", "12:24", "13:30", "17:54", astreinte_events=[{"debut": "21:00", "fin": "23:30"}]), _pointage("2026-07-15"), _pointage("2026-07-16"), _pointage("2026-07-17"), ] result = compute_week(pointages, [], HEURES_JOUR_MIN, PLAGES, ["2026-07-14"]) mercredi = result["jours"][2] assert mercredi["retour"] == "10:30" assert mercredi["cible_matin_entree"] == "10:30" assert mercredi["cible_matin"] == "12:00" # aprem : nominal 234 + 54 reportées = 288 depuis 13:30 → 18:18 assert mercredi["cible_aprem"] == "18:18" def test_compute_week_demi_journee_entierement_en_repos_compte_complete(freeze_today): """Avec un quota de 6h/jour (180 par demi-journée), un retour 13:00 (event 01:00→02:00 sur le mercredi, mardi astreinte) couvre toute la plage du matin : plus rien à y remplir, la demi-journée est complète d'office et l'absence de pointage matin ne casse pas le cumul.""" heures_jour = 360 pointages = [ _pointage("2026-07-13", "09:00", "12:00", "13:30", "16:30"), _pointage("2026-07-14", "09:00", "12:00", "13:30", "16:30"), _pointage("2026-07-15", aprem_entree="13:30", aprem_sortie="16:30", astreinte_events=[{"debut": "01:00", "fin": "02:00"}]), _pointage("2026-07-16", "09:00", "12:00", "13:30", "16:30"), _pointage("2026-07-17", "09:00", "12:00", "13:30", "16:30"), ] result = compute_week(pointages, [], heures_jour, PLAGES, ["2026-07-14"]) mercredi = result["jours"][2] assert mercredi["matin_nominal_min"] == 0 assert mercredi["is_complete"] is True assert mercredi["cumul_visible"] is True assert mercredi["du_min"] == 180 # 6h dues - 3h de présence matin retirées # ── Stockage des astreintes ─────────────────────────────────────────────────── def test_toggle_astreinte_persiste(): models.toggle_astreinte("2026-07-14", "u-astreinte") assert models.load_astreintes("u-astreinte") == ["2026-07-14"] models.toggle_astreinte("2026-07-14", "u-astreinte") assert models.load_astreintes("u-astreinte") == [] def test_load_facteurs_astreinte_defaut_si_absent(): facteurs = models.load_facteurs_astreinte() assert any(f["nom"] == "Nuit" for f in facteurs) # ── Fenêtre d'astreinte 18:00 → 18:00 ─────────────────────────────────────── def test_fenetre_astreinte_jour(): """Le jour déclaré couvre 18:00 → 18:00 le lendemain ; le lendemain n'est couvert que jusqu'à 18:00 ; deux astreintes consécutives se recollent.""" assert fenetre_astreinte_jour("2026-07-14", ["2026-07-14"]) == (ASTREINTE_DEBUT_MIN, 24 * 60 + ASTREINTE_DEBUT_MIN) assert fenetre_astreinte_jour("2026-07-15", ["2026-07-14"]) == (0, ASTREINTE_DEBUT_MIN) assert fenetre_astreinte_jour("2026-07-15", ["2026-07-14", "2026-07-15"]) == (0, 24 * 60 + ASTREINTE_DEBUT_MIN) assert fenetre_astreinte_jour("2026-07-16", ["2026-07-14"]) is None def test_decompte_hors_fenetre_18h_18h_ignore(): # Matin du jour AS déclaré : avant 18:00, hors astreinte d = decompte_astreinte("2026-07-14", [{"debut": "10:00", "fin": "12:00"}], None, (ASTREINTE_DEBUT_MIN, 24 * 60 + ASTREINTE_DEBUT_MIN)) assert d["total_min"] == 0 # Lendemain après 18:00 : hors astreinte d = decompte_astreinte("2026-07-15", [{"debut": "19:00", "fin": "20:00"}], None, (0, ASTREINTE_DEBUT_MIN)) assert d["total_min"] == 0 # Lendemain matin : dans la fenêtre d = decompte_astreinte("2026-07-15", [{"debut": "01:00", "fin": "02:00"}], None, (0, ASTREINTE_DEBUT_MIN)) assert d["total_min"] == 60 # Événement à cheval sur la borne 18:00 du lendemain : seul le fragment # intérieur compte (17:00→18:00 = 4 tranches) d = decompte_astreinte("2026-07-15", [{"debut": "17:00", "fin": "19:00"}], None, (0, ASTREINTE_DEBUT_MIN)) assert d["total_min"] == 60 def test_compute_week_intervention_avant_18h_hors_astreinte(freeze_today): """Mardi AS : l'event 16:00→17:00 (avant 18:00) ne compte pas ; le même créneau à 19:00→20:00 (dans la fenêtre) compte 1h (Journée ×1).""" pointages = [ _pointage("2026-07-13"), _pointage("2026-07-16"), _pointage("2026-07-17"), _pointage("2026-07-14", astreinte_events=[{"debut": "16:00", "fin": "17:00"}]), _pointage("2026-07-15"), ] result = compute_week(pointages, [], HEURES_JOUR_MIN, PLAGES, ["2026-07-14"]) assert result["astreinte"]["total_min"] == 0 assert result["jours"][1]["astreinte_min"] == 0 pointages[3] = _pointage("2026-07-14", astreinte_events=[{"debut": "19:00", "fin": "20:00"}]) result = compute_week(pointages, [], HEURES_JOUR_MIN, PLAGES, ["2026-07-14"]) assert result["astreinte"]["total_min"] == 60 assert result["astreinte"]["total_pondere_min"] == 120 # 14 juillet : férié ×2 def test_compute_week_astreinte_samedi_dimanche(freeze_today): """Samedi AS avec intervention nocturne : dû 0 (week-end), décompte en compteur astreinte (Nuit vendredi ×1,5 puis Samedi ×1,5 sur 08:00→20:00).""" pointages = [ _pointage("2026-07-13", "09:00", "12:24", "13:30", "17:54"), _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", "09:00", "12:24", "13:30", "17:54"), _pointage("2026-07-18", astreinte_events=[{"debut": "23:00", "fin": "09:00"}]), _pointage("2026-07-19"), ] result = compute_week(pointages, [], HEURES_JOUR_MIN, PLAGES, ["2026-07-18"]) samedi = result["jours"][5] assert samedi["du_min"] == 0 assert samedi["astreinte_min"] == 10 * 60 # 23:00→09:00 = 10h assert result["total_du"] == "39h00" # week-end hors dues # ── Forfait par jour d'astreinte déclaré ─────────────────────────────────────── def test_compute_week_forfait_ajoute_au_travaille_du_jour_declare(freeze_today): """Forfait 1h00 réglé : ajouté au travaillé du jour AS déclaré uniquement (pas du lendemain), sans toucher aux heures dues.""" plages = {**PLAGES, "astreinte_forfait": "01:00"} pointages = [ _pointage("2026-07-13", "09:00", "12:24", "13:30", "17:54"), _pointage("2026-07-14", "09:00", "12:24", "13:30", "17:54"), _pointage("2026-07-15"), _pointage("2026-07-16"), _pointage("2026-07-17"), ] result = compute_week(pointages, [], HEURES_JOUR_MIN, plages, ["2026-07-14"]) mardi = result["jours"][1] assert mardi["forfait_min"] == 60 assert mardi["travaille_min"] == HEURES_JOUR_MIN + 60 assert result["jours"][2]["forfait_min"] == 0 # lendemain : rien assert result["jours"][0]["forfait_min"] == 0 # jour sans astreinte assert result["total_travaille"] == "16h36" # 2 × 7h48 + 1h def test_compute_week_sans_forfait_comportement_inchange(freeze_today): pointages = [ _pointage("2026-07-13", "09:00", "12:24", "13:30", "17:54"), _pointage("2026-07-14", "09:00", "12:24", "13:30", "17:54", astreinte_events=[{"debut": "21:00", "fin": "22:00"}]), _pointage("2026-07-15"), _pointage("2026-07-16"), _pointage("2026-07-17"), ] result = compute_week(pointages, [], HEURES_JOUR_MIN, PLAGES, ["2026-07-14"]) assert result["jours"][1]["forfait_min"] == 0 assert result["total_travaille"] == "15h36"