الأدلة التقنية

دورة الشراء Procure-to-Pay من الطلب إلى الدفع

ماذا يحدث لطلب الشراء حتى يدفع المورد، ولماذا تعلق الفاتورة دون استلام، وكيف تحمي المطابقة الثلاثية وفصل المهام مال الشركة.

دورة الشراء Procure-to-Pay من الطلب إلى الدفع
⚠️
متطلبات الضريبة والفوترة الإلكترونية تتغير، فارجع إلى صفحات هيئة الزكاة والضريبة والجمارك وإلى المختص في مؤسستك قبل أي قرار.

يتصل بك مورد الاشتراك السنوي لأداة التذاكر ويقول إن فاتورته لم تدفع منذ شهرين وإنه سيوقف الخدمة يوم الأحد، فتسأل الحسابات الدائنة فتقول إن الفاتورة «معلقة لأنه لا يوجد استلام»، وتسأل المشتريات فتقول إن «أمر الشراء صدر وانتهى دورنا»، ولا يعرف أحد أن المطلوب منك أنت ضغطة واحدة في النظام تؤكد فيها أن الخدمة تعمل. وهذه القصة تتكرر في كل شركة لأن دورة الشراء تمر بخمسة أقسام، ولكل قسم فيها خطوة لا يراها الآخرون، والمهندس الذي يطلب أدوات وسيرفرات واشتراكات طوال السنة هو أكثر من يحتاج أن يرى الدورة كاملة.

وفي المقال السابق بنينا الميزانية، وهنا نتابع ما يحدث عندما تصرف منها فعلاً.

المواضيع في السلسلة:

وسوف نناقش في هذا المقال ما يلي:

  • خطوات الدورة من طلب الشراء إلى الدفع، ومن يملك كل خطوة.
  • مصفوفة الصلاحيات وعروض الأسعار وأمر الشراء.
  • الاستلام للبضائع والخدمات، ولماذا يوقف غيابه الدفع.
  • المطابقة الثلاثية Three-way Match وشروط الدفع مثل Net 30.
  • العقود والتجديد والاشتراكات السنوية المدفوعة مقدماً.
  • تسجيل الموردين، وتغيير الحساب البنكي، وحادثة سرقة بفواتير مزيفة.
  • ضريبة القيمة المضافة 15% في المملكة ومراحل الفوترة الإلكترونية Fatoora.
  • فصل المهام Segregation of Duties، وأشهر ملاحظات المراجعة.
  • مختبر: دورة شراء لقسم تقنية مع سكربت مطابقة ثلاثية، ثم نكسرها بفاتورة كميتها أكبر من المستلم وفاتورة مكررة.

الدورة كاملة في صورة واحدة

دورة الشراء إلى الدفع Procure-to-Pay، واختصاراً P2P، هي كل الخطوات من لحظة احتياجك لشيء إلى لحظة خروج المال من حساب الشركة إلى المورد، والصورة التالية تبينها مع صاحب كل خطوة:

ثماني خطوات مرقمة في صفين: 1 طلب الشراء Requisition من الموظف أو الفريق، 2 الموافقة Approval حسب مصفوفة الصلاحيات، 3 عروض الأسعار RFQ من المشتريات، 4 أمر الشراء PO من المشتريات، 5 الاستلام Receipt من المستودع أو صاحب الخدمة، 6 الفاتورة Invoice من المورد ثم الحسابات الدائنة، 7 المطابقة 3-way Match من النظام والحسابات الدائنة، 8 الدفع Payment من الخزينة، والخطوات 2 و4 و5 و6 و8 بإطار أحمر، وتحتها ملاحظة أن الخطوات ذات الإطار الأحمر يملكها أشخاص مختلفون وهذا ما يسمى فصل المهام
ثماني خطوات وخمسة أقسام على الأقل، ودورك كمهندس في الأولى والخامسة غالباً

وقد يتساءل البعض: لماذا كل هذه الخطوات لشراء عشرة حواسيب؟ والإجابة أن كل خطوة تمنع خطأ أو احتيالاً محدداً، فالموافقة تمنع الصرف خارج الميزانية، وعروض الأسعار تمنع الشراء بسعر مرتفع من مورد قريب لأحدهم، وأمر الشراء يثبت السعر والكمية قبل التسليم، والاستلام يثبت أن الشيء وصل فعلاً، والمطابقة تمنع دفع ما لم يطلب أو لم يصل. وإذا حذفت خطوة منها فإن الخطر الذي تمنعه لا يختفي، وإنما يصبح غير مرئي.

طلب الشراء ومصفوفة الصلاحيات

طلب الشراء Purchase Requisition، واختصاراً PR، هو طلب داخلي تقول فيه ماذا تحتاج وكم ولماذا وعلى أي مركز تكلفة، وهو ليس التزاماً للمورد بعد، وإنما طلب إذن من شركتك. وكلما كان الطلب كاملاً من البداية قصرت الدورة، والطلب الكامل فيه ما يلي:

  • الوصف والمواصفات، وإذا كان جهازاً فمن الموديلات المعتمدة كما شرحنا في قائمة الموديلات المعتمدة.
  • الكمية والسعر التقديري، ومصدره، مثل صفحة أسعار المورد أو عرض سابق.
  • مركز التكلفة وبند الميزانية، وهل هو رأسمالي أم تشغيلي كما شرحنا في المقال السابق.
  • تاريخ الحاجة، وفترة الخدمة إذا كان اشتراكاً.

وبعد الطلب تأتي الموافقة حسب مصفوفة الصلاحيات Delegation of Authority، واختصاراً DoA، وهي جدول تعتمده الإدارة يحدد من يوافق على أي مبلغ، والجدول التالي مثال توضيحي فقط، لأن الحدود تختلف من شركة إلى أخرى:

المبلغ قبل الضريبة (ريال)من يوافقعروض الأسعار المطلوبة
حتى 10,000مدير القسمعرض واحد
من 10,000 إلى 100,000مدير الإدارةعرضان على الأقل
من 100,000 إلى 500,000المدير المالي CFOثلاثة عروض
أكثر من 500,000الرئيس التنفيذي أو لجنةمنافسة رسمية

ولاحظ أن الحد محسوب على قيمة العقد كاملة، وليس على الدفعة الشهرية، فاشتراك بخمسة آلاف ريال شهرياً لثلاث سنوات قيمته 180,000 ريال، ويحتاج موافقة المدير المالي في هذا المثال. والخطأ المعروف هنا هو تقسيم الشراء Split Purchase، أي تقسيم طلب كبير إلى طلبات صغيرة تحت الحد حتى لا يحتاج موافقة أعلى، وهذا من أول ما تبحث عنه المراجعة، كما سيأتي.

عروض الأسعار وأمر الشراء

طلب عرض الأسعار Request for Quotation، واختصاراً RFQ، هو أن ترسل المواصفات نفسها إلى أكثر من مورد وتقارن العروض، ولكل نوع من الطلبات وثيقة مناسبة له:

  • طلب عرض الأسعار RFQ: عندما تعرف بالضبط ما تريد، والمقارنة بالسعر والتوريد، مثل عشرين شاشة بمواصفات محددة.
  • طلب العروض Request for Proposal، واختصاراً RFP: عندما تعرف المشكلة ولا تعرف الحل، مثل نظام تذاكر لقسم الدعم، والمقارنة بالحل والتكلفة الكاملة معاً، وقد شرحنا حساب التكلفة الكاملة في تشتري أم تبني؟.
  • تبرير المصدر الوحيد Single Source Justification: عندما لا يوجد إلا مورد واحد، مثل تجديد ترخيص منتج تستخدمه أصلاً، فتكتب السبب بدلاً من العروض.

وبعد الاختيار تصدر المشتريات أمر الشراء Purchase Order، واختصاراً PO، وهو أول وثيقة ملزمة للمورد، وفيه رقم الأمر والمورد والصنف والكمية وسعر الوحدة وشروط الدفع وعنوان التسليم. والقاعدة التي يجب أن تحفظها: لا تطلب من المورد البدء قبل أن يصله أمر الشراء، والسبب أن العمل الذي يبدأ قبل الأمر ينتهي بفاتورة لا يوجد ما تطابق عليه، فتعلق أسابيع، ويسمى ذلك في المراجعة أمر شراء بعد الحدث After-the-fact PO.

الاستلام: الخطوة التي يملكها المهندس

الاستلام Goods Receipt، ويسمى المستند غالباً Goods Received Note، واختصاراً GRN، هو تسجيل في النظام يقول ماذا وصل وكم ومتى ومن استلمه. وفي البضائع يسجله المستودع عادة بعد عد الصناديق، أما في الخدمات والاشتراكات فلا يوجد مستودع، فيسجله صاحب الخدمة، أي أنت، ويسمى قبول الخدمة Service Acceptance أو Service Entry Sheet في بعض الأنظمة.

وهذه هي الخطوة التي تنسى دائماً، والسبب أن المهندس يرى الخدمة تعمل فيظن أن العمل انتهى، بينما الفاتورة في الحسابات الدائنة لا تدفع حتى يوجد استلام، كما في قصة بداية المقال. ولهذا عندما يبدأ اشتراك جديد أو ينهي مستشار عمله، سجل الاستلام في اليوم نفسه، وسجل الكمية الصحيحة فقط، فإذا وصلت خمس عشرة شاشة من عشرين فسجل خمس عشرة، والسبب أن الاستلام هو الدليل الوحيد عند الشركة على أن المال يستحق للمورد.

💡
الاستلام في وقته يحل مشكلة ثانية في آخر الشهر: إذا وصلت الخدمة في سبتمبر ولم تصل الفاتورة، فإن المحاسب يرى الاستلام في النظام فيسجل المصروف المستحق في سبتمبر، كما شرحنا في أساس الاستحقاق، دون أن يرسل إليك رسالة يسأل فيها.

المطابقة الثلاثية Three-way Match

عندما تصل فاتورة المورد لا تدفع مباشرة، وإنما تطابق مع وثيقتين قبلها، والصورة التالية تبين المطابقة على مثال من مختبر هذا المقال:

ثلاث وثائق: أمر الشراء PO-1002 بعشرين شاشة بسعر 900 ووافق عليه it.director، والاستلام GRN-2 بخمس عشرة شاشة سجلها store.keeper، وفاتورة المورد D-778 بعشرين شاشة بسعر 900 وضريبة 2,700 ومجموع 20,700، وتتجه الأسهم الثلاثة إلى ترس المطابقة، والنتيجة أن السعر مطابق والكمية في الفاتورة 20 أكبر من المستلم 15 فتعلق الفاتورة حتى يصل الباقي أو يرسل المورد إشعاراً دائناً
السعر من أمر الشراء، والكمية من الاستلام، والفاتورة يجب أن تتفق مع الاثنين
  1. الفاتورة مقابل أمر الشراء: هل طلبنا هذا الصنف بهذا السعر من هذا المورد؟
  2. الفاتورة مقابل الاستلام: هل وصل ما يطلب المورد ثمنه؟ والكمية المفوترة حتى الآن لا تتجاوز الكمية المستلمة.
  3. الحدود المسموحة Tolerance: فرق صغير في السعر، مثل التقريب أو تغير سعر الصرف، قد يقبل حسب سياسة الشركة، وما فوقه يعلق.

وهناك مطابقة ثنائية Two-way Match بين الفاتورة وأمر الشراء فقط، وتستخدمها بعض الشركات للخدمات المتكررة الصغيرة، ولكنها تفقد أهم سؤال وهو «هل وصل؟»، ولهذا لا تقم باستخدامها للمبالغ الكبيرة.

وعندما تنجح المطابقة تدخل الفاتورة في دفعة الدفع Payment Run القادمة حسب شروط الدفع Payment Terms المكتوبة في أمر الشراء، وأشهرها ما يلي:

الشرطالمعنى
Net 30المبلغ كاملاً يستحق بعد ثلاثين يوماً من تاريخ الفاتورة
2/10 Net 30خصم 2% إذا دفعت خلال عشرة أيام، وإلا فالمبلغ كاملاً بعد ثلاثين يوماً
EOMيحسب الاستحقاق من نهاية الشهر End of Month وليس من تاريخ الفاتورة
الدفع مقدماً Prepaymentقبل التسليم، وشائع في الاشتراكات السنوية، ويحتاج حذراً لأن الاستلام يأتي بعده

العقود والتجديد والاشتراكات السنوية

أغلب صرف التقنية اليوم اشتراكات، والاشتراك يمر بالدورة نفسها مرة في السنة على الأقل، ولكن له أربعة أمور تحتاج انتباهاً خاصاً:

  • وثائق العقد: غالباً اتفاقية إطارية Master Services Agreement، واختصاراً MSA، فيها الشروط العامة، ونموذج طلب Order Form أو بيان عمل Statement of Work، واختصاراً SOW، فيه الكمية والسعر والمدة، فاقرأ الاثنين معاً، لأن الشروط المهمة مثل التجديد التلقائي تكون غالباً في الاتفاقية الإطارية.
  • التجديد التلقائي Auto-renewal: كثير من العقود تتجدد وحدها إذا لم ترسل إشعار إلغاء قبل مدة محددة، مثل ستين يوماً، فضع تاريخ الإشعار في التقويم وليس تاريخ التجديد، والسبب أن تاريخ التجديد يكون قد فات وقته للتفاوض.
  • زيادة السعر عند التجديد: اسأل عن سقف الزيادة السنوية قبل التوقيع، وضعه في العقد إن أمكن.
  • الدفع السنوي المقدم: يسجل مدفوعاً مقدماً ويوزع على أشهر الخدمة كما رأينا في مختبر المقال الأول، فاكتب فترة الخدمة على أمر الشراء.

وإذا كان المورد سيعالج بيانات مؤسستك فإن التعاقد يحتاج أيضاً مراجعة أمنية وقانونية لشروط البيانات، وطريقة الخروج من الخدمة وتصدير البيانات في نهاية العقد، وقد ناقشنا الارتباط بمورد واحد في قسم Vendor Lock-in.

تسجيل الموردين وتغيير الحساب البنكي

قبل أن يصدر أول أمر شراء لمورد جديد يجب أن يسجل في بيانات الموردين الرئيسية Vendor Master، وفي المملكة يطلب عادة ما يلي:

  • السجل التجاري والاسم الرسمي، حتى تطابق الاسم في العقد والفاتورة.
  • الرقم الضريبي لضريبة القيمة المضافة إذا كان المورد مسجلاً فيها، لأنه يظهر في الفاتورة الضريبية.
  • خطاب من البنك بالحساب الدولي IBAN باسم المورد نفسه.
  • جهة اتصال للمحاسبة، وشروط الدفع المتفق عليها.

وأخطر حقل في هذا السجل كله هو الحساب البنكي، والسبب أن من يغيره يغير وجهة كل دفعة قادمة لهذا المورد، دون أن يتغير أي شيء آخر في الدورة، فالطلب صحيح والاستلام صحيح والفاتورة صحيحة، والمال يذهب إلى المكان الخطأ.

📌
في ديسمبر 2019 حكمت محكمة أمريكية على الليتواني Evaldas Rimasauskas بالسجن خمس سنوات، بعد أن سجل في لاتفيا شركة تحمل اسم مصنع آسيوي حقيقي لعتاد الحاسوب يتعامل مع شركتين أمريكيتين كبيرتين للإنترنت، ثم أرسل إلى موظفيهما رسائل باسم المصنع تطلب تحويل المبالغ المستحقة له إلى حسابات بنكية جديدة في لاتفيا وقبرص، فحولت الشركتان أكثر من 120 مليون دولار بين 2013 و2015، كما في بيان وزارة العدل الأمريكية. والفواتير نفسها كانت لبضائع وخدمات حقيقية، والذي تغير هو الحساب البنكي فقط، ولهذا تتصل الشركات اليوم بالمورد على رقم هاتف معروف لديها مسبقاً قبل أي تغيير في حسابه، وليس على الرقم المكتوب في الرسالة التي تطلب التغيير.

ضريبة القيمة المضافة في الفاتورة

نسبة ضريبة القيمة المضافة Value Added Tax، واختصاراً VAT، في المملكة 15%، وقد طبقت هذه النسبة من 1 يوليو 2020 بعد أن كانت 5%، كما في إعلان هيئة الزكاة والضريبة والجمارك بتاريخ 30 يونيو 2020. وما يهمك منها كصاحب طلب ثلاثة أمور:

  • اكتب المبالغ في الطلب والميزانية قبل الضريبة وبعدها: حتى لا يحسب أحد ال 15% مرتين أو ينساها، فالخلط بينهما من أكثر أسباب تجاوز الموافقة لحدها.
  • ضريبة المدخلات Input VAT: الضريبة التي تدفعها الشركة المسجلة في الضريبة لمورديها تستردها غالباً من الضريبة التي تحصلها من عملائها، فلا تكون تكلفة عليها، بشرط وجود فاتورة ضريبية صحيحة، وهذه تفاصيل يحددها المختص الضريبي في شركتك.
  • الفاتورة الأجنبية: الاشتراك من مورد خارج المملكة قد يحتاج معالجة ضريبية مختلفة، فأرسله إلى المالية كما هو ولا تقم بإضافة الضريبة إليه بنفسك.

الفوترة الإلكترونية Fatoora

الفوترة الإلكترونية في المملكة، واسمها فاتورة Fatoora، تعني أن الفواتير الضريبية تصدر وتحفظ إلكترونياً بصيغة محددة من أنظمة متوافقة، وليست ملفاً من برنامج نصوص. وتطبق على مرحلتين كما في صفحة مراحل التطبيق في موقع الهيئة:

المرحلةالتاريخماذا تتطلب
الأولى: الإصدار Generationمن 4 ديسمبر 2021إصدار الفواتير وحفظها إلكترونياً من حل متوافق لكل المكلفين المقيمين، والفاتورة الورقية الممسوحة ضوئياً لا تعتبر فاتورة إلكترونية
الثانية: الربط والتكامل Integrationمن 1 يناير 2023 على موجاتربط نظام الفوترة بمنصة فاتورة وإصدار الفواتير بصيغة XML أو PDF/A-3 مع XML مضمن، وحقول إضافية

وفي المرحلة الثانية تختلف معاملة نوعين من الفواتير، كما يشرح الدليل التفصيلي للفوترة الإلكترونية من الهيئة (الإصدار الثاني، مايو 2023):

  • الفاتورة الضريبية Tax Invoice: لأغلب التعاملات بين الشركات، ترسل بصيغة XML إلى منصة فاتورة للاعتماد Clearance قبل إرسالها للمشتري، فتتحقق المنصة منها وتضيف إليها الختم التشفيري Cryptographic Stamp ورمز QR.
  • الفاتورة الضريبية المبسطة Simplified Tax Invoice: لأغلب البيع للأفراد، ويصدرها نظام البائع مع رمز QR، ثم يبلغ عنها Reporting إلى المنصة خلال 24 ساعة من إصدارها.

والهيئة تحدد الموجات بحجم الإيرادات الخاضعة للضريبة وتبلغ كل مكلف قبل موعده بستة أشهر على الأقل، وفي إعلانها بتاريخ 24 يوليو 2026 حددت الموجة الخامسة والعشرين بكل من تجاوزت إيراداته الخاضعة للضريبة 187,500 ريال في أي سنة من 2022 إلى 2025، على أن يربط حلوله بالمنصة في موعد أقصاه 1 فبراير 2027. وهذا يعني أن الربط وصل إلى الشركات الصغيرة جداً، فإذا كنت مسؤولاً عن تقنية شركة صغيرة فتأكد أن نظام الفوترة لديكم من الحلول المتوافقة مع المرحلة الثانية قبل أن يصلكم الإشعار.

وماذا يعني هذا لفريق التقنية؟ يعني أن نظام الفوترة أو ERP أصبح نظاماً متصلاً بجهة حكومية، له شهادات تشفير يجب أن تجدد وتحفظ بأمان، وواجهات API لها أخطاء وإشعارات فشل يجب أن يراقبها أحد، وفواتير مبسطة لها مهلة 24 ساعة للإبلاغ، فإذا توقف الربط يوماً كاملاً فهذه حادثة تشغيلية لها أثر ضريبي، وليست عطلاً عادياً. ومن جانب المشتري فإن الفواتير التي تصلك من الموردين الملزمين يجب أن تحمل الرقم الضريبي ورمز QR، وفاتورة بصيغة Word من مورد ملزم بالمرحلة الثانية سؤال يستحق أن ترفعه للمالية.

فصل المهام: لماذا لا يوافق أحد على طلبه

فصل المهام Segregation of Duties، واختصاراً SoD، هو أن توزع الوظائف الحساسة في الدورة على أشخاص مختلفين، حتى لا يستطيع شخص واحد أن يرتكب خطأ أو احتيالاً ويخفيه وحده، والصورة التالية تبين الوظائف الأربع والأزواج التي لا يجمعها شخص واحد:

أربعة صناديق: التفويض Authorization للموافقة على الطلب والأمر، والحيازة Custody للاستلام والمخزون، والتسجيل Recording لبيانات المورد والفواتير، والدفع Payment لإصدار التحويل البنكي، وبينها خطوط حمراء منقطة عليها علامة خطأ: بين التفويض والتسجيل يطلب موافقة لنفسه ويسجل فاتورة وهمية، وبين التفويض والحيازة يوافق على شراء ويؤكد استلام ما لم يصل، وبين التسجيل والدفع يغير حساب المورد ثم يدفع إليه، وبين الحيازة والدفع يستلم ويدفع فيخفي النقص، وبين التفويض والدفع يوافق ويدفع، وفي الأسفل ملاحظة أن الشركة الصغيرة التي لا تستطيع الفصل الكامل تعوض بمراجعة لاحقة من شخص ثالث
كل خط أحمر هو طريقة احتيال يحتاج فيها شخص واحد إلى وظيفتين

وفصل المهام ليس اتهاماً لأحد، وإنما هو حماية للموظف نفسه، فإذا وقع خطأ في دورة مفصولة فإن الدليل يبين أين حدث، أما في دورة يملكها شخص واحد فكل خطأ يبدو كأنه احتيال. ولهذا الأمر علاقة مباشرة بعملك كمهندس، فأنت من يعطي الصلاحيات في نظام ERP غالباً، والدور الذي يسمح لموظف بإنشاء مورد وتسجيل فاتورته وإصدار دفعتها مخالفة يكتشفها المراجع في أول زيارة. وعندما تبني الأدوار فاتبع ما شرحناه في تصميم الصلاحيات Authorization، وأضف قاعدة تمنع الجمع بين الأدوار المتعارضة.

أشهر ملاحظات المراجعة في دورة الشراء

وفيما يلي ما تجده المراجعة الداخلية والخارجية كثيراً في هذه الدورة، ومع كل ملاحظة كيف تكتشفها في البيانات:

الملاحظةكيف تظهر في البيانات
أمر شراء بعد الفاتورة After-the-fact POتاريخ أمر الشراء بعد تاريخ الفاتورة
تقسيم الشراء Split Purchaseعدة طلبات من الشخص نفسه للمورد نفسه في أيام قليلة، كل منها تحت حد الموافقة بقليل
دفع مكرر Duplicate Paymentالمورد نفسه والمبلغ نفسه ورقم فاتورة متشابه بعد إزالة الرموز والمسافات
تغيير الحساب البنكي دون تحققتغيير في بيانات المورد ثم دفعة إليه خلال أيام، دون سجل اتصال للتحقق
تعارض الصلاحيات SoD Conflictمستخدم واحد أنشأ أمر الشراء ووافق عليه، أو أنشأ المورد ودفع له
موردون خاملون Dormant Vendorsموردون لم يتعامل معهم أحد منذ سنوات وما زالوا فعالين، فيصلحون غطاء لدفعة وهمية
تجديد تلقائي دون موافقةفاتورة تجديد اشتراك دون طلب أو موافقة جديدة للسنة الجديدة
التزامات غير مسجلة Unrecorded Liabilitiesخدمات استلمت قبل نهاية السنة وفواتيرها سجلت بعدها، فظهرت السنة أربح مما هي

مختبر: دورة شراء لقسم تقنية مع مطابقة ثلاثية

الآن سوف نبني دورة شراء صغيرة لقسم تقنية في سبتمبر 2026 بجداول DuckDB: أربعة موردين، وأربعة أوامر شراء، والاستلام لكل منها، وفواتير الموردين، ثم سكربت يطابق كل فاتورة مع أمرها واستلامها، ويقترح دفعة الدفع في 30 سبتمبر. ونستخدم ال Image نفسها من مختبر المقال الأول.

📌
الموردون والأشخاص والأرقام الضريبية والحسابات البنكية في هذا المختبر مصطنعة بالكامل وليست صالحة، وحد السعر المسموح 2% اختيار منا للمثال.

وكنا نفكر في تشغيل نظام ERP مفتوح المصدر لهذا المختبر، ولكن الأنظمة الكاملة تحتاج عدة خدمات وذاكرة كبيرة لتعرض فكرة يكفيها سكربت واحد، والمنطق نفسه هو ما تطبقه هذه الأنظمة داخلها، لذلك اكتفينا بالسكربت التالي:

"""A small procure-to-pay flow for an IT department: purchase orders, goods/service receipts, supplier invoices,
three-way match, then a payment run. All vendors, people and amounts are invented (SAR, VAT 15%).
    python p2p.py            a clean month: every invoice has an approved PO and a receipt
    python p2p.py breakit    the same month plus the invoices and changes that a good process must stop
"""
import sys
import duckdb

BREAK = len(sys.argv) > 1 and sys.argv[1] == "breakit"
VAT = 0.15
PRICE_TOLERANCE = 0.02      # accept up to 2% price difference per unit (policy choice in this example)
con = duckdb.connect()
con.execute("""CREATE TABLE vendors(vendor TEXT PRIMARY KEY, name TEXT, vat_no TEXT, iban TEXT, terms_days INT,
               discount_pct DECIMAL(4,2), discount_days INT, bank_changed DATE)""")
con.execute("""CREATE TABLE po(po_no TEXT PRIMARY KEY, vendor TEXT, item TEXT, qty INT, unit_price DECIMAL(12,2),
               requested_by TEXT, approved_by TEXT, po_date DATE)""")
con.execute("CREATE TABLE receipt(grn_no TEXT, po_no TEXT, qty INT, received_by TEXT, grn_date DATE)")
con.execute("""CREATE TABLE invoice(inv_no TEXT, vendor TEXT, po_no TEXT, qty INT, unit_price DECIMAL(12,2),
               vat DECIMAL(12,2), inv_date DATE)""")

con.executemany("INSERT INTO vendors VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
    ("V01", "Laptop supplier", "300000000000003", "SA0000000000000000000001", 30, 2, 10, None),
    ("V02", "Display supplier", "300000000000013", "SA0000000000000000000002", 30, 0, 0, None),
    ("V03", "SaaS reseller", "300000000000023", "SA0000000000000000000003", 30, 0, 0, None),
    ("V04", "Network reseller", "300000000000033", "SA0000000000000000000004", 45, 0, 0, None),
])
con.executemany("INSERT INTO po VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
    ("PO-1001", "V01", "Laptop 14in", 10, 5200, "it.lead", "it.director", "2026-09-01"),
    ("PO-1002", "V02", "Monitor 27in", 20, 900, "it.lead", "it.director", "2026-09-02"),
    ("PO-1003", "V03", "Ticketing SaaS, 12 months", 1, 48000, "it.lead", "cfo", "2026-09-03"),
    ("PO-1004", "V04", "Access switch 48p", 2, 11000, "net.eng", "it.director", "2026-09-05"),
])
con.executemany("INSERT INTO receipt VALUES (?, ?, ?, ?, ?)", [
    ("GRN-1", "PO-1001", 10, "store.keeper", "2026-09-10"),
    ("GRN-2", "PO-1002", 20, "store.keeper", "2026-09-11"),
    ("GRN-3", "PO-1003", 1, "it.lead", "2026-09-12"),        # service receipt: the subscription is live
    ("GRN-4", "PO-1004", 2, "store.keeper", "2026-09-15"),
])
invoices = [
    ("INV-5001", "V01", "PO-1001", 10, 5200, "2026-09-12"),
    ("D-778", "V02", "PO-1002", 20, 900, "2026-09-12"),
    ("SUB-2026-09", "V03", "PO-1003", 1, 48000, "2026-09-13"),
    ("NR-1410", "V04", "PO-1004", 2, 11000, "2026-09-16"),
]
if BREAK:
    con.execute("UPDATE receipt SET qty = 15 WHERE grn_no = 'GRN-2'")                 # only 15 monitors arrived
    con.execute("UPDATE po SET approved_by = 'net.eng' WHERE po_no = 'PO-1004'")       # requester approved his own PO
    con.execute("UPDATE vendors SET iban = 'LV00000000000000000099', bank_changed = DATE '2026-09-28' WHERE vendor = 'V01'")
    invoices[3] = ("NR-1410", "V04", "PO-1004", 2, 11800, "2026-09-16")                # unit price 7.3% above the PO
    invoices += [
        ("INV5001", "V01", "PO-1001", 10, 5200, "2026-09-20"),     # the same invoice sent again, number typed differently
        ("CNS-88", "V03", None, 1, 15000, "2026-09-21"),           # work done and invoiced without any PO
    ]
for no, v, p, q, u, d in invoices:
    con.execute("INSERT INTO invoice VALUES (?, ?, ?, ?, ?, ?, ?)", [no, v, p, q, u, round(q * u * VAT, 2), d])

def show(title, sql):
    rows = con.execute(sql).fetchall(); cols = [c[0] for c in con.description]
    num = lambda v: type(v).__name__ in ("Decimal", "float")
    cells = [[f"{v:,.2f}" if num(v) else "-" if v is None else str(v) for v in r] for r in rows]
    w = [max([len(c)] + [len(r[i]) for r in cells]) for i, c in enumerate(cols)]
    print(f"\n== {title}"); print("  ".join(c.ljust(x) for c, x in zip(cols, w)))
    for r, raw in zip(cells, rows):
        print("  ".join(s.rjust(x) if num(v) else s.ljust(x) for s, v, x in zip(r, raw, w)))
    if not rows: print("(no rows)")

# Three-way match: every invoice line against its PO (what we ordered, at what price) and its receipt (what arrived)
con.execute(f"""CREATE VIEW checks AS
WITH rec AS (SELECT po_no, sum(qty) AS received FROM receipt GROUP BY po_no),
     inv AS (SELECT *, regexp_replace(upper(inv_no), '[^A-Z0-9]', '', 'g') AS inv_key,
                    sum(qty) OVER (PARTITION BY po_no ORDER BY inv_date, inv_no) AS invoiced_so_far,
                    row_number() OVER (PARTITION BY vendor, regexp_replace(upper(inv_no), '[^A-Z0-9]', '', 'g') ORDER BY inv_date) AS seen
             FROM invoice)
SELECT i.inv_no, i.vendor, i.po_no, i.qty, i.unit_price, i.qty * i.unit_price AS net, i.vat, i.qty * i.unit_price + i.vat AS total,
       i.inv_date, v.terms_days, v.discount_pct, v.discount_days, v.bank_changed, p.requested_by, p.approved_by,
       concat_ws('; ',
         CASE WHEN p.po_no IS NULL THEN 'no PO' END,
         CASE WHEN i.seen > 1 THEN 'duplicate of an earlier invoice' END,
         CASE WHEN p.po_no IS NOT NULL AND i.invoiced_so_far > p.qty THEN 'qty above PO' END,
         CASE WHEN p.po_no IS NOT NULL AND i.invoiced_so_far > coalesce(r.received, 0) THEN 'qty above received (' || coalesce(r.received, 0) || ')' END,
         CASE WHEN p.po_no IS NOT NULL AND abs(i.unit_price - p.unit_price) > p.unit_price * {PRICE_TOLERANCE}
              THEN 'price ' || round(100 * (i.unit_price - p.unit_price) / p.unit_price, 1) || '% vs PO' END,
         CASE WHEN p.requested_by = p.approved_by THEN 'PO approved by its requester' END,
         CASE WHEN abs(i.vat - round(i.qty * i.unit_price * {VAT}, 2)) > 0.01 THEN 'VAT not 15%' END
       ) AS problems
FROM inv i LEFT JOIN po p ON p.po_no = i.po_no LEFT JOIN rec r ON r.po_no = i.po_no LEFT JOIN vendors v ON v.vendor = i.vendor""")

show("Three-way match: PO x receipt x invoice", """
    SELECT inv_no, vendor, po_no, qty, unit_price, net, vat, total, CASE WHEN problems = '' THEN 'MATCHED' ELSE 'HOLD' END AS status,
           problems FROM checks ORDER BY inv_date, inv_no""")

# Payment run on 2026-09-30: matched invoices only, and never to a bank account changed in the last 30 days without a call-back
show("Payment run proposal, 2026-09-30", """
    SELECT inv_no, vendor, total,
           (inv_date + INTERVAL (terms_days) DAY)::DATE AS due_date,
           CASE WHEN discount_pct > 0 THEN (inv_date + INTERVAL (discount_days) DAY)::DATE END AS discount_until,
           CASE WHEN discount_pct > 0 THEN round(total * discount_pct / 100, 2) END AS discount_if_early,
           CASE WHEN bank_changed >= DATE '2026-09-30' - INTERVAL 30 DAY THEN 'BLOCK: bank account changed ' || bank_changed || ', call the vendor on a known number'
                ELSE 'pay' END AS action
    FROM checks WHERE problems = '' ORDER BY due_date""")

show("What skipping a 2/10 net 30 discount costs, as a yearly rate", """
    SELECT 2.0 AS discount_pct, 30 - 10 AS extra_days_of_credit,
           round(100 * (2.0 / 98.0) * (365.0 / 20.0), 1) AS implied_yearly_rate_pct""")
show("VAT on this month's matched invoices (input VAT the company can usually reclaim)", """
    SELECT count(*) AS invoices, sum(net) AS net, sum(vat) AS vat, sum(total) AS total FROM checks WHERE problems = ''""")

في السكربت أعلاه لاحظ التالي:

  • الاستعلام checks يحسب لكل فاتورة الكمية المفوترة حتى الآن على أمر الشراء نفسه، ويقارنها بمجموع الكمية المستلمة، فالفاتورة الثانية على الأمر نفسه تحسب مع الأولى.
  • رقم الفاتورة يوحد قبل البحث عن التكرار، فتحذف منه الرموز والمسافات ويحول إلى حروف كبيرة، والسبب أن الفاتورة المكررة تصل غالباً برقم مكتوب بطريقة مختلفة قليلاً.
  • الدفعة لا تذهب إلى مورد تغير حسابه البنكي خلال آخر ثلاثين يوماً حتى يتصل به أحد على رقم معروف، وهذه هي القاعدة التي تستهدف النوع نفسه من الاحتيال الذي رأيناه في حادثة Rimasauskas.
  • الضريبة تحسب 15% من صافي كل فاتورة، وأي فاتورة تختلف ضريبتها تعلق.

الشهر النظيف

docker run --name lab-fin2-p2p -v "$PWD:/w" lab-fin2-duckdb:1 python p2p.py

والمخرج سوف يكون كما يلي:


== Three-way match: PO x receipt x invoice
inv_no       vendor  po_no    qty  unit_price  net        vat       total      status   problems
D-778        V02     PO-1002  20       900.00  18,000.00  2,700.00  20,700.00  MATCHED          
INV-5001     V01     PO-1001  10     5,200.00  52,000.00  7,800.00  59,800.00  MATCHED          
SUB-2026-09  V03     PO-1003  1     48,000.00  48,000.00  7,200.00  55,200.00  MATCHED          
NR-1410      V04     PO-1004  2     11,000.00  22,000.00  3,300.00  25,300.00  MATCHED          

== Payment run proposal, 2026-09-30
inv_no       vendor  total      due_date    discount_until  discount_if_early  action
INV-5001     V01     59,800.00  2026-10-12  2026-09-22               1,196.00  pay   
D-778        V02     20,700.00  2026-10-12  -               -                  pay   
SUB-2026-09  V03     55,200.00  2026-10-13  -               -                  pay   
NR-1410      V04     25,300.00  2026-10-31  -               -                  pay   

== What skipping a 2/10 net 30 discount costs, as a yearly rate
discount_pct  extra_days_of_credit  implied_yearly_rate_pct
        2.00  20                                      37.20

== VAT on this month's matched invoices (input VAT the company can usually reclaim)
invoices  net         vat        total     
4         140,000.00  21,000.00  161,000.00

كل الفواتير الأربع طابقت أوامرها واستلامها، ودفعة الدفع ترتبها حسب تاريخ الاستحقاق. ولاحظ الفاتورة الأولى: مورد الحواسيب يعطي خصم 2% إذا دفعت خلال عشرة أيام، أي حتى 22 سبتمبر، ولكن دفعة الدفع الشهرية في 30 سبتمبر، فضاع خصم قدره 1,196 ريالاً لأن الفاتورة انتظرت موعد الدفعة. والجدول الثالث يبين لماذا يهتم المدير المالي بذلك: ترك خصم 2/10 Net 30 يساوي أن تقترض المبلغ عشرين يوماً بفائدة سنوية تقارب 37.2%، ولهذا تفصل كثير من الشركات الفواتير التي عليها خصم وتدفعها في دفعات أسبوعية. أما الجدول الأخير فيبين أن ضريبة المدخلات في هذا الشهر 21,000 ريال من أصل 161,000.

نكسر الدورة: فاتورة أكبر من المستلم وفاتورة مكررة

الآن نشغل الشهر نفسه مع أخطاء وصلت فعلاً إلى الحسابات الدائنة في شركات كثيرة: خمس عشرة شاشة فقط وصلت من عشرين وفوترت عشرون، ومورد الشبكة رفع سعر الوحدة 7.3% عن أمر الشراء، ومهندس الشبكة وافق على أمر شرائه بنفسه، وفاتورة الحواسيب وصلت مرة ثانية برقم مكتوب دون شرطة، وفاتورة استشارة وصلت دون أي أمر شراء، والحساب البنكي لمورد الحواسيب تغير قبل الدفعة بيومين:

docker run --name lab-fin2-p2p-breakit -v "$PWD:/w" lab-fin2-duckdb:1 python p2p.py breakit

والمخرج طويل، لذلك ننقل منه الجدولين الأولين كما ظهرا، والجدولان الآخران مثل الشهر النظيف مع أرقام أقل:

== Three-way match: PO x receipt x invoice
inv_no       vendor  po_no    qty  unit_price  net        vat       total      status   problems                                                              
D-778        V02     PO-1002  20       900.00  18,000.00  2,700.00  20,700.00  HOLD     qty above received (15)                                               
INV-5001     V01     PO-1001  10     5,200.00  52,000.00  7,800.00  59,800.00  MATCHED                                                                        
SUB-2026-09  V03     PO-1003  1     48,000.00  48,000.00  7,200.00  55,200.00  MATCHED                                                                        
NR-1410      V04     PO-1004  2     11,800.00  23,600.00  3,540.00  27,140.00  HOLD     price 7.3% vs PO; PO approved by its requester                        
INV5001      V01     PO-1001  10     5,200.00  52,000.00  7,800.00  59,800.00  HOLD     duplicate of an earlier invoice; qty above PO; qty above received (10)
CNS-88       V03     -        1     15,000.00  15,000.00  2,250.00  17,250.00  HOLD     no PO

== Payment run proposal, 2026-09-30
inv_no       vendor  total      due_date    discount_until  discount_if_early  action                                                                   
INV-5001     V01     59,800.00  2026-10-12  2026-09-22               1,196.00  BLOCK: bank account changed 2026-09-28, call the vendor on a known number
SUB-2026-09  V03     55,200.00  2026-10-13  -               -                  pay

في المخرج أعلاه لاحظ التالي:

  • فاتورة الشاشات معلقة لأن الكمية المفوترة 20 والمستلمة 15، وهذا هو الصحيح، والحل إما أن يصل الباقي ويسجل استلامه، أو يرسل المورد إشعاراً دائناً Credit Note بقيمة الخمس الناقصة.
  • الفاتورة المكررة INV5001 اكتشفت مع أن رقمها يختلف عن INV-5001، وظهرت عليها ثلاث مشكلات معاً، لأن كميتها مع الأولى تتجاوز أمر الشراء والمستلم. ولو كانت المطابقة بالرقم كما هو لدفعت الفاتورة مرتين، أي 59,800 ريال إضافية.
  • فاتورة مورد الشبكة معلقة لسببين مستقلين: السعر أعلى من الحد المسموح، وأمر الشراء وافق عليه من طلبه، وهذا تعارض في الصلاحيات يجب أن يصحح في النظام وليس في هذه الفاتورة فقط.
  • فاتورة الاستشارة دون أمر شراء، وهذا هو أمر الشراء بعد الحدث الذي ذكرناه، فيحتاج موافقة بأثر رجعي وتفسيراً.
  • فاتورة الحواسيب الأصلية طابقت تماماً، ومع ذلك أوقفتها دفعة الدفع لأن الحساب البنكي تغير في 28 سبتمبر، فالمطابقة الثلاثية وحدها لا تحمي من تغيير الحساب، لأن الوثائق الثلاث صحيحة.

ومن أصل ست فواتير بقيمة 239,890 ريالاً لم يبق للدفع إلا فاتورة واحدة، وهذا هو الشكل الطبيعي لدورة تعمل ضوابطها: التعليق ليس فشلاً، وإنما هو الدورة تسأل سؤالاً قبل أن يخرج المال، والإجابة عنه عادة عند صاحب الطلب أو المستلم، أي عندك.

حذف المختبر

docker rm lab-fin2-p2p lab-fin2-p2p-breakit

ما المطلوب منك كمهندس؟

أغلب الدورة يملكها غيرك، ولكن الخطوات التي تملكها هي التي تعلق الدورة غالباً، ولهذا احفظ القائمة التالية:

  1. اكتب طلب شراء كاملاً: المواصفات، والكمية، والسعر قبل الضريبة وبعدها، ومركز التكلفة، وفترة الخدمة للاشتراكات.
  2. لا تطلب من المورد البدء قبل أن يصله أمر الشراء.
  3. سجل الاستلام في يوم وصول الخدمة أو البضاعة، وبالكمية التي وصلت فعلاً.
  4. ضع تاريخ إشعار الإلغاء لكل عقد في التقويم، وليس تاريخ التجديد.
  5. لا تقم بتمرير أي طلب لتغيير حساب بنكي لمورد، وأرسله إلى المالية كما هو مع تحذير.
  6. إذا كنت تدير صلاحيات نظام ERP، فراجع الأدوار المتعارضة كل ربع سنة.

تدريب عملي

  1. أضف إلى p2p.py فحص تقسيم الشراء: كل أوامر الشراء من الطالب نفسه للمورد نفسه خلال سبعة أيام ومجموعها فوق 10,000 ريال وكل منها تحتها.
  2. أضف استلاماً ثانياً بخمس شاشات للأمر PO-1002 ثم أعد تشغيل وضع breakit، ولاحظ هل تطابق فاتورة الشاشات الآن.
  3. غير تاريخ دفعة الدفع إلى 20 سبتمبر، واحسب كم كانت الشركة ستوفر من الخصم.

الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

  • دورة الشراء ثماني خطوات تمر بخمسة أقسام، وكل خطوة تمنع خطأ أو احتيالاً محدداً.
  • حد الموافقة يحسب على قيمة العقد كاملة، وتقسيم الطلب للهروب منه مخالفة تبحث عنها المراجعة.
  • الاستلام هو خطوتك أنت في الخدمات والاشتراكات، ودونه لا تدفع الفاتورة.
  • المطابقة الثلاثية تقارن الفاتورة بأمر الشراء وبالاستلام، ولا تحمي وحدها من تغيير الحساب البنكي للمورد.
  • ضريبة القيمة المضافة 15%، والفوترة الإلكترونية في مرحلتها الثانية وصلت إلى الشركات التي تتجاوز إيراداتها 187,500 ريال.
  • فصل المهام يحمي الشركة والموظف معاً، والصلاحيات في نظام ERP جزء منه.

وفي المقال القادم بإذن الله سوف نتحدث عن دراسة الجدوى بالأرقام: كيف تحسب العائد والاسترداد والقيمة الحالية لمشروع تقني، وكيف تعرضه على المالية في صفحة واحدة.

سجل التحديثات

  • أكتوبر 2026: كتابة المقال، والاطلاع على صفحات هيئة الزكاة والضريبة والجمارك عن مراحل الفوترة الإلكترونية والموجة الخامسة والعشرين ونسبة 15%، والدليل التفصيلي للفوترة الإلكترونية، وبيان وزارة العدل الأمريكية، بتاريخ 14 أكتوبر 2026، وتشغيل المختبر على Python 3.13.16 وDuckDB 1.5.6 ببيانات مصطنعة.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.