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

235 lines
9.5 KiB
Markdown
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.
# 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:
```csv
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
```cron
10 2 * * * cd /var/www/html && php bin/console app:leave:rollover --no-interaction 2>&1
```
Prod
```cron
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:
```bash
php bin/console app:leave:rollover --force --no-interaction
```
Exemple de verification rapide:
```bash
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)