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,68 @@
"""Analyse la structure du classeur de masse salariale sans afficher d'identité ni de montant."""
from collections import Counter
from pathlib import Path
import re
from openpyxl import load_workbook
workbook_path = Path("/home/ubuntu/upload/TableaurecapMassesalarialeSANTINOVA-28.08.26.xlsx")
workbook = load_workbook(workbook_path, data_only=False)
values_workbook = load_workbook(workbook_path, data_only=True)
for sheet in workbook.worksheets:
print(f"sheet={sheet.title}|rows={sheet.max_row}|cols={sheet.max_column}")
for row_index in range(1, min(sheet.max_row, 8) + 1):
values = []
for column_index in range(1, sheet.max_column + 1):
value = sheet.cell(row_index, column_index).value
if isinstance(value, str) and value.startswith("="):
value = f"FORMULA:{value}"
if value is not None:
values.append(f"C{column_index}={value}")
if values:
print(f"row={row_index}|{' ; '.join(values)}")
formula_count = sum(
1
for row in sheet.iter_rows()
for cell in row
if isinstance(cell.value, str) and cell.value.startswith("=")
)
print(f"formula_count={formula_count}")
salary_sheet = workbook["Masse salariale"]
data_rows = [row for row in range(6, salary_sheet.max_row + 1) if salary_sheet.cell(row, 2).value]
print(f"salaries_with_identifier={len(data_rows)}")
for column, label in {
8: "2025_brut_avec_primes",
9: "2025_astreintes",
11: "2025_taux_charges",
12: "2025_evolution",
15: "2026_astreintes",
17: "2026_taux_charges",
18: "2026_evolution",
21: "2027_astreintes",
23: "2027_evolution",
24: "2027_prime",
}.items():
populated = sum(salary_sheet.cell(row, column).value is not None for row in data_rows)
formulas = sum(
isinstance(salary_sheet.cell(row, column).value, str)
and salary_sheet.cell(row, column).value.startswith("=")
for row in data_rows
)
print(f"field={label}|populated={populated}|formulas={formulas}")
for column, label in {7: "2025_hors_astreinte", 14: "2026_avec_primes", 20: "2027_avec_primes"}.items():
formulas = Counter()
for row in data_rows:
value = salary_sheet.cell(row, column).value
if isinstance(value, str) and value.startswith("="):
formulas[re.sub(r"\d+", "#", value)] += 1
for formula, count in formulas.items():
print(f"formula_pattern={label}|count={count}|formula={formula}")
values_sheet = values_workbook["Masse salariale"]
for column, label in {7: "2025_hors_astreinte", 10: "2025_mensuel", 13: "2026_hors_astreinte", 14: "2026_avec_primes", 16: "2026_mensuel", 19: "2027_hors_astreinte", 20: "2027_avec_primes", 22: "2027_mensuel"}.items():
calculated = sum(values_sheet.cell(row, column).value is not None for row in data_rows)
print(f"cached_value={label}|populated={calculated}")

View File

@@ -0,0 +1,20 @@
import XLSX from "xlsx";
const workbook = XLSX.readFile("/home/ubuntu/upload/TableaurecapMassesalarialeSANTINOVA-28.08.26.xlsx", { cellDates: true });
const sheet = workbook.Sheets["Masse salariale"];
if (!sheet) throw new Error("Feuille Masse salariale introuvable");
const rows = XLSX.utils.sheet_to_json<Array<unknown>>(sheet, { header: 1, raw: true, defval: null });
const candidates = rows.slice(5).filter((row) => row[1] !== null && row[1] !== undefined && row[1] !== "");
const typeCount = (index: number) => candidates.reduce<Record<string, number>>((counts, row) => {
const kind = row[index] === null ? "null" : row[index] instanceof Date ? "date" : typeof row[index];
counts[kind] = (counts[kind] ?? 0) + 1;
return counts;
}, {});
console.log(JSON.stringify({
candidates: candidates.length,
matriculeTypes: typeCount(1),
nomTypes: typeCount(2),
prenomTypes: typeCount(3),
posteTypes: typeCount(4),
dateEmbaucheTypes: typeCount(5),
}));

View File

@@ -0,0 +1,24 @@
import XLSX from "xlsx";
import { importMasseSalariale } from "../server/db";
import { parseMasseSalarialeWorkbookRows } from "../server/masseSalarialeWorkbookImport";
const workbookPath = "/home/ubuntu/upload/TableaurecapMassesalarialeSANTINOVA-28.08.26.xlsx";
const workbook = XLSX.readFile(workbookPath, { cellDates: true });
const sheet = workbook.Sheets["Masse salariale"];
if (!sheet) throw new Error("Feuille Masse salariale introuvable");
const rows = XLSX.utils.sheet_to_json<Array<string | number | Date | null>>(sheet, {
header: 1,
raw: true,
defval: null,
});
const parsed = parseMasseSalarialeWorkbookRows(rows);
const result = await importMasseSalariale(parsed.salaries, parsed.remunerations, {
replaceForecastYears: [2025, 2026, 2027],
});
const byYear = parsed.remunerations.reduce<Record<number, number>>((counts, row) => {
counts[row.annee] = (counts[row.annee] ?? 0) + 1;
return counts;
}, {});
console.log(JSON.stringify({ salaries: result.salaries, remunerationsByYear: byYear }));