Ajoute les astreintes : saisie de la fin de dernière activité, retour au bureau décalé de 11h de repos

Un nouveau bouton AS marque un jour d'astreinte. Ce jour et son lendemain,
une saisie « Fin activité » enregistre l'heure de fin de la dernière
intervention hors horaires (elle peut être le matin si la nuit a dépassé
minuit). L'app en déduit l'heure de retour au bureau (fin + 11h de repos
consécutif) et retire du décompte de la semaine toute la présence avant
cette heure : heures dues réduites, présence pointée avant le retour non
comptée, préconisations Fin cible démarrées à l'heure de retour sans
avertissement de retard, demi-journée entièrement en repos considérée
comme remplie. Les exports .ods gagnent les colonnes Astreinte et Retour,
et les stats n'ont plus les retards liés au repos.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
Jacquin Antoine
2026-09-08 22:05:10 +02:00
parent c31fcd28ec
commit 065d73e15c
9 changed files with 665 additions and 58 deletions

View File

@ -3,9 +3,13 @@ import statistics as _st
from datetime import datetime, timedelta, date as _date
from models import (
load_week_pointages, load_conges, list_all_weeks, load_plages, heures_jour_min,
load_week_pointages, load_conges, load_astreintes, list_all_weeks,
load_plages, heures_jour_min,
)
from calcul import (
hhmm_to_minutes, minutes_to_hhmm, heures_travaillees, heures_dues,
retour_apres_repos,
)
from calcul import hhmm_to_minutes, minutes_to_hhmm, heures_travaillees, heures_dues
MOIS_FR = [
"janvier", "février", "mars", "avril", "mai", "juin",
@ -60,6 +64,7 @@ def _compliance_over(all_days: list[tuple], cg_set: set, plages: dict) -> dict:
def compute_all_stats(user_id: str = "") -> dict:
all_weeks = sorted(list_all_weeks(user_id))
conges = load_conges(user_id)
astreintes = set(load_astreintes(user_id))
h_jour = heures_jour_min()
plages = load_plages(user_id)
cg_set = {(c["date"], c["type"]) for c in conges}
@ -107,6 +112,9 @@ def compute_all_stats(user_id: str = "") -> dict:
matin_filled = mc or (bool(p.get("matin_entree")) and bool(p.get("matin_sortie")))
aprem_filled = ac or (bool(p.get("aprem_entree")) and bool(p.get("aprem_sortie")))
# Retour au bureau après 11h de repos : calculé depuis la fin de la
# dernière activité d'astreinte (saisie sur le jour ou la veille qualifiés)
retour_min = retour_apres_repos(date, p_by_date, plages, astreintes)
all_days.append((date, p))
d = datetime.strptime(date, "%Y-%m-%d").date()
@ -119,8 +127,8 @@ def compute_all_stats(user_id: str = "") -> dict:
week_has_incomplete = True
continue
trav = heures_travaillees(p)
du = heures_dues(date, conges, h_jour)
trav = heures_travaillees(p, retour_min)
du = heures_dues(date, conges, h_jour, plages, retour_min)
week_delta += trav - du
if not mc:
@ -129,6 +137,9 @@ def compute_all_stats(user_id: str = "") -> dict:
v = hhmm_to_minutes(p[slot])
slot_pairs[slot].append((v, date))
op, thr = TARGETS[slot]
# Une arrivée décalée par le repos de 11h n'est pas un retard
if slot == "matin_entree" and retour_min:
thr = max(thr, retour_min)
comp_n[slot] += 1
comp_ok[slot] += 1 if (v <= thr if op == "le" else v >= thr) else 0
if not ac:
@ -137,6 +148,8 @@ def compute_all_stats(user_id: str = "") -> dict:
v = hhmm_to_minutes(p[slot])
slot_pairs[slot].append((v, date))
op, thr = TARGETS[slot]
if slot == "aprem_entree" and retour_min:
thr = max(thr, retour_min)
comp_n[slot] += 1
comp_ok[slot] += 1 if (v <= thr if op == "le" else v >= thr) else 0