تحليل مخزون — أمانة بضاعة العملاء (Consignment / Toll)

📦 أمانة بضاعة لكل عميل في المخازن — الوضع الحالي وخطة الإكمال

العميل (مصنع تصنيع-للغير) عايز: كل عميل من عملائه له رصيد أمانة من نفس المنتج، متميّز في المخزن، يشوف جرده لكل عميل، يصرف منه للإنتاج، ويقدر يستلف منه — إمّا يشتريه من العميل أو يرجّعه. وكل ده لازم يبقى option بيتقفل ويتشغّل من غير ما يخلّ بنظام المخازن الحالي.

📅 8 يوليو 2026 🔬 فحص كود Inventory + Production (Phase C) — code-verified 🤝 تحليل: Fable 5 + Opus 4.8 الحالة: تحليل وتوصية — قبل التنفيذ

✅ ~80% مبني وحيّ بالفعل

محرّك "Phase C" في الإنتاج: دفتر أمانة لكل عميل + استلام + استلاف + تسوية (شراء/إرجاع) + مخزن أمانة مفروض + شاشة + صلاحيات. مش هنبني من الصفر.

🔴 باج حيّ لازم يتصلّح أولًا

صرف التول بيستهلك مادة العميل فعليًا لكن recordIssue مالوش أي caller → رصيد أمانة العميل ما بينقصش → الأرصدة متضخّمة من وقت تشغيل Phase C.

🎯 الإكمال = 5 حركات مستهدفة

توصيل الصرف · استثناء التقييم · حراسة المنافذ · سطح مخزني · بوابة on/off. صفر جراحة على StockBalance، وصفر خطر على المحلات اللي البوابة عندها مقفولة.

١ الموجود بالفعل (مفحوص بالكود)

النظام بيشحن محرّك أمانة كامل تقريبًا (Phase C) — لكنه مربوط بالإنتاج، دايمًا شغّال، وسطحه في موديول الإنتاج مش المخازن.

القدرةالحالةالتفاصيل (code-verified)
جرد أمانة لكل عميلموجودConsignmentMaterialLedger — رصيد لكل (عميل + منتج + مخزن): received/issued/borrowed/settled/balance + تكلفة معلنة. هو "system of record" منفصل عن الـGL. = الجرد لكل عميل اللي إنت عايزه.
استلام مادة العميلموجودCreateConsignmentReceipt: يحدّث الدفتر + يضيف الكمية الفعلية لـStockBalance بالتكلفة المعلنة (بدون قيد GL) + ينشئ Batches.
فصل الأمانة في المخزنمفروضالاستلام والاستلاف يشترطان مخزن is_consignment=true (validation). يعني بضاعة الأمانة لازم في مخزن أمانة مخصّص — منفصلة فيزيائيًا عن مخزون الشركة.
الاستلاف (Borrow)موجودRecordBorrow: يستهلك المادة في WIP وقت الاستلاف → DR WIP / CR مواد مستحقة للعميل (التزام) بالتكلفة المعلنة. idempotent.
التسوية: شراء أو إرجاعموجودSettleBorrow: شراء = فاتورة شراء (المورّد = العميل، بلا ضريبة) → DR مخزون / CR دائنون + قيد تصفية للالتزام → العميل يبقى دائن AP عادي · إرجاع/replenish = إرجاع نفس المادة، بلا فاتورة، تصفية الالتزام + حركة إرجاع فعلية.
الشاشة + الصلاحيات + الحساباتموجودشاشة consignment (استلام/استلاف/تسوية/دفتر لكل عميل) · صلاحيات production.consignment.{view,receive,settle} · إعداد حساب production.consignment_clearing_account_id + materials_due_customer.
الخلاصة: اللي طلبته (جرد لكل عميل · استلام · صرف · استلاف · شراء/إرجاع) موجود كمحرّك مُختبَر وحيّ. الطريق الاحترافي = نرفعه ونكمّله ونعمله بوابة، مش نعيد بناءه.

! باج حيّ لازم يتصلّح أولًا (أولوية 0)

🔴 دفتر الأمانة بينحرف دلوقتي: ConsignmentService::recordIssue موجود لكن مفيش أي كود بينادي عليه خارج الاختبارات. صرف التول (IssueMaterials / ApproveIssue) بيحرّك المادة فعليًا ويختم السطر ownership=customer، لكن issued_quantity في دفتر العميل ما بيتحرّكشكل صرف تول من وقت Phase C ضخّم رصيد أمانة العميل (بيبان أكتر من الحقيقي). كمان recordIssue مالوش حارس رصيد (على عكس Borrow) → أمانة سالبة ممكنة.

ده مش جزء من الميزة الجديدة — ده تصحيح. لازم يتعمل أولًا (Phase 0) قبل أي بناء فوقه، وإلا كل جرد وبيان عميل هيتبني على أرقام غلط. الإصلاح: توصيل recordIssue في حلقة الصرف لكل سطر ownership=customer بمفتاح العميل $order->tollContract->business_partner_id + إضافة حارس الرصيد.

٢ الناقص لإكمال طلب العميل

① توصيل الصرف + حارس الرصيد باج

الصرف للإنتاج ما بينقّصش دفتر العميل (فوق). + منع الصرف أكتر من المتاح.

② استثناء الأمانة من التقييم

تقارير تقييم المخزون بتجمع StockBalance.total_value بدون استثناء مخازن الأمانة → دفتر المخزون الفرعي يزيد دائمًا عن حساب المخزون في الـGL بقيمة الأمانة (فرق وهمي في أي مطابقة).

③ حراسة المنافذ (Perimeter)

GRN المشتريات / التحويلات / التسويات العامة مفيهاش حارس يمنعها من لمس مخزن أمانة → أي تحويل/استلام غلط بيخلط تكلفة الشركة بالأمانة ويخلّي الدفتر ينحرف عن الفعلي (خصوصًا التحويل بيحرّك مخزون بدون تحريك الدفتر).

④ سطح مخزني (Inventory-side)

النهاردة كل حاجة في موديول الإنتاج. العميل عايزها "في المخازن": استلام + جرد لكل عميل + صرف من موديول المخازن نفسه.

⑤ استلاف على مستوى المخزون

RecordBorrow بيفترض الاستهلاك في WIP (لأمر). الاستلاف لمخزون الشركة (مش مربوط بأمر) — اللي طلبته صراحةً — لسه مش موجود.

⑥ البوابة (on/off)

Phase C دايمًا شغّال — مفيش flag. عايزينه production.enable_consignment يتقفل/يتشغّل بدون أي أثر على المحلات اللي مش بتعمل تول.

٣ القرار المعماري — إزاي نفصل لكل عميل؟

الاختيار الحاسم: نفصل بضاعة كل عميل بالمخزن ولّا بعمود مالك على الرصيد؟

الخيارالحكمالسبب
مخزن أمانة لكل عميل
(القائم + الموصى به)
✅ v1StockBalance مفتاحه أصلًا (منتج+مخزن) → الفصل لكل عميل مجاني. جرد كل عميل = تقرير مخزن عادي، FEFO ما يقدرش يخلط بضاعة عميل بعميل، والدفتر (عميل+منتج+مخزن) يبقى قاطع. صفر كود — شغّال النهاردة. 5–20 عميل = 5–20 مخزن (تافه). معالج wizard ينشئ مخزن لكل عميل.
عمود owner_partner_id على StockBalance✗ مرفوض v1بيغيّر الـunique key وكل lookup للرصيد في 15 موديول (getIssueCost/getProductCost/الحجز/السالب/الجرد/التحويل). فخ: MySQL بيعامل NULL كمتمايز في الـunique → صفوف مخزون-شركة مكرّرة. ومبيجيبش جديد — الحقيقة لكل عميل موجودة في الدفتر أصلًا. (ذكّرنا بباج حديث من نفس نوع الـlookup.)
الدفتر فقط (بلا StockBalance)جزئيلو شِلنا الكمية الفعلية من StockBalance → بنكسر FEFO/الصلاحية/الحجر/الجرد الفعلي — كله محتاجه الدواء. لأ.
التوصية: مخزن أمانة لكل عميل كنموذج تشغيل قياسي (يحل مشكلة FEFO عبر-العملاء تشغيليًا بلا كود). ندعم "مخزن أمانة مشترك" كمان (الدفتر بيتحمّله)، بس الافتراضي = مخزن لكل عميل. مسار الترقية: owner_partner_id يتعمل v3 فقط لو ظهر عميل حقيقي محتاج عدّة ملّاك في نفس المخزن الفيزيائي — وساعتها بميزانية تدقيق الـlookups. مفيش حاجة في v1 بتقفل الباب ده (الدفتر يقدر يعمل backfill).

٤ المعالجة المحاسبية

القاعدة الحاكمة: بضاعة الأمانة مش ملك الشركة — فيزيائيًا موجودة ومرئية، لكن قيمتها خارج ميزانية الشركة (نفس نمط SAP "customer special stock"). الدفتر (بالتكلفة المعلنة) هو مرجع الملكية.

الحدثالمعالجة
استلام أمانةدفتر: received++ · كمية فعلية لـStockBalance (مخزن الأمانة) بالتكلفة المعلنة · لا قيد GL.
GL: لا قيد (off-book)
صرف مباشر
أمر العميل من أمانته
دفتر: issued++ (مع حارس رصيد) · لا قيد GL (آلية WIP-exclusion بتتكفّل بالقيمة). ملكية السطر = customer.
استلاف (Borrow)
أمر تاني/شركة يستهلك أمانة العميل
التزام على الشركة بالتكلفة المعلنة:
DR WIP (أو DR مخزون لو الاستلاف لمخزون الشركة) CR مواد مستحقة للعميل
تسوية: شراء من العميلفاتورة شراء (المورّد=العميل) + قيد تصفية:
DR مخزون CR دائنون (AP) · DR مواد مستحقة CR مخزون
→ الالتزام يتصفّى، والعميل يبقى دائن AP عادي. ⚠️ ضريبة الفاتورة مغلقة على صفر ضريبة حاليًا = غير مطابق لعميل مسجّل VAT (يتصلح: نسمح ضريبة على فاتورة التسوية).
تسوية: إرجاع للعميلإرجاع نفس المادة → بلا فاتورة → تصفية الالتزام + حركة إرجاع فعلية. ⚠️ فرق تكلفة بيصرف مخزون الشركة بالتكلفة المعلنة بينما تكلفته الفعلية مختلفة (خصوصًا FIFO → انحراف GL عن المخزون). الإصلاح: يصرف بالتكلفة الفعلية والفرق يروح حساب ربح/خسارة تسوية أمانة (P&L).

٥ قاعدة الصرف — استهلاك مباشر مقابل استلاف

مفتاح القرار: أمر مين بيستهلك مادة مين.

المستهلِكمالك المادةالمسارالقيد
أمر تول للعميل Aأمانة Aصرف مباشرrecordIssue (issued+=)لا قيد (WIP-exclusion)
أمر الشركة، أو أمر العميل Bأمانة AاستلافRecordBorrowDR WIP / CR مواد مستحقة لـA
⚠️ ممنوع التحويل التلقائي للاستلاف: استلاف بضاعة الغير التزام قانوني — لازم يبقى فعلًا مقصودًا ومصرّحًا (زي RecordBorrow بالظبط). لو الصرف لأمر الشركة لقى نقص ولقى أمانة من نفس المنتج → يعرض زر "استلف من الأمانة" يفتح تدفّق الاستلاف الصريح، مش يستلف بصمت.

٦ البوابة (on/off) — بدون إخلال بالنظام

الإعداد production.enable_consignment (افتراضي false) — زي sales.enable_delivery_notes
3 طبقات حجب routes/controllers (403) · FE (القوائم + خانة is_consignment مخفية) · الأكشنز نفسها ترفض
الصلاحيات تفضل production.consignment.* موجودة — الصلاحية = مين، البوابة = هل

٧ سجل المخاطر (مُرتّب)

#الخطرالمعالجة
1انحراف الدفتر — بيحصل الآنrecordIssue مش موصول → بيانات العملاء غلط لأي استهلاك تول حيّ. → Phase 0.
2تسرّب التقييمقيمة الأمانة في المخزون الفرعي بلا مقابل GL → استثناء مخازن الأمانة من التقييم/التكلفة.
3منافذ غير محروسةGRN/تحويل/تسوية على مخزن أمانة → انحراف فعلي-دفتر صامت. → حراسة المنافذ.
4أمانة سالبةلا حارس رصيد على recordIssue → إضافة حارس (نسخة من Borrow).
5فرق GL/مخزون في الإرجاع (FIFO)صرف بالتكلفة المعلنة بدل الفعلية → حساب ربح/خسارة تسوية.
6خلط عبر-العملاء في مخزن مشتركFEFO أعمى للملكية → نموذج مخزن-لكل-عميل يحلها تشغيليًا.
7ترحيل البوابةلازم تفعيل تلقائي للعملاء اللي عندهم بيانات أمانة وإلا الإطلاق يقفل ميزة حيّة.

٨ خطة المراحل (كل مرحلة تُشحن مستقلة، وكله خلف البوابة من Phase 1)

Phase 0 — التصحيح (أولًا، إصلاح باجات، بلا flag)
  • توصيل recordIssue في IssueMaterials لكل سطر ownership=customer + حارس الرصيد.
  • استثناء مخازن is_consignment من تقارير التقييم/التكلفة.
  • حراسة المنافذ: منع GRN المشتريات / التحويل / التسوية العامة على مخزن أمانة.
Phase 1 — البوابة + السطح المخزني (القيمة الظاهرة للعميل)
  • إعداد production.enable_consignment + seeder + ترحيل تفعيل تلقائي للعملاء الحاليين. حجب routes/قوائم/أكشنز.
  • شاشات في المخازن: استلام أمانة (إعادة استخدام CreateConsignmentReceipt) · تقرير جرد لكل عميل (الدفتر + رصيد مخزن الأمانة، مع عمود فرق فعلي-دفتر = نظام إنذار مبكر).
  • معالج ينشئ مخزن أمانة لكل عميل.
Phase 2 — إكمال الاستلاف
  • borrow_target: stock — استلاف على مستوى المخزون (مش مربوط بأمر): تحويل من مخزن الأمانة لمخزن الشركة + DR مخزون / CR مواد مستحقة.
  • الشراء/الإرجاع في واجهة المخازن · ضريبة على فاتورة الشراء · إصلاح فرق تكلفة الإرجاع (ربح/خسارة) · التقاط Batch على الإرجاع.
Phase 3 — التمتين والتلميع
  • طباعة بيان حساب أمانة بالعربي (العميل هيسلّمه لعملائه — قيمة عالية) · تحويل مخزن أمانة بحركة دفتر مقترنة · تقرير مطابقة/تدقيق · دراسة owner-dimension فقط لو ظهرت حاجة multi-owner حقيقية.

٩ قرارات مطلوبة منك

القرارالخياراتالتوصية
الفصلمخزن لكل عميل · مخزن أمانة مشتركمخزن لكل عميل (افتراضي؛ ندعم المشترك كمان)
البوابة الافتراضيةOFF للجميع + تفعيل تلقائي للحاليينOFF + backfill ON للحاليين
ضريبة على شراء الأمانةبضريبة · بلا ضريبةبضريبة لعميل مسجّل VAT (يُصلَح في Phase 2)
الإرجاعنفس الـlot · مادة مكافئةمكافئة (قياسي في التول) مع التقاط Batch للتتبّع
استلاف لمخزون الشركةمطلوب الآن · لاحقًاPhase 2 (اللي طلبته صراحةً — مش موجود حاليًا)
الخلاصة للمالك: ~80% من الميزة مبني ومُختبَر وحيّ. الإكمال = (1) إصلاح باج توصيل بيضخّم الأرصدة دلوقتي، (2) شرط WHERE في تقارير التقييم، (3) بوابة on/off بالنمط القائم، (4) واجهة مخزنية فوق أكشنز موجودة، (5) نوع استلاف صغير إضافي. لا جراحة على StockBalance، ولا خطر على المحلات اللي البوابة عندها مقفولة — بالنسبالهم التغيير الوحيد كود ما بيشتغلش أصلًا.