Forfait d'astreinte : bonifié comme réalisé en journée du jour AS

Le forfait était suivi brut, sans bonification. Considéré réalisé en
journée du jour où il est attribué, il reçoit désormais le facteur
journée correspondant : ×1 en semaine, ×1,5 le samedi, ×2 le dimanche
ou un jour férié (suit les règles de ratios configurées). Toujours hors
heures travaillées ; affiché pondéré sur la ligne du jour, dans les
totaux du récapitulatif et expliqué dans Réglages.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
Jacquin Antoine
2026-09-10 17:05:05 +02:00
parent f50d1ef7fc
commit d760842121
7 changed files with 125 additions and 16 deletions

View File

@ -197,6 +197,32 @@ def _regles_astreinte(facteurs: list[dict] | None) -> list[dict]:
] ]
MIDI_MIN = 12 * 60 # minute représentative de la journée (le forfait AS y est réputé réalisé)
def regle_journee_astreinte(date_str: str, facteurs: list[dict] | None = None,
feries_declares: list[str] | set[str] | None = None) -> dict | None:
"""Règle de bonification des heures de journée du jour donné : la première
règle dont le type de jour et la fenêtre horaire couvrent midi, minute
représentative de la journée. Le forfait d'astreinte est considéré réalisé
en journée du jour où il est attribué : c'est ce facteur qui le pondère
(semaine ×1, samedi ×1,5, dimanche/férié ×2 avec les règles par défaut).
None si aucune règle ne couvre midi ce jour-là (forfait non bonifié)."""
regles = _regles_astreinte(facteurs)
jour0 = datetime.strptime(date_str, "%Y-%m-%d").date()
feries = _jours_feries(jour0.year) | _jours_feries(jour0.year + 1)
for s in (feries_declares or []):
try:
feries.add(datetime.strptime(s, "%Y-%m-%d").date())
except (TypeError, ValueError):
continue
cat = _categorie_jour(jour0, feries)
r = next((r for r in regles if cat in r["jours"] and _dans_fenetre(MIDI_MIN, r["debut"], r["fin"])), None)
if r is None:
return None
return {"nom": r["nom"], "facteur": r["facteur"], "facteur_txt": _facteur_txt(r["facteur"])}
def decompte_astreinte(date_str: str, events: list[dict], facteurs: list[dict] | None = None, def decompte_astreinte(date_str: str, events: list[dict], facteurs: list[dict] | None = None,
fenetre: tuple[int, int] | None = None, fenetre: tuple[int, int] | None = None,
feries_declares: list[str] | set[str] | None = None) -> dict: feries_declares: list[str] | set[str] | None = None) -> dict:
@ -586,6 +612,7 @@ def compute_week(pointages: list[dict], conges: list[dict], heures_jour_min: int
as_total_min = 0 as_total_min = 0
as_total_pondere_min = 0 as_total_pondere_min = 0
as_forfait_min = 0 as_forfait_min = 0
as_forfait_pondere_min = 0
as_cats: dict[str, dict] = {} as_cats: dict[str, dict] = {}
for p in pointages: for p in pointages:
@ -603,8 +630,14 @@ def compute_week(pointages: list[dict], conges: list[dict], heures_jour_min: int
if jour_astreinte_qualifie(p["date"], astreintes) and events else None) if jour_astreinte_qualifie(p["date"], astreintes) and events else None)
matin_ex, aprem_ex = exclusion_retour(conges_date, retour_min, plages, jour_ferie) matin_ex, aprem_ex = exclusion_retour(conges_date, retour_min, plages, jour_ferie)
forfait = forfait_as_min if p["date"] in astreintes else 0 forfait = forfait_as_min if p["date"] in astreintes else 0
# Le forfait d'astreinte est considéré réalisé en journée du jour AS
# déclaré : il bénéficie de la bonification « journée » de ce jour
# (×1 en semaine, ×1,5 le samedi, ×2 le dimanche ou férié par défaut).
regle_journee = regle_journee_astreinte(p["date"], facteurs, feries) if forfait else None
forfait_pondere = int(forfait * (regle_journee or {"facteur": 1.0})["facteur"] + 0.5) if forfait else 0
# Le forfait d'astreinte ne compte jamais dans les heures travaillées : # Le forfait d'astreinte ne compte jamais dans les heures travaillées :
# il est suivi à part dans le récapitulatif d'astreinte. # il est suivi à part dans le récapitulatif d'astreinte, pondéré par la
# bonification journée.
# #
# Les heures de présence obligatoire couvertes par le repos de 11h # Les heures de présence obligatoire couvertes par le repos de 11h
# (matin_ex + aprem_ex) sont comptées comme effectuées : créditées au # (matin_ex + aprem_ex) sont comptées comme effectuées : créditées au
@ -662,8 +695,13 @@ def compute_week(pointages: list[dict], conges: list[dict], heures_jour_min: int
"total_pondere_h": dec["total_pondere_h"] if dec else "", "total_pondere_h": dec["total_pondere_h"] if dec else "",
"forfait_min": forfait, "forfait_min": forfait,
"forfait_h": minutes_to_hhmm(forfait) if forfait else "", "forfait_h": minutes_to_hhmm(forfait) if forfait else "",
"forfait_pondere_min": forfait_pondere,
"forfait_pondere_h": minutes_to_hhmm(forfait_pondere) if forfait_pondere else "",
"forfait_regle": regle_journee["nom"] if regle_journee else "",
"forfait_facteur_txt": regle_journee["facteur_txt"] if regle_journee else "1",
}) })
as_forfait_min += forfait as_forfait_min += forfait
as_forfait_pondere_min += forfait_pondere
if dec and dec["total_min"] > 0: if dec and dec["total_min"] > 0:
as_total_min += dec["total_min"] as_total_min += dec["total_min"]
as_total_pondere_min += dec["total_pondere_min"] as_total_pondere_min += dec["total_pondere_min"]
@ -875,6 +913,8 @@ def compute_week(pointages: list[dict], conges: list[dict], heures_jour_min: int
"total_pondere_h": minutes_to_hhmm(as_total_pondere_min), "total_pondere_h": minutes_to_hhmm(as_total_pondere_min),
"total_forfait_min": as_forfait_min, "total_forfait_min": as_forfait_min,
"total_forfait_h": minutes_to_hhmm(as_forfait_min), "total_forfait_h": minutes_to_hhmm(as_forfait_min),
"total_forfait_pondere_min": as_forfait_pondere_min,
"total_forfait_pondere_h": minutes_to_hhmm(as_forfait_pondere_min),
"categories": as_categories, "categories": as_categories,
"jours": as_jours, "jours": as_jours,
}, },

View File

@ -35,7 +35,7 @@ from calcul import (
compute_week, minutes_to_hhmm, minutes_to_time, hhmm_to_minutes, heures_dues, compute_week, minutes_to_hhmm, minutes_to_time, hhmm_to_minutes, heures_dues,
heures_travaillees, jour_astreinte_qualifie, exclusion_retour, couvert_detail, heures_travaillees, jour_astreinte_qualifie, exclusion_retour, couvert_detail,
nominaux_du_jour, retour_apres_repos, retour_repos, retour_txt, decompte_astreinte, nominaux_du_jour, retour_apres_repos, retour_repos, retour_txt, decompte_astreinte,
fenetre_astreinte_jour, fenetre_astreinte_jour, regle_journee_astreinte,
) )
from stats import compute_all_stats from stats import compute_all_stats
from export import export_semaine_ods, export_annee_ods from export import export_semaine_ods, export_annee_ods
@ -370,6 +370,11 @@ def _build_jour_ctx(date, conges, h_jour, user_id):
matin_ex, aprem_ex = exclusion_retour(conges_date, retour_min, plages, jour_ferie) matin_ex, aprem_ex = exclusion_retour(conges_date, retour_min, plages, jour_ferie)
forfait = (hhmm_to_minutes(plages["astreinte_forfait"]) if plages.get("astreinte_forfait") else 0) \ forfait = (hhmm_to_minutes(plages["astreinte_forfait"]) if plages.get("astreinte_forfait") else 0) \
if date in astreintes else 0 if date in astreintes else 0
# Le forfait est considéré réalisé en journée du jour AS déclaré : il reçoit
# la bonification « journée » de ce jour (×1 semaine, ×1,5 samedi, ×2
# dimanche/férié par défaut) — pondéré, mais toujours hors heures travaillées.
regle_journee = regle_journee_astreinte(date, load_facteurs_astreinte(), feries) if forfait else None
forfait_pondere = int(forfait * (regle_journee or {"facteur": 1.0})["facteur"] + 0.5) if forfait else 0
# Les heures de présence obligatoire couvertes par le repos de 11h sont # Les heures de présence obligatoire couvertes par le repos de 11h sont
# comptées comme effectuées : créditées au travaillé, dues complètes (le # comptées comme effectuées : créditées au travaillé, dues complètes (le
# forfait d'astreinte, lui, reste hors heures travaillées). Sans heures # forfait d'astreinte, lui, reste hors heures travaillées). Sans heures
@ -395,6 +400,10 @@ def _build_jour_ctx(date, conges, h_jour, user_id):
"ferie": jour_ferie, "ferie": jour_ferie,
"forfait_min": forfait, "forfait_min": forfait,
"forfait_h": minutes_to_hhmm(forfait) if forfait else "", "forfait_h": minutes_to_hhmm(forfait) if forfait else "",
"forfait_pondere_min": forfait_pondere,
"forfait_pondere_h": minutes_to_hhmm(forfait_pondere) if forfait_pondere else "",
"forfait_regle": regle_journee["nom"] if regle_journee else "",
"forfait_facteur_txt": regle_journee["facteur_txt"] if regle_journee else "1",
"du_full_min": du_full, "du_full_min": du_full,
"matin_excl_min": matin_ex, "aprem_excl_min": aprem_ex, "matin_excl_min": matin_ex, "aprem_excl_min": aprem_ex,
"couvert_min": couvert, "couvert_min": couvert,
@ -573,8 +582,10 @@ def _as_block_html(date, jour):
hint += (f'<span class="as-hint" title="Présence obligatoire couverte par le repos ({_e(jour["couvert_detail"])}) : comptée comme effectuées dans la colonne Travaillé">' hint += (f'<span class="as-hint" title="Présence obligatoire couverte par le repos ({_e(jour["couvert_detail"])}) : comptée comme effectuées dans la colonne Travaillé">'
f'+{_e(jour["couvert_h"])} de présence comptées</span>') f'+{_e(jour["couvert_h"])} de présence comptées</span>')
if jour.get("forfait_min"): if jour.get("forfait_min"):
hint += (f'<span class="as-hint" title="Forfait du jour AS déclaré (réglé dans Réglages) : suivi dans le récapitulatif d&#39;astreinte, hors heures travaillées">' f_txt = jour.get("forfait_facteur_txt") or "1"
f'forfait {_e(jour["forfait_h"])}</span>') f_regle = f" ({_e(jour['forfait_regle'])})" if jour.get("forfait_regle") else ""
hint += (f'<span class="as-hint" title="Forfait du jour AS déclaré (réglé dans Réglages), considéré réalisé en journée du jour : pondéré ×{f_txt}{f_regle} — suivi dans le récapitulatif d&#39;astreinte, hors heures travaillées">'
f'forfait {_e(jour["forfait_h"])} → {_e(jour["forfait_pondere_h"])} pondérées</span>')
total = "" total = ""
if jour.get("astreinte_min"): if jour.get("astreinte_min"):
total = (f'<span class="as-total" title="Durée réelle des interventions arrondie au quart d\'heure supérieur (2, 5 ou 12 min → 15 ; 22 ou 28 → 30) — compteur séparé, hors heures de semaine">' total = (f'<span class="as-total" title="Durée réelle des interventions arrondie au quart d\'heure supérieur (2, 5 ou 12 min → 15 ; 22 ou 28 → 30) — compteur séparé, hors heures de semaine">'

View File

@ -148,7 +148,7 @@
</button> </button>
{% if j.retour_min is not none %}<span class="as-hint" title="Repos après la dernière activité : 11h consécutives (quotidien), et 35h sur 7 jours glissants si le repos hebdomadaire est activé (Réglages). Toute intervention pendant le repos redémarre le décompte ; retour arrondi au quart d'horloge supérieur : :00, :15, :30 ou :45">retour ≥ {{ j.retour }}</span>{% endif %} {% if j.retour_min is not none %}<span class="as-hint" title="Repos après la dernière activité : 11h consécutives (quotidien), et 35h sur 7 jours glissants si le repos hebdomadaire est activé (Réglages). Toute intervention pendant le repos redémarre le décompte ; retour arrondi au quart d'horloge supérieur : :00, :15, :30 ou :45">retour ≥ {{ j.retour }}</span>{% endif %}
{% if j.couvert_min %}<span class="as-hint" title="Présence obligatoire couverte par le repos ({{ j.couvert_detail }}) : comptée comme effectuées dans la colonne Travaillé">+{{ j.couvert_h }} de présence comptées</span>{% endif %} {% if j.couvert_min %}<span class="as-hint" title="Présence obligatoire couverte par le repos ({{ j.couvert_detail }}) : comptée comme effectuées dans la colonne Travaillé">+{{ j.couvert_h }} de présence comptées</span>{% endif %}
{% if j.forfait_min %}<span class="as-hint" title="Forfait du jour AS déclaré (Réglages) : suivi dans le récapitulatif d'astreinte, hors heures travaillées">forfait {{ j.forfait_h }}</span>{% endif %} {% if j.forfait_min %}<span class="as-hint" title="Forfait du jour AS déclaré (Réglages), considéré réalisé en journée du jour : pondéré ×{{ j.forfait_facteur_txt }}{% if j.forfait_regle %} ({{ j.forfait_regle }}){% endif %} — suivi dans le récapitulatif d'astreinte, hors heures travaillées">forfait {{ j.forfait_h }}{% if j.forfait_pondere_min != j.forfait_min %} → {{ j.forfait_pondere_h }} pondérées{% endif %}</span>{% endif %}
{% if j.astreinte_min %}<span class="as-total" title="Durée réelle des interventions arrondie au quart d'heure supérieur (2, 5 ou 12 min → 15 ; 22 ou 28 → 30), même sans tomber sur un quart d'heure de l'horloge — compteur séparé, hors heures de semaine">astreinte : {{ j.astreinte_h }}</span>{% endif %} {% if j.astreinte_min %}<span class="as-total" title="Durée réelle des interventions arrondie au quart d'heure supérieur (2, 5 ou 12 min → 15 ; 22 ou 28 → 30), même sans tomber sur un quart d'heure de l'horloge — compteur séparé, hors heures de semaine">astreinte : {{ j.astreinte_h }}</span>{% endif %}
</div> </div>
<div class="as-body"> <div class="as-body">
@ -165,7 +165,7 @@
data-date="{{ j.date }}" maxlength="200" placeholder="description" data-date="{{ j.date }}" maxlength="200" placeholder="description"
value="{{ ev.desc or '' }}"> value="{{ ev.desc or '' }}">
<button type="button" class="as-del" title="Supprimer" <button type="button" class="as-del" title="Supprimer"
onclick="this.closest('.as-event').remove()">×</button> onclick="asDelEvent(this)">×</button>
</div> </div>
</div> </div>
{% else %} {% else %}
@ -178,7 +178,7 @@
<input type="text" class="as-desc" name="astreinte_desc" <input type="text" class="as-desc" name="astreinte_desc"
data-date="{{ j.date }}" maxlength="200" placeholder="description" value=""> data-date="{{ j.date }}" maxlength="200" placeholder="description" value="">
<button type="button" class="as-del" title="Supprimer" <button type="button" class="as-del" title="Supprimer"
onclick="this.closest('.as-event').remove()">×</button> onclick="asDelEvent(this)">×</button>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}

View File

@ -91,7 +91,7 @@
<div class="as-jour"> <div class="as-jour">
<div class="as-jour-head"> <div class="as-jour-head">
<span class="as-jour-date">{{ j.jour }} {{ j.date[8:] }}/{{ j.date[5:7] }}</span> <span class="as-jour-date">{{ j.jour }} {{ j.date[8:] }}/{{ j.date[5:7] }}</span>
{% if j.forfait_min %}<span class="as-forfait" title="Forfait du jour AS déclaré (réglé dans Réglages) : suivi dans le récapitulatif d'astreinte, jamais compté dans les heures travaillées ni dans les compteurs pondérés">forfait {{ j.forfait_h }}</span>{% endif %} {% if j.forfait_min %}<span class="as-forfait" title="Forfait du jour AS déclaré (réglé dans Réglages), considéré réalisé en journée du jour : pondéré par le facteur journée correspondant ({{ j.forfait_regle }} ×{{ j.forfait_facteur_txt }}), jamais compté dans les heures travaillées">forfait {{ j.forfait_h }}{% if j.forfait_pondere_min != j.forfait_min %} → {{ j.forfait_pondere_h }} pondérées{% endif %}</span>{% endif %}
{% if j.total_min %}<span class="as-jour-total">astreinte {{ j.total_h }} → {{ j.total_pondere_h }} pondérées</span>{% endif %} {% if j.total_min %}<span class="as-jour-total">astreinte {{ j.total_h }} → {{ j.total_pondere_h }} pondérées</span>{% endif %}
</div> </div>
{% for ev in j.events %} {% for ev in j.events %}
@ -111,8 +111,8 @@
{% endfor %} {% endfor %}
<div class="as-totaux"> <div class="as-totaux">
{% for c in result.astreinte.categories %}<span class="as-cat">{{ c.nom }} : {{ c.min_h }} ×{{ c.facteur_txt }} = {{ c.pondere_h }}</span>{% endfor %} {% for c in result.astreinte.categories %}<span class="as-cat">{{ c.nom }} : {{ c.min_h }} ×{{ c.facteur_txt }} = {{ c.pondere_h }}</span>{% endfor %}
{% if result.astreinte.total_forfait_min %}<span class="as-forfait" title="Forfaits des jours AS déclarés : suivis à part, jamais comptés dans les heures travaillées">forfaits {{ result.astreinte.total_forfait_h }} <em>(hors heures travaillées)</em></span>{% endif %} {% if result.astreinte.total_forfait_min %}<span class="as-forfait" title="Forfaits des jours AS déclarés, considérés réalisés en journée de leur jour : pondérés par le facteur journée (semaine ×1, sam ×1,5, dim/férié ×2 par défaut) — jamais comptés dans les heures travaillées">forfaits {{ result.astreinte.total_forfait_h }} → {{ result.astreinte.total_forfait_pondere_h }} pondérés <em>(hors heures travaillées)</em></span>{% endif %}
<span class="as-grand">total {{ result.astreinte.total_h }} → {{ result.astreinte.total_pondere_h }} pondérées</span> <span class="as-grand">total {{ result.astreinte.total_h }} → {{ result.astreinte.total_pondere_h }} pondérées{% if result.astreinte.total_forfait_pondere_min %} (+ {{ result.astreinte.total_forfait_pondere_h }} de forfaits pondérés){% endif %}</span>
</div> </div>
</div> </div>
{% endif %} {% endif %}

View File

@ -182,8 +182,11 @@ function asDelRatio(btn) { btn.closest('.as-ratio-edit').remove(); }
<p class="settings-p"> <p class="settings-p">
Durée forfaitaire attribuée à chaque jour d'astreinte déclaré (bouton <b>AS</b>), Durée forfaitaire attribuée à chaque jour d'astreinte déclaré (bouton <b>AS</b>),
en plus des interventions saisies. Elle ne compte jamais dans les heures 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 travaillées : suivie à part dans le récapitulatif d'astreinte, elle est
semaine ; le lendemain de l'astreinte n'est pas concerné. 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. Vide = pas de forfait.
</p> </p>

View File

@ -2,7 +2,8 @@
from calcul import ( from calcul import (
compute_week, decompte_astreinte, heures_dues, heures_travaillees, hhmm_to_minutes, compute_week, decompte_astreinte, heures_dues, heures_travaillees, hhmm_to_minutes,
jour_astreinte_qualifie, exclusion_retour, nominaux_du_jour, fenetre_astreinte_jour, 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 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): def test_compute_week_forfait_hors_heures_travaillees(freeze_today):
"""Forfait 1h00 réglé : attribué au jour AS déclaré uniquement (pas du """Forfait 1h00 réglé : attribué au jour AS déclaré uniquement (pas du
lendemain), mais jamais compté dans les heures travaillées — seulement 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"} plages = {**PLAGES, "astreinte_forfait": "01:00"}
pointages = [ pointages = [
_pointage("2026-07-13", "09:00", "12:24", "13:30", "17:54"), _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["jours"][0]["forfait_min"] == 0 # jour sans astreinte
assert result["total_travaille"] == "15h36" # 2 × 7h48, forfait exclu assert result["total_travaille"] == "15h36" # 2 × 7h48, forfait exclu
# récapitulatif d'astreinte : le jour AS apparaît avec son forfait même sans # 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"] ast = result["astreinte"]
assert ast["actif"] is True assert ast["actif"] is True
assert ast["total_min"] == 0 assert ast["total_min"] == 0
assert ast["total_forfait_min"] == 60 assert ast["total_forfait_min"] == 60
assert ast["total_forfait_h"] == "1h00" 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): def test_compute_week_sans_forfait_comportement_inchange(freeze_today):

View File

@ -96,3 +96,20 @@ def test_retour_decale_affiche_le_credit_de_presence(monkeypatch):
assert "+2h00 repos" in r.text assert "+2h00 repos" in r.text
assert "matin 09:00→11:00" in r.text # détail des plages couvertes 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 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