Compare commits

...

8 Commits

Author SHA1 Message Date
gitea-actions
a1110069b5 chore: bump version to v0.1.60
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
Build Release Artefact / build (push) Successful in 1m21s
2026-03-20 10:53:51 +00:00
4901c58ebf Merge remote-tracking branch 'origin/develop' into develop
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
2026-03-20 11:53:34 +01:00
4de891579c feat : ajout des congés bonus pour les forfaits si ils travaillent un weekend ou férié 2026-03-20 11:53:24 +01:00
gitea-actions
a17d6a67cf chore: bump version to v0.1.59
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
Build Release Artefact / build (push) Successful in 1m59s
2026-03-20 10:26:29 +00:00
29db3b5025 fix : calcule des RTT sur les contrats 4h
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
2026-03-20 11:26:17 +01:00
gitea-actions
6df9110187 chore: bump version to v0.1.58
All checks were successful
Auto Tag Develop / tag (push) Successful in 5s
Build Release Artefact / build (push) Successful in 1m39s
2026-03-20 07:13:49 +00:00
f0dfb30566 Merge remote-tracking branch 'origin/develop' into develop
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
2026-03-20 08:13:34 +01:00
049e64288e fix : calcule des RTT 2026-03-20 08:13:20 +01:00
7 changed files with 115 additions and 15 deletions

View File

@@ -43,8 +43,10 @@
## Overtime Rules ## Overtime Rules
- Contracts <= 35h: +25% from 35h to 43h, +50% beyond - Contracts <= 35h: +25% from 35h to 43h, +50% beyond
- Contracts >= 39h: +25% from 39h to 43h, +50% beyond - Contracts >= 39h: +25% from 39h to 43h, +50% beyond
- CUSTOM contracts (weeklyHours ≠ 35 and ≠ 39, not INTERIM/FORFAIT): reference = actual contractual hours, no 25%/50% bonuses (1h overtime = 1h recovery), deficit doesn't impact balance
- INTERIM: no overtime bonuses, no recovery time - INTERIM: no overtime bonuses, no recovery time
- Driver contracts: no overtime calculation - Driver contracts: no overtime calculation
- FORFAIT weekend/holiday bonus: each weekend or public holiday day worked gives bonus leave (full day if morning+afternoon, 0.5 if only one). Added to acquired days, no cap. PRESENCE mode only.
## Frais (MileageAllowance) ## Frais (MileageAllowance)
- Onglet "Frais" (anciennement "Frais Kms") sur la fiche employé - Onglet "Frais" (anciennement "Frais Kms") sur la fiche employé

View File

@@ -1,2 +1,2 @@
parameters: parameters:
app.version: '0.1.57' app.version: '0.1.60'

View File

@@ -122,6 +122,10 @@ Documents complementaires:
- Semaine en déficit (heures travaillées < heures contrat): - Semaine en déficit (heures travaillées < heures contrat):
- le déficit est déduit du cumul RTT : d'abord des heures à 50%, puis des heures à 25% - le déficit est déduit du cumul RTT : d'abord des heures à 50%, puis des heures à 25%
- si aucun solde 50% ni 25%, les heures à 25% deviennent négatives - si aucun solde 50% ni 25%, les heures à 25% deviennent négatives
- Contrats CUSTOM (heures hebdo ≠ 35h et ≠ 39h, hors INTERIM/FORFAIT):
- référence heures sup = heures contractuelles réelles (ex: 4h → référence 4h)
- pas de bonus 25% ni 50% : 1 heure sup = 1 heure de récupération
- le déficit (travail < contrat) ne génère pas de récup mais n'impacte pas le solde
- Nature `INTERIM`: - Nature `INTERIM`:
- pas de bonus 25% - pas de bonus 25%
- pas de bonus 50% - pas de bonus 50%
@@ -227,6 +231,7 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer.
- en cas de suspension en cours de mois, l'acquisition est proratisée en jours ouvrés (lun-ven hors fériés) travaillés / 22 - en cas de suspension en cours de mois, l'acquisition est proratisée en jours ouvrés (lun-ven hors fériés) travaillés / 22
- contrat `FORFAIT`: - contrat `FORFAIT`:
- base annuelle: `jours ouvrés de l'exercice (lundi-vendredi, hors jours fériés métropole) - 218` - base annuelle: `jours ouvrés de l'exercice (lundi-vendredi, hors jours fériés métropole) - 218`
- bonus weekend/férié: chaque jour travaillé un weekend ou jour férié donne 1 jour de congé supplémentaire (journée ≥ 5h = 1.0 jour, demi-journée > 0h et < 5h = 0.5 jour), sans plafond
- prorata: en cas de démarrage/fin de contrat en cours d'année civile, le calcul ne couvre que l'intervalle actif du contrat dans l'année - prorata: en cas de démarrage/fin de contrat en cours d'année civile, le calcul ne couvre que l'intervalle actif du contrat dans l'année
- reste à prendre: `acquis - absences` (toutes absences, demi-journées incluses) - reste à prendre: `acquis - absences` (toutes absences, demi-journées incluses)
- pas de samedi (`0`) - pas de samedi (`0`)

View File

@@ -191,6 +191,57 @@ final class WorkHourRepository extends ServiceEntityRepository implements WorkHo
return $result; return $result;
} }
/**
* Count weekend and public holiday worked days for forfait bonus leave (PRESENCE mode only).
* Morning + afternoon = 1.0 day, one only = 0.5 day.
*
* @param list<string> $publicHolidayDates Y-m-d formatted weekday public holiday dates
*/
public function countWeekendAndHolidayWorkedDays(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to, array $publicHolidayDates = []): float
{
$targetDates = [];
// Collect weekend dates in range
for ($cursor = $from; $cursor <= $to; $cursor = $cursor->modify('+1 day')) {
if ((int) $cursor->format('N') >= 6) {
$targetDates[] = $cursor;
}
}
// Add weekday public holidays
foreach ($publicHolidayDates as $date) {
$targetDates[] = new DateTimeImmutable($date);
}
if ([] === $targetDates) {
return 0.0;
}
$dateStrings = array_map(static fn (DateTimeImmutable $d): string => $d->format('Y-m-d'), $targetDates);
/** @var list<WorkHour> $rows */
$rows = $this->createQueryBuilder('w')
->andWhere('w.employee = :employee')
->andWhere('w.workDate IN (:dates)')
->andWhere('w.isPresentMorning = true OR w.isPresentAfternoon = true')
->setParameter('employee', $employee)
->setParameter('dates', $dateStrings)
->getQuery()
->getResult()
;
$total = 0.0;
foreach ($rows as $row) {
if ($row->isPresentMorning() && $row->isPresentAfternoon()) {
$total += 1.0;
} else {
$total += 0.5;
}
}
return $total;
}
/** /**
* Return the set of Y-m-d dates where the employee has worked hours on the given dates. * Return the set of Y-m-d dates where the employee has worked hours on the given dates.
* *

View File

@@ -206,20 +206,36 @@ final readonly class RttRecoveryComputationService
continue; continue;
} }
$weekAnchorNature = $naturesByDate[$employeeId][$weekDays[0]] ?? ContractNature::CDI; $weekAnchorNature = $naturesByDate[$employeeId][$weekDays[0]] ?? ContractNature::CDI;
$weekAnchorContract = $employeeContractsByDate[$weekDays[0]] ?? null; $weekAnchorContract = $employeeContractsByDate[$weekDays[0]] ?? null;
$isWeekPresenceTracking = TrackingMode::PRESENCE->value === $weekAnchorContract?->getTrackingMode(); $isWeekPresenceTracking = TrackingMode::PRESENCE->value === $weekAnchorContract?->getTrackingMode();
$disableOvertimeBonuses = $this->hasDisabledOvertimeBonuses($weekAnchorContract, $weekAnchorNature); $disableOvertimeBonuses = $this->hasDisabledOvertimeBonuses($weekAnchorContract, $weekAnchorNature);
$overtimeReferenceMinutes = $this->computeWeeklyOvertimeReferenceMinutes($weekDays, $employeeContractsByDate); $weekContractType = ContractType::resolve(
$weekAnchorContract?->getName(),
$weekAnchorContract?->getTrackingMode(),
$weekAnchorContract?->getWeeklyHours()
);
$isCustomContract = ContractType::CUSTOM === $weekContractType;
$overtimeReferenceMinutes = $isCustomContract
? $this->computeWeeklyCustomReferenceMinutes($weekDays, $employeeContractsByDate)
: $this->computeWeeklyOvertimeReferenceMinutes($weekDays, $employeeContractsByDate);
$overtime25StartMinutes = $this->computeWeeklyOvertime25StartMinutes($weekDays, $employeeContractsByDate); $overtime25StartMinutes = $this->computeWeeklyOvertime25StartMinutes($weekDays, $employeeContractsByDate);
$weeklyOvertimeTotalMinutes = $isWeekPresenceTracking $weeklyOvertimeTotalMinutes = $isWeekPresenceTracking
? 0 ? 0
: $weeklyTotalMinutes - $overtimeReferenceMinutes; : $weeklyTotalMinutes - $overtimeReferenceMinutes;
$base25 = ($isWeekPresenceTracking || $disableOvertimeBonuses) ? 0 : max(0, min($weeklyTotalMinutes, 43 * 60) - $overtime25StartMinutes); $base25 = ($isWeekPresenceTracking || $disableOvertimeBonuses || $isCustomContract) ? 0 : max(0, min($weeklyTotalMinutes, 43 * 60) - $overtime25StartMinutes);
$bonus25 = ($isWeekPresenceTracking || $disableOvertimeBonuses) ? 0 : (int) round($base25 * 0.25); $bonus25 = ($isWeekPresenceTracking || $disableOvertimeBonuses || $isCustomContract) ? 0 : (int) round($base25 * 0.25);
$base50 = ($isWeekPresenceTracking || $disableOvertimeBonuses) ? 0 : max(0, $weeklyTotalMinutes - 43 * 60); $base50 = ($isWeekPresenceTracking || $disableOvertimeBonuses || $isCustomContract) ? 0 : max(0, $weeklyTotalMinutes - 43 * 60);
$bonus50 = ($isWeekPresenceTracking || $disableOvertimeBonuses) ? 0 : (int) round($base50 * 0.5); $bonus50 = ($isWeekPresenceTracking || $disableOvertimeBonuses || $isCustomContract) ? 0 : (int) round($base50 * 0.5);
if ($isWeekPresenceTracking || $disableOvertimeBonuses) {
$totalMinutes = 0;
} elseif ($isCustomContract) {
$totalMinutes = max(0, $weeklyOvertimeTotalMinutes);
} else {
$totalMinutes = $weeklyOvertimeTotalMinutes + $bonus25 + $bonus50;
}
$results[$weekKey] = new WeekRecoveryDetail( $results[$weekKey] = new WeekRecoveryDetail(
overtimeMinutes: $weeklyOvertimeTotalMinutes, overtimeMinutes: $weeklyOvertimeTotalMinutes,
@@ -227,9 +243,7 @@ final readonly class RttRecoveryComputationService
bonus25Minutes: $bonus25, bonus25Minutes: $bonus25,
base50Minutes: $base50, base50Minutes: $base50,
bonus50Minutes: $bonus50, bonus50Minutes: $bonus50,
totalMinutes: ($isWeekPresenceTracking || $disableOvertimeBonuses) totalMinutes: $totalMinutes,
? 0
: $weeklyOvertimeTotalMinutes + $bonus25 + $bonus50,
); );
} }
@@ -326,6 +340,23 @@ final readonly class RttRecoveryComputationService
return max(0, $end - $start); return max(0, $end - $start);
} }
/**
* @param list<string> $days
* @param array<string, ?Contract> $contractsByDate
*/
private function computeWeeklyCustomReferenceMinutes(array $days, array $contractsByDate): int
{
$total = 0;
foreach ($days as $date) {
$isoDay = (int) new DateTimeImmutable($date)->format('N');
$contract = $contractsByDate[$date] ?? null;
$hours = $contract?->getWeeklyHours();
$total += $this->resolveDailyReferenceMinutes($hours, $isoDay);
}
return $total;
}
/** /**
* @param list<string> $days * @param list<string> $days
* @param array<string, ?Contract> $contractsByDate * @param array<string, ?Contract> $contractsByDate

View File

@@ -535,10 +535,21 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
$type = $employee->getContract()?->getType(); $type = $employee->getContract()?->getType();
if (ContractType::FORFAIT === $type) { if (ContractType::FORFAIT === $type) {
$businessDaysInPeriod = $this->countBusinessDays($from, $to); $businessDaysInPeriod = $this->countBusinessDays($from, $to);
$publicHolidays = $this->buildPublicHolidayMap($from, $to);
$weekdayHolidays = array_filter(
array_keys($publicHolidays),
static fn (string $date): bool => (int) new DateTimeImmutable($date)->format('N') <= 5
);
$bonusDays = $this->workHourRepository->countWeekendAndHolidayWorkedDays(
$employee,
$from,
$to,
array_values($weekdayHolidays)
);
return [ return [
'ruleCode' => LeaveRuleCode::FORFAIT_218->value, 'ruleCode' => LeaveRuleCode::FORFAIT_218->value,
'acquiredDays' => (float) max(0, $businessDaysInPeriod - self::FORFAIT_TARGET_WORKED_DAYS), 'acquiredDays' => (float) max(0, $businessDaysInPeriod - self::FORFAIT_TARGET_WORKED_DAYS) + $bonusDays,
'acquiredSaturdays' => 0.0, 'acquiredSaturdays' => 0.0,
'accrualPerMonth' => 0.0, 'accrualPerMonth' => 0.0,
'saturdayAccrualPerMonth' => 0.0, 'saturdayAccrualPerMonth' => 0.0,

View File

@@ -267,6 +267,6 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
} }
} }
return $today; return $weekEnd;
} }
} }