feat(overtime-contingent) : heures supp structurelles (>35h) ajoutées au contingent
Auto Tag Develop / tag (push) Successful in 6s
Auto Tag Develop / tag (push) Successful in 6s
Les heures contractuelles au-delà de 35h (ex. 39h → 17,33h décimales = 17h20/mois) sont payées chaque mois sans transiter par les paiements RTT (référence 39h). Elles manquaient au contingent. Ajout via StructuralOvertimeContingentCalculator : (weeklyHours-35)×260 min/mois, généralisé aux contrats non-forfait/non-intérim >35h, proratisé aux jours sous contrat. Branché sur l'encart fiche et l'export PDF. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ final readonly class OvertimeContingentExportBuilder
|
||||
public function __construct(
|
||||
private EmployeeRttPaymentRepository $rttPaymentRepository,
|
||||
private OvertimePaidContingentCalculator $calculator,
|
||||
private StructuralOvertimeContingentCalculator $structuralCalculator,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -49,7 +50,13 @@ final readonly class OvertimeContingentExportBuilder
|
||||
}
|
||||
|
||||
$employeePayments = $byEmployee[$employeeId] ?? [];
|
||||
$months = $this->calculator->monthlyBaseMinutes($employeePayments, $civilYear);
|
||||
$paidMonths = $this->calculator->monthlyBaseMinutes($employeePayments, $civilYear);
|
||||
$structuralMonths = $this->structuralCalculator->monthlyStructuralMinutes($employee, $civilYear);
|
||||
|
||||
$months = [];
|
||||
for ($m = 1; $m <= 12; ++$m) {
|
||||
$months[$m] = $paidMonths[$m] + $structuralMonths[$m];
|
||||
}
|
||||
|
||||
$rows[] = new OvertimeContingentRow(
|
||||
employeeId: $employeeId,
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\WorkHours;
|
||||
|
||||
use App\Entity\Employee;
|
||||
use App\Enum\ContractType;
|
||||
use DateTimeImmutable;
|
||||
|
||||
/**
|
||||
* Heures supplémentaires « structurelles » payées chaque mois pour les contrats
|
||||
* au-dessus de 35h (hors forfait/intérim) : les (weeklyHours − 35) h/semaine
|
||||
* au-delà de la durée légale sont payées chaque mois, lissées sur l'année :
|
||||
* (weeklyHours − 35) × 52/12 h/mois = (weeklyHours − 35) × 260 min/mois.
|
||||
*
|
||||
* Ces heures ne transitent pas par les paiements RTT (la référence d'un 39h est
|
||||
* 39h, pas 35h) mais comptent dans le contingent légal d'heures supplémentaires.
|
||||
* Elles sont proratisées aux jours réellement sous contrat dans chaque mois.
|
||||
*/
|
||||
final readonly class StructuralOvertimeContingentCalculator
|
||||
{
|
||||
/** 60 min × 52 semaines / 12 mois = minutes mensuelles par heure hebdo au-delà de 35h. */
|
||||
private const int MINUTES_PER_WEEKLY_HOUR_PER_MONTH = 260;
|
||||
|
||||
/**
|
||||
* @return array<int, int> clé 1..12 -> minutes structurelles payées (proratisées)
|
||||
*/
|
||||
public function monthlyStructuralMinutes(Employee $employee, int $civilYear): array
|
||||
{
|
||||
$accumulated = array_fill(1, 12, 0.0);
|
||||
|
||||
foreach ($employee->getContractPeriods() as $period) {
|
||||
$contract = $period->getContract();
|
||||
if (null === $contract) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $contract->getType();
|
||||
if (ContractType::FORFAIT === $type || ContractType::INTERIM === $type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$weeklyHours = $contract->getWeeklyHours();
|
||||
if (null === $weeklyHours || $weeklyHours <= 35) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fullMonthlyMinutes = ($weeklyHours - 35) * self::MINUTES_PER_WEEKLY_HOUR_PER_MONTH;
|
||||
$periodStart = $period->getStartDate();
|
||||
$periodEnd = $period->getEndDate();
|
||||
|
||||
for ($month = 1; $month <= 12; ++$month) {
|
||||
$monthStart = new DateTimeImmutable(sprintf('%04d-%02d-01', $civilYear, $month));
|
||||
$monthEnd = $monthStart->modify('last day of this month');
|
||||
$daysInMonth = (int) $monthEnd->format('d');
|
||||
|
||||
$overlapStart = $periodStart > $monthStart ? $periodStart : $monthStart;
|
||||
$overlapEnd = (null !== $periodEnd && $periodEnd < $monthEnd) ? $periodEnd : $monthEnd;
|
||||
if ($overlapStart > $overlapEnd) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$overlapDays = $overlapStart->diff($overlapEnd)->days + 1;
|
||||
$accumulated[$month] += $fullMonthlyMinutes * $overlapDays / $daysInMonth;
|
||||
}
|
||||
}
|
||||
|
||||
$months = [];
|
||||
for ($month = 1; $month <= 12; ++$month) {
|
||||
$months[$month] = (int) round($accumulated[$month]);
|
||||
}
|
||||
|
||||
return $months;
|
||||
}
|
||||
|
||||
public function totalStructuralMinutes(Employee $employee, int $civilYear): int
|
||||
{
|
||||
return array_sum($this->monthlyStructuralMinutes($employee, $civilYear));
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ use App\Entity\Employee;
|
||||
use App\Repository\EmployeeRepository;
|
||||
use App\Repository\EmployeeRttPaymentRepository;
|
||||
use App\Service\WorkHours\OvertimePaidContingentCalculator;
|
||||
use App\Service\WorkHours\StructuralOvertimeContingentCalculator;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
@@ -22,6 +23,7 @@ final readonly class EmployeeOvertimeContingentProvider implements ProviderInter
|
||||
private RequestStack $requestStack,
|
||||
private EmployeeRttPaymentRepository $rttPaymentRepository,
|
||||
private OvertimePaidContingentCalculator $calculator,
|
||||
private StructuralOvertimeContingentCalculator $structuralCalculator,
|
||||
private EmployeeRepository $employeeRepository,
|
||||
) {}
|
||||
|
||||
@@ -51,9 +53,10 @@ final readonly class EmployeeOvertimeContingentProvider implements ProviderInter
|
||||
|
||||
$output = new EmployeeOvertimeContingent();
|
||||
$output->year = $year;
|
||||
$output->paidMinutes = $this->calculator->totalBaseMinutes($payments, $year);
|
||||
$output->isDriver = $employee->getIsDriver();
|
||||
$output->capHours = $this->calculator->capHours($output->isDriver);
|
||||
$output->paidMinutes = $this->calculator->totalBaseMinutes($payments, $year)
|
||||
+ $this->structuralCalculator->totalStructuralMinutes($employee, $year);
|
||||
$output->isDriver = $employee->getIsDriver();
|
||||
$output->capHours = $this->calculator->capHours($output->isDriver);
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user