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:
14
README.md
14
README.md
@ -228,12 +228,16 @@ arrondie au quart supérieur : fin 23:30 → retour 10:30 le lendemain ; 22:39
|
|||||||
23:49 (fin comptée 23:54) → retour 11:00 ; fin 02:00 → retour 13:00 le jour même.
|
23:49 (fin comptée 23:54) → retour 11:00 ; fin 02:00 → retour 13:00 le jour même.
|
||||||
Un repos qui se termine avant l'ouverture ne contraint rien.
|
Un repos qui se termine avant l'ouverture ne contraint rien.
|
||||||
|
|
||||||
À partir de cette heure de retour, tout ce qui la précède sur la journée quitte le
|
À partir de cette heure de retour, la présence obligatoire couverte par le repos
|
||||||
décompte de la semaine :
|
est **comptée comme effectuée** :
|
||||||
|
|
||||||
- la présence obligatoire avant l'heure de retour est retirée des heures dues
|
- les heures de présence obligatoire avant l'heure de retour sont créditées au
|
||||||
(ex: retour 13:00 → la plage 09:00-12:00 ne compte plus) ;
|
**Travaillé** du jour, affichées sous le total par un badge explicite
|
||||||
- une présence pointée avant l'heure de retour ne compte pas dans le travaillé ;
|
(« +2h00 repos », détail des plages au survol — ex. retour 11:00 → la plage
|
||||||
|
09:00-11:00 compte comme faite) ; les heures dues restent complètes, le solde
|
||||||
|
est le même que lorsqu'on retirait ces heures des dues ;
|
||||||
|
- une présence pointée avant l'heure de retour ne compte pas dans le travaillé
|
||||||
|
(seul le crédit de présence s'y ajoute) ;
|
||||||
- les préconisations **Fin cible** démarrent à l'heure de retour (arrivée du matin
|
- les préconisations **Fin cible** démarrent à l'heure de retour (arrivée du matin
|
||||||
décalée d'autant, sans avertissement de retard) ;
|
décalée d'autant, sans avertissement de retard) ;
|
||||||
- une demi-journée entièrement couverte par le repos est considérée comme remplie.
|
- une demi-journée entièrement couverte par le repos est considérée comme remplie.
|
||||||
|
|||||||
@ -108,8 +108,22 @@ def exclusion_retour(conges_date: set, retour_min: int | None, plages: dict, fer
|
|||||||
return matin_ex, aprem_ex
|
return matin_ex, aprem_ex
|
||||||
|
|
||||||
|
|
||||||
|
def couvert_detail(plages: dict, retour_min: int | None, matin_ex: int, aprem_ex: int) -> str:
|
||||||
|
"""Description des plages de présence obligatoire couvertes par le repos de
|
||||||
|
11h (affichée avec le crédit d'heures effectuées). N'apparaît que couvert > 0,
|
||||||
|
donc retour_min défini."""
|
||||||
|
parts = []
|
||||||
|
if matin_ex:
|
||||||
|
fin = minutes_to_time(min(hhmm_to_minutes(plages["matin_fin"]), retour_min or 0))
|
||||||
|
parts.append(f"matin {plages['matin_debut']}→{fin}")
|
||||||
|
if aprem_ex:
|
||||||
|
fin = minutes_to_time(min(hhmm_to_minutes(plages["aprem_fin"]), retour_min or 0))
|
||||||
|
parts.append(f"après-midi {plages['aprem_debut']}→{fin}")
|
||||||
|
return ", ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
def nominaux_du_jour(conges_date: set, du_full_min: int, matin_excl_min: int, aprem_excl_min: int,
|
def nominaux_du_jour(conges_date: set, du_full_min: int, matin_excl_min: int, aprem_excl_min: int,
|
||||||
ferie: bool = False) -> tuple[int, int]:
|
ferie: bool = False) -> tuple[int, int]:
|
||||||
"""Nominaux (attendus) matin et aprem en minutes, après congés, jour férié et
|
"""Nominaux (attendus) matin et aprem en minutes, après congés, jour férié et
|
||||||
exclusion liée à l'heure de retour."""
|
exclusion liée à l'heure de retour."""
|
||||||
if ferie or "jour" in conges_date:
|
if ferie or "jour" in conges_date:
|
||||||
@ -470,9 +484,18 @@ def compute_week(pointages: list[dict], conges: list[dict], heures_jour_min: int
|
|||||||
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 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.
|
||||||
travaille = heures_travaillees(p, retour_min)
|
#
|
||||||
du = heures_dues(p["date"], conges, heures_jour_min, plages, retour_min, jour_ferie)
|
# Les heures de présence obligatoire couvertes par le repos de 11h
|
||||||
du_full = du + matin_ex + aprem_ex
|
# (matin_ex + aprem_ex) sont comptées comme effectuées : créditées au
|
||||||
|
# travaillé du jour, les heures dues restent complètes. Le solde est le
|
||||||
|
# même qu'en soustrayant couvert du dû, mais le calcul est explicite :
|
||||||
|
# travaillé = pointé + couvert, dû = nominal complet. Sans heures dues
|
||||||
|
# (week-end, férié, congé) il n'y a rien à créditer, et le crédit ne
|
||||||
|
# dépasse jamais le nominal du jour.
|
||||||
|
du_full = heures_dues(p["date"], conges, heures_jour_min, plages, None, jour_ferie)
|
||||||
|
couvert = min(matin_ex + aprem_ex, du_full)
|
||||||
|
travaille = 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)
|
matin_nom, aprem_nom = nominaux_du_jour(conges_date, du_full, matin_ex, aprem_ex, jour_ferie)
|
||||||
delta = travaille - du
|
delta = travaille - du
|
||||||
total_travaille += travaille
|
total_travaille += travaille
|
||||||
@ -492,6 +515,9 @@ def compute_week(pointages: list[dict], conges: list[dict], heures_jour_min: int
|
|||||||
"du_full_min": du_full,
|
"du_full_min": du_full,
|
||||||
"matin_excl_min": matin_ex,
|
"matin_excl_min": matin_ex,
|
||||||
"aprem_excl_min": aprem_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,
|
"matin_nominal_min": matin_nom,
|
||||||
"aprem_nominal_min": aprem_nom,
|
"aprem_nominal_min": aprem_nom,
|
||||||
"astreinte": p["date"] in astreintes,
|
"astreinte": p["date"] in astreintes,
|
||||||
|
|||||||
27
app/main.py
27
app/main.py
@ -33,7 +33,7 @@ from models import (
|
|||||||
)
|
)
|
||||||
from calcul import (
|
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,
|
heures_travaillees, jour_astreinte_qualifie, exclusion_retour, couvert_detail,
|
||||||
nominaux_du_jour, retour_apres_repos, decompte_astreinte, fenetre_astreinte_jour,
|
nominaux_du_jour, retour_apres_repos, decompte_astreinte, fenetre_astreinte_jour,
|
||||||
)
|
)
|
||||||
from stats import compute_all_stats
|
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)
|
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
|
||||||
trav = heures_travaillees(p, retour_min) # forfait d'astreinte : hors heures travaillées
|
# Les heures de présence obligatoire couvertes par le repos de 11h sont
|
||||||
du = heures_dues(date, conges, h_jour, plages, retour_min, jour_ferie)
|
# comptées comme effectuées : créditées au travaillé, dues complètes (le
|
||||||
du_full = du + matin_ex + aprem_ex
|
# 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)
|
matin_nom, aprem_nom = nominaux_du_jour(conges_date, du_full, matin_ex, aprem_ex, jour_ferie)
|
||||||
delta = trav - du
|
delta = trav - du
|
||||||
# Décompte astreinte du jour (compteur séparé, hors heures de semaine) :
|
# 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 "",
|
"forfait_h": minutes_to_hhmm(forfait) if forfait else "",
|
||||||
"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_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,
|
"matin_nominal_min": matin_nom, "aprem_nominal_min": aprem_nom,
|
||||||
"astreinte": date in astreintes,
|
"astreinte": date in astreintes,
|
||||||
"astreinte_qualifiee": jour_astreinte_qualifie(date, 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):
|
def _oob_calc(date, jour):
|
||||||
trav_inner = f'<span class="calc-num">{"—" if not jour["travaille_min"] else _e(jour["travaille"])}</span>'
|
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"]:
|
if jour["travaille_min"]:
|
||||||
cls = "delta-pos" if jour["delta_min"] > 0 else ("delta-neg" if jour["delta_min"] < 0 else "delta-zero")
|
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 ""
|
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:
|
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">'
|
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>')
|
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"):
|
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'astreinte, hors heures travaillées">'
|
hint += (f'<span class="as-hint" title="Forfait du jour AS déclaré (réglé dans Réglages) : suivi dans le récapitulatif d'astreinte, hors heures travaillées">'
|
||||||
f'forfait {_e(jour["forfait_h"])}</span>')
|
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.
|
# 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))):
|
if jour_astreinte_qualifie(date, set(load_astreintes_effectives(user_id))):
|
||||||
events = []
|
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)
|
deb, fin = clean(deb), clean(fin)
|
||||||
if deb and fin:
|
if deb and fin:
|
||||||
|
desc = astreinte_desc[i] if i < len(astreinte_desc) else ""
|
||||||
events.append({"debut": deb, "fin": fin,
|
events.append({"debut": deb, "fin": fin,
|
||||||
"desc": (desc or "").strip()[:200] or None})
|
"desc": (desc or "").strip()[:200] or None})
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -52,6 +52,7 @@
|
|||||||
<td class="col-calc" data-label="Travaillé">
|
<td class="col-calc" data-label="Travaillé">
|
||||||
<span id="calc-{{ j.date }}-trav">
|
<span id="calc-{{ j.date }}-trav">
|
||||||
<span class="calc-num">{{ j.travaille if j.travaille_min else '—' }}</span>
|
<span class="calc-num">{{ j.travaille if j.travaille_min else '—' }}</span>
|
||||||
|
{% if j.couvert_min %}<span class="calc-couvert" title="Présence obligatoire couverte par le repos de 11h ({{ j.couvert_detail }}) : comptée comme effectuée">+{{ j.couvert_h }} repos</span>{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@ -146,6 +147,7 @@
|
|||||||
<span class="as-caret">▸</span> {{ nb_as }} intervention{{ 's' if nb_as != 1 else '' }}
|
<span class="as-caret">▸</span> {{ nb_as }} intervention{{ 's' if nb_as != 1 else '' }}
|
||||||
</button>
|
</button>
|
||||||
{% if j.retour_min is not none %}<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">retour ≥ {{ j.retour }}</span>{% endif %}
|
{% if j.retour_min is not none %}<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">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.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) : suivi dans le récapitulatif d'astreinte, hors heures travaillées">forfait {{ j.forfait_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 %}
|
{% 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>
|
||||||
|
|||||||
@ -499,6 +499,20 @@
|
|||||||
|
|
||||||
.col-calc { text-align: right; padding-left: .5rem; }
|
.col-calc { text-align: right; padding-left: .5rem; }
|
||||||
.calc-num { font-family: var(--mono); font-size: 13px; color: var(--muted); }
|
.calc-num { font-family: var(--mono); font-size: 13px; color: var(--muted); }
|
||||||
|
/* Crédit d'heures de présence couvertes par le repos de 11h (retour décalé
|
||||||
|
après astreinte) : affiché sous le total travaillé, détail au survol. */
|
||||||
|
.calc-couvert {
|
||||||
|
display: block;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--accent-2);
|
||||||
|
margin-top: .12rem;
|
||||||
|
margin-left: auto;
|
||||||
|
width: fit-content;
|
||||||
|
cursor: help;
|
||||||
|
border-bottom: 1px dotted rgba(184,106,8,.45);
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
.delta-pos { font-family: var(--mono); font-size: 13px; font-weight: 500; color: var(--pos); }
|
.delta-pos { font-family: var(--mono); font-size: 13px; font-weight: 500; color: var(--pos); }
|
||||||
.delta-neg { font-family: var(--mono); font-size: 13px; font-weight: 500; color: var(--neg); }
|
.delta-neg { font-family: var(--mono); font-size: 13px; font-weight: 500; color: var(--neg); }
|
||||||
.delta-zero{ font-family: var(--mono); font-size: 13px; color: var(--rule); }
|
.delta-zero{ font-family: var(--mono); font-size: 13px; color: var(--rule); }
|
||||||
|
|||||||
@ -311,7 +311,8 @@ def test_nominaux_sans_exclusion_reste_la_moitié():
|
|||||||
def test_compute_week_decompte_absent_des_heures_de_semaine(freeze_today):
|
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
|
"""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,
|
vide : les heures d'astreinte n'entrent jamais dans total_travaille/total_du,
|
||||||
seulement dans result["astreinte"]."""
|
seulement dans result["astreinte"] ; seules les 3h de présence obligatoire du
|
||||||
|
matin couvertes par le repos (retour 12:00) sont créditées comme effectuées."""
|
||||||
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"),
|
||||||
_pointage("2026-07-14", astreinte_events=[{"debut": "23:00", "fin": "01:00"}]),
|
_pointage("2026-07-14", astreinte_events=[{"debut": "23:00", "fin": "01:00"}]),
|
||||||
@ -321,8 +322,9 @@ def test_compute_week_decompte_absent_des_heures_de_semaine(freeze_today):
|
|||||||
]
|
]
|
||||||
result = compute_week(pointages, [], HEURES_JOUR_MIN, PLAGES, ["2026-07-14"])
|
result = compute_week(pointages, [], HEURES_JOUR_MIN, PLAGES, ["2026-07-14"])
|
||||||
|
|
||||||
# seul le pointé compte : 2 × 468 = 15h36, astreinte exclue
|
# pointé (2 × 7h48) + 3h de présence couvertes créditées ; astreinte exclue
|
||||||
assert result["total_travaille"] == "15h36"
|
assert result["total_travaille"] == "18h36"
|
||||||
|
assert result["jours"][2]["couvert_min"] == 180
|
||||||
|
|
||||||
ast = result["astreinte"]
|
ast = result["astreinte"]
|
||||||
assert ast["actif"] is True
|
assert ast["actif"] is True
|
||||||
@ -341,8 +343,8 @@ def test_compute_week_decompte_absent_des_heures_de_semaine(freeze_today):
|
|||||||
|
|
||||||
def test_compute_week_fin_nocturne_decale_le_decompte_et_les_cibles(freeze_today):
|
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) :
|
"""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
|
retour mercredi 13:00, présence du matin (3h) créditée comme effectuée,
|
||||||
matin, après-midi 13:30→18:18 (nominal 288 = 234 + report)."""
|
aucune cible matin, après-midi 13:30→18:18 (nominal 288 = 234 + report)."""
|
||||||
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"),
|
||||||
_pointage("2026-07-14", "09:00", "12:24", "13:30", "17:54"),
|
_pointage("2026-07-14", "09:00", "12:24", "13:30", "17:54"),
|
||||||
@ -353,11 +355,16 @@ def test_compute_week_fin_nocturne_decale_le_decompte_et_les_cibles(freeze_today
|
|||||||
result = compute_week(pointages, [], HEURES_JOUR_MIN, PLAGES, ["2026-07-14"])
|
result = compute_week(pointages, [], HEURES_JOUR_MIN, PLAGES, ["2026-07-14"])
|
||||||
mercredi = result["jours"][2]
|
mercredi = result["jours"][2]
|
||||||
|
|
||||||
assert mercredi["du_min"] == 468 - 180 # matin 09:00→12:00 exclu par le retour 13:00
|
# dues complètes, présence matin 09:00→12:00 couverte créditée au travaillé
|
||||||
|
assert mercredi["du_min"] == 468
|
||||||
|
assert mercredi["couvert_min"] == 180
|
||||||
|
assert mercredi["travaille_min"] == 180
|
||||||
|
assert mercredi["couvert_detail"] == "matin 09:00→12:00"
|
||||||
assert mercredi["retour"] == "13:00" # 02:00 + 11h
|
assert mercredi["retour"] == "13:00" # 02:00 + 11h
|
||||||
assert mercredi["cible_matin"] is None
|
assert mercredi["cible_matin"] is None
|
||||||
assert mercredi["cible_aprem"] == "18:18"
|
assert mercredi["cible_aprem"] == "18:18"
|
||||||
assert result["total_du"] == "36h00" # 39h - 3h de présence retirées
|
assert result["total_du"] == "39h00" # dues nominales complètes
|
||||||
|
assert result["total_travaille"] == "18h36" # 2 × 7h48 + 3h de présence créditée
|
||||||
|
|
||||||
|
|
||||||
def test_compute_week_nuit_finissant_au_matin(freeze_today):
|
def test_compute_week_nuit_finissant_au_matin(freeze_today):
|
||||||
@ -375,8 +382,10 @@ def test_compute_week_nuit_finissant_au_matin(freeze_today):
|
|||||||
mercredi = result["jours"][2]
|
mercredi = result["jours"][2]
|
||||||
|
|
||||||
assert mercredi["retour"] == "17:00"
|
assert mercredi["retour"] == "17:00"
|
||||||
# matin (180) + 14:00→17:00 (180) exclus → dû = 108
|
# matin (180) + 14:00→17:00 (180) couverts → crédités 360, dues complètes 468
|
||||||
assert mercredi["du_min"] == 468 - 360
|
assert mercredi["du_min"] == 468
|
||||||
|
assert mercredi["couvert_min"] == 360
|
||||||
|
assert mercredi["couvert_h"] == "6h00"
|
||||||
assert mercredi["cible_matin"] is None
|
assert mercredi["cible_matin"] is None
|
||||||
# aprem démarre à 17:00, nominal 108 (54 matin + 54 aprem) → 18:48
|
# aprem démarre à 17:00, nominal 108 (54 matin + 54 aprem) → 18:48
|
||||||
assert mercredi["cible_aprem_entree"] == "17:00"
|
assert mercredi["cible_aprem_entree"] == "17:00"
|
||||||
@ -386,6 +395,32 @@ def test_compute_week_nuit_finissant_au_matin(freeze_today):
|
|||||||
assert result["astreinte"]["total_pondere_min"] == 630
|
assert result["astreinte"]["total_pondere_min"] == 630
|
||||||
|
|
||||||
|
|
||||||
|
def test_compute_week_presence_couvertes_comptees_effectuees(freeze_today):
|
||||||
|
"""Retour décalé à 11:00 (event mardi 22:39→23:49) : les 2h de présence
|
||||||
|
obligatoire du matin (09:00→11:00) sont comptées comme effectuées —
|
||||||
|
créditées au travaillé, dues complètes, Δ inchangé par rapport à
|
||||||
|
l'ancienne soustraction des dues."""
|
||||||
|
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": "22:39", "fin": "23:49"}]),
|
||||||
|
_pointage("2026-07-15", "11:00", "12:00", "14:00", "17: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["retour"] == "11:00"
|
||||||
|
assert mercredi["couvert_min"] == 120 # présence 09:00→11:00
|
||||||
|
assert mercredi["couvert_h"] == "2h00"
|
||||||
|
assert mercredi["couvert_detail"] == "matin 09:00→11:00"
|
||||||
|
assert mercredi["travaille_min"] == 240 + 120 # pointé (1h + 3h) + couvert
|
||||||
|
assert mercredi["travaille"] == "6h00"
|
||||||
|
assert mercredi["du_min"] == 468 # dues complètes
|
||||||
|
assert mercredi["delta_min"] == 360 - 468
|
||||||
|
|
||||||
|
|
||||||
def test_compute_week_event_ignore_hors_jour_qualifie(freeze_today):
|
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
|
"""Un event saisi un jour non qualifié (pas d'astreinte) ne change rien au
|
||||||
décompte ni au décompte d'astreinte."""
|
décompte ni au décompte d'astreinte."""
|
||||||
@ -447,7 +482,8 @@ def test_compute_week_demi_journee_entierement_en_repos_compte_complete(freeze_t
|
|||||||
assert mercredi["matin_nominal_min"] == 0
|
assert mercredi["matin_nominal_min"] == 0
|
||||||
assert mercredi["is_complete"] is True
|
assert mercredi["is_complete"] is True
|
||||||
assert mercredi["cumul_visible"] is True
|
assert mercredi["cumul_visible"] is True
|
||||||
assert mercredi["du_min"] == 180 # 6h dues - 3h de présence matin retirées
|
assert mercredi["du_min"] == 360 # dues complètes du jour (6h)
|
||||||
|
assert mercredi["couvert_min"] == 180 # 3h de présence matin couvertes, créditées
|
||||||
|
|
||||||
|
|
||||||
# ── Stockage des astreintes ───────────────────────────────────────────────────
|
# ── Stockage des astreintes ───────────────────────────────────────────────────
|
||||||
|
|||||||
@ -76,3 +76,23 @@ def test_bascule_congen_regenere_le_menu_d_etat(monkeypatch):
|
|||||||
# Le swap OOB régénère le bouton avec la pastille MA active
|
# Le swap OOB régénère le bouton avec la pastille MA active
|
||||||
assert 'cg-chip st-ma">MA<' in r.text
|
assert 'cg-chip st-ma">MA<' in r.text
|
||||||
assert 'id="cg-btn-' + TODAY + '"' in r.text
|
assert 'id="cg-btn-' + TODAY + '"' in r.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_retour_decale_affiche_le_credit_de_presence(monkeypatch):
|
||||||
|
"""Retour mercredi 11:00 (event mardi 22:39→23:49) : la colonne Travaillé
|
||||||
|
crédite les 2h de présence couvertes, avec badge et détail explicites."""
|
||||||
|
monkeypatch.setattr(main, "datetime", _FrozenDatetime)
|
||||||
|
cookies = _login("u-ui-credit")
|
||||||
|
client.post("/astreinte/2026-07-14/toggle", cookies=cookies, follow_redirects=False)
|
||||||
|
r = client.post("/pointage/2026-07-14", cookies=cookies,
|
||||||
|
data={"astreinte_debut": ["22:39"], "astreinte_fin": ["23:49"]},
|
||||||
|
headers={"HX-Request": "true"}, follow_redirects=False)
|
||||||
|
assert r.status_code == 200
|
||||||
|
|
||||||
|
# Le crédit s'affiche sur la ligne du lendemain (jour du retour décalé)
|
||||||
|
r = client.get("/semaine/2026/29", cookies=cookies, follow_redirects=False)
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert 'class="calc-couvert"' in r.text
|
||||||
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user