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:
97
server/masseSalarialeWorkbookImport.ts
Normal file
97
server/masseSalarialeWorkbookImport.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { MasseSalarialeImportRemuneration, MasseSalarialeImportSalarie } from "./db";
|
||||
|
||||
type SpreadsheetValue = string | number | Date | null | undefined;
|
||||
|
||||
function cents(value: SpreadsheetValue): number {
|
||||
if (typeof value === "number") return Math.round(value * 100);
|
||||
if (typeof value !== "string") return 0;
|
||||
const normalized = value.replace(/[\s\u00a0]/g, "").replace(",", ".");
|
||||
const parsed = Number(normalized);
|
||||
return Number.isFinite(parsed) ? Math.round(parsed * 100) : 0;
|
||||
}
|
||||
|
||||
function bps(value: SpreadsheetValue): number | null {
|
||||
if (value === null || value === undefined || value === "") return null;
|
||||
const numeric = typeof value === "number" ? value : Number(String(value).replace(",", "."));
|
||||
return Number.isFinite(numeric) ? Math.round(numeric * 10_000) : null;
|
||||
}
|
||||
|
||||
function isoDate(value: SpreadsheetValue): string | null {
|
||||
if (value instanceof Date && !Number.isNaN(value.valueOf())) return value.toISOString().slice(0, 10);
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
const excelDate = new Date(Date.UTC(1899, 11, 30) + Math.round(value * 86_400_000));
|
||||
return Number.isNaN(excelDate.valueOf()) ? null : excelDate.toISOString().slice(0, 10);
|
||||
}
|
||||
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
|
||||
if (typeof value === "string" && /^\d{2}\/\d{2}\/\d{4}$/.test(value)) {
|
||||
const [day, month, year] = value.split("/");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function text(value: SpreadsheetValue): string {
|
||||
return typeof value === "string" || typeof value === "number" ? String(value).trim() : "";
|
||||
}
|
||||
|
||||
export type MasseSalarialeWorkbookImport = {
|
||||
salaries: MasseSalarialeImportSalarie[];
|
||||
remunerations: MasseSalarialeImportRemuneration[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Convertit les lignes de la feuille modèle (index Excel 1, en-tête sur la
|
||||
* ligne 5) sans évaluer de formules côté application : les valeurs calculées
|
||||
* et sauvegardées par le fichier fourni servent de référence importée.
|
||||
*/
|
||||
export function parseMasseSalarialeWorkbookRows(rows: SpreadsheetValue[][]): MasseSalarialeWorkbookImport {
|
||||
const salaries: MasseSalarialeImportSalarie[] = [];
|
||||
const remunerations: MasseSalarialeImportRemuneration[] = [];
|
||||
|
||||
for (const row of rows.slice(5)) {
|
||||
// SheetJS supprime la cellule A vide de certaines lignes. On repère donc
|
||||
// la présence effective de cette cellule plutôt que de supposer un index.
|
||||
const originalColumn = (excelColumn: number) => row[excelColumn + (row[0] === null || row[0] === undefined ? 0 : -1)];
|
||||
const matricule = text(originalColumn(1));
|
||||
const nom = text(originalColumn(2));
|
||||
const prenom = text(originalColumn(3));
|
||||
const poste = text(originalColumn(4));
|
||||
const dateEmbauche = isoDate(originalColumn(5));
|
||||
if (!matricule || !nom || !prenom || !poste || !dateEmbauche) continue;
|
||||
|
||||
salaries.push({ matricule, nom, prenom, poste, dateEmbauche });
|
||||
const addForecast = (annee: number, startIndex: number, tauxChargeBps: number | null, periodeReference: string) => {
|
||||
const brutHorsAstreinte = cents(originalColumn(startIndex));
|
||||
const brutAvecPrimes = cents(originalColumn(startIndex + 1));
|
||||
const primeAstreinte = cents(originalColumn(startIndex + 2));
|
||||
const brutMensuel = cents(originalColumn(startIndex + 3));
|
||||
const evolution = cents(originalColumn(startIndex + 5));
|
||||
const prime = annee === 2027 ? cents(originalColumn(startIndex + 6)) : 0;
|
||||
if (brutHorsAstreinte === 0 && brutAvecPrimes === 0 && brutMensuel === 0) return;
|
||||
|
||||
remunerations.push({
|
||||
matricule,
|
||||
annee,
|
||||
nature: "previsionnel",
|
||||
salaireAnnuelBrutHorsPrimesCents: brutHorsAstreinte,
|
||||
salaireAnnuelBrutAvecPrimesCents: brutAvecPrimes,
|
||||
salaireMensuelBrutHorsPrimesCents: brutMensuel,
|
||||
tauxChargeBps,
|
||||
primeAstreinteCents: primeAstreinte,
|
||||
primeCents: prime,
|
||||
evolutionCents: evolution,
|
||||
primeReferenceCents: prime,
|
||||
evolutionReferenceCents: evolution,
|
||||
periodeReference,
|
||||
statut: annee === 2025 ? "annuel" : "previsionnel",
|
||||
source: "fichier_modele_2026-08-28",
|
||||
});
|
||||
};
|
||||
|
||||
addForecast(2025, 6, bps(originalColumn(10)), "2025-12");
|
||||
addForecast(2026, 12, bps(originalColumn(16)), "2026-08");
|
||||
addForecast(2027, 18, null, "2027-01");
|
||||
}
|
||||
|
||||
return { salaries, remunerations };
|
||||
}
|
||||
Reference in New Issue
Block a user