مراجعة عميقة — دورة المشتريات + المخازن + المحاسبة

🛒 دورة المشتريات والمخازن — إزاي المفروض تمشي، إزاي بتمشي فعلًا، وإيه الغلط

مراجعة للدورة اللي وصفتها: طلب شراء → أمر شراء → استلام مبدئي على الأمر → استلام جزئي/كلي → إذن إضافة للمخزن → فاتورة على أساس المُستَلم فعلًا. الخلاصة: الدورة موجودة كمستندات وحالات بالفعل — بس محاسبتها محاسبة دورة مبسّطة. التحليل مبني على فحص كود مباشر، متقاطع من مصدرين مستقلّين (Codex deep code audit + Fable 5).

📅 8 يوليو 2026 🔬 فحص كود Purchases + Inventory (file:line) 🤝 Codex (تحليل عميق) + Fable 5 (الدورة الصح) — متقاطعان الحل: بالإعدادات + تظبيط مستهدف

✅ الخطوات 1→5 بالإعدادات

PR → PO → GRN (ببوابة جودة) → إذن إضافة → رصيد: كلها موجودة وتتفعّل بضبط grn_mode=grn_quality + إعدادات. بدون كود.

🔴 محاسبة الخطوة 6 مكسورة

مفيش GR/IR (بضاعة مستلمة بلا فاتورة) ولا 3-way match. الفاتورة بتخصم "مخزون/دائنون" مباشرة في كل الأوضاع، بلا التحقق من المُستَلم.

🎯 الحل بالإعدادات + محورين

محور مادي (grn_mode) + محور محاسبي جديد (inventory_recognition_point) — نفس نمط sales.cogs_recognition_point المُصلَّح. direct يفضل الافتراضي.

١ الدورة الصح — بالخطوات والقيد المحاسبي

للمصنع الدوائي (دُفعات/صلاحية + بوابة جودة إلزامية)، ده المرجع الصح. لاحظ فصل الاستلام عن الفاتورة محاسبيًا عبر حساب GR/IR.

① طلب شراء من المخزن/الإنتاجلا قيد
② أمر شراء من الطلب أو مستقل + اعتمادلا قيد (التزام)
③ استلام مبدئي GRN مسودة على الأمر + بوابة جودةلا رصيد / لا قيد
④ إذن إضافة اعتماد → يضيف الرصيد فعلًا (بالكمية المقبولة)DR مخزون / CR GR-IR
⑤ فاتورة 3-way match على المُستَلمDR GR-IR / CR دائنون

القيد الكامل عند كل خطوة

الخطوةالقيدملاحظة
③ الاستلام الفعلي (إذن إضافة)DR مخزون / CR GR-IR clearing = كمية مقبولة × تكلفة الأمرالرصيد يدخل هنا. GR-IR = التزام "بضاعة مستلمة لم تصلها فاتورة" — رصيده آخر الشهر = مخصص المستلم-غير-المفوتر.
④ الفاتورة (3-way matched)DR GR-IR DR ض.ق.م مدخلات ±DR/CR فرق سعر PPV / CR دائنونالفاتورة تصفّي GR-IR (مش تخصم مخزون تاني). فرق السعر يروح حساب PPV. الضريبة تفضل عند الفاتورة (صح قانونيًا).
الدفعDR دائنون / CR البنك/الخزنةموجود (PurchasePayment).
المرتجعDR GR-IR (لو مش مفوتر) أو DR دائنون / CR مخزونموجود (PurchaseReturn).
الثابت المحاسبي (invariant): لكل سطر أمر شراء، حساب GR-IR يوصل صفر لما يتستلم بالكامل ويتفوتر بالكامل. أي رصيد مفتوح = مستلم-غير-مفوتر (مخصص مستحق) أو مفوتر-غير-مستلم (تراجع المورّد). ده اللي المدقّق بيدوّر عليه — ومش موجود دلوقتي.

٢ إزاي بتمشي فعلًا دلوقتي — الأوضاع الثلاثة

إعداد purchases.grn_mode = direct | grn | grn_quality (افتراضي direct). ده اللي بيحدّد إزاي البضاعة بتدخل.

الوضعوصول البضاعة / إدخال الرصيدقيد الاستلامقيد الفاتورةتحقّق كمية
direct
(الافتراضي)
مفيش مستند استلام — الفاتورة نفسها تنشئ InventoryReceipt وتعتمده بكمية الفاتورة.DR مخزون/CR دائنون بكمية الفاتورة + يضيف الرصيدلا
grnGRN مسودة → اعتماد ينشئ InventoryReceipt ويضيف الرصيد (كمية GRN).لا قيدDR مخزون/CR دائنون نفسها (مش بيصفّي حاجة)لا
grn_qualityGRN → بوابة جودة (مقبول/مرفوض) → اعتماد يضيف الرصيد بالكمية المقبولة + دُفعة/صلاحية.لا قيدDR مخزون/CR دائنون نفسهالا
🔴 المشكلة الجوهرية: قيد الفاتورة واحد في كل الأوضاع (PostPurchaseBill.php:36-40 · 63-166) — بيخصم "مخزون/دائنون" مباشرة. يعني:
  • في direct: الفاتورة بتقود المخزون — ممكن تعمل رصيد من فاتورة حتى لو البضاعة ما وصلتش.
  • في grn/grn_quality: الرصيد بيدخل عند اعتماد GRN بلا أي قيد GL، والفاتورة لسه بتخصم "مخزون" تاني → الـGL يتحدّث عند الفاتورة مش الاستلام، ومفيش GR-IR يربطهم. نفس باج الـsales-partial-issue بالظبط.

📌 ملاحظات تدفّق (Codex): PR→PO موصول (requests/{id}/convert-to-order) لكن مفيش تحويل PO→GRN ولا GRN→فاتورة (تُنشأ يدويًا بمرجع). "reorder المخزون → PR" مش موصول (المخزون فيه تنبيهات بس)؛ "MRP الإنتاج → PR" موصول. الفاتورة-من-الأمر بتاخد المتبقّي المطلوب لا المُستَلم.

٣ اللي موجود وضعه إيه — الأخطاء (بالـfile:line، متقاطعة)

التشخيص الحاكم: مستندات دورة صح، ومحاسبة دورة مبسّطة. 12 خطأ مُرتّب + باجان كامنان.

#الخطأالشدّةالأثر · file:line
1مفيش محاسبة GR/IR إطلاقًاCriticalالفاتورة تخصم مخزون وتدائن دائنون مباشرة؛ الاستلام مالوش قيد → التزام "مستلم-غير-مفوتر" غير مرئي، القوائم تُظهر التزامات أقل. PostPurchaseBill.php:96-151 · بحث exact عن grni/grir = صفر.
2direct: الفاتورة تقود المخزون الفعليCriticalاعتماد الفاتورة يسجّل دائنون ويزيد المخزون بلا حدث استلام مستقل → رصيد من فواتير لبضاعة ما وصلتش. PostPurchaseBill.php:185-190, 233-261 · الافتراضي direct SettingDefinitionSeeder.php:965.
3غير-direct: الفاتورة تخصم مخزون رغم إضافته في GRN، بلا قيد استلامCriticalالمخزون الفرعي يتحدّث عند GRN، والـGL عند الفاتورة → فرق توقيت + فشل تدقيق، بلا GR-IR. PurchaseGrnController.php:365-406 + PostPurchaseBill.php:85-104.
4كمية الفاتورة لا تُطابَق بالمُستَلم — فوترة بضاعة غير مستلمةHighالفاتورة-من-الأمر تاخد "المطلوب−المفوتر" لا "المستلم". PurchaseBillController.php:427-453 · PurchaseOrderItem.php:95-97.
5فاتورة يدوية تقدر تتخطّى كمية الأمر (over-bill)Highمفيش سقف مقابل الأمر/المستلم؛ يزيد billed_quantity بلا تحقق. StorePurchaseBillRequest.php:38-40 · PurchaseBillController.php:509-528.
6GRN يقدر يستلم أكتر من الأمر (over-receive)Highمفيش تحقق مقابل remainingReceiveQuantity(). PurchaseGrnController.php:508-525.
7اعتماد GRN يدمج "المبدئي" و"إذن الإضافة" في خطوة واحدةHighمفيش بوابة إضافة-للمخزن منفصلة؛ الرصيد يدخل فور اعتماد GRN. PurchaseGrnController.php:365-406.
8direct: التقييم بتكلفة الفاتورة لا تكلفة الاستلامHighطبقة تكلفة المخزون تاخد سعر الفاتورة → فروق السعر غير مرئية. PostPurchaseBill.php:247-253.
9grn_quality: مقبول+مرفوض غير مُقيَّد بكمية GRNMediumممكن قبول أكتر من المستلم فعلًا → تضخّم رصيد. PurchaseGrnController.php:290-306.
10إذن إضافة يدوي يُعتمد تلقائيًا بلا PO/GRNMediumتخطّي ضوابط المشتريات + 3-way. InventoryReceiptController.php:126-130.
11مرتجع مستقل يخصم مخزون بلا ربط بفاتورةMediumPostPurchaseReturn.php:176-207.
12المرتجع يخصم بتكلفة المرتجع لا المتوسط الحالي (WAC)Mediumتشويه تقييم في المتوسط المرجّح. StockService.php:135-139.
🐛 باجان كامنان (Fable): (1) fallback مختلفPurchaseGrnController::getGrnMode() يرجع 'grn' عند غياب الإعداد (سطر 494) بينما PostPurchaseBill يرجع 'direct' (سطر 187) → شركة بلا صف إعداد: GRN يضيف رصيد والفاتورة تعيد الاستلاممخزون مضاعف. الإصلاح: توحيد الاثنين على 'direct'. (2) فحص الوضع فضفاض (!== 'direct') → أي نص غير معروف يوقف إدخال الرصيد → يُقيَّد بالـenum.

٤ الحل — بالإعدادات + تظبيط مستهدف

الفكرة: محورين مستقلّين، كلاهما على مستوى الشركة. الوضع الحالي يفضل الافتراضي — الدورة الصح opt-in.

الإعدادالقيمالمعنى
purchases.grn_mode موجودdirect / grn / grn_qualityالتدفّق المادي: مين/امتى البضاعة تدخل المخزن.
purchases.inventory_recognition_point جديدbill (افتراضي) / receiptالمحاسبة: أي مستند يحمل قيمة المخزون. receipt = GR-IR (نفس نمط sales.cogs_recognition_point).

يتحقّق بالإعدادات (بدون كود)

  • grn_mode = grn_quality → الخطوات 3→5 (GRN مبدئي + جودة + إذن إضافة + استلام جزئي).
  • enable_purchase_requests = true → الخطوة 1 (أصلًا افتراضي).
  • approval_workflow = simple + thresholds → حوكمة الأمر (الخطوة 2).
  • inventory.auto_approve_receipts = false · auto_approve_issues = false · allow_negative = false.

يحتاج كود (الحدّ)

  • قيد GR-IR عند الاستلام + تصفيته عند الفاتورة (المحور المحاسبي).
  • 3-way match — الأعمدة موجودة (received_quantity/billed_quantity)، الحارس ناقص.
  • منع فاتورة بلا PO/استلام (require_po_for_bill).
  • إصلاح الباجين الكامنين + تقرير تسوية GR-IR.
الإضافة الأدنى للمحاسبة: حسابات جديدة purchases.grni_account_id (GR-IR، ضمن الالتزامات — نفس نمط landed_cost_clearing_account_id) + purchases.price_variance_account_id (PPV). عند اعتماد GRN (في وضع receipt): DR مخزون/CR GR-IR (idempotency purchase_grn:{id}). في الفاتورة: تخصم GR-IR بدل المخزون، والفرق لـPPV. direct يفضل يشتغل زي ما هو (المسارات الجديدة ما تتنفّذش). قاعدة تحقّق: receipt يتطلّب grn_mode≠direct + grni معرّف.

٥ المطابقة الثلاثية والاستلام الجزئي

المطابقة على مستوى سطر أمر الشراء (لا سطر GRN — مبالغة v1). الأعمدة موجودة، الحارس بس ناقص.

٦ خطة المراحل (كل مرحلة تُشحن مستقلة، وكله خلف الإعدادات)

Phase 0 — إعدادات فقط (يوم)بدون كود
  • grn_mode=grn_quality · enable_purchase_requests=true · approval_workflow=simple+thresholds · auto_approve_receipts/issues=false · allow_negative=false.
  • الخطوات 1→5 (طلب→أمر→GRN مبدئي→جودة→إذن إضافة) تشتغل فورًا. ضابط تعويضي مؤقت: مخصص "مستلم-غير-مفوتر" يدوي شهري لحد Phase 2.
Phase 1 — الحُرّاس (أيام، رقع صغيرة)
  • إصلاح الباجين الكامنين (fallback + enum). حارس 3-way للكمية + billing_tolerance_percent. require_po_for_bill.
Phase 2 — نواة المحاسبة (أسبوع–أسبوعين)
  • inventory_recognition_point + grni_account_id + price_variance_account_id. قيد GR-IR عند اعتماد GRN؛ الفاتورة تصفّي GR-IR + PPV؛ عكوس الإلغاء/المرتجع؛ اختبارات Pest على نمط CogsAtDeliveryCancelTest.
Phase 3 — التقارير
  • تقرير تقادُم/تسوية GR-IR · لوحة إنجاز أوامر الشراء · (اختياري) مطابقة سطر-GRN وتلميع landed-cost.

٧ الإعدادات الموصى بها لهذا المصنع

الإعدادالقيمةالمرحلة
purchases.grn_modegrn_qualityPhase 0
purchases.enable_purchase_requeststruePhase 0
purchases.approval_workflow + thresholdssimple (حدود حسب المالك)Phase 0
purchases.use_supplier_ap_accounttruePhase 0
purchases.price_includes_taxfalsePhase 0
inventory.auto_approve_receipts / auto_approve_issuesfalsePhase 0
inventory.allow_negativefalsePhase 0
purchases.inventory_recognition_pointreceiptPhase 2
purchases.grni_account_idحساب GR-IR جديد (التزامات)Phase 2
purchases.price_variance_account_idحساب فرق سعر PPV جديدPhase 2
purchases.billing_tolerance_percent · require_po_for_bill0 · truePhase 1
الترحيل نظيف بشكل مميز: في وضع direct الحالي، الاستلام والفاتورة حدث واحد — فوقت التحويل يكون المستلم-غير-المفوتر صفر بنيويًاGR-IR يفتح على صفر، بلا قيد رصيد افتتاحي. الخطوات: اختر بداية شهر → اعتمد الفواتير المسودة القديمة → أنشئ حسابي GR-IR وPPV → فعّل grn_quality → فعّل inventory_recognition_point=receipt. المستندات القديمة تفضل تاريخًا صحيحًا، مفيش إعادة ترحيل.

٨ منهج التحليل ومصادره