Files
pointeuse-optimisator/app/tests/test_ferie.py
Jacquin Antoine dc80e81db2 Ajoute les jours fériés, l'astreinte week-end 0h-0h et les ratios éditables
- Bouton F : marque un jour férié (dû 0, sans consommer de congé) ; il
  qualifie les règles « ferie » et étend la fenêtre d'astreinte du jour.
- Samedi, dimanche et fériés : l'astreinte compte de 0h00 à 0h00 ; un jour
  AS déclaré garde sa borne du lendemain 18h pour ne pas tronquer les
  interventions nocturnes.
- Ratios d'astreinte éditables à la main dans Réglages (nom, jours, fenêtre,
  ratio), avec ajout/suppression de règles et validation.
- Un quart d'heure entamé par deux interventions qui se suivent n'est compté
  qu'une fois : les lignes d'événements somment au total du jour.
- Décompte mieux expliqué dans l'interface : quarts d'horloge (:00/:15/:30/:45),
  fenêtre couverte par événement, exemple chiffré.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-09-08 23:30:15 +02:00

146 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Jours fériés marqués à la main : stockage, décompte (dû 0, fenêtre d'astreinte
0h00 → 0h00) et ratios d'astreinte éditables depuis la page Réglages."""
import sys
# DATA_DIR temporaire est positionné par conftest.py avant l'import de models.
import models # noqa: F401
sys.path.insert(0, ".")
import main
from fastapi.testclient import TestClient
from calcul import compute_week, heures_dues
client = TestClient(main.app)
HEURES_JOUR_MIN = 468 # 39h / 5j
def _login(uid: str) -> dict:
import bcrypt
models.save_auth(uid, email=f"{uid}@x.fr",
password_hash=bcrypt.hashpw(b"x" * 12, bcrypt.gensalt()).decode())
r = client.post("/login/password",
data={"email": f"{uid}@x.fr", "password": "x" * 12},
follow_redirects=False)
assert r.status_code == 302, r.text
return {"user_id": r.cookies.get("user_id")}
def _pointage(date, astreinte_events=None):
return {"date": date, "matin_entree": None, "matin_sortie": None,
"aprem_entree": None, "aprem_sortie": None,
"astreinte_events": astreinte_events or []}
# ── Stockage ──────────────────────────────────────────────────────────────────
def test_toggle_ferie_persiste():
models.toggle_ferie("2026-07-15", "u-ferie")
assert models.load_feries("u-ferie") == ["2026-07-15"]
models.toggle_ferie("2026-07-15", "u-ferie")
assert models.load_feries("u-ferie") == []
# ── Décompte ──────────────────────────────────────────────────────────────────
def test_heures_dues_ferie_zero():
"""Un mercredi marqué férié ne doit aucune heure, congés ou pas."""
assert heures_dues("2026-07-15", [], HEURES_JOUR_MIN, ferie=True) == 0
assert heures_dues("2026-07-15", [], HEURES_JOUR_MIN, ferie=True,
retour_min=10 * 60) == 0
def test_compute_week_ferie_du_zero_jour_complet(freeze_today):
"""Mercredi marqué F : dû 0, jour complet d'office, badge férié, heures
pointées toujours comptées (delta positif)."""
pointages = [
_pointage("2026-07-13", None), _pointage("2026-07-14", None),
_pointage("2026-07-15", None), _pointage("2026-07-16", None),
_pointage("2026-07-17", None),
]
pointages[2].update(matin_entree="09:00", matin_sortie="12:24",
aprem_entree="13:30", aprem_sortie="17:54")
result = compute_week(pointages, [], HEURES_JOUR_MIN, {}, [], None, {"2026-07-15"})
mercredi = result["jours"][2]
assert mercredi["ferie"] is True
assert mercredi["du_min"] == 0
assert mercredi["is_complete"] is True
assert mercredi["travaille_min"] == HEURES_JOUR_MIN # pointé : compte en delta
assert result["jours"][1]["ferie"] is False
assert result["jours"][3]["ferie"] is False
def test_compute_week_ferie_etend_fenetre_astreinte(freeze_today):
"""AS mardi + F mercredi : l'intervention de mercredi 20:00→21:00 (après la
borne 18:00 des jours ouvrés) compte — Nuit ×1,5 = 1h30 pondérées."""
pointages = [
_pointage("2026-07-13", None),
_pointage("2026-07-14", None),
_pointage("2026-07-15", [{"debut": "20:00", "fin": "21:00"}]),
_pointage("2026-07-16", None), _pointage("2026-07-17", None),
]
result = compute_week(pointages, [], HEURES_JOUR_MIN, {}, ["2026-07-14"], None,
{"2026-07-15"})
assert result["jours"][2]["astreinte_min"] == 60
assert result["astreinte"]["total_pondere_min"] == 90
# ── Routes ────────────────────────────────────────────────────────────────────
def test_route_toggle_ferie():
cookies = _login("u-ferie-route")
r = client.post("/ferie/2026-07-15/toggle", cookies=cookies, follow_redirects=False)
assert r.status_code == 303
assert models.load_feries("u-ferie-route") == ["2026-07-15"]
r = client.get("/semaine/2026/29", cookies=cookies, follow_redirects=False)
assert r.status_code == 200
assert "ferie-on" in r.text # bouton F actif
assert "ferie-badge" in r.text # badge « férié » sur la ligne du jour
client.post("/ferie/2026-07-15/toggle", cookies=cookies, follow_redirects=False)
assert models.load_feries("u-ferie-route") == []
def test_settings_astreinte_facteurs_editables():
"""POST de règles valides : enregistrées et rechargées ; règle au nom vide
supprimée ; ratio invalide rejette tout sans rien changer."""
cookies = _login("u-facteurs")
avant = models.load_facteurs_astreinte()
data = {
"f_nom_0": "Nuit", "f_debut_0": "20:00", "f_fin_0": "08:00", "f_facteur_0": "1.5",
"f_jours_0": ["semaine", "dim"],
"f_nom_1": "Garde", "f_debut_1": "18:00", "f_fin_1": "09:00", "f_facteur_1": "2,5",
"f_jours_1": ["sam"],
}
r = client.post("/settings/astreinte-facteurs", cookies=cookies, data=data,
follow_redirects=False)
assert r.status_code == 303
assert "facteurs_saved=1" in r.headers["location"]
facteurs = models.load_facteurs_astreinte()
assert [f["nom"] for f in facteurs] == ["Nuit", "Garde"]
assert facteurs[1]["facteur"] == 2.5 # virgule acceptée
assert facteurs[1]["jours"] == ["sam"]
# suppression par nom vidé : la règle disparaît
r = client.post("/settings/astreinte-facteurs", cookies=cookies, data={
"f_nom_0": "Nuit", "f_debut_0": "20:00", "f_fin_0": "08:00", "f_facteur_0": "1.5",
"f_jours_0": ["semaine"],
"f_nom_1": "", "f_debut_1": "18:00", "f_fin_1": "09:00", "f_facteur_1": "2",
}, follow_redirects=False)
assert r.status_code == 303
assert [f["nom"] for f in models.load_facteurs_astreinte()] == ["Nuit"]
# ratio invalide : rejet, rien n'est modifié
r = client.post("/settings/astreinte-facteurs", cookies=cookies, data={
"f_nom_0": "Bug", "f_debut_0": "20:00", "f_fin_0": "08:00",
"f_facteur_0": "0", "f_jours_0": ["semaine"],
}, follow_redirects=False)
assert r.status_code == 303
assert "facteurs_error=1" in r.headers["location"]
assert [f["nom"] for f in models.load_facteurs_astreinte()] == ["Nuit"]
# remise aux règles par défaut pour les autres tests
models.save_facteurs_astreinte(avant)