Durée forfaitaire attribuée à chaque jour d'astreinte déclaré (bouton AS), en plus des interventions saisies. Elle ne compte jamais dans les heures - travaillées : elle est suivie à part dans le récapitulatif d'astreinte de la - semaine ; le lendemain de l'astreinte n'est pas concerné. + travaillées : suivie à part dans le récapitulatif d'astreinte, elle est + considérée réalisée en journée du jour AS et bénéficie donc de la + bonification « journée » correspondante (×1 en semaine, ×1,5 le samedi, + ×2 le dimanche ou un jour férié) ; le lendemain de l'astreinte n'est pas + concerné. Vide = pas de forfait.
diff --git a/app/tests/test_astreinte.py b/app/tests/test_astreinte.py index eb39c4e..6894770 100644 --- a/app/tests/test_astreinte.py +++ b/app/tests/test_astreinte.py @@ -2,7 +2,8 @@ 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, + retour_apres_repos, regle_journee_astreinte, DEFAULT_PLAGES, PAS_ASTREINTE_MIN, + ASTREINTE_DEBUT_MIN, ) import models @@ -620,7 +621,8 @@ def test_compute_week_astreinte_samedi_dimanche(freeze_today): def test_compute_week_forfait_hors_heures_travaillees(freeze_today): """Forfait 1h00 réglé : attribué au jour AS déclaré uniquement (pas du lendemain), mais jamais compté dans les heures travaillées — seulement - suivi dans le récapitulatif d'astreinte.""" + suivi dans le récapitulatif d'astreinte, pondéré par la bonification + journée du jour (2026-07-14 est férié : ×2).""" plages = {**PLAGES, "astreinte_forfait": "01:00"} pointages = [ _pointage("2026-07-13", "09:00", "12:24", "13:30", "17:54"), @@ -635,13 +637,49 @@ def test_compute_week_forfait_hors_heures_travaillees(freeze_today): assert result["jours"][0]["forfait_min"] == 0 # jour sans astreinte assert result["total_travaille"] == "15h36" # 2 × 7h48, forfait exclu # récapitulatif d'astreinte : le jour AS apparaît avec son forfait même sans - # intervention ; le forfait est rappelé mais hors compteurs pondérés + # intervention ; le forfait est bonifié « journée » du jour (férié → ×2) ast = result["astreinte"] assert ast["actif"] is True assert ast["total_min"] == 0 assert ast["total_forfait_min"] == 60 assert ast["total_forfait_h"] == "1h00" - assert [(j["date"], j["forfait_min"], j["events"]) for j in ast["jours"]] == [("2026-07-14", 60, [])] + assert ast["total_forfait_pondere_min"] == 120 + assert ast["total_forfait_pondere_h"] == "2h00" + [(jour_as)] = ast["jours"] + assert (jour_as["date"], jour_as["forfait_min"], jour_as["events"]) == ("2026-07-14", 60, []) + assert (jour_as["forfait_pondere_min"], jour_as["forfait_regle"], jour_as["forfait_facteur_txt"]) == (120, "Dimanche / férié", "2") + + +def test_forfait_bonifie_selon_la_journee_du_jour(freeze_today): + """Le forfait est considéré réalisé en journée : il bénéficie de la + bonification « journée » du jour où il est attribué — ×1 en semaine, ×1,5 + le samedi, ×2 le dimanche ou un férié (règles par défaut).""" + plages = {**PLAGES, "astreinte_forfait": "01:00"} + pointages = [_pointage(d) for d in + ["2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16", "2026-07-17", + "2026-07-18", "2026-07-19"]] + result = compute_week(pointages, [], HEURES_JOUR_MIN, plages, + ["2026-07-15", "2026-07-18", "2026-07-19"]) + ast = result["astreinte"] + details = {j["date"]: (j["forfait_min"], j["forfait_pondere_min"], j["forfait_regle"]) + for j in ast["jours"]} + assert details["2026-07-15"] == (60, 60, "Journée") # mercredi : ×1 + assert details["2026-07-18"] == (60, 90, "Samedi") # samedi : ×1,5 + assert details["2026-07-19"] == (60, 120, "Dimanche / férié") # dimanche : ×2 + assert ast["total_forfait_min"] == 180 + assert ast["total_forfait_pondere_min"] == 270 + assert ast["total_forfait_pondere_h"] == "4h30" + + +def test_regle_journee_astreinte_suit_les_regles_configurees(): + """La bonification journée du forfait suit les règles configurées : première + règle couvrant midi pour le type du jour.""" + regles = [{"nom": "Journée spéciale", "jours": ["semaine"], "debut": "06:00", "fin": "22:00", "facteur": 1.25}] + r = regle_journee_astreinte("2026-07-15", regles) + assert r is not None + assert (r["nom"], r["facteur"], r["facteur_txt"]) == ("Journée spéciale", 1.25, "1,25") + # Aucune règle ne couvre midi ce jour-là : pas de bonification + assert regle_journee_astreinte("2026-07-15", [{"nom": "Nuit", "jours": ["semaine"], "debut": "22:00", "fin": "06:00", "facteur": 1.5}]) is None def test_compute_week_sans_forfait_comportement_inchange(freeze_today): diff --git a/app/tests/test_ui_refonte.py b/app/tests/test_ui_refonte.py index 36b93bc..d44cc07 100644 --- a/app/tests/test_ui_refonte.py +++ b/app/tests/test_ui_refonte.py @@ -96,3 +96,20 @@ def test_retour_decale_affiche_le_credit_de_presence(monkeypatch): assert "+2h00 repos" in r.text assert "matin 09:00→11:00" in r.text # détail des plages couvertes assert "de présence comptées" in r.text # rappel dans le bloc astreinte + + +def test_forfait_bonifie_journee_affiche_dans_le_recap(monkeypatch): + """Forfait 1h00 réglé + AS le samedi 18/07 : le récapitulatif montre le + forfait pondéré par la bonification journée du samedi (×1,5 → 1h30).""" + monkeypatch.setattr(main, "datetime", _FrozenDatetime) + cookies = _login("u-ui-forfait") + r = client.post("/settings/astreinte-forfait", cookies=cookies, + data={"astreinte_forfait": "01:00"}, follow_redirects=False) + assert r.status_code == 303, r.text + client.post("/astreinte/2026-07-18/toggle", cookies=cookies, follow_redirects=False) + + r = client.get("/semaine/2026/29", cookies=cookies, follow_redirects=False) + assert r.status_code == 200, r.text + assert "forfait 1h00 → 1h30 pondérées" in r.text # ligne du jour (bloc AS) + assert "forfaits 1h00 → 1h30 pondérés" in r.text # totaux du récapitulatif + assert "considéré réalisé en journée" in r.text # explication au survol