Charger les codes Silae du dossier

Le point d'arrêt de PLAN.md §0 est levé : les codes viennent du paramétrage
Silae du compte, relevé le 11 août 2026, et non d'une documentation publique —
il n'en existe pas. Silae impose que le fichier d'import soit le miroir du
dossier, si bien qu'un code n'a de sens que pour le client qui le porte.

Ils vivent donc dans le seed, jamais dans src/domain/payroll. Écrire HS-HS50
dans un calcul rendrait l'outil inutilisable au deuxième dossier et
incorrigible sans livraison le jour où le cabinet renumérote.

Trois éléments restés ouverts à la conception sont résolus : heures
supplémentaires à 50 % (HS-HS50), complémentaires à 10 % (HS-HC10) et à 25 %
(HS-HC25). Les cinq types d'absence semés reçoivent le leur.

Semés confirmés, contrairement aux propositions déduites d'un libellé : ceux-ci
sont relevés dans la configuration active, pas devinés.

Deux manques sont signalés au lieu d'être comblés. Le forfait jours n'a aucune
rubrique au dossier — l'export échouera au premier cadre autonome, ce qui vaut
mieux qu'un code inventé. Et quatre rubriques du dossier portent un préfixe
« AB- » sans numéro (évènement familial, repos compensateur de nuit, repos
compensateur d'habillement, visite médicale) : reprendre un code tronqué ferait
échouer l'import, ou pire, le ferait réussir en imputant à la mauvaise rubrique.

Le catalogue complet du dossier est conservé, y compris les rubriques hors
périmètre actuel. Le jour où les heures de nuit ou les paniers repas entrent au
calcul, le code est déjà là et n'aura pas à être redemandé au cabinet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5 committed 2026-08-11 12:53:42 +02:00
1 parent 8c12b08961
commit f0ef6f12a1
2 files changed
+190 -13

No files matched your search

+147
View File
@@ -0,0 +1,147 @@
/**
* Correspondances Silae du dossier FROUARD DISTRIBUTION.
*
* **Données de dossier, pas de produit.** Ces codes appartiennent au
* paramétrage Silae de ce client et n'ont aucune valeur pour un autre : deux
* clients du même cabinet ne partagent pas leurs rubriques. Ils vivent donc
* dans le seed, jamais dans `src/domain/payroll` — écrire `HS-HS50` dans un
* calcul rendrait l'outil inutilisable au deuxième dossier, et incorrigible
* sans livraison le jour où le cabinet renumérote.
*
* **Source** : configuration de l'intégration Silae du compte, relevée le
* 11 août 2026. Ce ne sont pas des codes déduits d'une documentation publique —
* il n'en existe pas : Silae impose que le fichier d'import soit le miroir du
* paramétrage du dossier.
*
* `À VALIDER` — quatre rubriques du dossier portent un préfixe `AB-` **sans
* numéro** (voir `INCOMPLETE_IN_DOSSIER`). Elles ne sont pas reprises ici :
* importer un code tronqué ferait échouer l'import de paie, ou pire, le ferait
* réussir en imputant à la mauvaise rubrique.
*/
/** Rubriques laissées vides ou incomplètes dans le paramétrage du dossier. */
export const INCOMPLETE_IN_DOSSIER = [
{ label: 'Évènement familial', observed: 'AB-' },
{ label: 'Repos compensateur de nuit', observed: 'AB-' },
{ label: "Repos compensateur d'habillement", observed: 'AB-' },
{ label: 'Visite médicale', observed: 'AB-' },
] as const;
/**
* Code d'absence par type PlanFlow.
*
* La valeur est la **partie après `AB-`**, comme le stocke
* `AbsenceType.silaeCode` : le préfixe est ajouté à la sérialisation.
*/
export const ABSENCE_CODES: Record<string, string> = {
// Congés payés → AB-300
CP: '300',
// RTT → AB-310
RTT: '310',
// Arrêt maladie - Initial → AB-100. L'extension n'est pas paramétrée au
// dossier : un arrêt prolongé se saisit aujourd'hui à la main côté paie.
MAL: '100',
// Congé sans solde → AB-632
SS: '632',
// Repos compensateur de remplacement → AB-335
RC: '335',
};
/**
* Code Silae par élément de paie calculé.
*
* Les trois codes qui manquaient à la conception — 50 %, complémentaires à
* 10 % et à 25 % — sont désormais connus. `FORFAIT_DAYS` reste absent : aucune
* rubrique de forfait jours n'existe au paramétrage, et l'inventer produirait
* un export refusé au premier cadre autonome.
*/
export const ELEMENT_CODES: Record<string, string> = {
WORKED_DAYS: 'Nombre total de jours travailles',
WORKED_HOURS: 'Heures travaillees',
MISSING_HOURS: 'Heures manquantes au contrat',
SUNDAY_HOURS: 'EV-HDimanche',
HOLIDAY_HOURS: 'EV-HFerie',
OVERTIME_25: 'HS-HS25',
OVERTIME_50: 'HS-HS50',
COMPLEMENTARY_10: 'HS-HC10',
COMPLEMENTARY_25: 'HS-HC25',
};
/**
* Catalogue complet relevé au dossier.
*
* Conservé en entier — y compris les rubriques que PlanFlow ne calcule pas
* encore — parce qu'il documente ce que le dossier accepte. Le jour où les
* heures de nuit ou les paniers repas entrent au périmètre, le code est déjà
* là et n'aura pas à être redemandé au cabinet.
*
* Les taux de nuit sont recopiés **tels quels**. Leur numérotation surprend
* (`EV-HNuit24-2` pour 10 %, `EV-HNuit2-4` pour 20 %) : c'est le paramétrage du
* dossier, et le « corriger » ici imputerait la majoration à la mauvaise
* rubrique.
*/
export const DOSSIER_CATALOGUE: Array<{ label: string; code: string }> = [
{ label: 'Absence formation rémunérée', code: 'AB-410' },
{ label: 'Absence injustifiée - Non payé', code: 'AB-630' },
{ label: 'Absence injustifiée - Payé', code: 'AB-631' },
{ label: 'Absence justifiée - Non payé', code: 'AB-630' },
{ label: 'Absence justifiée - Payé', code: 'AB-631' },
{ label: 'Accident du travail - Initial', code: 'AB-120' },
{ label: 'Arrêt maladie - Initial', code: 'AB-100' },
{ label: 'Chômage partiel', code: 'AB-609' },
{ label: 'Congé maternité', code: 'AB-200' },
{ label: 'Congé parental', code: 'AB-230' },
{ label: 'Congé paternité', code: 'AB-210' },
{ label: 'Congé sans solde', code: 'AB-632' },
{ label: 'Congés payés', code: 'AB-300' },
{ label: 'Jours fériés chômés (heures)', code: 'AB-301' },
{ label: 'Maladie professionnelle - Initial', code: 'AB-110' },
{ label: 'Mise à pied conservatoire', code: 'AB-641' },
{ label: 'Mise à pied disciplinaire', code: 'AB-640' },
{ label: 'Repos compensateur de remplacement', code: 'AB-335' },
{ label: 'Retard', code: 'AB-630' },
{ label: 'RTT', code: 'AB-310' },
{ label: 'Heures complémentaires (5 %)', code: 'HS-HC5' },
{ label: 'Heures complémentaires (10 %)', code: 'HS-HC10' },
{ label: 'Heures complémentaires (25 %)', code: 'HS-HC25' },
{ label: 'Heures supplémentaires (0 %)', code: 'HS-HS00' },
{ label: 'Heures supplémentaires (10 %)', code: 'HS-HS10' },
{ label: 'Heures supplémentaires (15 %)', code: 'HS-HS15' },
{ label: 'Heures supplémentaires (20 %)', code: 'HS-HS20' },
{ label: 'Heures supplémentaires (25 %)', code: 'HS-HS25' },
{ label: 'Heures supplémentaires (50 %)', code: 'HS-HS50' },
{ label: 'Heures supplémentaires (100 %)', code: 'HS-HS100' },
{ label: 'Heures travaillées majorées (dimanche) 10 %', code: 'HS-HS10-D' },
{ label: 'Heures travaillées majorées (dimanche) 15 %', code: 'HS-HS15-D' },
{ label: 'Heures travaillées majorées (dimanche) 30 %', code: 'HS-HS30-D' },
{ label: 'Heures travaillées majorées (dimanche) 33 %', code: 'HS-HS33-D' },
{ label: 'Heures travaillées majorées (dimanche) 50 %', code: 'HS-HS50-D' },
{ label: 'Heures travaillées majorées (dimanche) 100 %', code: 'EV-HDimanche' },
{ label: 'Heures travaillées majorées (1er mai)', code: 'C01' },
{ label: 'Heures travaillées majorées (jours fériés)', code: 'EV-HFerie' },
{ label: 'Coupures', code: 'EV-nb-coupure' },
{ label: 'Heures de nuit (0 %)', code: 'EV-HNuit-0' },
{ label: 'Heures de nuit (5 %)', code: 'EV-HNuit2-5' },
{ label: 'Heures de nuit (10 %)', code: 'EV-HNuit24-2' },
{ label: 'Heures de nuit (20 %)', code: 'EV-HNuit2-4' },
{ label: 'Heures de nuit (25 %)', code: 'EV-HNuit2-6' },
{ label: 'Heures de nuit (30 %)', code: 'EV-HNuit2-3' },
{ label: 'Heures de nuit (50 %)', code: 'EV-HNuit' },
{ label: 'Repas : avantage en nature', code: 'EV-A01' },
{ label: 'Repas : indemnité repas', code: 'EV-A02' },
{ label: 'Repas : prime de panier', code: 'EV-PrimePanier' },
{ label: 'Repas : titre restaurant', code: 'EV-TitreRestaurant' },
{ label: 'Entrée / Sortie', code: 'Entree / Sortie' },
{ label: 'Heures manquantes au contrat', code: 'Heures manquantes au contrat' },
{ label: 'Heures normales', code: 'Heures normales' },
{ label: 'Heures travaillées', code: 'Heures travaillees' },
{ label: 'Heures travaillées (extra)', code: 'Heures travaillees (extra)' },
{ label: 'Nombre total de jours travaillés', code: 'Nombre total de jours travailles' },
{ label: 'Note transport', code: 'Note transport' },
{ label: 'Nouveau solde RCR', code: 'Nouveau solde RCR' },
{ label: 'RCR à payer', code: 'RCR a payer' },
{ label: 'Solde de modulation', code: 'Solde de modulation' },
];
+43 -13
View File
@@ -22,6 +22,11 @@ import {
} from '../src/domain/compliance/idcc1517';
import { frenchHolidays } from '../src/domain/absences/holidays';
import { PAYROLL_ELEMENT_DEFINITIONS } from '../src/domain/payroll/elements';
import {
ABSENCE_CODES,
ELEMENT_CODES,
INCOMPLETE_IN_DOSSIER,
} from './seed-data/silae-frouard';
import { evaluateSchedule } from '../src/server/compliance/evaluate';
import { withTenant } from '../src/server/tenant';
@@ -586,9 +591,10 @@ async function main() {
console.log('→ Types d’absence');
// `silaeCode` reste **null** : les codes AB-* du dossier sont connus, leur
// signification ne l'est pas. Les associer ici reviendrait à deviner à quel
// type d'absence correspond AB-300, ce qui produirait une paie fausse.
// `silaeCode` vient du paramétrage Silae du dossier, relevé le 11 août 2026
// (voir `seed-data/silae-frouard.ts`). Il n'a rien d'universel : la
// correspondance entre AB-300 et « congés payés » appartient à ce dossier, et
// un autre client du même cabinet peut la numéroter autrement.
const absenceTypes = [
{ code: 'CP', name: 'Congés payés', colorKey: 'cp', isPaid: true, social: false, notice: 30 },
{ code: 'RTT', name: 'RTT', colorKey: 'rtt', isPaid: true, social: false, notice: 7 },
@@ -600,12 +606,17 @@ async function main() {
for (const type of absenceTypes) {
await prisma.absenceType.upsert({
where: { accountId_code: { accountId: account.id, code: type.code } },
update: { name: type.name, colorKey: type.colorKey },
update: {
name: type.name,
colorKey: type.colorKey,
silaeCode: ABSENCE_CODES[type.code] ?? null,
},
create: {
accountId: account.id,
code: type.code,
name: type.name,
colorKey: type.colorKey,
silaeCode: ABSENCE_CODES[type.code] ?? null,
isPaid: type.isPaid,
countsAsWorkTime: false,
affectsPaidLeaveAccrual: !type.social,
@@ -709,12 +720,18 @@ async function main() {
console.log(` ${demoAbsences.length} absences`);
console.log('→ Correspondances Silae');
// Semées **non confirmées**, y compris quand le code se lit dans son libellé
// (`EV-HDimanche`). Proposer n'est pas confirmer : seul le gestionnaire de
// paie sait si le code est le bon dans ce dossier, et l'export refuse de
// tourner tant qu'il ne l'a pas dit.
// Les codes viennent du paramétrage du dossier, actif et coché côté Combo :
// ils sont donc semés **confirmés**. Un code déduit d'un libellé le serait
// non confirmé — proposer n'est pas confirmer — mais ceux-ci sont relevés
// dans la configuration réelle, pas devinés.
//
// Ce qui n'a pas de code au dossier reste absent plutôt que semé à vide :
// l'export refuse alors de tourner en listant les manques, ce qui est le
// comportement voulu (§8).
let seededMappings = 0;
for (const definition of PAYROLL_ELEMENT_DEFINITIONS) {
if (!definition.suggestedCode) continue;
const code = ELEMENT_CODES[definition.key] ?? definition.suggestedCode;
if (!code) continue;
const existing = await prisma.silaeCodeMapping.findFirst({
where: { accountId: account.id, sourceKey: definition.key },
});
@@ -724,16 +741,29 @@ async function main() {
data: {
accountId: account.id,
sourceKey: definition.key,
silaeCode: definition.suggestedCode,
silaeCode: code,
label: definition.label,
kind: definition.kind,
confirmed: false,
confirmed: Boolean(ELEMENT_CODES[definition.key]),
},
});
seededMappings += 1;
}
console.log(
` ${PAYROLL_ELEMENT_DEFINITIONS.filter((d) => d.suggestedCode).length} proposées, aucune confirmée`,
const missingElements = PAYROLL_ELEMENT_DEFINITIONS.filter(
(definition) =>
!ELEMENT_CODES[definition.key] && !definition.suggestedCode,
);
console.log(` ${seededMappings} correspondances`);
if (missingElements.length > 0) {
console.log(
` ⚠ sans code au dossier : ${missingElements.map((d) => d.label).join(', ')}`,
);
}
if (INCOMPLETE_IN_DOSSIER.length > 0) {
console.log(
` ⚠ ${INCOMPLETE_IN_DOSSIER.length} rubriques incomplètes côté Silae (préfixe « AB- » sans numéro) : ${INCOMPLETE_IN_DOSSIER.map((row) => row.label).join(', ')}`,
);
}
console.log('→ Matricules Silae');
// Fictifs, à la forme observée dans le dossier : cinq chiffres cadrés à zéro.