Checkpoint: Ajout de la projection annuelle prévisionnel-réel dans Masse salariale : import contrôlé du fichier modèle 2025-2027, réel 2026 provenant uniquement des liasses Salaires, ajustements administrateur limités aux primes et évolutions de l'année courante/future, conservation des centimes, modèle de calcul testé et documentation. 72 tests, TypeScript et build validés.

This commit is contained in:
Manus
2026-08-31 10:58:02 +00:00
parent 5c9618b204
commit 28570234d0
17 changed files with 2415 additions and 204 deletions

View File

@@ -0,0 +1,112 @@
/**
* Calculs purs de la fenêtre Masse salariale. Les montants sont conservés en
* centimes afin que les projections et cumuls ne subissent aucun arrondi
* intermédiaire. Les réels ne sont jamais extrapolés : ils cumulent seulement
* les liasses mensuelles effectivement archivées.
*/
export type AnnualForecastInput = {
annee: number;
brutHorsAstreinteReferenceCents: number;
brutAvecPrimesReferenceCents: number;
primeAstreinteCents: number;
primeCents: number;
primeReferenceCents: number;
evolutionCents: number;
evolutionReferenceCents: number;
tauxChargeBps: number | null;
};
export type AnnualProjection = {
brutHorsAstreinteCents: number;
brutAvecPrimesCents: number;
brutMensuelHorsAstreinteCents: number;
primeAstreinteCents: number;
primeComplementaireCents: number;
evolutionCents: number;
tauxChargeBps: number | null;
};
export type PayrollMonthlyInput = {
annee: number;
mois: number;
matricule: string;
brutMensuelCents: number;
brutAvecPrimesCents: number;
primeAstreinteCents: number;
};
export type PayrollAnnualActual = Omit<AnnualProjection, "evolutionCents" | "tauxChargeBps"> & {
moisDisponibles: number[];
};
/**
* Restitue exactement la référence importée, puis applique seulement les
* écarts manuels des primes et évolutions. Ainsi, un import Excel conserve ses
* calculs initiaux tandis que toute correction autorisée est automatiquement
* répercutée sur le brut prévisionnel dérivé.
*/
export function buildAnnualForecasts(inputs: AnnualForecastInput[]): Map<number, AnnualProjection> {
const forecasts = new Map<number, AnnualProjection>();
for (const current of [...inputs].sort((a, b) => a.annee - b.annee)) {
const adjustmentDelta =
(current.primeCents - current.primeReferenceCents)
+ (current.evolutionCents - current.evolutionReferenceCents);
const brutHorsAstreinteCents = current.brutHorsAstreinteReferenceCents + adjustmentDelta;
const brutAvecPrimesCents = current.brutAvecPrimesReferenceCents + adjustmentDelta;
forecasts.set(current.annee, {
brutHorsAstreinteCents,
brutAvecPrimesCents,
brutMensuelHorsAstreinteCents: Math.round(brutHorsAstreinteCents / 12),
primeAstreinteCents: current.primeAstreinteCents,
primeComplementaireCents: current.primeCents,
evolutionCents: current.evolutionCents,
tauxChargeBps: current.tauxChargeBps,
});
}
return forecasts;
}
/**
* Agrège les bulletins archivés par salarié et année. Le brut hors astreinte
* ne retire que les primes d'astreinte : les autres éléments compris dans le
* brut (activité, responsabilité, vacances…) sont donc préservés.
*/
export function buildAnnualPayrollActuals(inputs: PayrollMonthlyInput[]): Map<string, Map<number, PayrollAnnualActual>> {
const grouped = new Map<string, Map<number, PayrollMonthlyInput[]>>();
for (const input of inputs) {
const byYear = grouped.get(input.matricule) ?? new Map<number, PayrollMonthlyInput[]>();
byYear.set(input.annee, [...(byYear.get(input.annee) ?? []), input]);
grouped.set(input.matricule, byYear);
}
const actuals = new Map<string, Map<number, PayrollAnnualActual>>();
for (const [matricule, byYear] of Array.from(grouped.entries())) {
const projections = new Map<number, PayrollAnnualActual>();
for (const [annee, monthlyRows] of Array.from(byYear.entries())) {
const brutAvecPrimesCents = monthlyRows.reduce((sum, row) => sum + row.brutAvecPrimesCents, 0);
const primeAstreinteCents = monthlyRows.reduce((sum, row) => sum + row.primeAstreinteCents, 0);
const brutHorsAstreinteCents = brutAvecPrimesCents - primeAstreinteCents;
const primeComplementaireCents = monthlyRows.reduce(
(sum, row) => sum + (row.brutAvecPrimesCents - row.brutMensuelCents - row.primeAstreinteCents),
0,
);
const moisDisponibles = Array.from(new Set<number>(monthlyRows.map((row) => row.mois))).sort((a, b) => a - b);
projections.set(annee, {
brutHorsAstreinteCents,
brutAvecPrimesCents,
brutMensuelHorsAstreinteCents: Math.round(brutHorsAstreinteCents / moisDisponibles.length),
primeAstreinteCents,
primeComplementaireCents,
moisDisponibles,
});
}
actuals.set(matricule, projections);
}
return actuals;
}
/** Les ajustements de prévision sont bloqués sur les exercices clôturés. */
export function isForecastAdjustmentEditable(annee: number, currentYear: number): boolean {
return annee >= currentYear;
}