# WP1b — [FIN/BLOCKER] الآثار على الخطة لازم ترجع بوحدة الخطة، مش بالوحدة الأساسية

**التذكرة:** ISS-2026-9260 · **الريبو:** BE فقط · **[FIN] — بيمسّ الحجز والكميات المستهلكة وبوابة الصرف الزائد** · **migration:** لا

---

## المشكلة — انحدار **أدخله إصلاح WP1 نفسه**

إصلاح WP1 حوّل `applyEffects` تقرا `$item->base_quantity` وتستخدمها في **كل** الآثار. ده **صحيح تمامًا للجانب المالي** (تكلفة WIP، القيد، `actual_material_cost`، توزيع اللوطات) — والمراجعة أكّدت إن فجوة الـ55 اتقفلت على المسارين وبكل أشكال الوحدات، وإن عند معامل 1.0 السلوك مطابق بايت-ببايت.

**لكن الجانب المخطّط (الخطة والحجز) مش بالوحدة الأساسية أصلًا:**

| الحقيقة | الدليل |
|---|---|
| `planned_quantity` بيتنسخ من عقدة الـBOM **بلا أي تحويل**، و`unit_id` بياخد وحدة العقدة | `ReleaseProductionOrder::ensureMaterialSnapshot` — `'planned_quantity' => $plannedQty` من `$node['quantity_required']` و`'unit_id' => (int) $node['unit_id']` |
| الحجز نفسه بيتعمل بنفس الرقم الخام و**بلا `$unitId`** | `ReleaseProductionOrder.php:79-86` — `reserve(company, product, warehouse, $quantity, $variant)` بلا وحدة؛ و`StockService::reserve/release` بيحوّلوا **فقط** لما وحدة تتبعت |
| على المسار المباشر المستخدم **مايقدرش** يصرف بغير وحدة الخطة | `IssueMaterials::resolveLines` (:1027) بيختم `unit_id` من `$material->unit_id` |

⇒ **الحجز محفوظ بوحدة الخطة، والـ`planned_quantity` بوحدة الخطة، وبعد WP1 الـ`consumed_quantity` والـ`release()` بقوا بالوحدة الأساسية.** عمودان في **نفس الصف** بوحدتين مختلفتين.

### القياس الفعلي (المراجع شغّل probe: BOM = 2 كرتونة، الكرتونة 12 قطعة، أمر تاني حاجز 20 قطعة على نفس الرصيد، صرف 1 كرتونة)

| | القيمة |
|---|---|
| المحجوز بعد الإصدار (الخطة 2 كراتين) | **2.0** |
| المحجوز قبل الصرف (2 + 20 للأمر التاني) | 22.0 |
| المحجوز بعد الصرف | **10.0** ← اتفكّ **12** مقابل مساهمة قدرها **2** |
| المخزون بعد الصرف | 88.0 ✓ |
| `planned` / `consumed` | **2.0 / 12.0** (المتبقّي −10) |
| `actual_material_cost` | 60.0 ✓ |

⇒ **10 وحدات من حجز أمر تاني اتدمّرت** (`release()` بيقصّ عند صفر على إجمالي الصف، مش لكل حاجز)، والصف المخطّط بقى غير صالح للاستخدام. **قبل الإصلاح** نفس الحالة كانت بتفك 1 وتستهلك 1 — **متسقة مع الخطة** (رغم إن الـWIP كان ناقص 12 ضعف). يعني: المال اتصلّح والخطة اتكسرت.

### نطاق الضرر لو ساب زي ما هو
`consumed(أساسية)` مقابل `planned(وحدة الخطة)` بيسمّم: بوابة الصرف الزائد (:747-750، :825-832 ⇒ `allowedFree` = 0 ⇒ كل صرف تالي **يترفض** أو يتعلّق) · الكوكبِت والمتبقّي (`OrderCockpitController:147`، `ProductionOrderController:464,548`) · `BuildComponentOrders:62` · `BackflushFromStaging:380` (الباكفلَش مايسحبش حاجة) · SQL نقص الـMRP (`MrpController:408`) · و`ComputeOrderVariances:187-211` حيث `act_qty` (أساسية) مقابل `std_qty` (وحدة BOM) **بيوزّع فرق السعر/الاستخدام غلط** (المجموع لسه بيطابق `actual − std` فالإجمالي في الدفاتر سليم، لكن الحسابين منفصلين غلط).

---

## القرار — **الحل الضيّق، مش تطبيع الخطة**

المراجع اقترح تطبيع الخطة للوحدة الأساسية عند اللقطة (`ensureMaterialSnapshot` + 4 كتّاب لقطة تانيين). **مرفوض لهذه التذكرة**، لسببين:
1. بيغيّر **دلالة بيانات موجودة** في كل قواعد العملاء (صفوف `production_order_materials` منشورة بالفعل بوحدة الخطة) ⇒ محتاج backfill migration وقرار عميل — **خارج النطاق المجمّد تمامًا**.
2. المشكلة الجذرية (`reserve()` بيحفظ رقم بوحدة الخطة في عمود بالوحدة الأساسية) **موجودة قبل التذكرة دي** ومالهاش علاقة بالفيتشر.

**⇒ المطلوب: الآثار على الجانب المخطّط ترجع بوحدة الخطة، والجانب المالي يفضل بالوحدة الأساسية.**

| الأثر | الوحدة الصحيحة | ليه |
|---|---|---|
| تكلفة WIP · القيد · `actual_material_cost` · `unit_cost` · توزيع اللوطات (`allocateLegLots`) · كمية سطر `MfgMaterialIssue` | **الأساسية** (`base_quantity`) | ده اللي خرج فعلًا من `stock_balances`، و`unit_cost` مسعّر للوحدة الأساسية (`StockService::getIssueCost` بيحوّل للأساسية قبل التسعير) |
| `consumed_quantity` · `release()` · **بوابة الصرف الزائد** (`checkOverIssueAllowance` + `consumeOverIssueAllowances`) | **وحدة الخطة** (`$material->unit_id`) | لأن `planned_quantity` والحجز الاتنين بوحدة الخطة |

### التنفيذ
اشتقّ **رقمين لكل سطر** بدل واحد، وسمّهم بوضوح:
- `$baseQuantity` = `$item->base_quantity ?? $item->issued_quantity ?? $item->quantity` (سلسلة WP1 الحالية — **ماتغيّرهاش**).
- `$planQuantity` = الكمية بوحدة الخطة = حوّل `$baseQuantity` من الأساسية لـ`$material->unit_id` (لو `material === null` أو الوحدتين واحدة ⇒ نفس الرقم). استعمل `UnitConversionService` الموجود — **ماتخترعش حساب**؛ لو مفيش دالة عكسية جاهزة، اقسم على `factorToBase($product, $material->unit_id)` مع حراسة القسمة على صفر ونفس التقريب (3 خانات) المستخدم في الملف.

**التحقّق الحاسم — عدم الانحدار:** على المسار المباشر `$item->unit_id === $material->unit_id` دايمًا (`resolveLines` :1027)، فـ`$planQuantity` لازم تطلع **مطابقة تمامًا للكمية المدخلة** ⇒ سلوك الخطة والحجز **مطابق بايت-ببايت لما قبل WP1**. أثبت ده باختبار.

**⛔ ماتلمسش:** `ReleaseProductionOrder` · أي كاتب لقطة · `StockService::reserve/release` · أي migration. تطبيع الخطة **تذكرة منفصلة**.

---

## النتائج الأخرى من نفس المراجعة (كلها في نفس الملفات — نفّذها في نفس الـWP)

**M1 — نسب اللوطات (genealogy) لسه بالوحدة المدخلة بينما سطر التصنيع بقى أساسي.**
`IssueMaterials.php:1209` (`autoAllocateFefo`) و`:1121-1200` (`resolveBatchAllocations`) بيحجزوا `$line['quantity']` (مدخلة) على `mfg_batches.allocated_pending`. صرف 1 كرتونة بيكتب سطر تصنيع 12 وحجز نسب 1 — قبل الإصلاح الاتنين كانوا متفقين. الحجز ده بيقابل أرصدة لوطات بالوحدة الأساسية ⇒ **يتحوّل للأساسية** (`$baseQuantity`). انحراف تتبّع/GMP، مش دفاتر.

**M2 — نفس عيب «سطرين لنفس الخامة» موجود في الجار ولم يُصلَح.**
`BackflushFromStaging.php:333-345` و`:197-210` — فرع السطور الصريحة بيجيب نسخة `ProductionOrderMaterial` **منفصلة لكل سطر** وبيعمل نفس الـread-modify-write على `consumed_quantity` ⇒ سطرين لنفس الخامة بيدهسوا بعض، **بالظبط** العيب اللي اتصلح جنبه في `IssueMaterials`. (الفرع الافتراضي بيلفّ على `$order->materials` وآمن.) ⇒ صلّحه بنفس أسلوب المشاركة، والأفضل `increment()` على مستوى قاعدة البيانات عشان الحل يبقى في الطبقة المشتركة. **شغّل كل suite الإنتاج بعده** — الملف ده في مسار الباكفلَش الآلي.

**L1 — حارس إعادة كتابة `unit_id` أضيق من التحويل اللي بيحرسه.**
`IssueMaterials.php:385` بيشترط `product.base_unit_id !== null`، لكن `factorToBase` ممكن ترجع ≠1 عن طريق صف `ProductUnit` حتى لو `base_unit_id` فاضية (`UnitConversionService.php:62-73`) ⇒ منتج كده بياخد كمية محوَّلة والـ`unit_id` المدخلة لسه ملزوقة بيها، و`ComputeOwnMaterialRecovery` بيعيد بثّها على فاتورة تشغيل. ⇒ اجعل الحارس **على التحويل نفسه** (حصل تحويل فعلي؟) مش على `base_unit_id`.

**L2 — تعليق مبالغ فيه.** `whereNull('deleted_at')` على `production_orders`: العمود موجود فمفيش خطأ SQL، لكن `ProductionOrder` **مابيستعملش `SoftDeletes`** ⇒ الشرط خامل والتعليق («الأوامر المحذوفة مستبعدة») بيوصف حماية غير موجودة. صحّح التعليق (أو شيل الشرط) — **ماتضيفش SoftDeletes**.

---

## اختبارات

في `Modules/Production/tests/Feature/ManualIssueAgainstOrderTest.php`:

1. **الاختبار الحالي `:604-630` غلط — صلّحه.** هو بيحطّ `material.unit_id = box` وسايب `planned_quantity = 20` والحجز 20 بالوحدة الأساسية، وبيؤكّد consumed 12 / reserved 8. دي **حالة `ReleaseProductionOrder` ماتقدرش تنتجها أصلًا** — بتشفّر الافتراض بدل ما تختبره، وعشان كده الانحدار عدّى أخضر. خلّي التركيبة واقعية: خطة بالكرتونة ⇒ `planned_quantity` بالكراتين والحجز بنفس الرقم.
2. **الاختبار الحاسم (المقياس اللي أثبت العيب):** خطة 2 كرتونة (12 قطعة/كرتونة) + **أمر تاني حاجز 20 قطعة على نفس الرصيد** + صرف 1 كرتونة ⇒ المحجوز يقع من 22 لـ**21** (مش 10)، `consumed` = **1** (بوحدة الخطة)، `planned − consumed` = **1** (موجب)، **و`actual_material_cost` = 60 و`base_quantity` = 12** (الجانب المالي فاضل مظبوط). ← ده الاختبار اللي بيمنع رجوع الانحدار.
3. **عدم الانحدار عند وحدة = وحدة الخطة:** صرف عادي بوحدة الخطة ⇒ `consumed`/الحجز/البوابة **مطابقين للسلوك قبل WP1** بالظبط.
4. **البوابة بوحدة الخطة:** خطة 20 (بوحدة الخطة)، صرف سطر بكمية تتجاوزها بعد التحويل ⇒ البوابة **بتشتغل** (كانت بتتخطّى قبل كده لأنها كانت بتقارن المدخل بالأساسي).
5. **M1:** صرف بوحدة غير أساسية على منتج بلوطات ⇒ `mfg_batches.allocated_pending` بيتحرّك بالكمية **الأساسية** ومطابق لسطر التصنيع.
6. **M2:** إذن باكفلَش بسطرين صريحين لنفس الخامة (10 ثم 3) ⇒ `consumed_quantity` = **13** مش 3.

**شغّل بـCLI php صراحةً** (الافتراضي php-cgi وبيقتل الرَنَر بصمت — exit 255 بلا مخرجات):
```bash
cd /home/moonui2/moon-erp-be && /opt/cpanel/ea-php82/root/usr/bin/php -d memory_limit=1G vendor/bin/pest Modules/Production/tests
/opt/cpanel/ea-php82/root/usr/bin/php -d memory_limit=1G vendor/bin/pest Modules/Inventory/tests
```
**الأساس — ماتتعدّاهوش:** Production **594 passed / 10 failed** · Inventory **541 / 1**. (العشرة السابقة: 6 `ProductionVarianceTest` · 3 `CostAiAnomalyVarianceTest` · 1 `ConsignmentFoundationTest`.)
⚠️ الشجرة فيها شغل WP2 غير ملتزم (`ProductionOrderController`, `ProductionOrder`, `routes/api.php`, `ProductionSettingDefinitionSeeder`, وملفات `QuickProductionOrder*`) — **ماتلمسهاش ولا ترجّعها**.

## بعد الانتهاء
`./vendor/bin/pint` + `chown moonui2:moonui2` على الملفات الملموسة. **بلا commit وبلا git.**
