from calcul import ( compute_week, heures_dues, heures_travaillees, hhmm_to_minutes, minutes_to_hhmm, ) HEURES_JOUR_MIN = 468 # 7h48 = 7.8 * 60 def _pointage(date, matin_entree=None, matin_sortie=None, aprem_entree=None, aprem_sortie=None): return { "date": date, "matin_entree": matin_entree, "matin_sortie": matin_sortie, "aprem_entree": aprem_entree, "aprem_sortie": aprem_sortie, } def test_hhmm_to_minutes(): assert hhmm_to_minutes("09:00") == 540 assert hhmm_to_minutes("00:00") == 0 assert hhmm_to_minutes(None) == 0 def test_minutes_to_hhmm(): assert minutes_to_hhmm(468) == "7h48" assert minutes_to_hhmm(0) == "0h00" assert minutes_to_hhmm(-90) == "-1h30" def test_heures_dues_weekend(): assert heures_dues("2026-07-18", [], HEURES_JOUR_MIN) == 0 # samedi def test_heures_dues_conge_jour(): conges = [{"date": "2026-07-13", "type": "jour"}] assert heures_dues("2026-07-13", conges, HEURES_JOUR_MIN) == 0 def test_heures_dues_conge_demi_journee(): conges = [{"date": "2026-07-13", "type": "matin"}] assert heures_dues("2026-07-13", conges, HEURES_JOUR_MIN) == HEURES_JOUR_MIN // 2 def test_heures_dues_matin_et_aprem_congés_egale_jour_complet(): conges = [{"date": "2026-07-13", "type": "matin"}, {"date": "2026-07-13", "type": "aprem"}] assert heures_dues("2026-07-13", conges, HEURES_JOUR_MIN) == 0 def test_heures_dues_jour_normal(): assert heures_dues("2026-07-13", [], HEURES_JOUR_MIN) == HEURES_JOUR_MIN def test_heures_travaillees_journee_complete(): p = _pointage("2026-07-13", "09:00", "12:24", "13:30", "17:54") assert heures_travaillees(p) == 204 + 264 # 468 def test_heures_travaillees_pas_pointe(): assert heures_travaillees(_pointage("2026-07-13")) == 0 def test_heures_travaillees_en_cours_aujourdhui(freeze_today): # freeze_today = 2026-07-17 10:30 ; entrée à 09:00 -> 90 min écoulées p = _pointage("2026-07-17", matin_entree="09:00") assert heures_travaillees(p) == 90 def _semaine_complete(dates_travaillees): """4 jours complets à exactement HEURES_JOUR_MIN, pas de congé.""" return [ _pointage(d, "09:00", "12:24", "13:30", "17:54") for d in dates_travaillees ] def test_compute_week_semaine_entierement_complete_solde_zero(): dates = ["2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16", "2026-07-17"] pointages = _semaine_complete(dates) result = compute_week(pointages, [], HEURES_JOUR_MIN) assert result["total_travaille"] == "39h00" assert result["total_du"] == "39h00" assert result["solde_min"] == 0 assert all(j["is_complete"] for j in result["jours"]) def test_compute_week_cumul_visible_seulement_si_chaine_ininterrompue(): dates = ["2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16", "2026-07-17"] pointages = _semaine_complete(dates) # mercredi incomplet (pas d'aprem) pointages[2] = _pointage("2026-07-15", "09:00", "12:24") result = compute_week(pointages, [], HEURES_JOUR_MIN) jours = result["jours"] assert jours[0]["cumul_visible"] is True assert jours[1]["cumul_visible"] is True assert jours[2]["cumul_visible"] is False # jour incomplet assert jours[3]["cumul_visible"] is False # chaîne cassée après assert jours[4]["cumul_visible"] is False def test_compute_week_cible_repartie_sur_seule_demi_journee_restante(freeze_today): """Régression: capture exactement les seuils 9h/12h/13h30/17h codés en dur. Lundi-jeudi complets à 468 min. Vendredi (aujourd'hui, frozen) pointé seulement le matin 09:00-12:00 (180 min, sous les 234 min nominales de la demi-journée). Comme il ne reste qu'une seule demi-journée non traitée (l'après-midi de vendredi), tout le manque de la semaine s'y retrouve — même résultat que l'ancien algorithme "cliff-edge" dans ce cas précis. """ dates_completes = ["2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16"] pointages = _semaine_complete(dates_completes) pointages.append(_pointage("2026-07-17", matin_entree="09:00", matin_sortie="12:00")) result = compute_week(pointages, [], HEURES_JOUR_MIN) vendredi = result["jours"][4] assert vendredi["cible_matin"] is None # matin déjà pointé (entrée+sortie), plus rien à y faire assert vendredi["cible_aprem"] == "18:18" def test_compute_week_jour_futur_non_entame_recoit_une_cible_repartie(freeze_today): """Un jour futur non entamé fait maintenant partie de la répartition (spread sur la semaine).""" dates_completes = ["2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16"] pointages = _semaine_complete(dates_completes) pointages.append(_pointage("2026-07-17", matin_entree="09:00", matin_sortie="12:00")) pointages.append(_pointage("2026-07-20")) # lundi suivant, rien pointé result = compute_week(pointages, [], HEURES_JOUR_MIN) # Le manque (288 min) est maintenant réparti entre l'après-midi de vendredi et # les deux demi-journées de lundi, au lieu d'être entièrement dumpé sur vendredi soir. vendredi = result["jours"][4] lundi_suivant = result["jours"][5] assert vendredi["cible_aprem"] is not None assert lundi_suivant["cible_matin"] is not None or lundi_suivant["cible_aprem"] is not None def test_compute_week_jour_futur_a_egalite_pas_de_cible(freeze_today): """Quand le rythme normal des demi-journées restantes suffit déjà, aucune cible n'est affichée.""" dates_completes = ["2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16", "2026-07-17"] pointages = _semaine_complete(dates_completes) pointages.append(_pointage("2026-07-20")) # lundi suivant, rien pointé — mais la semaine est déjà à l'équilibre result = compute_week(pointages, [], HEURES_JOUR_MIN) lundi_suivant = result["jours"][5] assert lundi_suivant["cible_matin"] is None assert lundi_suivant["cible_aprem"] is None def test_compute_week_jour_courant_non_entame_rythme_normal_suffit(freeze_today): """"Aujourd'hui" sans rien pointé n'est pas traité comme un jour futur figé : ses deux demi-journées entrent dans la répartition. Ici le manque (468 min) correspond exactement au nominal des deux demi-journées restantes : le rythme normal suffit, pas de cible.""" dates_completes = ["2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16"] pointages = _semaine_complete(dates_completes) pointages.append(_pointage("2026-07-17")) result = compute_week(pointages, [], HEURES_JOUR_MIN) vendredi = result["jours"][4] assert vendredi["cible_matin"] is None assert vendredi["cible_aprem"] is None def test_compute_week_plages_personnalisees(freeze_today): """Les seuils de plage sont bien paramétrables, pas juste figés sur les défauts.""" plages = { "matin_debut": "08:00", "matin_fin": "13:00", "aprem_debut": "14:00", "aprem_fin": "18:00", "pause_dejeuner_fin": "14:00", } heures_jour = (13 * 60 - 8 * 60) + (18 * 60 - 14 * 60) # 540 = journée complète sous ce schéma dates_completes = ["2026-07-13", "2026-07-14", "2026-07-15", "2026-07-16"] pointages = [_pointage(d, "08:00", "13:00", "14:00", "18:00") for d in dates_completes] # Matin écourté (240 min au lieu des 300 nominales) : le manque doit être # rattrapé sur l'unique demi-journée restante, avec les seuils personnalisés. pointages.append(_pointage("2026-07-17", "08:00", "12:00")) result = compute_week(pointages, [], heures_jour, plages) vendredi = result["jours"][4] assert vendredi["cible_matin"] is None assert vendredi["cible_aprem"] == "19:00"