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
This commit is contained in:
145
app/tests/test_ferie.py
Normal file
145
app/tests/test_ferie.py
Normal file
@ -0,0 +1,145 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user