Files
SIRH/doc/leave-rollover.md
T
tristan cf2e12c8ba
Auto Tag Develop / tag (push) Successful in 9s
[#SIRH-32] Ajouter l'exercice 2026/2027 dans les congés/RTT (#20)
| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

- [x] Pas de régression
- [x] TU/TI/TF rédigée
- [x] TU/TI/TF OK
- [ ] CHANGELOG modifié

Reviewed-on: #20
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-05-26 14:09:02 +00:00

9.5 KiB
Raw Blame History

Rollover Conges - Regles et Mise en Production

Document de reference pour expliquer le fonctionnement metier du report N-1 et preparer le lancement en production.

1) Objectif

Eviter les recalculs "depuis le debut du contrat" et fiabiliser les soldes.

Principe:

  • le solde est stocké par exercice
  • au changement d'exercice, on ouvre la nouvelle période avec un "solde d'ouverture" (report N-1)
  • un indicateur de cloture (contractPaidLeaveSettled) permet de couper la continuité entre 2 contrats

2) Exercices metier

  • CDI / CDD non forfait:
    • exercice: 1er juin au 31 mai
    • year = annee de fin d'exercice (ex: 2026 = 01/06/2025 -> 31/05/2026)
  • FORFAIT:
    • exercice: 1er janvier au 31 decembre
    • year = annee civile
  • INTERIM:
    • hors perimetre conges

3) Logique de compteurs

  • acquis:
    • correspond au report N-1 (solde d'ouverture)
  • en cours d'acquisition:
    • correspond aux droits generes sur l'exercice en cours
  • pris:
    • non forfait: absences type C (conge)
    • forfait: toutes absences
  • restant:
    • acquis + en_cours - pris (borne a 0 dans l'affichage)

4) Effet du "solde de tout compte"

Le champ de cloture contractPaidLeaveSettled est saisi lors de la fermeture d'une periode contrat.

  • false:
    • continuite des droits entre contrats
  • true:
    • pas de reprise des droits precedents
    • reset de continuite au lendemain de la date de cloture

5) Table cible

Table employee_leave_balances (une ligne par employe et exercice):

  • employee_id
  • rule_code (CDI_CDD_NON_FORFAIT ou FORFAIT_218)
  • year
  • opening_days
  • opening_saturdays
  • accrued_days
  • accrued_saturdays (optionnel selon implementation)
  • taken_days
  • taken_saturdays
  • closing_days
  • closing_saturdays
  • is_locked
  • created_at, updated_at

Contrainte unique recommandee:

  • (employee_id, rule_code, year)

Etat implementation:

  • la table est creee
  • le calcul de synthese conges lit en priorite opening_days/opening_saturdays de cette table quand une ligne existe pour (employee, rule_code, year)
  • si aucune ligne n'existe, le calcul reste base sur le report dynamique N-1
  • le report dynamique (LeaveBalanceComputationService::computeDynamicClosingForYear, qui alimente le solde d'ouverture de l'exercice suivant) ancre lui aussi sur cette table : pour chaque exercice de sa boucle, si une ligne bootstrap existe il part de opening_days/opening_saturdays (et ajoute l'offset taken_days/taken_saturdays) au lieu de recalculer depuis l'embauche. Sans cet ancrage, la clôture d'un exercice consulté en avance (ex. exercice suivant) cumulerait une année pleine d'acquisition par exercice antérieur à la mise en service — aucune absence historique n'étant saisie (cas Aurore : 88 jours au lieu de 31).
  • la commande app:leave:rollover recalcule toujours le report via computeDynamicClosingForYear(N-1) (et ne se fie plus au closing_days stocké, qui n'est qu'un placeholder = opening), puis fige ce résultat dans le closing_days de l'exercice qui se termine ; voir § 6

Definition des colonnes

  • employee_id:
    • identifiant employe (FK vers employees)
    • une ligne de solde par employe / regle / exercice
  • rule_code:
    • code de regle appliquee (CDI_CDD_NON_FORFAIT, FORFAIT_218)
    • permet de savoir quelles regles de calcul sont utilisees
  • year:
    • annee d'exercice
    • non forfait: annee de fin d'exercice (2026 = 01/06/2025 -> 31/05/2026)
    • forfait: annee civile (2026 = 01/01/2026 -> 31/12/2026)
  • opening_days:
    • report N-1 en jours (solde d'ouverture)
  • opening_saturdays:
    • report N-1 "samedis" (0 pour forfait)
  • accrued_days:
    • droits generes sur l'exercice courant (N)
  • accrued_saturdays:
    • droits samedis generes sur N (0 pour forfait)
  • taken_days:
    • jours poses sur l'exercice
  • taken_saturdays:
    • samedis poses sur l'exercice (0 pour forfait)
  • closing_days:
    • solde de cloture jours (opening_days + accrued_days - taken_days)
  • closing_saturdays:
    • solde de cloture samedis (opening_saturdays + accrued_saturdays - taken_saturdays)
  • is_locked:
    • false sur exercice ouvert (recalcul possible)
    • true apres validation RH (exercice fige)
  • created_at, updated_at:
    • trace technique creation / mise a jour

6) Rollover automatique

Commande quotidienne (cron) idempotente.

  • commande Symfony: php bin/console app:leave:rollover
  • comportement date metier:
    • le 01/01: traite uniquement FORFAIT_218
    • le 01/06: traite uniquement CDI_CDD_NON_FORFAIT
    • les autres jours: sortie sans action
  • option manuelle: --force pour executer hors date metier (reprise/correction)

Date d'effet:

  • forfait: au 1er janvier
  • non forfait: au 1er juin

Traitement par employe:

  1. determiner le report de l'exercice precedent:
    • si cloture paidLeaveSettled=true sur la periode precedente => report 0
    • sinon report = cloture reelle recalculee via computeDynamicClosingForYear(exercicePrecedent) (acquisition + samedis + fractionnes pris, ancree sur l'opening_days bootstrap de chaque exercice). On ne se fie PAS au closing_days stocke : il n'est jamais recalcule apres creation (toujours egal a l'opening), donc s'y fier propagerait l'ouverture sans jamais crediter l'acquisition de l'annee (cas Aurore : report 0 au lieu de 31).
  2. figer ce report dans closing_days/closing_saturdays de la ligne de l'exercice qui se termine (la colonne contient enfin un vrai solde de cloture, auditable).
  3. creer la ligne du nouvel exercice avec ce report en opening_*
  4. initialiser accrued/taken/closing pour le nouvel exercice (= opening a la creation)

Correction manuelle d'un solde (RH / comptable)

Le verrouillage (is_locked) n'est pas utilise ; les corrections se font directement en BDD. Deux garde-fous rendent cela sur :

  • Idempotence : le cron ne cree la ligne d'un exercice que si elle n'existe pas (les lignes existantes sont ignorees). Une ligne corrigee a la main n'est donc jamais ecrasee par un passage ulterieur du cron (meme avec --force).
  • Le bon levier est opening_days, pas closing_days : computeDynamicClosingForYear part de l'opening_days de chaque exercice comme ancre. Corriger l'opening_days d'un exercice (ou la donnee de fond : absence, fractionne, paye) se propage automatiquement aux reports des exercices suivants. Editer un closing_days d'un exercice pas encore bascule est inutile (il sera recalcule a la bascule) ; une fois la ligne suivante creee, plus rien n'y touche.

7) Donnees a fournir au go-live

La RH doit fournir un import d'ouverture:

Colonnes minimales:

  • employee_identifier (id interne ou matricule)
  • rule_code
  • year
  • opening_days
  • opening_saturdays (0 pour forfait)
  • source_date (date de reference du relevé RH)
  • comment (optionnel)

Format recommande:

  • CSV UTF-8
  • separateur ;
  • decimales en point (7.5)

Exemple:

employee_id;rule_code;year;opening_days;opening_saturdays;source_date;comment
42;CDI_CDD_NON_FORFAIT;2026;12.5;2;2026-05-31;Reprise fichier RH
17;FORFAIT_218;2026;8;0;2025-12-31;Reprise fichier RH

8) Checklist mise en prod

  1. Valider le mapping employe RH -> employe applicatif
  2. Importer les soldes d'ouverture N-1
  3. Verifier 5 cas metier:
    • CDI simple sans changement de contrat
    • CDD -> CDI avec paidLeaveSettled=false
    • CDD -> CDI avec paidLeaveSettled=true
    • Forfait sur annee complete
    • Forfait avec debut en cours d'annee
  4. Activer le cron de rollover
  5. Geler (is_locked) les exercices historicises valides

Exemple cron (tous les jours a 02:10): Dev

10 2 * * * cd /var/www/html && php bin/console app:leave:rollover --no-interaction 2>&1

Prod

10 2 * * * cd /var/www/sirh && php bin/console app:leave:rollover --no-interaction 2>&1

Explication de la ligne cron:

  • 10 2 * * *: planification
    • 10 = minute
    • 2 = heure
    • * = tous les jours du mois
    • * = tous les mois
    • * = tous les jours de la semaine
  • cd /var/www/html: se place dans le dossier de l application Symfony
  • php bin/console app:leave:rollover --no-interaction: execute le rollover sans demander de confirmation
    • hors 01/01 et 01/06, la commande sort en no-op (normal)
  • >> var/log/leave-rollover.log: ajoute la sortie standard dans le fichier de log (sans ecraser l historique)
  • 2>&1: redirige aussi les erreurs dans le meme fichier de log

Execution manuelle forcee:

php bin/console app:leave:rollover --force --no-interaction

Exemple de verification rapide:

tail -n 50 /var/www/html/var/log/leave-rollover.log

9) Points de vigilance

  • Ne jamais recalculer les soldes historiques apres validation RH sans procedure explicite
  • Garder une trace de toute correction manuelle (auteur, date, motif)
  • Aligner strictement les regles UI et API sur les memes compteurs (pas de formule differente front/back)

10) Regle de consommation des droits

Regle metier:

  • un employe peut poser des conges en cours d'acquisition
  • la consommation se fait par ordre:
    1. acquis (report N-1)
    2. en cours d'acquisition (droits N)

Effet attendu:

  • si acquis = 0 et en cours = 7.5, puis prise de 7, alors:
    • acquis reste 0
    • en cours devient 0.5
  • si acquis = 0 et en cours = 2.5, puis prise de 3, alors:
    • acquis reste 0
    • en cours devient -0.5 (dette)
    • le mois suivant, une acquisition de 2.5 ramené en cours a 2.0

Formule de lecture recommandée:

  • restant_acquis = max(0, acquis - pris)
  • reste_a_imputer_sur_en_cours = max(0, pris - acquis)
  • restant_en_cours = en_cours - reste_a_imputer_sur_en_cours (valeur negative autorisee)