مصنعٌ فيه مئة موظف، خمسة وعشرون منهم سعوديون. نسبة التوطين ٢٥٪، والنطاق أخضر منخفض في ٢٠٢٦. لم يستقل أحد، ولم يُوظَّف أحد، ولم يتغيّر شيء في المنشأة — وفي ٢٠٢٧ يصبح النطاق أحمر.
هذا ليس خطأً في الحساب، بل هو تصميم البرنامج نفسه. نطاقات المطور استبدل الجدول الثابت القديم بمنحنى، وثوابت ذلك المنحنى ترتفع سنةً بعد سنة. فالمنشأة التي تقف على الخط اليوم تسقط تحته العام القادم دون أن تفعل شيئاً.
أي نظام موارد بشرية يعرض «نسبة التوطين» كرقم واحد لهذا الشهر يخفي هذه الحقيقة عن مديره. وهذا الدليل يبني البديل: محرّك يحسب النطاق بالمعادلة الرسمية، ويعرف مَن يُحتسب فعلاً ومَن لا يُحتسب، ويقول لك في أي سنة تسقط ومَن تحتاج توظيفه قبل ذلك.
غطّينا ربط أنظمة الموارد البشرية بمنصة قوى — توثيق العقود، مستويات التكامل، أخطاء الـ API الشائعة — في دليل تكامل قوى مع أنظمة الموارد البشرية. ذلك المقال يشرح كيف تصل إلى البيانات. وهذا الدليل يشرح ماذا تفعل بها بعد أن تصل.
المتطلبات المسبقة
قبل أن تبدأ، تأكّد من توفّر:
- Node.js 20 فأحدث و TypeScript 5 فأحدث
vitestأو أي مشغّل اختبارات مكافئ- إلمام أساسي بـ discriminated unions ودوال JavaScript الرياضية
- نسخة من الدليل الإجرائي لبرنامج نطاقات المطور الصادر عن وزارة الموارد البشرية والتنمية الاجتماعية — تحديداً المرفق رقم ١، فهو مصدر كل ثابت في هذا الدليل
ما يفعله هذا المحرّك وما لا يفعله. يحسب هذا الكود النسب المنشورة لكيانٍ بنشاطٍ وحجمٍ معيّنين، بالمعادلة نفسها التي تطبّقها الوزارة. لكنه لا يقرأ ملف منشأتك: قوى وحدها تعرف رمز نشاطك المسجَّل، وفترات السماح، والكيانات التابعة، وأعداد الموظفين المعتمدة لديها. فإذا اختلف المحرّك مع قوى، فالمعتمد ما تعرضه قوى. مهمة المحرّك أن يحسب القاعدة ويكشف الفجوة مبكراً، لا أن يحلّ محل المنصة.
ما الذي ستبنيه
وحدةً واحدة مؤلَّفة من ستّ قطع:
- جدول الثوابت — المرفق رقم ١ بصيغة TypeScript مُنمَّطة
- حاسب العتبات — منحنى ص = م × لوغ(س) + ث، مع تصنيف النطاق
- حساب الفجوة — كم سعودياً يلزم فعلاً، لا كم يبدو أنه يلزم
- العدد القابل للاحتساب — من كشف الرواتب إلى الأرقام التي يعترف بها البرنامج
- توقّع السنوات — القراءة نفسها بثوابت ٢٠٢٧ و ٢٠٢٨
- محرّك التنبيهات — الإنذار قبل الهبوط، لا بعده
الخطوة ١: المعادلة، ولماذا لا يصلح الجدول الثابت
كان المطلوب في النسخة القديمة من نطاقات نسبةً تُقرأ من جدول. أمّا نطاقات المطور فيحسبها:
ص = م × لوغ(س) + ث
- ص النسبة الدنيا المطلوبة لذلك النطاق
- م ثابت المنحنى، لكل نشاط ونطاق
- ث ثابت التسوية، لكل نشاط ونطاق وسنة
- س إجمالي موظفي الكيان
- لوغ اللوغاريتم الطبيعي — الدليل ينصّ على «القيمة اللوغاريثمية الطبيعية»، أي
Math.logلاMath.log10
هذه النقطة الأخيرة تستحق التوقّف: استخدام اللوغاريتم العشري بدل الطبيعي يعطي رقماً معقولاً في مظهره وخاطئاً تماماً في نتيجته، ولن يُنبّهك أي اختبار إن لم تكتبه بنفسك.
والنتيجة العملية للمنحنى أنّ النسبة المطلوبة تتحرّك مع حجم المنشأة. في أغلب الأنشطة ترتفع كلما كبر الكيان. وفي المقاولات والنظافة ثابت المنحنى سالب، فتنخفض النسبة كلما كبرت المنشأة. لهذا لا يمكن لأي جدول ثابت أن يجيب، ولهذا يسأل المحرّك عن النشاط لا عن العدد وحده:
| النشاط | إجمالي الموظفين | الحد الأدنى للأخضر المنخفض (٢٠٢٦) |
|---|---|---|
| البنية التحتية لتقنية المعلومات | ٣٠ | ٣٠٫٠٥٪ |
| مقاولات التشييد والبناء | ٣٠ | ١٢٫٩١٪ |
| مقاولات التشييد والبناء | ١٠٠٠ | ١١٫٦١٪ |
نفس العدد، وضعفان ونصف من الالتزام. أي واجهة تعرض «نسبة التوطين المطلوبة» دون أن تعرف النشاط تعرض رقماً مخترعاً.
الخطوة ٢: تنميط المرفق رقم ١
ابدأ بالأنواع. النطاقات مرتّبة، والترتيب جزء من المنطق لا تفصيل عرضي:
// nitaqat/types.ts
export const BAND_ORDER = ['lowGreen', 'midGreen', 'highGreen', 'platinum'] as const;
export type BandKey = (typeof BAND_ORDER)[number];
/** Red is not a threshold — it is where you are when you clear none of them. */
export type BandStatus = BandKey | 'red';
export const NITAQAT_YEARS = [2026, 2027, 2028] as const;
export type NitaqatYear = (typeof NITAQAT_YEARS)[number];
/**
* Annex 1 gives, per activity and band, one curve constant and one levelling
* constant per commitment year.
*/
export type BandConstants = {
m: number;
c: readonly [number, number, number];
};
export type Activity = {
id: string;
label: string;
bands: Record<BandKey, BandConstants>;
};ثم الثوابت. المرفق رقم ١ يحمل ٤١ نشاطاً في ٦٥٦ ثابتاً؛ وثلاثة أنشطة تكفي لهذا الدليل، على أن تُدخل البقية بالطريقة نفسها:
// nitaqat/annex1.ts
import type { Activity } from './types';
export const ACTIVITIES: Record<string, Activity> = {
manufacturing: {
id: 'manufacturing',
label: 'الصناعات',
bands: {
lowGreen: { m: 1.68, c: [15.08, 18.08, 21.08] },
midGreen: { m: 1.87, c: [21.87, 24.87, 27.87] },
highGreen: { m: 2.08, c: [23.97, 26.97, 29.97] },
platinum: { m: 2.08, c: [29.87, 32.87, 35.87] },
},
},
construction: {
id: 'construction',
label: 'مقاولات التشييد والبناء',
bands: {
lowGreen: { m: -0.37, c: [14.17, 16.17, 18.17] },
midGreen: { m: -0.37, c: [16.17, 18.17, 20.17] },
highGreen: { m: 0, c: [17.5, 19.5, 21.5] },
platinum: { m: 0, c: [22.5, 24.5, 26.5] },
},
},
itInfrastructure: {
id: 'itInfrastructure',
label: 'البنية التحتية لتقنية المعلومات',
bands: {
lowGreen: { m: 3.61, c: [17.77, 19.77, 21.77] },
midGreen: { m: 3.61, c: [24.64, 26.64, 28.64] },
highGreen: { m: 3.61, c: [40, 42, 44] },
platinum: { m: 3.61, c: [50, 52, 54] },
},
},
};لاحظ ثابت المقاولات السالب.
m: -0.37ليس خطأً مطبعياً. المقاولات والنظافة هما النشاطان اللذان يخفّ التزامهما مع الحجم، وأي «تصحيح» لهذه الإشارة يكسر المحرّك بصمت.
الخطوة ٣: العتبات وتصنيف النطاق
// nitaqat/thresholds.ts
import { BAND_ORDER, NITAQAT_YEARS } from './types';
import type { Activity, BandKey, BandStatus, NitaqatYear } from './types';
/** The curve applies from six workers up; below that a flat rule governs. */
export const CURVE_MIN_HEADCOUNT = 6;
export function bandThresholds(
activity: Activity,
totalWorkforce: number,
year: NitaqatYear = 2026,
): Record<BandKey, number> {
const yearIndex = Math.max(0, NITAQAT_YEARS.indexOf(year));
// ln(0) is -Infinity and ln of a fraction is negative, so floor the size.
const ln = Math.log(Math.max(totalWorkforce, 1));
const out = {} as Record<BandKey, number>;
for (const band of BAND_ORDER) {
const { m, c } = activity.bands[band];
// Clamped: the curve is an empirical fit, not an identity. A negative
// constant at a small headcount can produce a faithful negative percentage.
out[band] = Math.min(100, Math.max(0, m * ln + c[yearIndex]));
}
return out;
}
/** The highest band a rate actually clears. */
export function classifyBand(
rate: number,
thresholds: Record<BandKey, number>,
): BandStatus {
let status: BandStatus = 'red';
for (const band of BAND_ORDER) {
if (rate >= thresholds[band]) status = band;
}
return status;
}الحدّ الأدنى (Math.max(totalWorkforce, 1)) ليس تجميلاً: منشأة بصفر موظفين تُنتج Math.log(0) === -Infinity، فتصبح كل العتبات -Infinity، ويصنّف المحرّك المنشأة الفارغة بلاتينية. هذا نوع الخطأ الذي يمرّ من المراجعة ويظهر في تقرير للإدارة.
والقصّ عند صفر ومئة له سبب مماثل: المنحنى تقريبٌ إحصائي لا هويّة رياضية، وثابتٌ سالب عند عدد صغير قد يعطي نسبةً تحت الصفر — صحيحة حسابياً، بلا معنى عملياً.
الخطوة ٤: قاعدة المنشآت الصغيرة
من عنده خمسة عاملين فأقل لا تنطبق عليه المعادلة اللوغاريتمية — لكن الالتزام لا يسقط. الوزارة تنصّ: «المنشأة التي لديها عدد العاملين ٥ فأقل يتطلب إضافة عامل سعودي واحد فقط».
الفرق بين «لا ينطبق نطاقات» و«المطلوب سعودي واحد» هو الفرق بين منشأة ممتثلة وأخرى تكتشف المشكلة عند أول طلب تأشيرة:
// nitaqat/small-entity.ts
import { CURVE_MIN_HEADCOUNT } from './thresholds';
export const SMALL_ENTITY_SAUDI_REQUIREMENT = 1;
export type SmallEntityCheck = {
applies: true;
met: boolean;
required: number;
} | null;
/** Null once the curve takes over — the caller should read the band instead. */
export function smallEntityCheck(total: number, saudis: number): SmallEntityCheck {
if (total === 0 || total >= CURVE_MIN_HEADCOUNT) return null;
return {
applies: true,
met: saudis >= SMALL_ENTITY_SAUDI_REQUIREMENT,
required: SMALL_ENTITY_SAUDI_REQUIREMENT,
};
}عندما تشكّ في الاتجاه الذي تخطئ فيه، اختر الاتجاه الذي يبالغ في الالتزام لا الذي يقلّل منه. أن تخبر عميلاً بأنه ملتزم وهو ليس كذلك أسوأ من العكس بكثير.
الخطوة ٥: الفجوة — المقام ينمو معك
هنا يخطئ الجميع تقريباً، بمن فيهم جداول الإكسل التي تدير التوطين في شركات كبيرة.
من عنده ٢٠ سعودياً من ١٠٠ ويريد الوصول إلى ٣٠٪ يحسب: ٣٠ ناقص ٢٠ يساوي ١٠ توظيفات. والصحيح ١٥. لأن كل توظيف سعودي يرفع البسط والمقام معاً: بعد عشر توظيفات يصبح لديك ٣٠ سعودياً من ١١٠، أي ٢٧٫٢٧٪ لا ٣٠٪.
المعادلة الصحيحة:
(س + ن) ÷ (الإجمالي + ن) ≥ الهدف
ومنها: ن ≥ (الهدف × الإجمالي − السعوديون) ÷ (١ − الهدف)
// nitaqat/gap.ts
export type Gap = {
hiresNeeded: number;
nonSaudiReduction: number;
};
export function gapTo(targetPercent: number, saudis: number, total: number): Gap {
const t = targetPercent / 100;
if (t >= 1) throw new RangeError('a 100% target has no finite hiring solution');
const rate = total === 0 ? 0 : (saudis / total) * 100;
if (rate >= targetPercent) return { hiresNeeded: 0, nonSaudiReduction: 0 };
// Hiring lifts both terms: (saudis + x) / (total + x) >= t
const hiresNeeded = Math.max(0, Math.ceil((t * total - saudis) / (1 - t)));
// The other lever — shrink the denominator: saudis / (saudis + y) >= t
const nonSaudis = total - saudis;
const allowedNonSaudis = t === 0 ? Infinity : Math.floor(saudis / t) - saudis;
const nonSaudiReduction = Number.isFinite(allowedNonSaudis)
? Math.max(0, nonSaudis - Math.max(0, allowedNonSaudis))
: 0;
return { hiresNeeded, nonSaudiReduction };
}اعرض الرقمين دائماً. المديرون يتّخذون قراراً مختلفاً حين يرون أن الوصول إلى النطاق التالي يكلّف ثماني توظيفات سعودية أو تقليص ثمانية عشر وافداً؛ وعرض أحد الرقمين وحده يحوّل قراراً إلى أمر واقع.
الخطوة ٦: أي الرؤوس تُحتسب أصلاً
هذه هي الخطوة التي تفصل الحاسبة عن نظام الامتثال.
منذ ١٥ أبريل ٢٠٢٦، لا يُحتسب السعودي ضمن نسبة التوطين إلا إذا كان عقده موثَّقاً إلكترونياً على قوى. أي أن موظفاً سعودياً يعمل لديك فعلاً، ويُصرف له راتب، ومسجّلاً في التأمينات الاجتماعية، قد يكون غير محتسَب لأن عقده لم يُوثَّق.
نظام الموارد البشرية عندك يعدّه؛ ونطاقات لا يعدّه. والفارق بين الرقمين هو ما يجعل لوحة المؤشرات تكذب:
// nitaqat/countable.ts
export type EmployeeRecord = {
id: string;
nationality: 'SA' | 'NON_SA';
/** Qiwa contract authentication state, mirrored from the platform. */
contractAuthenticated: boolean;
/** Whether GOSI shows an open contribution record for the period. */
gosiActive: boolean;
/** For the cases the programme weights differently than one head. */
weight?: number;
};
export type CountableWorkforce = {
/** Saudis that actually count toward the ratio. */
saudis: number;
/** Everyone on an open GOSI record, counted toward Saudization or not. */
total: number;
/** Saudis sitting in the denominator but contributing nothing to the numerator. */
uncountedSaudis: string[];
};
export function countableWorkforce(roster: EmployeeRecord[]): CountableWorkforce {
let saudis = 0;
let total = 0;
const uncountedSaudis: string[] = [];
for (const e of roster) {
// No open GOSI contribution record, no place in either term.
if (!e.gosiActive) continue;
const weight = e.weight ?? 1;
total += weight;
if (e.nationality !== 'SA') continue;
// Since 15 April 2026 a Saudi whose Qiwa contract is not authenticated
// still occupies the denominator but adds nothing to the numerator.
if (!e.contractAuthenticated) {
uncountedSaudis.push(e.id);
continue;
}
saudis += weight;
}
return { saudis, total, uncountedSaudis };
}لاحظ معالجة السعودي غير الموثَّق عقده: يبقى في المقام ويخرج من البسط. هذه هي القراءة المتحفّظة، وهي الاتجاه الصحيح للخطأ. لكن صالِح دائماً total و saudis مع الأعداد التي تعرضها قوى نفسها، واعتبر أي فارق بينهما حادثة تستحق التحقيق لا رقماً يُقرَّب. راجع محرّك اشتراكات التأمينات الاجتماعية لتفاصيل مطابقة سجلّات التأمينات.
الخطوة ٧: السنة التي تسقط فيها
الآن نعود إلى المصنع الذي بدأنا به. ثابت التسوية ث يرتفع ثلاث نقاط كل سنة في أغلب أنشطة الصناعات، والقراءة نفسها بثوابت السنة التالية تكشف المنحدر:
// nitaqat/forecast.ts
import { bandThresholds, classifyBand } from './thresholds';
import { NITAQAT_YEARS } from './types';
import type { Activity, BandStatus, NitaqatYear } from './types';
export type YearOutlook = {
year: NitaqatYear;
rate: number;
band: BandStatus;
lowGreenThreshold: number;
};
/** One unchanging workforce, read against each published year's constants. */
export function ratchetOutlook(
activity: Activity,
saudis: number,
total: number,
): YearOutlook[] {
const rate = total === 0 ? 0 : (saudis / total) * 100;
return NITAQAT_YEARS.map((year) => {
const thresholds = bandThresholds(activity, total, year);
return {
year,
rate: Number(rate.toFixed(2)),
band: classifyBand(rate, thresholds),
lowGreenThreshold: Number(thresholds.lowGreen.toFixed(2)),
};
});
}وناتجها لمصنعنا — ٢٥ سعودياً من ١٠٠:
| السنة | النسبة | الحد الأدنى للأخضر المنخفض | النطاق |
|---|---|---|---|
| ٢٠٢٦ | ٢٥٫٠٠٪ | ٢٢٫٨٢٪ | أخضر منخفض |
| ٢٠٢٧ | ٢٥٫٠٠٪ | ٢٥٫٨٢٪ | أحمر |
| ٢٠٢٨ | ٢٥٫٠٠٪ | ٢٨٫٨٢٪ | أحمر |
والرقم الذي يجعل هذا قابلاً للتصرّف: الوصول إلى عتبة ٢٠٢٧ من وضع اليوم يحتاج توظيفَين اثنين. أمّا انتظار الهبوط إلى الأحمر فيعني تعليق خدمات التأشيرات ونقل الخدمات قبل أن تبدأ التوظيف أصلاً. الفارق بين تنبيهٍ مبكر وأزمةٍ متأخّرة هنا هو موظفان.
الخطوة ٨: التنبيه قبل الحافّة لا عندها
النظام الذي ينبّهك عند الهبوط نظامٌ متأخّر. المطلوب هامش:
// nitaqat/alerts.ts
import { bandThresholds, classifyBand } from './thresholds';
import { ratchetOutlook } from './forecast';
import { BAND_ORDER } from './types';
import type { Activity, BandKey, NitaqatYear } from './types';
export type Alert = {
level: 'info' | 'warn' | 'critical';
code: string;
message: string;
};
/** Percentage points between the current rate and the floor it sits on. */
export function bandBuffer(
rate: number,
thresholds: Record<BandKey, number>,
): number {
const current = classifyBand(rate, thresholds);
if (current === 'red') return 0;
return Number((rate - thresholds[current]).toFixed(2));
}
export function reviewCompliance(input: {
activity: Activity;
saudis: number;
total: number;
uncountedSaudis: string[];
year?: NitaqatYear;
bufferPoints?: number;
}): Alert[] {
const {
activity, saudis, total, uncountedSaudis,
year = 2026, bufferPoints = 2,
} = input;
const alerts: Alert[] = [];
const rate = total === 0 ? 0 : (saudis / total) * 100;
const thresholds = bandThresholds(activity, total, year);
const band = classifyBand(rate, thresholds);
const buffer = bandBuffer(rate, thresholds);
if (band === 'red') {
alerts.push({
level: 'critical',
code: 'BAND_RED',
message: `Red band: ${rate.toFixed(2)}% against a ${thresholds.lowGreen.toFixed(2)}% floor.`,
});
} else if (buffer < bufferPoints) {
alerts.push({
level: 'warn',
code: 'BAND_MARGIN_THIN',
message: `Only ${buffer} points above the ${band} floor — one departure may cost the band.`,
});
}
if (uncountedSaudis.length > 0) {
alerts.push({
level: 'warn',
code: 'CONTRACTS_UNAUTHENTICATED',
message: `${uncountedSaudis.length} Saudi employees have no authenticated Qiwa contract and are not counting.`,
});
}
const falls = ratchetOutlook(activity, saudis, total).find((o) => o.band === 'red');
if (falls && band !== 'red') {
alerts.push({
level: 'critical',
code: 'RATCHET_FALL',
message: `Unchanged, this workforce falls to red in ${falls.year}.`,
});
}
return alerts;
}الهامش الافتراضي نقطتان مئويتان، وهو اختيار وليس قاعدة: في منشأة من مئة موظف، استقالة سعودي واحد تكلّف نحو نقطة مئوية كاملة. اضبط bufferPoints بحسب حجم الكيان لا بحسب ذوقك.
اختبار المحرّك
الاختبارات هنا ليست شكلية. الأرقام الأربعة الأولى مأخوذة من منحنى الصناعات عند مئة موظف، وأي انحراف فيها يعني أن أحدهم بدّل اللوغاريتم أو أخطأ في نسخ ثابت:
// nitaqat/engine.test.ts
import { describe, expect, it } from 'vitest';
import { ACTIVITIES } from './annex1';
import { bandThresholds, classifyBand } from './thresholds';
import { gapTo } from './gap';
import { ratchetOutlook } from './forecast';
import { countableWorkforce } from './countable';
describe('thresholds', () => {
it('computes the 2026 manufacturing curve at 100 employees', () => {
const t = bandThresholds(ACTIVITIES.manufacturing, 100, 2026);
expect(t.lowGreen).toBeCloseTo(22.82, 2);
expect(t.midGreen).toBeCloseTo(30.48, 2);
expect(t.highGreen).toBeCloseTo(33.55, 2);
expect(t.platinum).toBeCloseTo(39.45, 2);
});
it('eases with size where the curve constant is negative', () => {
const small = bandThresholds(ACTIVITIES.construction, 10, 2026).lowGreen;
const large = bandThresholds(ACTIVITIES.construction, 1000, 2026).lowGreen;
expect(small).toBeCloseTo(13.32, 2);
expect(large).toBeCloseTo(11.61, 2);
expect(large).toBeLessThan(small);
});
it('separates two activities that share a headcount', () => {
const it = bandThresholds(ACTIVITIES.itInfrastructure, 30, 2026).lowGreen;
const con = bandThresholds(ACTIVITIES.construction, 30, 2026).lowGreen;
expect(it).toBeCloseTo(30.05, 2);
expect(con).toBeCloseTo(12.91, 2);
});
it('does not call an empty establishment platinum', () => {
const t = bandThresholds(ACTIVITIES.manufacturing, 0, 2026);
expect(Number.isFinite(t.lowGreen)).toBe(true);
expect(classifyBand(0, t)).toBe('red');
});
});
describe('the gap', () => {
it('accounts for the denominator growing with each hire', () => {
expect(gapTo(30.48, 25, 100).hiresNeeded).toBe(8);
});
it('offers the reduction path as well', () => {
expect(gapTo(30.48, 25, 100).nonSaudiReduction).toBe(18);
});
it('returns zero once the target is already met', () => {
expect(gapTo(20, 25, 100)).toEqual({ hiresNeeded: 0, nonSaudiReduction: 0 });
});
});
describe('the ratchet', () => {
it('drops a static workforce a band without anyone moving', () => {
const outlook = ratchetOutlook(ACTIVITIES.manufacturing, 25, 100);
expect(outlook[0].band).toBe('lowGreen');
expect(outlook[1].band).toBe('red');
expect(outlook[1].lowGreenThreshold).toBeCloseTo(25.82, 2);
});
});
describe('countable workforce', () => {
it('keeps an unauthenticated Saudi in the denominator only', () => {
const w = countableWorkforce([
{ id: 'a', nationality: 'SA', contractAuthenticated: true, gosiActive: true },
{ id: 'b', nationality: 'SA', contractAuthenticated: false, gosiActive: true },
{ id: 'c', nationality: 'NON_SA', contractAuthenticated: true, gosiActive: true },
]);
expect(w.saudis).toBe(1);
expect(w.total).toBe(3);
expect(w.uncountedSaudis).toEqual(['b']);
});
});يمكنك تجربة الأرقام نفسها يدوياً في حاسبة نطاقات قبل أن تثق بمخرجات المحرّك: إن اختلف الاثنان، فأحدهما يحمل خطأً في نسخ ثابت.
أخطاء شائعة
استخدام Math.log10 بدل Math.log. أشيع الأخطاء وأصعبها اكتشافاً، لأن النتيجة تبقى نسبةً معقولة المظهر. الدليل ينصّ على اللوغاريتم الطبيعي.
قراءة ث من عمود السنة الخطأ. الثوابت الثلاثة لكل نطاق هي ٢٠٢٦ و ٢٠٢٧ و ٢٠٢٨ بهذا الترتيب. خلطها يعطي نطاقاً صحيحاً في سنةٍ خاطئة.
«تصحيح» الإشارة السالبة في المقاولات. -0.37 مقصود.
عدّ رؤوس كشف الرواتب بدل الرؤوس المحتسَبة. العقد غير الموثَّق على قوى لا يُحتسب مهما كان الراتب مصروفاً.
عرض النسبة دون النطاق. ٢٢٪ رقمٌ لا معنى له: هو أخضر مريح لمقاول، وأحمر لشركة بنية تحتية تقنية.
تجاهل الكيانات التابعة. الحساب على مستوى الكيان كما يعرّفه ملف المنشأة في قوى، وليس على مستوى الفرع الذي تصادف أنه في قاعدة بياناتك.
الخطوات التالية
المحرّك أعلاه يحسب القاعدة. وما يحوّله إلى نظام حقيقي هو المصدر الذي يغذّيه: مزامنة يومية من قوى للعقود الموثَّقة، ومن التأمينات الاجتماعية للسجلّات المفتوحة، ومن كشف الرواتب لتواريخ نهاية العقود. عندها يصبح ratchetOutlook تقريراً شهرياً للإدارة بدل دالّة في ملف.
- تكامل قوى مع أنظمة الموارد البشرية — كيف تصل إلى بيانات العقود الموثَّقة
- محرّك اشتراكات التأمينات الاجتماعية ومطابقتها — مصدر السجلّات المفتوحة
- مدد وحماية الأجور للمطوّرين — الطرف الآخر من ملف الرواتب
- حاسبة نطاقات — للتحقق اليدوي من أي حالة
الخلاصة
نسبة التوطين ليست رقماً واحداً، بل موقعاً على منحنى يتحرّك تحتك. المحرّك الذي بنيناه يحسب العتبات من ثوابت المرفق رقم ١ بدل جدولٍ ثابت، ويحسب فجوة التوظيف بمقامٍ ينمو، ويفصل الرؤوس المحتسَبة عن رؤوس كشف الرواتب، ويقرأ السنوات القادمة بثوابتها هي.
والمكسب الحقيقي ليس دقّة الحساب، بل الوقت: توظيفان اليوم أرخص من نطاقٍ أحمر بعد أربعة أشهر.
تتابع التوطين على جدول إكسل؟ إن كانت نسبتك تُحسب يدوياً مرةً في الشهر، فأنت تعرف موقعك متأخّراً دائماً — وتكتشف العقود غير الموثَّقة عند أول طلب تأشيرة مرفوض. نربط أنظمة الموارد البشرية بقوى والتأمينات والرواتب حتى تُحتسب المؤشرات تلقائياً وتصل التنبيهات قبل الحافّة لا بعدها. تحدّث إلينا لمراجعة وضع منشأتك.