fix : le cron de bascule recalcule la vraie clôture au lieu du placeholder
LeaveRolloverCommand::resolveCarry se fiait au closing_days stocké quand une ligne existait pour l'exercice précédent. Or closing_days n'est jamais recalculé après création (toujours = opening, ou 0 sur un bootstrap), donc le report propageait l'ouverture sans créditer l'acquisition de l'année. Cas Aurore : bascule 2026->2027 aurait reporté 0 au lieu de 31. resolveCarry calcule désormais toujours la clôture réelle via computeDynamicClosingForYear (bootstrap-aware, intègre acquisition + samedis + fractionnés − pris), puis fige ce résultat dans closing_days de l'exercice qui se termine. Vérifié sur données réelles : report 2027 d'Aurore = 31,00 j / 5,00 samedis (au lieu de 0). Corrections manuelles préservées : le cron reste idempotent (ne réécrit pas une ligne existante) et le bon levier de correction devient opening_days (propagé par le recalcul), pas closing_days. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+12
-5
@@ -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)`
|
- 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
|
- 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).
|
- **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
|
### Definition des colonnes
|
||||||
|
|
||||||
@@ -121,12 +121,19 @@ Date d'effet:
|
|||||||
- non forfait: au `1er juin`
|
- non forfait: au `1er juin`
|
||||||
|
|
||||||
Traitement par employe:
|
Traitement par employe:
|
||||||
1. lire l'exercice precedent
|
1. determiner le report de l'exercice precedent:
|
||||||
2. determiner le report:
|
|
||||||
- si cloture `paidLeaveSettled=true` sur la periode precedente => report `0`
|
- 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_*`
|
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
|
## 7) Donnees a fournir au go-live
|
||||||
|
|
||||||
|
|||||||
@@ -188,24 +188,35 @@ final class LeaveRolloverCommand extends Command
|
|||||||
private function resolveCarry(Employee $employee, LeaveRuleCode $ruleCode, int $targetYear): array
|
private function resolveCarry(Employee $employee, LeaveRuleCode $ruleCode, int $targetYear): array
|
||||||
{
|
{
|
||||||
$previousYear = $targetYear - 1;
|
$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);
|
[$from, $to] = $this->leaveBalanceComputationService->resolvePeriodBounds($ruleCode, $previousYear);
|
||||||
$hasSettlement = $this->leaveBalanceComputationService
|
$hasSettlement = $this->leaveBalanceComputationService
|
||||||
->hasPaidLeaveSettledClosureBetween($employee, $from, $to)
|
->hasPaidLeaveSettledClosureBetween($employee, $from, $to)
|
||||||
;
|
;
|
||||||
|
|
||||||
if ($hasSettlement) {
|
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];
|
return [$carryDays, $carrySaturdays];
|
||||||
|
|||||||
Reference in New Issue
Block a user