Astreintes : fenêtre 18h-18h, week-end affiché, forfait réglable
Un jour d'astreinte déclaré couvre désormais 18:00 → 18:00 le lendemain :
les interventions hors fenêtre ne comptent ni au décompte ni pour les
11h de repos, et deux astreintes consécutives se recollent sans trou.
La fin de la dernière intervention est arrondie au quart d'heure entamé
avant de poser le retour. Samedi et dimanche apparaissent dans la
semaine (heures dues nulles) pour pouvoir y déclarer des astreintes,
dans les exports aussi. Un réglage par utilisateur ajoute une durée
forfaitaire au temps travaillé de chaque jour d'astreinte déclaré.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
@ -1,8 +1,8 @@
|
||||
"""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,
|
||||
retour_apres_repos, DEFAULT_PLAGES, PAS_ASTREINTE_MIN,
|
||||
jour_astreinte_qualifie, exclusion_retour, nominaux_du_jour, fenetre_astreinte_jour,
|
||||
retour_apres_repos, DEFAULT_PLAGES, PAS_ASTREINTE_MIN, ASTREINTE_DEBUT_MIN,
|
||||
)
|
||||
import models
|
||||
|
||||
@ -82,12 +82,30 @@ def test_retour_fin_nocturne_sur_le_lendemain():
|
||||
assert retour_apres_repos("2026-07-15", p_by_date, PLAGES, ["2026-07-14"]) == 13 * 60
|
||||
|
||||
|
||||
def test_retour_activite_matinale_sur_le_jour_meme():
|
||||
"""Event 05:00→06:00 sur le jour d'astreinte : l'activité s'est réellement
|
||||
terminée à 06:00 ce jour-là → repos jusqu'à 17:00 le même jour."""
|
||||
p_by_date = {"2026-07-14": _pointage("2026-07-14",
|
||||
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-14", p_by_date, PLAGES, ["2026-07-14"]) == 17 * 60
|
||||
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():
|
||||
@ -98,6 +116,21 @@ def test_retour_fin_du_soir_sur_le_jour_d_astreinte():
|
||||
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."""
|
||||
@ -387,3 +420,103 @@ def test_toggle_astreinte_persiste():
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user