Appearance
Planification — Moteur unifié Budget/Forecast
Spécification technique pour Heriade Rapport. Formalise un moteur de projection commun à la construction d'un Budget initial et à la mise à jour d'un Forecast en cours d'année, structuré autour de trois types de projection (Métier, RH, Comptable) et d'un mécanisme de détection des divergences entre réel et trajectoire projetée.
1. Principe unifié — Budget et Forecast sont deux instanciations du même moteur
Budget et Forecast partagent la même mécanique : une base + des ajustements + une ventilation mensuelle, par agrégat et par établissement. Ce qui les distingue n'est pas la logique de calcul, mais deux paramètres :
| Paramètre | Budget initial | Forecast en cours d'année |
|---|---|---|
| Base de départ | N-1 réel, N-1 budget + croissance, ou zero-based | Budget de l'année en cours, ou dernier Forecast publié |
| Premier mois de projection | Janvier (aucun réel disponible) | Mois suivant le dernier réel clôturé |
| YTD réel disponible | Non | Oui |
| Mécanisme de reprise nécessaire | Non (rien à raccorder) | Oui (cf. section 4) |
Concept générique introduit : le premier mois de projection n'est plus une notion propre au Forecast, mais une propriété de toute campagne de planification — le mois à partir duquel il n'existe plus de réel disponible. Un Budget a cette valeur fixée à janvier ; un Forecast la fait avancer à chaque clôture mensuelle.
2. Les trois types de projection
Type A — Projection métier
Calculée par quantité × prix unitaire (ex. industrie, hôtellerie), à partir des différents flux de chiffre d'affaires. Peut se décliner par projet (ex. services informatiques : partie récurrente + partie pipe commercial).
Type B — Projection RH
Deux composantes :
- Extrapolation de la masse salariale actuelle avec hypothèses (hausse du SMIC à une date donnée, départ d'un salarié identifié, remplacement décalé dans le temps, ancienneté) — s'appuie sur le registre réel des salariés (catalogue Salarié, Phase 5 du méta-modèle), pas sur un montant agrégé abstrait.
- Calcul des charges dérivées (taux de charges patronales, médecine du travail, etc.) à partir de la masse salariale projetée en (1).
Type C — Projection comptable
Le reste des comptes, non couverts par A ou B. Part d'une base nommée (ex. "Forecast P10 2026"), ajustée par :
- Montant annuel modifié directement
- Taux d'inflation (valeur par défaut modifiable ligne par ligne)
- Choix de ventilation mensuelle : 1/12ème, saisonnalité N-1, ou manuelle
3. Le problème de discontinuité réel/projection (déclencheur de cette spec)
Dès qu'un YTD réel existe (Forecast), une ligne projetée ne doit jamais présenter de saut injustifié entre le dernier réel connu et la trajectoire future. Exemple de référence : un taux d'occupation à 80 % en réel de février ne peut pas remonter à 93 % en YTG de mars sans justification opérationnelle explicite (campagne d'admission, ouverture d'aile...).
Ce risque existe dans les trois types à des degrés différents :
- Type A : le plus exposé — taux et quantités opérationnels discontinus sans raison métier.
- Type B : risque plus faible (la base part du registre réel des salariés), mais le taux de charges calculé peut diverger si l'hypothèse budgétée ne correspond plus à ce qui est réellement observé.
- Type C : risque plus discret — si le YTD réel dépasse déjà largement le prorata attendu de la base annuelle, garder le même montant annuel lissé sur les mois restants peut produire un YTG déraisonnable.
4. Modes de reprise — par défaut au niveau du type, jamais par ligne en configuration initiale
Un mode de reprise détermine comment une ligne projetée se raccorde au dernier réel connu.
ts
type ModeReprise = 'plat' | 'convergence' | 'decalage' | 'manuel'- Plat : la dernière valeur réelle connue est tenue pour tous les mois suivants — choix par défaut le plus sûr pour un taux (type A), tant qu'aucun événement n'est déclaré.
- Convergence : la trajectoire part du dernier réel et rejoint une cible (ex. objectif annuel initial) à une échéance choisie, par interpolation.
- Décalage : l'écart cumulé entre réel YTD et base YTD est calculé, puis appliqué en translation à toutes les valeurs futures de la base — conserve la forme de la courbe (saisonnalité déjà choisie en type C), recalée sur la réalité.
- Manuel : ajustement mois par mois par l'utilisateur, réservé aux cas où un événement connu justifie un écart délibéré (ex. campagne de recrutement programmée).
Mapping par défaut
| Type de projection | Mode de reprise par défaut |
|---|---|
| A — Métier | Plat |
| B — RH (masse salariale) | Continuité structurelle via le registre Salarié, pas de mode à choisir |
| B — RH (charges dérivées) | Plat |
| C — Comptable | Décalage |
Principe non négociable : ces modes s'appliquent au niveau du type de projection, jamais ligne par ligne en configuration initiale — configurer un mode par compte ou par poste serait trop lourd pour un usage régulier (cf. section 5 pour la seule exception légitime).
5. Mécanisme de détection — la charge de vigilance revient au système, pas à l'utilisateur
Plutôt que d'imposer un choix de mode par ligne en amont, le système détecte automatiquement les lignes où le réel diverge fortement de ce que prévoyait la base, et ne sollicite l'utilisateur que sur ces lignes-là.
ts
interface SeuilDivergenceReprise {
typeProjection: 'A' | 'B' | 'C'
seuilRelatif?: number // ex. 15 (%) d'écart entre réel YTD et base YTD prorata
seuilAbsolu?: number // pour les lignes à faible base, où un seuil relatif serait trop sensible
}Si |réel YTD − base YTD prorata| dépasse le seuil du type
→ ligne signalée : mode de reprise à trancher explicitement (Plat / Convergence / Décalage / Manuel)
Sinon
→ mode par défaut du type appliqué silencieusement, aucune action requiseReprend le même principe que le seuil d'alerte sur écart aberrant (docs/spec-refonte-ecarts.md, section 1.4) : le signalement doit rester rare pour conserver son utilité, pas déclenché sur toute variation normale.
Effet attendu : sur un compte de résultat de plusieurs dizaines de lignes, seule une poignée (5 à 15 selon l'ampleur des écarts réels du mois) est signalée à chaque rafraîchissement — pas un écran de configuration exhaustif à remplir.
6. Modèle de données
ts
interface CampagnePlanification {
id: string
type: 'budget' | 'forecast'
base: { source: 'n1_reel' | 'n1_budget_croissance' | 'zero_based' | 'budget_courant' | 'dernier_forecast', reference?: string }
premierMoisProjection: { annee: number, mois: number }
}
interface LigneProjection {
id: string
campagneId: string
typeProjection: 'A' | 'B' | 'C'
perimetre: { axis: string, code: string, etablissement: string }
modeRepriseEffectif: ModeReprise // hérité du défaut du type, ou surchargé
modeRepriseSurcharge?: boolean // true si divergent du défaut, ex. suite à un signalement traité
ventilation?: '1_12eme' | 'saisonnalite_n1' | 'manuelle' // pertinent surtout pour le type C
}
interface SignalementDivergence {
id: string
ligneProjectionId: string
ecartConstate: number
statut: 'en_attente' | 'traite'
modeChoisi?: ModeReprise
}7. État réel du code (audit Claude Code) et phasage révisé
Correction de cadrage : un audit du code existant montre que l'implémentation a avancé dans un ordre différent de celui prévu ici. À date :
| Brique | État |
|---|---|
Modèle unifié Budget/Forecast (ForecastCampaign, cutoffMonth, baseScenario) | ✅ Codé, conforme à la section 1 |
| Indicateurs structure (référentiel permanent) | ✅ Codé, hors périmètre campagne, conforme au méta-modèle |
| Type C — comptable (base + inflation + ventilation) | ✅ Codé (méthode reste, cœur du moteur) |
Type A — métier (méthodes volume, projets) | ⚠️ Amorcé, version basique (pas de moteur de drivers/formules configurables) |
Type B — RH (méthode masse_salariale, ForecastEmployee) | ⚠️ Amorcé, registre simplifié (pas d'ancienneté auto, pas de statut mensuel arrivée/départ/remplacé, pas d'import paie) |
Bases zero_based et n1_budget_croissance | ❌ Non codées (seules budget, n1, campaign existent) |
| Premier mois de projection générique (section 1) | ❌ N'existe pas en tant que tel — cutoffMonth en tient lieu partiellement, sans le recadrage générique prévu |
| Modes de reprise (Plat/Convergence/Décalage/Manuel) | ❌ Inexistant — aucun mécanisme de raccordement réel↔projection |
| Détection de divergence et signalement (section 5) | ❌ Inexistant |
Conséquence concrète et non théorique : sans le mécanisme de reprise, le problème de discontinuité qui a motivé cette spec (ton exemple : TO à 80 % en réel, projeté à 93 % sans justification) peut se produire aujourd'hui, sur n'importe quelle campagne Forecast déjà en usage, sans aucun garde-fou.
Phasage révisé (remplace le phasage initial de cette section)
- Étape 1 (déjà faite) : moteur type C, modèle unifié Budget/Forecast, Indicateurs structure comme référentiel permanent. Ne rien reconstruire.
- Étape 1b (reste à faire, priorité immédiate) : ajouter les bases manquantes
zero_basedetn1_budget_croissance(+ champcroissanceDefault) — complète l'étape 1, périmètre restreint et isolé. - Étape 2 (priorité la plus élevée, contrairement au phasage initial) : mécanisme de reprise (modes Plat/Convergence/Décalage/Manuel) et détection de divergence avec signalement (section 5). C'est la brique manquante la plus risquée en usage réel — à traiter avant de continuer à enrichir A et B, pas après.
- Étape 3 : consolidation des types A et B déjà amorcés — moteur de drivers/formules configurables pour A, enrichissement du registre Salarié pour B (ancienneté automatique, statuts mensuels, import paie) — cohérent avec le catalogue Salarié de la Phase 5 du méta-modèle.
8. Point de vigilance
Le seuil de divergence (section 5) doit être calibré par type, pas uniforme — un taux d'occupation (type A) et un compte de charges externes (type C) n'ont pas la même volatilité naturelle. Un seuil relatif unique appliqué partout produirait soit trop de signalements sur les comptes volatils par nature, soit pas assez sur les taux métier où même un petit écart en points de pourcentage peut représenter une vraie anomalie opérationnelle (cf. exemple TO 80 % → 93 %, un écart de 13 points qui n'est pas énorme en relatif mais reste peu plausible d'un mois sur l'autre sans événement).
Point de vigilance additionnel : tant que l'étape 2 révisée n'est pas faite, toute campagne Forecast déjà utilisée par un client peut produire des trajectoires incohérentes avec le réel sans qu'aucune alerte ne le signale — à mentionner explicitement si des clients pilotes utilisent déjà le Forecast en conditions réelles.
9. Cadrage d'implémentation de l'étape 2 (décisions figées le 2026-07-07)
Décisions actées avant écriture du code. Étape 2 scopée au type C (reste) — seul type pleinement opérationnel ; les modes Plat/Convergence appliqués aux types A/B relèvent de leur consolidation en étape 3.
9.1 Premier mois de projection — dérivé, non stocké
premierMoisProjection = cutoffMonth + 1, exposé comme notion du moteur mais jamais stocké : cutoffMonth reste la source unique de vérité (pas de seconde valeur susceptible de diverger). Budget (cutoff=0) → premier mois = janvier, YTD vide → l'étape de reprise est inerte automatiquement (rien à raccorder), sans branche « budget vs forecast » dédiée.
9.2 Modèle de données du mode de reprise
- Défaut par type = constante de code (dérivée de
method), non stocké :reste(C)→decalage;volume/projets(A)→plat;masse_salariale(B)→placeholderplat(reprise N/A, la MS est structurelle). - Surcharge par ligne = champ
modeReprise(nullable) surForecastNodeConfig(null = défaut du type). Colonne additive (SQL manuel, lue/écrite en$queryRaw/$executeRawcommecroissanceDefault, cf. §7). C'est lemodeRepriseSurchargede la §6. - Principe non négociable (rappel §4) : le mode est un défaut par type ; la surcharge par ligne n'existe que pour traiter une divergence signalée — pas de bascule par ligne exposée en configuration initiale.
9.3 Signalement de divergence — calculé, non historisé
Le signalement est un signal dérivé recalculé à chaque chargement (|réel YTD − base YTD prorata| > seuil du type), jamais persisté : le réel change à chaque clôture, un signalement stocké se périmerait. Seule la décision persiste : la surcharge modeReprise fait office de marqueur « traité » (une ligne surchargée n'est plus re-signalée tant que rien ne change).
- Correspondance §6 :
en_attente= divergent && pas de surcharge ;traité=modeRepriseposé. - Pas de table d'historique à ce stade : le signalement est un outil de travail courant du CDG, pas un enjeu de gouvernance/audit (contrairement au
traiteParexigé pour les éléments non classés,spec-onboarding-entreprise.md). Si un besoin de traçabilité (qui a choisi quel mode, quand) émerge, on enrichiraForecastNodeConfigd'untraitePar/dateTraitement— sans construire de table dédiée.
9.4 Algorithme du mode Décalage — écart cumulé moyenné (option A)
Pour une ligne type C, cutoff = c, base mensuelle inflatée infBase[m] :
δ = moyenne_{m ≤ c} ( réel[m] − infBase[m] ) // écart moyen YTD réel ↔ base
rb[m>c] = infBase[m] + δ // translation ; forme (saisonnalité) conservée
ytg[m>c] = rb[m] + adj[m] // puis ajustement manuel éventuelComportement unique, pas de bascule par ligne (le choix cumulé-vs-dernier-mois est un détail d'algorithme, pas un levier métier — l'exposer referait la complexité écartée en §4). Le cumulé moyenné lisse les à-coups mensuels et évite qu'un mois atypique (facture exceptionnelle, régularisation) ne déforme toute la trajectoire — conforme à « conserve la forme de la courbe, recalée sur la réalité » (§4). L'option « dernier mois seul » (δ = réel[c] − infBase[c]), plus réactive mais sur-réactive à un accident ponctuel, a été écartée.
Autres modes : Plat rb[m>c] = réel[c] ; Manuel = pas de transformation auto (mécanisme adj existant) ; Convergence interpolation linéaire de réel[c] (mois c) vers une cible mensuelle à décembre — cible = annualTarget / nYtg si un objectif annuel est posé, sinon le niveau base de décembre infBase[12] ; échéance figée à décembre (pas de paramètre d'échéance dans cette version).
9.5 Seuils de détection
|réel YTD − base YTD prorata| (base = infBase, inflatée) vs seuil par type (relatif ET absolu, §5/§8). Une ligne est signalée si elle dépasse les deux seuils et n'a pas encore de surcharge de mode (traitement).
- Type C : relatif 15 % + absolu 10 000 € — défauts (
SEUIL_DIVERGENCE_C) calibrés sur le P&L de référence (EHPAD Serenis, dim « Gestion », 40 postes) : 2 signalements (621100 Intérim soignants 42,9k€/16 %, 621200 Intérim hôtelier 11,2k€/16 %), rare et matériel, l'absolu filtrant le bruit des petits postes. - Configurables par campagne : colonnes additives
seuilDivergenceRel/seuilDivergenceAbs(défauts 0.15 / 10000), éditables dans le panneau de réglages de l'Ecran de saisie (visible quandcutoff>0). Types A/B calibrés à l'étape 3.
9.6 Sous-lots d'implémentation
| Lot | Contenu | Schéma | Etat |
|---|---|---|---|
| 2·4 | premierMoisProjection dérivé ; reprise inerte si cutoff=0 | aucun | ✅ fait |
| 2·2 | modeReprise sur ForecastNodeConfig + API set-mode + résolution/exposition du mode effectif | 1 colonne additive | ✅ fait |
| 2·1 | transforms des 4 modes (Décalage = 9.4) dans getForecastGrid, type C | aucun | ✅ fait (Décalage/Plat/Manuel/Convergence) |
| 2·detect | calcul divergence par ligne → flag signalement + UI de traitement (badge ⚠ + sélecteur de mode dans l'Ecran de saisie) | aucun | ✅ fait |
| 2·3 | seuils type C (15 % + 10 000 €) configurables par campagne (seuilDivergenceRel/Abs) | 2 colonnes additives | ✅ fait (A/B à l'étape 3) |
Etape 2 complète pour le type C (moteur, détection, UI de traitement, seuils configurables). Reste : types A/B (étape 3).
