Le 27 octobre 2026, le taux de saoudisation des professions comptables passe de 40 % à 50 %. C'est la phase deux de la décision ministérielle 103108, et c'est celle qui piège les entreprises, parce que rien n'a besoin de changer pour qu'un établissement sorte de la conformité. Celui qui avait franchi les 40 % d'une seule tête en octobre dernier sera en infraction en octobre prochain sans avoir recruté personne, licencié personne, ni rien modifié.
Ce tutoriel construit le moteur qui vous le dit à l'avance. Ce n'est pas le moteur Nitaqat. Si vous avez déjà construit le moteur de saoudisation de l'annexe 1, n'en réutilisez aucun calcul ici : cette décision applique un taux fixe et non une courbe logarithmique, possède son propre dénominateur qui exclut la majeure partie de vos effectifs, et impose trois conditions de comptabilisation que Nitaqat n'applique pas. Une bande Nitaqat verte n'offre aucune protection.
Prérequis
- Node.js 20 ou plus récent et TypeScript 5.x
- Une aisance avec les unions discriminées et les types littéraux
- Un accès en lecture à deux sources de données que vous ne joignez probablement pas aujourd'hui : les intitulés de poste enregistrés auprès de la GOSI, et le statut d'accréditation SOCPA
- Une lecture de la décision elle-même, résumée dans la saoudisation comptable atteint 50 % le 27 octobre 2026
Ce que vous allez construire
Un module pur, sans dépendances, qui répond à quatre questions pour n'importe quelle date :
- Cet établissement entre-t-il seulement dans le champ de la décision ?
- Quel taux s'applique à cette date ?
- Lesquels de mes comptables saoudiens comptent réellement, lesquels sont exclus et pourquoi ?
- Combien de personnes doivent changer, et le recrutement comble-t-il l'écart ou l'élargit-il ?
Chaque constante ci-dessous provient du guide procédural du ministère. Chaque nombre des exemples résolus a été exécuté avant d'être écrit.
Étape 1 : pourquoi le modèle Nitaqat a la mauvaise forme
Nitaqat pose une seule question sur l'établissement entier : quelle fraction de l'ensemble est saoudienne, mesurée contre une courbe qui bouge avec votre taille et votre activité. La décision 103108 pose une question plus étroite et plus stricte.
Trois différences structurelles gouvernent toute la conception :
- Le dénominateur, ce sont les seules professions comptables. Une société de 400 personnes dont 23 comptables divise par 23, pas par 400.
- Le taux est fixe et échelonné par date, non dérivé d'une courbe. C'est un nombre entier de points de pourcentage, et cela compte davantage qu'il n'y paraît.
- L'obligation est parallèle. Elle s'applique au niveau de l'entité quelle que soit la bande Nitaqat, et se cumule avec la localisation à 70 % des professions de gestion de projet au 14 février 2027. Un établissement peut être en vert et enfreindre les deux.
Traitez-la donc comme un moteur autonome avec ses propres entrées, et laissez le tableau de bord afficher les deux côte à côte plutôt que de replier l'un dans l'autre.
Étape 2 : le calendrier des phases, et l'inversion de la phase cinq
La plupart des résumés publiés présentent le calendrier comme une échelle montante — 40, 50, 60, 70. La phase cinq n'est pas un barreau de cette échelle. Elle fait deux choses à la fois : elle maintient 70 % pour les établissements de cinq comptables et plus, et elle fait entrer pour la première fois dans le champ ceux qui en comptent trois ou quatre, à 30 %.
Si vous modélisez une phase par un taux unique, la phase cinq devient irreprésentable. Modélisez-la comme un ensemble de tranches :
// tawteen/phases.ts
/** A size bracket within a phase. `max: null` means "and above". */
export type Bracket = {
readonly min: number;
readonly max: number | null;
/** Whole percentage points. Never store this as a fraction. */
readonly ratePercent: number;
};
export type Phase = {
readonly ordinal: 1 | 2 | 3 | 4 | 5;
/** ISO date the phase takes effect, inclusive. */
readonly effectiveFrom: string;
readonly brackets: readonly Bracket[];
};
export const ACCOUNTING_PHASES: readonly Phase[] = [
{ ordinal: 1, effectiveFrom: '2025-10-27', brackets: [{ min: 5, max: null, ratePercent: 40 }] },
{ ordinal: 2, effectiveFrom: '2026-10-27', brackets: [{ min: 5, max: null, ratePercent: 50 }] },
{ ordinal: 3, effectiveFrom: '2027-10-27', brackets: [{ min: 5, max: null, ratePercent: 60 }] },
{ ordinal: 4, effectiveFrom: '2028-10-27', brackets: [{ min: 5, max: null, ratePercent: 70 }] },
{
ordinal: 5,
effectiveFrom: '2029-10-27',
brackets: [
// New in phase five: small establishments enter scope.
{ min: 3, max: 4, ratePercent: 30 },
{ min: 5, max: null, ratePercent: 70 },
],
},
];La résolution est une recherche, pas un calcul. Renvoyez null pour le hors-champ plutôt que zéro — zéro est un taux, et un établissement sous le seuil n'a aucun taux :
// tawteen/rate.ts
import { ACCOUNTING_PHASES, type Phase } from './phases';
export type ApplicableRate = {
readonly phase: Phase['ordinal'];
readonly ratePercent: number;
};
function phaseOn(onDate: string): Phase | null {
let found: Phase | null = null;
// ISO strings compare lexicographically; the table is in ascending order.
for (const p of ACCOUNTING_PHASES) if (p.effectiveFrom <= onDate) found = p;
return found;
}
/** `null` means the establishment is outside the decision's scope on that date. */
export function rateFor(totalAccountants: number, onDate: string): ApplicableRate | null {
const phase = phaseOn(onDate);
if (!phase) return null;
for (const b of phase.brackets) {
if (totalAccountants < b.min) continue;
if (b.max !== null && totalAccountants > b.max) continue;
return { phase: phase.ordinal, ratePercent: b.ratePercent };
}
return null;
}Un établissement de quatre comptables obtient null au 27 octobre 2026 et phase 5, 30% au 27 octobre 2029. Cette seule transition mérite une alerte dédiée, car elle survient sans aucun changement de son côté.
Le taux le plus élevé prévaut. Lorsqu'une profession comptable est également visée par une autre décision à un taux différent, c'est le plus élevé des deux qui s'applique. Si vous localisez plusieurs familles de métiers, résolvez tous les taux applicables au code métier d'un salarié et prenez le maximum — ne supposez pas que cette table est la seule à toucher un code donné.
Étape 3 : les codes métier sont le dénominateur
La décision nomme des codes métier issus de la classification saoudienne unifiée des professions, et la liste est plus large que ne le suggère le mot « comptable ». Le contrôleur des stocks et l'assistant comptable y figurent. Chacune de ces têtes siège dans votre dénominateur, que le SIRH les considère ou non comme du personnel financier.
// tawteen/job-codes.ts
/**
* Job codes targeted by Ministerial Decision 103108, as transcribed from the
* ministry's procedural guide. This table IS the denominator: a worker whose
* GOSI job code is absent from it is outside the calculation entirely.
*/
export const TARGETED_JOB_CODES = {
'121101': 'Financial manager',
'121102': 'Accounts manager',
'121103': 'Tariff accounts manager',
'121104': 'Treasury manager',
'121105': 'Budget manager',
'121106': 'Audit manager',
'121107': 'Internal audit manager',
'121113': 'Collections manager',
'121116': 'Treasury director',
'241101': 'Accountant',
'241102': 'Cost accountant',
'241103': 'Internal auditor',
'241105': 'Chartered accountant',
'241106': 'Financial controller',
'241107': 'Financial budgeting specialist',
'241108': 'Tax accounts specialist',
'241109': 'Inventory control specialist',
'331301': 'Accounts assistant',
'331302': 'Inventory controller',
'335202': 'Tax officer',
'431101': 'Accounts clerk',
'431201': 'Finance clerk',
} as const;
export type TargetedJobCode = keyof typeof TARGETED_JOB_CODES;
export function isTargeted(code: string): code is TargetedJobCode {
return code in TARGETED_JOB_CODES;
}N'inscrivez le nombre de codes en dur nulle part. Les résumés publiés de cette décision divergent : la table contiendrait 21 ou 22 entrées. Dérivez le compte de la table elle-même et figez-le par un test face au guide officiel, pour qu'une erreur de recopie fasse échouer un build au lieu de rétrécir silencieusement votre dénominateur.
Il y a une chose que la table ne peut pas exprimer, et il ne faut pas prétendre le contraire dans le code. Le guide précise que la décision suit le travail réellement effectué, et pas seulement l'intitulé enregistré : confier les missions d'une profession localisée à un non-Saoudien sous un autre intitulé constitue une infraction explicite. Un moteur fondé sur les codes ne peut pas le détecter. Considérez sa sortie comme le plancher de votre exposition, jamais le plafond.
Étape 4 : trois systèmes qui doivent concorder
C'est ici que cette décision diffère de tout ratio d'effectifs que vous avez écrit auparavant. Être saoudien et travailler en comptabilité ne suffit pas. Trois conditions doivent être réunies simultanément, et elles vivent dans trois systèmes différents :
- Le code métier enregistré auprès de la GOSI figure dans la table.
- Le salaire soumis à cotisation atteint au moins 6 000 SAR pour une licence ou équivalent, ou 4 500 SAR pour un diplôme ou équivalent.
- L'accréditation professionnelle SOCPA est active.
Qu'une seule échoue et le salarié quitte le numérateur. Surtout, il ne quitte pas le dénominateur — il reste un travailleur d'une profession comptable. Cette asymétrie est toute la raison pour laquelle les tableurs se trompent.
// tawteen/countable.ts
import { isTargeted } from './job-codes';
export type Qualification = 'BACHELOR' | 'DIPLOMA';
/** Minimum GOSI contributory wage in SAR for a Saudi accountant to be counted. */
export const WAGE_FLOOR_SAR: Record<Qualification, number> = {
BACHELOR: 6000,
DIPLOMA: 4500,
};
export type ExclusionReason =
| 'JOB_CODE_NOT_TARGETED'
| 'WAGE_BELOW_FLOOR'
| 'SOCPA_ACCREDITATION_INACTIVE'
| 'QUALIFICATION_UNMAPPED';
export type Employee = {
readonly id: string;
readonly nationality: 'SA' | 'NON_SA';
/** The code as registered with GOSI — not the internal HR title. */
readonly gosiJobCode: string;
/** Undefined when HR holds no usable qualification record. */
readonly qualification?: Qualification;
/** The GOSI contributory wage, not gross pay and not basic salary. */
readonly contributoryWageSar: number;
readonly socpaAccreditationActive: boolean;
};
export type EmployeeAssessment = {
readonly inDenominator: boolean;
readonly inNumerator: boolean;
readonly reasons: readonly ExclusionReason[];
};
export function assessEmployee(e: Employee): EmployeeAssessment {
if (!isTargeted(e.gosiJobCode)) {
// Outside the profession family: neither term. Not an exclusion, a filter.
return { inDenominator: false, inNumerator: false, reasons: ['JOB_CODE_NOT_TARGETED'] };
}
// A non-Saudi accountant is denominator-only by definition.
if (e.nationality !== 'SA') {
return { inDenominator: true, inNumerator: false, reasons: [] };
}
const reasons: ExclusionReason[] = [];
if (e.qualification === undefined) {
// Never assume the lower floor to be generous. An unknown qualification is
// a data incident to resolve, not a number to guess.
reasons.push('QUALIFICATION_UNMAPPED');
} else if (e.contributoryWageSar < WAGE_FLOOR_SAR[e.qualification]) {
reasons.push('WAGE_BELOW_FLOOR');
}
if (!e.socpaAccreditationActive) {
reasons.push('SOCPA_ACCREDITATION_INACTIVE');
}
return { inDenominator: true, inNumerator: reasons.length === 0, reasons };
}Collectez tous les motifs d'échec au lieu de sortir au premier. Un comptable à la fois sous le plancher salarial et non accrédité demande deux corrections, et une liste de remédiation qui ne révèle le second problème qu'une fois le premier résolu vous coûtera l'échéance.
Le salaire, ici, est le salaire soumis à cotisation enregistré auprès de la GOSI, qui n'est très souvent pas le nombre que votre état de paie appelle salaire. Si vous n'avez pas déjà rapproché ces deux chiffres, le moteur de cotisations GOSI traite de leurs divergences. Un comptable enregistré à 5 800 SAR et payé 6 400 SAR est exclu, et aucun état de paie ne vous le dira.
Étape 5 : l'arrondi — là où un flottant vous coûte une tête
Le taux est exigé à l'entier le plus proche, une moitié s'arrondissant vers le haut. Le guide du ministère traite un établissement de 23 comptables : 23 × 50 % = 11,5, requis = 12. Son exemple de phase un arrondit 9,2 vers le bas, à 9.
L'implémentation évidente est fausse :
// Wrong. Do not ship this.
const required = Math.round(total * (ratePercent / 100));ratePercent / 100 n'est pas exactement représentable en virgule flottante binaire pour 30 ni pour 70. Avec 45 comptables à 70 %, 45 * 0.7 s'évalue à 31.499999999999996, et Math.round renvoie 31 là où la bonne réponse est 32. Avec 85 comptables, elle renvoie 59 au lieu de 60. Il vous manque un comptable saoudien pour atteindre le taux, et votre tableau de bord est vert.
Gardez l'arithmétique en entiers de bout en bout. Les points de pourcentage sont des entiers : multipliez d'abord, ajoutez la moitié du diviseur, puis divisez :
// tawteen/required.ts
/**
* Required Saudi accountants, rounded to nearest with halves rounding up.
* Integer-only: `total * ratePercent` is exact, so no float ever enters.
*/
export function requiredSaudis(totalAccountants: number, ratePercent: number): number {
return Math.floor((totalAccountants * ratePercent + 50) / 100);
}Vérifié face au guide : requiredSaudis(23, 50) renvoie 12, et requiredSaudis(23, 40) renvoie 9. Face au piège du flottant : requiredSaudis(45, 70) renvoie 32 et requiredSaudis(85, 70) renvoie 60, tous deux corrects là où Math.round ne l'est pas.
Exprimez aussi le taux atteint en entiers. Tronquer les points de base fait errer vers le bas, ce qui est la direction sûre pour un chiffre de conformité :
/** Achieved rate in basis points, truncated. 4000 = 40.00%. */
export function achievedBasisPoints(countableSaudis: number, totalAccountants: number): number {
if (totalAccountants === 0) return 0;
return Math.floor((countableSaudis * 10000) / totalAccountants);
}Étape 6 : assembler l'évaluation
Reliez maintenant les pièces. La sortie d'un moteur de conformité ne devrait jamais être un nombre seul — mais un nombre accompagné des éléments qui l'ont produit, car le nombre seul ne peut être ni audité ni exploité.
// tawteen/assess.ts
import { assessEmployee, type Employee, type ExclusionReason } from './countable';
import { rateFor } from './rate';
import { achievedBasisPoints, requiredSaudis } from './required';
export type ExcludedSaudi = {
readonly employeeId: string;
readonly reasons: readonly ExclusionReason[];
};
export type Assessment =
| { readonly inScope: false; readonly totalAccountants: number }
| {
readonly inScope: true;
readonly phase: number;
readonly ratePercent: number;
readonly totalAccountants: number;
readonly countableSaudis: number;
readonly requiredSaudis: number;
/** Heads short, by replacement. Zero when compliant. */
readonly replacementGap: number;
readonly achievedBasisPoints: number;
readonly excludedSaudis: readonly ExcludedSaudi[];
};
export function assess(roster: readonly Employee[], onDate: string): Assessment {
let total = 0;
let countable = 0;
const excludedSaudis: ExcludedSaudi[] = [];
for (const e of roster) {
const a = assessEmployee(e);
if (!a.inDenominator) continue;
total += 1;
if (a.inNumerator) {
countable += 1;
} else if (e.nationality === 'SA') {
// A Saudi in the denominator but not the numerator is the remediation list.
excludedSaudis.push({ employeeId: e.id, reasons: a.reasons });
}
}
const rate = rateFor(total, onDate);
if (!rate) return { inScope: false, totalAccountants: total };
const required = requiredSaudis(total, rate.ratePercent);
return {
inScope: true,
phase: rate.phase,
ratePercent: rate.ratePercent,
totalAccountants: total,
countableSaudis: countable,
requiredSaudis: required,
replacementGap: Math.max(0, required - countable),
achievedBasisPoints: achievedBasisPoints(countable, total),
excludedSaudis,
};
}Exécutez-le sur un petit effectif où deux comptables saoudiens échouent pour des raisons différentes — l'un enregistré à 5 800 SAR contre un plancher licence de 6 000, l'autre dont l'accréditation a expiré — et un dénominateur de cinq têtes que le SIRH lit à 80 % saoudien ressort à 40 %. Les deux nombres ne sont pas voisins, et un seul est celui que le ministère calcule.
Étape 7 : recruter trois ne comble pas un écart de trois
L'exemple résolu du guide : 23 comptables, phase deux, requis 12, neuf Saoudiens comptabilisables. Il dit de remplacer trois comptables non saoudiens. C'est exact — et c'est exact précisément parce qu'il dit remplacer.
Le remplacement maintient le dénominateur à 23. Le recrutement fait croître les deux termes à la fois : un écart de trois ne se referme donc pas après trois embauches. Faites tourner l'arithmétique et regardez la cible s'enfuir :
| Saoudiens recrutés | Total comptables | Requis à 50 % | Saoudiens comptabilisables | Manque encore |
|---|---|---|---|---|
| 3 | 26 | 13 | 12 | 1 |
| 4 | 27 | 14 | 13 | 1 |
| 5 | 28 | 14 | 14 | 0 |
Cinq recrutements nets pour combler un écart de trois. Tout tableau de bord qui affiche un « écart : 3 » nu et laisse un recruteur y lire un objectif d'embauche ment d'environ 67 %. Calculez les deux nombres et nommez-les :
// tawteen/gap.ts
import { requiredSaudis } from './required';
export type Remediation = {
/** Non-Saudi accountants to replace with Saudis. Denominator unchanged. */
readonly byReplacement: number;
/** Net new countable Saudi hires, if you replace nobody. Denominator grows. */
readonly byHiringOnly: number;
};
export function remediation(
totalAccountants: number,
countableSaudis: number,
ratePercent: number,
): Remediation {
const byReplacement = Math.max(0, requiredSaudis(totalAccountants, ratePercent) - countableSaudis);
let hires = 0;
// Each hire adds one to both terms; the target moves, so solve rather than divide.
while (countableSaudis + hires < requiredSaudis(totalAccountants + hires, ratePercent)) {
hires += 1;
if (hires > 10_000) throw new Error('remediation did not converge');
}
return { byReplacement, byHiringOnly: hires };
}La boucle termine pour tout taux inférieur à 100 % et se borne en pratique à quelques itérations, mais gardez la garde : un futur taux de 100 tournerait indéfiniment, et une tâche de conformité qui se bloque en silence à 3 h du matin est pire qu'une tâche qui lève une exception.
Étape 8 : la date où vous tombez sans qu'un seul salarié parte
Voilà l'alerte qui justifie à elle seule la construction du moteur. Un établissement assis aujourd'hui exactement sur le taux requis sera en infraction à la prochaine date de phase sans avoir rien fait.
// tawteen/forecast.ts
import { ACCOUNTING_PHASES } from './phases';
import { rateFor } from './rate';
import { requiredSaudis } from './required';
export type FutureBreach = {
readonly effectiveFrom: string;
readonly phase: number;
readonly ratePercent: number;
readonly requiredSaudis: number;
readonly shortfall: number;
};
/** Phase dates on which the current roster would breach, if nothing changes. */
export function forecast(
totalAccountants: number,
countableSaudis: number,
fromDate: string,
): FutureBreach[] {
const out: FutureBreach[] = [];
for (const phase of ACCOUNTING_PHASES) {
if (phase.effectiveFrom <= fromDate) continue;
const rate = rateFor(totalAccountants, phase.effectiveFrom);
if (!rate) continue; // Still out of scope at that size.
const required = requiredSaudis(totalAccountants, rate.ratePercent);
if (countableSaudis >= required) continue;
out.push({
effectiveFrom: phase.effectiveFrom,
phase: rate.phase,
ratePercent: rate.ratePercent,
requiredSaudis: required,
shortfall: required - countableSaudis,
});
}
return out;
}Vingt comptables et dix Saoudiens comptabilisables sont conformes au 27 octobre 2026 — requis 10, détenus 10. Au 27 octobre 2027, l'exigence devient 12 et le même effectif en manque deux. Le moteur aurait dû le dire un an plus tôt.
La même fonction attrape l'entrée en phase cinq. Un établissement de quatre comptables et un Saoudien comptabilisable reste hors champ jusqu'au 27 octobre 2029, puis il lui faut requiredSaudis(4, 30), soit 1 — donc conforme. Passez-le à quatre comptables et zéro Saoudien comptabilisable, et la prévision se déclenche à une date que personne n'avait au calendrier.
Tester votre implémentation
Trois familles de tests, et la première n'est pas optionnelle.
Figez les exemples officiels résolus. Ce sont la seule vérification externe dont vous disposez sur la règle d'arrondi :
import { strict as assert } from 'node:assert';
import { requiredSaudis } from './required';
import { remediation } from './gap';
// Ministry procedural guide, phase two: 23 x 50% = 11.5 -> 12.
assert.equal(requiredSaudis(23, 50), 12);
// Phase one from the same guide: 9.2 -> 9.
assert.equal(requiredSaudis(23, 40), 9);
// The float trap: Math.round(45 * 0.7) is 31 and wrong.
assert.equal(requiredSaudis(45, 70), 32);
assert.equal(requiredSaudis(85, 70), 60);
// Replacement and hiring are different numbers, and the gap is the smaller one.
assert.deepEqual(remediation(23, 9, 50), { byReplacement: 3, byHiringOnly: 5 });Vérifiez la forme des tables de référence, pas seulement le comportement. Une liste de codes métier silencieusement raccourcie est le mode de défaillance que vous ne remarquerez pas :
import { TARGETED_JOB_CODES } from './job-codes';
import { ACCOUNTING_PHASES } from './phases';
const codes = Object.keys(TARGETED_JOB_CODES);
assert.equal(new Set(codes).size, codes.length, 'duplicate job code');
assert.ok(codes.every((c) => /^\d{6}$/.test(c)), 'job codes are six digits');
assert.equal(ACCOUNTING_PHASES.length, 5);Testez les invariants par propriétés. Sur des totaux aléatoires et les quatre taux en vigueur : le nombre requis n'excède jamais le total, byHiringOnly n'est jamais inférieur à byReplacement, et appliquer byReplacement remplacements produit toujours une évaluation conforme. Ces trois propriétés tiennent pour chaque taux de la table et attraperont une erreur d'unité dans l'arrondi bien avant un auditeur.
Pour une vérification manuelle, entrez les mêmes totaux dans le calculateur Nitaqat avec 50 comme taux requis — en gardant à l'esprit qu'ici le dénominateur, ce sont les comptables seuls, pas l'établissement. Si les deux divergent, l'un d'eux porte une constante mal recopiée.
Dépannage
Le moteur affiche 43 % et le tableau de bord RH 52 %. Attendu, et c'est la raison d'être du moteur. Imprimez excludedSaudis et vous trouverez généralement l'écart dans deux ou trois accréditations SOCPA expirées et un salaire soumis à cotisation à quelques centaines de riyals sous le plancher.
Tous les salariés reviennent en JOB_CODE_NOT_TARGETED. Vous lisez les intitulés internes du SIRH au lieu des codes enregistrés auprès de la GOSI. Ce sont deux champs distincts qui divergent en permanence — et cette divergence est elle-même le constat, pas un bug de la jointure.
Le taux bascule entre deux valeurs d'une exécution à l'autre. Quelque chose dans le pipeline recalcule total depuis une requête vivante tandis que countableSaudis vient d'un instantané en cache. Évaluez un effectif immuable lu à un instant unique, sinon les deux termes appartiennent à deux moments différents.
Un établissement au bord d'une tranche entre et sort du champ. Réel, pas un bug : cinq comptables est le seuil, et le cinquième est peut-être sur le départ. Alertez sur l'approche du bord, pas seulement sur son franchissement.
Prochaines étapes
- Faites tourner l'évaluation chaque nuit et comparez-la à la veille, en alertant sur tout Saoudien nouvellement exclu plutôt que sur le taux global. Les accréditations expirent en silence.
- Ajoutez l'obligation parallèle. Les professions de gestion de projet atteignent 70 % le 14 février 2027 avec leurs propres codes métier et leur propre dénominateur, et la même forme de moteur les couvre.
- Tirez l'effectif comptabilisable de la plateforme plutôt que d'un tableur — l'intégration Qiwa pour les SIRH explique d'où viennent les données de contrats authentifiés.
- Gardez la vue Nitaqat à côté de celle-ci. Le moteur de l'annexe 1 répond à une autre question sur la même société, et être en vert là-bas ne dit rien de cette décision.
Conclusion
La difficulté de cette décision n'est pas le pourcentage. C'est que le nombre calculé par le ministère est assemblé à partir de trois systèmes que votre organisation lit séparément, et que l'arithmétique qui les réunit punit un flottant et un tableur dans la même direction : l'optimisme.
Un moteur qui stocke les taux en entiers, conserve tous les motifs d'échec au lieu du premier, distingue le remplacement du recrutement et anticipe la prochaine date de phase vous dira en août ce que vous découvririez sinon en novembre. C'est là toute la valeur, et il en reste moins de deux mois avant le 27 octobre.
Si vous préférez que ce rapprochement tourne automatiquement plutôt qu'il ne soit rafraîchi à la main une fois par trimestre, réservez une session de diagnostic gratuite — nous examinerons où vivent réellement vos codes métier, vos salaires et vos données d'accréditation, et où les trois cessent de concorder.