Astreinte : repos hebdomadaire de 35h en option (7 jours glissants)
Un week-end d'astreinte fragmenté peut ne contenir aucun trou de 35h
consécutives sans activité : le retour est alors repoussé à dernière
activité + 35h, et toute nouvelle intervention redémarre le décompte.
Désactivé par défaut, activable par compte dans Réglages ; la journée
couverte est créditée comme pour les 11h quotidiennes.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
This commit is contained in:
60
app/main.py
60
app/main.py
@ -34,7 +34,8 @@ 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, couvert_detail,
|
||||
nominaux_du_jour, retour_apres_repos, decompte_astreinte, fenetre_astreinte_jour,
|
||||
nominaux_du_jour, retour_apres_repos, retour_repos, retour_txt, decompte_astreinte,
|
||||
fenetre_astreinte_jour,
|
||||
)
|
||||
from stats import compute_all_stats
|
||||
from export import export_semaine_ods, export_annee_ods
|
||||
@ -304,6 +305,16 @@ def _weeks_by_year(all_weeks: list) -> dict:
|
||||
return result
|
||||
|
||||
|
||||
def _contexte_semaine_precedente(year: int, week: int, dates: list[str], user_id: str) -> list[dict]:
|
||||
"""Pointages de la semaine ISO précédente (jours avant la semaine affichée) :
|
||||
la fenêtre glissante de 7 jours du repos hebdomadaire de 35h déborde sur la
|
||||
semaine d'avant."""
|
||||
prev = (datetime.strptime(dates[0], "%Y-%m-%d").date() - timedelta(days=7)).isocalendar()
|
||||
if (prev.year, prev.week) == (year, week):
|
||||
return []
|
||||
return [r for r in load_week_pointages(prev.year, prev.week, user_id) if r.get("date") < dates[0]]
|
||||
|
||||
|
||||
def _build_soldes_ctx(year, week, conges, h_jour, user_id):
|
||||
dates = week_dates(year, week)
|
||||
raw = load_week_pointages(year, week, user_id)
|
||||
@ -313,7 +324,8 @@ def _build_soldes_ctx(year, week, conges, h_jour, user_id):
|
||||
"aprem_entree": None, "aprem_sortie": None})
|
||||
for d in dates
|
||||
]
|
||||
result = compute_week(pointages, conges, h_jour, load_plages(user_id), load_astreintes_effectives(user_id), load_facteurs_astreinte(), load_feries(user_id))
|
||||
contexte = _contexte_semaine_precedente(year, week, dates, user_id)
|
||||
result = compute_week(pointages, conges, h_jour, load_plages(user_id), load_astreintes_effectives(user_id), load_facteurs_astreinte(), load_feries(user_id), contexte)
|
||||
result["heures_jour"] = minutes_to_hhmm(h_jour)
|
||||
# Panneau « aujourd'hui » : uniquement si le jour courant appartient à la
|
||||
# semaine affichée (re-rendu à chaque enregistrement via _soldes.html).
|
||||
@ -341,13 +353,20 @@ def _build_jour_ctx(date, conges, h_jour, user_id):
|
||||
astreintes = set(load_astreintes_effectives(user_id))
|
||||
feries = set(load_feries(user_id))
|
||||
p_by_date = {p["date"]: p for p in raw}
|
||||
# Repos hebdomadaire de 35h (7 jours glissants) : la fenêtre des 6 jours
|
||||
# précédents peut déborder sur la semaine ISO précédente.
|
||||
debut_fenetre = (d - timedelta(days=6)).isocalendar()
|
||||
if (debut_fenetre.year, debut_fenetre.week) != (iso.year, iso.week):
|
||||
for row in load_week_pointages(debut_fenetre.year, debut_fenetre.week, user_id):
|
||||
p_by_date.setdefault(row.get("date"), row)
|
||||
p = p_by_date.get(date, {"date": date, "matin_entree": None, "matin_sortie": None,
|
||||
"aprem_entree": None, "aprem_sortie": None})
|
||||
conges_date = {c["type"] for c in conges if c["date"] == date}
|
||||
jour_ferie = date in feries
|
||||
# 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)
|
||||
# Retour au bureau après repos, calculé depuis la fin de la dernière
|
||||
# activité d'astreinte (11h quotidiennes) et, si activée, la contrainte de
|
||||
# repos hebdomadaire de 35h sur 7 jours glissants
|
||||
retour_min = retour_repos(date, p_by_date, plages, astreintes)
|
||||
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
|
||||
@ -385,7 +404,7 @@ def _build_jour_ctx(date, conges, h_jour, user_id):
|
||||
"astreinte": date in astreintes,
|
||||
"astreinte_qualifiee": jour_astreinte_qualifie(date, astreintes),
|
||||
"retour_min": retour_min,
|
||||
"retour": minutes_to_time(retour_min) if retour_min is not None else None,
|
||||
"retour": retour_txt(retour_min, date) if retour_min is not None else None,
|
||||
"astreinte_min": dec["total_min"] if dec else 0,
|
||||
"astreinte_h": dec["total_h"] if dec else "",
|
||||
}
|
||||
@ -394,7 +413,7 @@ 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 '
|
||||
trav_inner += (f'<span class="calc-couvert" title="Présence obligatoire couverte par le repos (11h quotidiennes, ou 35h hebdomadaires si activées) '
|
||||
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")
|
||||
@ -548,7 +567,7 @@ def _as_block_html(date, jour):
|
||||
rows = "".join(_as_event_html(date, ev) for ev in events) or _as_event_html(date, {})
|
||||
hint = ""
|
||||
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="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">'
|
||||
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é">'
|
||||
@ -809,7 +828,7 @@ def semaine(request: Request, year: int, week: int):
|
||||
"aprem_entree": None, "aprem_sortie": None})
|
||||
for d in dates
|
||||
]
|
||||
result = compute_week(pointages, conges, h_jour, plages, load_astreintes_effectives(user_id), load_facteurs_astreinte(), load_feries(user_id))
|
||||
result = compute_week(pointages, conges, h_jour, plages, load_astreintes_effectives(user_id), load_facteurs_astreinte(), load_feries(user_id), _contexte_semaine_precedente(year, week, dates, user_id))
|
||||
|
||||
prev_d = datetime.strptime(f"{year}-W{week:02d}-1", "%G-W%V-%u").date() - timedelta(weeks=1)
|
||||
next_d = datetime.strptime(f"{year}-W{week:02d}-1", "%G-W%V-%u").date() + timedelta(weeks=1)
|
||||
@ -1052,6 +1071,7 @@ def settings_page(request: Request):
|
||||
"forfait_saved": request.query_params.get("forfait_saved") == "1",
|
||||
"forfait_error": request.query_params.get("forfait_error") == "1",
|
||||
"astreinte_saved": request.query_params.get("astreinte_saved") == "1",
|
||||
"repos_hebdo_saved": request.query_params.get("repos_hebdo_saved") == "1",
|
||||
"facteurs_saved": request.query_params.get("facteurs_saved") == "1",
|
||||
"facteurs_error": request.query_params.get("facteurs_error") == "1",
|
||||
})
|
||||
@ -1172,6 +1192,28 @@ def settings_astreinte_active(request: Request, astreinte_active: str = Form("")
|
||||
return RedirectResponse("/settings?astreinte_saved=1", status_code=303)
|
||||
|
||||
|
||||
@app.post("/settings/repos-hebdo", response_class=HTMLResponse)
|
||||
def settings_repos_hebdo(request: Request, repos_hebdo_active: str = Form("")):
|
||||
"""Préférence personnelle : respect du repos hebdomadaire de 35h (24h + 11h
|
||||
consécutives, une fois par période de 7 jours glissants). Activée, un
|
||||
week-end d'astreinte sans trou de 35h repousse l'heure de retour à dernière
|
||||
activité + 35h — toute nouvelle intervention avant cette échéance
|
||||
redémarre le décompte."""
|
||||
user_id = _get_user(request)
|
||||
if not user_id:
|
||||
return RedirectResponse("/login", status_code=302)
|
||||
|
||||
actif = repos_hebdo_active == "1"
|
||||
plages = load_plages(user_id)
|
||||
plages["repos_hebdo_active"] = actif
|
||||
save_plages(user_id, plages)
|
||||
logger.info("repos hebdomadaire 35h %s: user_id=%r", "activé" if actif else "désactivé", user_id)
|
||||
append_log(user_id, "settings", "Repos hebdomadaire 35h",
|
||||
"activé (retour repoussé à dernière activité + 35h quand la semaine glissante n'a pas son repos)"
|
||||
if actif else "désactivé (seul le repos quotidien de 11h décale le retour)")
|
||||
return RedirectResponse("/settings?repos_hebdo_saved=1", status_code=303)
|
||||
|
||||
|
||||
@app.post("/settings/plages", response_class=HTMLResponse)
|
||||
def settings_plages_save(
|
||||
request: Request,
|
||||
|
||||
Reference in New Issue
Block a user