diff --git a/doc/leave-rollover.md b/doc/leave-rollover.md index 5dc0200..ad2831f 100644 --- a/doc/leave-rollover.md +++ b/doc/leave-rollover.md @@ -69,7 +69,7 @@ Etat implementation: - 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` calcule aussi le report dynamique N-1 si la ligne N-1 n'est pas encore persistée (pas de reset a 0 par defaut) +- 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 @@ -121,12 +121,19 @@ Date d'effet: - non forfait: au `1er juin` Traitement par employe: -1. lire l'exercice precedent -2. determiner le report: +1. determiner le report de l'exercice precedent: - si cloture `paidLeaveSettled=true` sur la periode precedente => report `0` - - sinon report = `closing` exercice precedent + - 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 +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 diff --git a/src/Command/LeaveRolloverCommand.php b/src/Command/LeaveRolloverCommand.php index 59f5ef7..8152937 100644 --- a/src/Command/LeaveRolloverCommand.php +++ b/src/Command/LeaveRolloverCommand.php @@ -188,24 +188,35 @@ final class LeaveRolloverCommand extends Command private function resolveCarry(Employee $employee, LeaveRuleCode $ruleCode, int $targetYear): array { $previousYear = $targetYear - 1; - $previous = $this->leaveBalanceRepository->findOneByEmployeeRuleAndYear($employee, $ruleCode, $previousYear); - if (null !== $previous) { - $carryDays = $previous->getClosingDays() + $previous->getFractionedDays(); - $carrySaturdays = LeaveRuleCode::CDI_CDD_NON_FORFAIT === $ruleCode - ? $previous->getClosingSaturdays() - : 0.0; - } else { - [$carryDays, $carrySaturdays] = $this->leaveBalanceComputationService - ->computeDynamicClosingForYear($employee, $ruleCode, $previousYear) - ; - } [$from, $to] = $this->leaveBalanceComputationService->resolvePeriodBounds($ruleCode, $previousYear); $hasSettlement = $this->leaveBalanceComputationService ->hasPaidLeaveSettledClosureBetween($employee, $from, $to) ; + if ($hasSettlement) { - return [0.0, 0.0]; + $carryDays = 0.0; + $carrySaturdays = 0.0; + } else { + // Compute the REAL closing of the ending exercise. computeDynamicClosingForYear + // is bootstrap-aware (it anchors on the persisted opening balance of each year) + // and already folds in accrual, taken absences and fractioned days. We must NOT + // trust the stored closing_days: it is only ever written equal to the opening at + // row creation (placeholder), so trusting it would propagate the opening and + // ignore the year's accrual (cas Aurore : report 0 au lieu de 31). + [$carryDays, $carrySaturdays] = $this->leaveBalanceComputationService + ->computeDynamicClosingForYear($employee, $ruleCode, $previousYear) + ; + } + + // Freeze the computed closing on the ending exercise's row so the column finally + // holds a real, auditable value. The cron is idempotent — it never reaches here for + // an already-rolled target year (existing rows are skipped upstream) — so a row that + // was corrected manually in the DB afterwards is never overwritten. + $previous = $this->leaveBalanceRepository->findOneByEmployeeRuleAndYear($employee, $ruleCode, $previousYear); + if (null !== $previous) { + $previous->setClosingDays($carryDays); + $previous->setClosingSaturdays($carrySaturdays); } return [$carryDays, $carrySaturdays];