Rend les plages horaires et les rappels ntfy configurables, supprime le
classement, ajoute une suite de tests pytest - calcul.py: matin_debut/fin, aprem_debut/fin et pause_dejeuner_fin sont paramétrables via config.json > plages (moteur de calcul de l'heure de sortie optimale inclus, pas seulement l'affichage). Défauts identiques à l'ancien comportement, validé par la suite de tests. - stats.py: les seuils de conformité utilisent la même config. - main.py/templates: warn de saisie et suffixes de cible dynamiques. - Rappels ntfy: serveur, topic et plage horaire (rappel_debut_h/fin_h) configurables par utilisateur depuis /settings, plus en dur dans le code. - Page classement supprimée (pas de compétition entre utilisateurs). - Nouvelle suite pytest (app/tests/), à lancer via `docker compose run --rm pointeuse pytest -v`.
This commit is contained in:
@ -1,7 +1,13 @@
|
||||
"""Business logic: compute worked hours and expected hours per day/week."""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
APREM_DEBUT_MIN = 13 * 60 + 30 # 13:30 — pause déjeuner de base 12h00-13h30
|
||||
DEFAULT_PLAGES = {
|
||||
"matin_debut": "09:00", # présence obligatoire à partir de cette heure le matin
|
||||
"matin_fin": "12:00", # présence obligatoire jusqu'à cette heure le matin
|
||||
"aprem_debut": "14:00", # présence obligatoire à partir de cette heure l'après-midi
|
||||
"aprem_fin": "17:00", # présence obligatoire jusqu'à cette heure l'après-midi (plancher de sortie)
|
||||
"pause_dejeuner_fin": "13:30", # reprise par défaut projetée tant que l'entrée aprem n'est pas saisie
|
||||
}
|
||||
|
||||
|
||||
def hhmm_to_minutes(val: str | None) -> int:
|
||||
@ -62,8 +68,15 @@ def heures_dues(date_str: str, conges: list[dict], heures_jour_min: int) -> int:
|
||||
return heures_jour_min
|
||||
|
||||
|
||||
def compute_week(pointages: list[dict], conges: list[dict], heures_jour_min: int) -> dict:
|
||||
def compute_week(pointages: list[dict], conges: list[dict], heures_jour_min: int, plages: dict | None = None) -> dict:
|
||||
"""Compute totals for a list of pointages (one week)."""
|
||||
plages = {**DEFAULT_PLAGES, **(plages or {})}
|
||||
matin_debut_min = hhmm_to_minutes(plages["matin_debut"])
|
||||
matin_fin_min = hhmm_to_minutes(plages["matin_fin"])
|
||||
aprem_fin_min = hhmm_to_minutes(plages["aprem_fin"])
|
||||
pause_dejeuner_fin_min = hhmm_to_minutes(plages["pause_dejeuner_fin"])
|
||||
matin_window = matin_fin_min - matin_debut_min
|
||||
|
||||
total_travaille = 0
|
||||
total_du = 0
|
||||
jours = []
|
||||
@ -124,9 +137,10 @@ def compute_week(pointages: list[dict], conges: list[dict], heures_jour_min: int
|
||||
du_remaining_mat = sum(j["du_min"] for j in jours[i + 1:])
|
||||
if du_remaining_mat == 0 and not jour.get("matin_entree"):
|
||||
needed_today_mat = max(0, total_du - worked_before - du_remaining_mat)
|
||||
if needed_today_mat > 3 * 60: # plus que le créneau 9h-12h obligatoire
|
||||
entree_min = 12 * 60 - needed_today_mat
|
||||
if entree_min >= 6 * 60: # avant 9h obligatoire, pas avant 6h
|
||||
if needed_today_mat > matin_window: # plus que le créneau matin obligatoire
|
||||
entree_min = matin_fin_min - needed_today_mat
|
||||
floor_min = matin_debut_min - matin_window # pas avant ce plancher symétrique
|
||||
if entree_min >= floor_min:
|
||||
jour["entree_cible"] = f"{entree_min // 60:02d}:{entree_min % 60:02d}"
|
||||
worked_before += jour["travaille_min"]
|
||||
continue
|
||||
@ -138,16 +152,16 @@ def compute_week(pointages: list[dict], conges: list[dict], heures_jour_min: int
|
||||
if jour.get("matin_entree") and jour.get("matin_sortie"):
|
||||
worked_morning = max(0, hhmm_to_minutes(jour["matin_sortie"]) - hhmm_to_minutes(jour["matin_entree"]))
|
||||
elif jour.get("matin_entree") and jour["date"] == today:
|
||||
# Pas de sortie pointée le matin : projeter une matinée normale jusqu'à midi
|
||||
# Pas de sortie pointée le matin : projeter une matinée normale jusqu'à la fin de la plage
|
||||
# (sinon la cible dumperait tout le quota du jour sur l'après-midi dès l'arrivée).
|
||||
entree_min = hhmm_to_minutes(jour["matin_entree"])
|
||||
elapsed = max(0, now_min - entree_min)
|
||||
projected_to_noon = max(0, 12 * 60 - entree_min)
|
||||
worked_morning = max(elapsed, projected_to_noon)
|
||||
projected_to_fin_matin = max(0, matin_fin_min - entree_min)
|
||||
worked_morning = max(elapsed, projected_to_fin_matin)
|
||||
|
||||
needed_aprem = max(0, needed_today - worked_morning)
|
||||
aprem_start = hhmm_to_minutes(jour["aprem_entree"]) if jour.get("aprem_entree") else APREM_DEBUT_MIN
|
||||
sortie_min = max(aprem_start + needed_aprem, 17 * 60)
|
||||
aprem_start = hhmm_to_minutes(jour["aprem_entree"]) if jour.get("aprem_entree") else pause_dejeuner_fin_min
|
||||
sortie_min = max(aprem_start + needed_aprem, aprem_fin_min)
|
||||
|
||||
if sortie_min > 23 * 60 + 59:
|
||||
jour["sortie_cible"] = None # deficit too large to recover in one afternoon
|
||||
@ -155,9 +169,9 @@ def compute_week(pointages: list[dict], conges: list[dict], heures_jour_min: int
|
||||
jour["sortie_cible"] = f"{sortie_min // 60:02d}:{sortie_min % 60:02d}"
|
||||
|
||||
entree_cible = None
|
||||
if du_remaining == 0 and not jour.get("aprem_entree") and needed_aprem > (17 * 60 - APREM_DEBUT_MIN):
|
||||
entree_min = 17 * 60 - needed_aprem
|
||||
if 12 * 60 < entree_min < APREM_DEBUT_MIN: # avant l'heure de reprise par défaut, après le déjeuner
|
||||
if du_remaining == 0 and not jour.get("aprem_entree") and needed_aprem > (aprem_fin_min - pause_dejeuner_fin_min):
|
||||
entree_min = aprem_fin_min - needed_aprem
|
||||
if matin_fin_min < entree_min < pause_dejeuner_fin_min: # après le déjeuner, avant la reprise par défaut
|
||||
entree_cible = f"{entree_min // 60:02d}:{entree_min % 60:02d}"
|
||||
jour["entree_cible"] = entree_cible
|
||||
worked_before += jour["travaille_min"]
|
||||
|
||||
Reference in New Issue
Block a user