fix(leave) : forfait work-target ignores bonus/fractioned days
The header target subtracted summary.acquiredDays, which includes fractionedDays (and bonusDays via the full-year acquired), so a full-year forfait with weekend work or HR fractioned days showed <218. Full year = contractual 218 (capped at period business days); entry year = businessDays − entry acquired (repos + carried CP, excluding bonus/fractioned). Extracted computeForfaitWorkTargetDays + 3 tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -134,11 +134,18 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
||||
|
||||
[$periodFrom, $periodTo] = $this->resolvePeriodBounds($employee, $year, $phase);
|
||||
|
||||
// Forfait : jours à travailler sur l'exercice = jours ouvrés de la période − congés acquis.
|
||||
// Année pleine → 218 (252 − 34) ; entrée en cours d'année → prorata (ex. 168 − 13 ≈ 155).
|
||||
// Forfait : jours à travailler sur l'exercice.
|
||||
// Année pleine → cible contractuelle 218 ; les bonus week-end/férié et les jours
|
||||
// fractionnés sont des congés EN PLUS, ils ne réduisent pas la cible. Entrée en cours
|
||||
// d'année → jours ouvrés de la période − congés acquis de l'entrée (repos proratisés +
|
||||
// CP reportés), via yearSummary['acquiredDays'] (hors fractionnés/bonus). Ex. Grégory : 168 − 13 ≈ 155.
|
||||
if (LeaveRuleCode::FORFAIT_218->value === $summary->ruleCode) {
|
||||
$businessDaysInPeriod = $this->countBusinessDays($periodFrom, $periodTo, $this->buildRawPublicHolidayMap($periodFrom, $periodTo));
|
||||
$summary->forfaitWorkTargetDays = $businessDaysInPeriod - $summary->acquiredDays;
|
||||
$summary->forfaitWorkTargetDays = $this->computeForfaitWorkTargetDays(
|
||||
$businessDaysInPeriod,
|
||||
$this->isForfaitEntryYear($phase, $year),
|
||||
$yearSummary['acquiredDays'],
|
||||
);
|
||||
}
|
||||
|
||||
// Forfait-only: leaves taken from N-1 stock do NOT decrement presence days.
|
||||
@@ -890,6 +897,24 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
|
||||
return $repoDaysYear * $businessDaysPeriod / $businessDaysYear;
|
||||
}
|
||||
|
||||
/**
|
||||
* Jours à travailler d'un forfait sur l'exercice consulté.
|
||||
*
|
||||
* - Année pleine : cible contractuelle 218 (bornée aux jours ouvrés de la période si
|
||||
* celle-ci en compte moins). Les bonus week-end/férié et jours fractionnés sont des
|
||||
* congés EN PLUS et ne réduisent pas la cible.
|
||||
* - Entrée en cours d'année : jours ouvrés de la période − congés acquis de l'entrée
|
||||
* (repos proratisés + CP reportés, hors fractionnés/bonus). Ex. Grégory : 168 − 13 ≈ 155.
|
||||
*/
|
||||
private function computeForfaitWorkTargetDays(int $businessDaysInPeriod, bool $isEntryYear, float $entryAcquiredDays): float
|
||||
{
|
||||
if ($isEntryYear) {
|
||||
return $businessDaysInPeriod - $entryAcquiredDays;
|
||||
}
|
||||
|
||||
return (float) min($businessDaysInPeriod, self::FORFAIT_TARGET_WORKED_DAYS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vrai si la phase FORFAIT démarre en cours de l'année civile consultée
|
||||
* (donc avec une période partielle), faux pour une année pleine ou un démarrage le 1er janvier.
|
||||
|
||||
@@ -86,6 +86,34 @@ final class EmployeeLeaveSummaryProviderTest extends TestCase
|
||||
self::assertEqualsWithDelta(25.0 / 12.0, $result, 0.0001);
|
||||
}
|
||||
|
||||
public function testComputeForfaitWorkTargetDaysEntryYearProrates(): void
|
||||
{
|
||||
$provider = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->newInstanceWithoutConstructor();
|
||||
|
||||
// Grégory : 168 jours ouvrés sur la période − 12.94 congés acquis (repos + CP reportés) ≈ 155.06
|
||||
$result = $this->invokePrivate($provider, 'computeForfaitWorkTargetDays', 168, true, 12.94);
|
||||
|
||||
self::assertEqualsWithDelta(155.06, $result, 0.001);
|
||||
}
|
||||
|
||||
public function testComputeForfaitWorkTargetDaysFullYearIs218IgnoringBonusAndFractioned(): void
|
||||
{
|
||||
$provider = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->newInstanceWithoutConstructor();
|
||||
|
||||
// Année pleine : la cible reste 218 quelle que soit la valeur des congés acquis
|
||||
// (les bonus week-end/férié et jours fractionnés ne réduisent pas la cible).
|
||||
self::assertSame(218.0, $this->invokePrivate($provider, 'computeForfaitWorkTargetDays', 252, false, 34.0));
|
||||
self::assertSame(218.0, $this->invokePrivate($provider, 'computeForfaitWorkTargetDays', 252, false, 40.0));
|
||||
}
|
||||
|
||||
public function testComputeForfaitWorkTargetDaysFullYearCapsAtBusinessDaysWhenFewer(): void
|
||||
{
|
||||
$provider = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->newInstanceWithoutConstructor();
|
||||
|
||||
// Période avec moins de 218 jours ouvrés (phase forfait clôturée en cours d'année) → cap aux jours ouvrés.
|
||||
self::assertSame(200.0, $this->invokePrivate($provider, 'computeForfaitWorkTargetDays', 200, false, 5.0));
|
||||
}
|
||||
|
||||
public function testComputeProratedForfaitRepoDaysGregoryCase(): void
|
||||
{
|
||||
$provider = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->newInstanceWithoutConstructor();
|
||||
|
||||
Reference in New Issue
Block a user