تحليل قبل التنفيذ — قراءة كود موثّقة بالسطر · ١٠ أغسطس ٢٠٢٦ · لم يُكتب أي كود

التحويل الجزئي: طلب شراء ← أكتر من أمر شراء

عايز طلب فيه ٢٠ صنف تحوّل منه ١٠ دلوقتي، والعشرة الباقيين يفضلوا متاحين لتحويل تاني.

النهارده مستحيل — والقفل مزدوج: التحويل بينسخ كل البنود إجباريًا، والطلب بيتقفل على حالة «محوَّل» نهائيًا وبيحمل عمود واحد لأمر شراء واحد.

الخبر الكويس: البند ده محلَّل عندنا من قبل ومسجّل في الباك-لوج، والنمط اللي هنبني بيه موجود ومجرَّب مرتين في نفس الموديول (الاستلام الجزئي والفوترة الجزئية) — مش اختراع جديد.

القفل: ١→١ صريح إعداد جديد: واحد عيوب حيّة اتكشفت: ٣ حزم عمل: ٦ اختبارات تغطي التحويل النهارده: ٢ فقط

١ المشكلة

أمين المشتريات بيعمل طلب شراء فيه ٢٠ صنف. الأصناف دي مش بتتشترى كلها مرة واحدة ولا من نفس المورد — ١٠ منها متاحين عند مورد دلوقتي، والباقي هيتأخر أو هييجي من مورد تاني.

المطلوب: يحوّل الـ١٠ المتاحين لأمر شراء، ويفضل الطلب شغّال والعشرة الباقيين ظاهرين ومتاحين لتحويل تاني بعدين. والطلب مايتقفلش غير لما كل بنوده تتحوّل.

وطلبت إنها تبقى إعداد — يعني اللي مش عايزها تفضل عنده الدنيا زي ما هي بالظبط.

٢ الوضع الحالي

مسار التحويل

الحاجةالوضعالموضع
الـendpointPOST /api/purchases/requests/{id}/convert-to-orderroutes/api.php:38
الصلاحيةpurchases.requests.convertPurchaseOrderController.php:57
الجسم المُرسَلالمورد بس — مفيش أي بنودpurchase-request.service.ts:103
بناء بنود الأمرحلقة على كل البنود بالكمية كاملةPurchaseOrderController.php:539-555
حالة الطلب بعدهاConverted — نهائية، مش قابلة للتعديل ولا الإلغاء…:559-563

القفل ١→١ — بالكود

أ) الحالة: canConvert() بيسمح بالتحويل من Approved بس، والوجهة Converted وهي حالة نهائيةcanCancel() وisEditable() الاتنين بيرفضوها. مفيش حالة «محوَّل جزئيًا» أصلًا في الـenum (٦ حالات بس). PurchaseRequestStatus.php:7-12, 46-49

ب) الرابط: purchase_requests.converted_to_order_id — عمود واحد رقمي، من غير مفتاح أجنبي ومن غير فهرس. يعني حتى لو الحالة اتفكّت، الطلب مش قادر يشاور على أكتر من أمر. migration 2026_02_25_100001…:31

ج) التتبّع: purchase_order_items مافيهوش purchase_request_item_id، وpurchase_request_items مافيهوش converted_quantity. يعني مفيش أي طريقة تعرف بيها إن البند ده اتحوّل ولا لأ.

النمط اللي هنبني عليه موجود فعلًا — مرتين

نفس المشكلة اتحلّت قبل كده في نفس الموديول للاستلام والفوترة، وبنفس الشكل بالظبط:

يعني التصميم المطلوب مش اجتهاد — هو نسخ حرفي لنمط شغّال ومختبَر في نفس الملفات.

٣ ٣ عيوب حيّة اتكشفت أثناء الفحص

عيب ١ — باب خلفي غير محروس بيحرق الطلب

فيه مسارين بيعلّموا الطلب «محوَّل»، مش واحد. المسار التاني هو POST /api/purchases/orders العادي: الـFormRequest بتقبل purchase_request_id (StorePurchaseOrderRequest.php:26)، والكنترولر بعدها بيحرق الطلب كله (PurchaseOrderController.php:168-178).

والمسار ده أضعف بكتير من المسار الرسمي: البنود اللي بتتبعت مش بتتطابق مع بنود الطلب خالص، ومفيش فحص assertApprovedForPost، والتحقق exists:purchase_requests,id مش مقصور على الشركة — يعني رقم طلب بتاع شركة تانية بيعدّي التحقق.

أي تصميم للتحويل الجزئي لازم يقفل الباب ده كمان، وإلا هيبقى منفذ للالتفاف على كل الحسابات.

عيب ٢ — تاريخ التحويل «بيتكتب» وهو مش موجود

المسارين الاتنين بيكتبوا 'converted_at' => now() (:175 و:562). العمود ده مش موجود في أي migration، ومش في $fillable. يعني Eloquent بيرمي المفتاح بصمت — من غير خطأ ومن غير ما يتكتب حاجة.

النتيجة: مفيش تاريخ تحويل في النظام كله. ومحدش واخد باله لأنه مابيرميش خطأ.

عيب ٣ — إلغاء أمر الشراء بيسيب الطلب ميت

cancel() (:447-477) وdestroy() (:253-266) مابيلمسوش الطلب خالص — لا بيرجّعوا حالته ولا بيمسحوا المؤشر.

وبما إن converted_to_order_id مالوش مفتاح أجنبي، حذف الأمر بيسيب مؤشر معلّق على طلب متقفل على «محوَّل» — وcanCancel() وisEditable() الاتنين بيرفضوه. الطلب بيبقى سجل ميت للأبد.

ده عيب موجود النهارده، بس مع التحويل الجزئي بيبقى حرِج: لو ألغيت الأمر الأول، كمياته لازم ترجع للمتاح — وإلا الباقي مايتطلبش تاني أبدًا.

٤ الفجوة

البندالموجود النهاردهالمطلوبالتغيير
اختيار البنودكل البنود إجباريًااختيار بنود/كمياتحوار جديد + items[] في الجسم
تتبّع البندغير موجودكام اتحوّل وكام باقيعمود converted_quantity
ربط السطر بالسطرغير موجودسطر الأمر يعرف أصلهعمود purchase_request_item_id
حالة الطلبمحوَّل (نهائية)محوَّل جزئيًا ← محوَّلحالة جديدة في الـenum
الطلب ← أوامرهعمود واحد بلا علاقةعلاقة hasManyعلاقة + فهرس
الإلغاءمابيرجّعش حاجةالكمية ترجع للمتاحخطّاف في cancel()
التحققمفيش FormRequest أصلًاتحقق كاملFormRequest جديدة
الإعدادغير موجودتشغيل/إطفاءمفتاح واحد، افتراضي مطفي

٥ الإعداد المقترح

المفتاح: purchases.allow_partial_request_conversion · النوع boolean · النطاق company · الافتراضي false.

لما يكون مطفي (الافتراضي): السلوك الحالي حرفيًا — التحويل بياخد كل البنود، والطلب يروح Converted. مفيش أي فرق يحسّه أي عميل شغّال دلوقتي.

لما يكون شغّال: الحوار بيعرض البنود بكمياتها المتبقية، والطلب بيروح PartiallyConverted طالما فيه باقي، وConverted لما يخلص.

احترس من العيب المتكرر عندنا: إعداد بيتخزّن ويتعرض في الشاشة بس محدش بيقراه. عشان كده القراءة لازم تكون بـgetBool('…', $companyId, default: false) — النمط اللي ProcurementPolicy ماشي عليه — مش get() مع مقارنة === false، لأن ده لو صف التعريف ناقص بيرجّع null والبوابة تفتح بصمت.

٦ الحالات الحدّية

الحالةالمعالجة المقترحة
تحويل جزئي بالكمية (١٠٠ من ٢٥٠)مدعوم — نفس نمط الاستلام الجزئي بالظبط. القرار ١ تحت.
تحويل زيادة (٣٠٠ من ٢٥٠)مرفوض بـ٤٢٢. الحد في ٣ طبقات: الواجهة [max] + FormRequest + قفل صف داخل المعاملة.
إلغاء أمر محوَّل جزئيًاالكمية ترجع للمتاح، والطلب يرجع «محوَّل جزئيًا» أو «معتمد». القرار ٢.
تعديل بند بعد تحويل جزئيممنوع تقليل الكمية تحت المحوَّل فعلًا. الطلب أصلًا غير قابل للتعديل بعد الاعتماد.
أمر من أكتر من طلبخارج النطاق. الربط يفضل أمر واحد ← طلب واحد؛ الجزئية في الاتجاه التاني.
الموافقاتزي ما هي: الطلب لازم يكون معتمد + مفيش دورة موافقة معلّقة. بيتفحص كل تحويل مش أول واحد بس.
الصلاحياتنفس purchases.requests.convert — مفيش صلاحية جديدة.
تعدد الشركاتالباب الخلفي (عيب ١) بيتقفل بفحص الشركة.

٧ معاينة الواجهة — قبل وبعد

ده أهم قسم: وافق على شكل الشاشة قبل أي كود واجهة.

🔴 قبل — حوار التحويل الحالي (requests.component.html:423-472، عرض ٤٥٠px)
تحويل إلى أمر شراء
المورد *
اختر المورد ▾
إلغاء تحويل
⚠ خانة واحدة بس. مفيش أي بنود — بيحوّل الـ٢٠ صنف كلهم إجباريًا.
🟢 بعد — الحوار الجديد لما الإعداد شغّال (عرض ~٦٥٠px). نفس شكل حوار «إنشاء إذن استلام» الموجود دلوقتي في أوامر الشراء.
تحويل إلى أمر شراء — PR-2026-00042
المورد *
النور للتوريدات ▾
#الصنفالوحدةالمتبقيالكمية المحوَّلة
1Plastic Bottle 120قطعة500500
2White Plastic Capقطعة1,200400
3Induction Sealقطعة8000
4Dropper Neck 22قطعة3000
البنود المحوَّلة بالكامل مش بتظهر أصلًا. اللي مش عايزه دلوقتي — سيبه صفر.
إلغاء تحويل (٢ بند)

ليه الشكل ده بالذات؟ لأنه مش جديد — هو حرفيًا نفس تفاعل حوار «إنشاء إذن استلام» و«إنشاء فاتورة» في شاشة أوامر الشراء (orders.component.ts:691-753 / 756-800): البنود المستهلكة بالكامل بتختفي، الكمية بتيجي معبّاة على المتبقي، واللي عايز تستبعده بتصفّره. المستخدم عارف الشكل ده من قبل كده.

🟢 بعد — جدول بنود الطلب في شاشة العرض (requests.component.html:336-368): عمودين جداد
#الصنفالكميةالمحوَّلالمتبقيالسعر التقديري
1Plastic Bottle 120500500مكتمل2.50
2White Plastic Cap1,2004008001.10
3Induction Seal80008000.75
🟢 بعد — صف الطلب في القائمة: الحالة الجديدة وزرار التحويل فاضل ظاهر
رقم الطلبالتاريخالحالةالإجماليإجراءات
PR-2026-000422026-08-10محوَّل جزئيًا4,250.00👁   ➜ تحويل
PR-2026-000412026-08-09محوَّل1,900.00👁
زرار التحويل بيفضل ظاهر طالما فيه باقي — ده التغيير الأساسي في الشرط عند html:102.

٨ الملفات المتأثرة

الطبقةالملفالتغيير
الخادمmigrations (جديدة)purchase_request_items.converted_quantity · purchase_order_items.purchase_request_item_id · فهرس على purchase_orders.purchase_request_id · converted_at
Enums/PurchaseRequestStatus.phpحالة PartiallyConverted + تعديل canConvert()
Models/PurchaseRequest.phpعلاقة purchaseOrders(): HasMany + إعادة حساب الحالة
Http/Requests/ (جديدة)ConvertPurchaseRequestToOrderRequest — أول تحقق على الإطلاق للمسار ده
PurchaseOrderController.php:502-575التحويل الجزئي + تحديث العدّادات
PurchaseOrderController.php:168-178قفل الباب الخلفي (عيب ١)
PurchaseOrderController.php:447-477إرجاع الكمية عند الإلغاء (عيب ٣)
الواجهةpurchase-request.service.ts:103الجسم يحمل items[]
requests.component.html:423-472الحوار الجديد
requests.component.html:336-368عمودَي «المحوَّل / المتبقي»
requests.component.ts / .model.tsالحالة الجديدة في ٥ مواضع: الـunion · قائمة الفلتر · خريطة الألوان · شرط الزرار · شرط الإلغاء
i18n ar/en ~5499PARTIALLY_CONVERTED — جنب PARTIALLY_RECEIVED وPARTIALLY_BILLED الموجودين

ملاحظتان من فحص الواجهة:

١) قائمة حالات الفلتر في requests.component.ts:171-178 مكتوبة نصوص عربية مباشرة في الكود، مش مفاتيح ترجمة. هنمشي على نفس النمط ونسجّلها كـدَين فني.

٢) فيه مسار NgRx كامل للتحويل (requests.effects.ts:167-183) الشاشة مش بتستخدمه — بتنادي الخدمة مباشرة. هنحدّث الاتنين علشان ما يفضلش فيه مسار ميت متعارض.

٩ خطة التنفيذ

#النطاقالطبقةالاختبار
WP1الأعمدة + الحالة الجديدة + العلاقة + الفهرس + إصلاح converted_at (عيب ٢)migration + modelsالأعمدة موجودة؛ الحالة القديمة زي ما هي
WP2الإعداد + قراءته بـgetBoolseeder + validation + langمطفي ⟵ السلوك القديم حرفيًا
WP3ConvertPurchaseRequestToOrderRequest + التحويل الجزئي + منع الزيادة + إعادة حساب الحالةBE٢٠ بند ← ١٠ ← ١٠؛ الزيادة ٤٢٢؛ آخر بند يقفل الطلب
WP4قفل الباب الخلفي (عيب ١) + فحص الشركةBEطلب شركة تانية ٤٢٢/٤٠٣
WP5إرجاع الكمية عند إلغاء الأمر (عيب ٣)BEألغِ الأمر ← الكمية رجعت وأمكن تحويلها تاني
WP6الحوار + العمودين + الحالة في ٥ مواضع + الترجماتFEng build أخضر

الترتيب مقصود: WP1-2 مايغيّروش أي سلوك (بنية + إعداد مطفي). WP3 هو الميزة. WP4-5 إصلاح عيوب موجودة أصلًا لازم تسبق الواجهة، لأن التحويل الجزئي من غيرهم بيبقى قابل للالتفاف وبيحبس الكميات. WP6 آخر حاجة.

١٠ قرارات محتاجة رأيك

٣ قرارات

١) الجزئية على مستوى البند بس، ولا الكمية كمان؟
انت وصفتها «١٠ أصناف من ٢٠» — يعني على مستوى البند. لكن السؤال الطبيعي بعدها: ٥٠٠ من أصل ١٢٠٠ من نفس الصنف؟
ترشيحي: الكمية. التكلفة الإضافية شبه صفر (نفس العمود ونفس الحوار)، والبند بيبقى حالة خاصة من الكمية (حوّل الكمية كلها أو صفر). ولو عملناها بالبند بس دلوقتي، إضافة الكمية بعدين هتبقى migration تاني وتغيير في الواجهة.

٢) إلغاء أمر الشراء — الكمية ترجع للمتاح؟
ترشيحي: أيوة. لو ماترجعش، إلغاء أمر بالغلط بيحبس البضاعة في طلب مقفول للأبد — وده بالظبط العيب ٣ الموجود دلوقتي. وأنبّه: الأمر الملغي بيفضل ظاهر في تاريخ الطلب، الكمية بس هي اللي بترجع للمتاح.

٣) الإعداد افتراضيًا مطفي ولا شغّال؟
ترشيحي: مطفي. كل العملاء الشغالين دلوقتي مايحسّوش بأي فرق، وانت بتفتحه على نسختك وتجرّبه. لو عايزه شغّال من الأول لكل النسخ قول وأغيّره.

١١ ملاحظة عن الأثر الأوسع

البند ده مسجّل عندنا كبند مؤجَّل رقم ٢ في باك-لوج المشتريات، ومربوط بموديول مقارنة عروض الموردين (RFQ) اللي اتصمّم بالكامل ومستني: بوابة تسعير للموردين بلينك مخصوص لكل مورد، ومصفوفة مقارنة صنف×مورد، وتوليد أمر شراء لكل مورد فايز.

الموديول ده كان محتاج «طلب ← أكتر من أمر» كأساس — وهو بالظبط اللي بتطلبه دلوقتي. يعني الشغل ده مش بند معزول، ده حجر الأساس لحاجة أكبر كانت متوقفة عليه.