Retour décalé après astreinte : la présence obligatoire couverte compte comme effectuée

Quand le repos de 11h décale le retour au bureau (ex. 11:00), les heures de
présence obligatoire couvertes (ex. 09:00→11:00) sont créditées à la colonne
Travaillé au lieu d'être retirées des heures dues, qui restent complètes.
Le solde est inchangé, mais le calcul est explicite : badge « +2h00 repos »
sous le total (plages couvertes au survol) et rappel dans le bloc astreinte.
Rien n'est crédité sans heures dues (week-end, férié, congé), et le crédit
ne dépasse pas le nominal du jour.

Corrige aussi la perte des interventions postées sans description (le zip
s'arrêtait sur la liste vide des descriptions).

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
Jacquin Antoine
2026-09-09 21:05:50 +02:00
parent 3d38abd853
commit 77ae115c4f
7 changed files with 143 additions and 24 deletions

View File

@ -33,7 +33,7 @@ from models import (
)
from calcul import (
compute_week, minutes_to_hhmm, minutes_to_time, hhmm_to_minutes, heures_dues,
heures_travaillees, jour_astreinte_qualifie, exclusion_retour,
heures_travaillees, jour_astreinte_qualifie, exclusion_retour, couvert_detail,
nominaux_du_jour, retour_apres_repos, decompte_astreinte, fenetre_astreinte_jour,
)
from stats import compute_all_stats
@ -351,9 +351,14 @@ def _build_jour_ctx(date, conges, h_jour, user_id):
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) \
if date in astreintes else 0
trav = heures_travaillees(p, retour_min) # forfait d'astreinte : hors heures travaillées
du = heures_dues(date, conges, h_jour, plages, retour_min, jour_ferie)
du_full = du + matin_ex + aprem_ex
# 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
# forfait d'astreinte, lui, reste hors heures travaillées). Sans heures
# dues (week-end, férié, congé), rien à créditer.
du_full = heures_dues(date, conges, h_jour, plages, None, jour_ferie)
couvert = min(matin_ex + aprem_ex, du_full)
trav = heures_travaillees(p, retour_min) + couvert
du = du_full
matin_nom, aprem_nom = nominaux_du_jour(conges_date, du_full, matin_ex, aprem_ex, jour_ferie)
delta = trav - du
# Décompte astreinte du jour (compteur séparé, hors heures de semaine) :
@ -373,6 +378,9 @@ def _build_jour_ctx(date, conges, h_jour, user_id):
"forfait_h": minutes_to_hhmm(forfait) if forfait else "",
"du_full_min": du_full,
"matin_excl_min": matin_ex, "aprem_excl_min": aprem_ex,
"couvert_min": couvert,
"couvert_h": minutes_to_hhmm(couvert) if couvert else "",
"couvert_detail": couvert_detail(plages, retour_min, matin_ex, aprem_ex) if couvert else "",
"matin_nominal_min": matin_nom, "aprem_nominal_min": aprem_nom,
"astreinte": date in astreintes,
"astreinte_qualifiee": jour_astreinte_qualifie(date, astreintes),
@ -385,6 +393,9 @@ def _build_jour_ctx(date, conges, h_jour, user_id):
def _oob_calc(date, jour):
trav_inner = f'<span class="calc-num">{"—" if not jour["travaille_min"] else _e(jour["travaille"])}</span>'
if jour.get("couvert_min"):
trav_inner += (f'<span class="calc-couvert" title="Présence obligatoire couverte par le repos de 11h '
f'({_e(jour["couvert_detail"])}) : comptée comme effectuée">+{_e(jour["couvert_h"])} repos</span>')
if jour["travaille_min"]:
cls = "delta-pos" if jour["delta_min"] > 0 else ("delta-neg" if jour["delta_min"] < 0 else "delta-zero")
sign = "+" if jour["delta_min"] > 0 else ""
@ -539,6 +550,9 @@ def _as_block_html(date, jour):
if jour.get("retour_min") is not None:
hint = (f'<span class="as-hint" title="11h de repos consécutif après la dernière activité (durée arrondie au quart d\'heure supérieur), retour arrondi au quart d\'horloge supérieur : :00, :15, :30 ou :45">'
f'retour ≥ {_e(jour["retour"])}</span>')
if jour.get("couvert_min"):
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>')
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'forfait {_e(jour["forfait_h"])}</span>')
@ -843,9 +857,12 @@ def save_pointage(
# au lieu de les écraser à vide. Les paires début/fin invalides sont ignorées.
if jour_astreinte_qualifie(date, set(load_astreintes_effectives(user_id))):
events = []
for deb, fin, desc in zip(astreinte_debut, astreinte_fin, astreinte_desc):
# Les descriptions sont optionnelles : sans elles (POST programmatique),
# zip s'arrêterait à la liste la plus courte et perdrait les événements.
for i, (deb, fin) in enumerate(zip(astreinte_debut, astreinte_fin)):
deb, fin = clean(deb), clean(fin)
if deb and fin:
desc = astreinte_desc[i] if i < len(astreinte_desc) else ""
events.append({"debut": deb, "fin": fin,
"desc": (desc or "").strip()[:200] or None})
else: