عايز طلب فيه ٢٠ صنف تحوّل منه ١٠ دلوقتي، والعشرة الباقيين يفضلوا متاحين لتحويل تاني.
النهارده مستحيل — والقفل مزدوج: التحويل بينسخ كل البنود إجباريًا،
والطلب بيتقفل على حالة «محوَّل» نهائيًا وبيحمل عمود واحد لأمر شراء واحد.
الخبر الكويس: البند ده محلَّل عندنا من قبل ومسجّل في الباك-لوج،
والنمط اللي هنبني بيه موجود ومجرَّب مرتين في نفس الموديول (الاستلام الجزئي والفوترة الجزئية) —
مش اختراع جديد.
أمين المشتريات بيعمل طلب شراء فيه ٢٠ صنف. الأصناف دي مش بتتشترى كلها مرة واحدة ولا من نفس المورد — ١٠ منها متاحين عند مورد دلوقتي، والباقي هيتأخر أو هييجي من مورد تاني.
المطلوب: يحوّل الـ١٠ المتاحين لأمر شراء، ويفضل الطلب شغّال والعشرة الباقيين ظاهرين ومتاحين لتحويل تاني بعدين. والطلب مايتقفلش غير لما كل بنوده تتحوّل.
وطلبت إنها تبقى إعداد — يعني اللي مش عايزها تفضل عنده الدنيا زي ما هي بالظبط.
| الحاجة | الوضع | الموضع |
|---|---|---|
| الـendpoint | POST /api/purchases/requests/{id}/convert-to-order | routes/api.php:38 |
| الصلاحية | purchases.requests.convert | PurchaseOrderController.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. يعني مفيش أي طريقة
تعرف بيها إن البند ده اتحوّل ولا لأ.
نفس المشكلة اتحلّت قبل كده في نفس الموديول للاستلام والفوترة، وبنفس الشكل بالظبط:
purchase_grn_items.purchase_order_item_id
(…300002:14 + فهرس :43) وpurchase_bill_items.purchase_order_item_id
(…400002:14).purchase_order_items.received_quantity وbilled_quantity
(…200002:26-27).pending / partial / complete بتتحسب منهم:
recalculateReceiveStatus() PurchaseOrder.php:199-222.يعني التصميم المطلوب مش اجتهاد — هو نسخ حرفي لنمط شغّال ومختبَر في نفس الملفات.
فيه مسارين بيعلّموا الطلب «محوَّل»، مش واحد. المسار التاني هو
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)| # | الصنف | الوحدة | المتبقي | الكمية المحوَّلة |
|---|---|---|---|---|
| 1 | Plastic Bottle 120 | قطعة | 500 | 500 |
| 2 | White Plastic Cap | قطعة | 1,200 | 400 |
| 3 | Induction Seal | قطعة | 800 | 0 |
| 4 | Dropper Neck 22 | قطعة | 300 | 0 |
ليه الشكل ده بالذات؟ لأنه مش جديد — هو حرفيًا نفس تفاعل حوار «إنشاء إذن استلام» و«إنشاء فاتورة» في شاشة أوامر الشراء (orders.component.ts:691-753 / 756-800): البنود المستهلكة بالكامل بتختفي، الكمية بتيجي معبّاة على المتبقي، واللي عايز تستبعده بتصفّره. المستخدم عارف الشكل ده من قبل كده.
requests.component.html:336-368): عمودين جداد| # | الصنف | الكمية | المحوَّل | المتبقي | السعر التقديري |
|---|---|---|---|---|---|
| 1 | Plastic Bottle 120 | 500 | 500 | مكتمل | 2.50 |
| 2 | White Plastic Cap | 1,200 | 400 | 800 | 1.10 |
| 3 | Induction Seal | 800 | 0 | 800 | 0.75 |
| رقم الطلب | التاريخ | الحالة | الإجمالي | إجراءات |
|---|---|---|---|---|
| PR-2026-00042 | 2026-08-10 | محوَّل جزئيًا | 4,250.00 | 👁 ➜ تحويل |
| PR-2026-00041 | 2026-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 ~5499 | PARTIALLY_CONVERTED — جنب PARTIALLY_RECEIVED وPARTIALLY_BILLED الموجودين |
ملاحظتان من فحص الواجهة:
١) قائمة حالات الفلتر في requests.component.ts:171-178 مكتوبة نصوص عربية مباشرة في الكود، مش مفاتيح ترجمة. هنمشي على نفس النمط ونسجّلها كـدَين فني.
٢) فيه مسار NgRx كامل للتحويل (requests.effects.ts:167-183) الشاشة مش بتستخدمه — بتنادي الخدمة مباشرة. هنحدّث الاتنين علشان ما يفضلش فيه مسار ميت متعارض.
| # | النطاق | الطبقة | الاختبار |
|---|---|---|---|
| WP1 | الأعمدة + الحالة الجديدة + العلاقة + الفهرس + إصلاح converted_at (عيب ٢) | migration + models | الأعمدة موجودة؛ الحالة القديمة زي ما هي |
| WP2 | الإعداد + قراءته بـgetBool | seeder + validation + lang | مطفي ⟵ السلوك القديم حرفيًا |
| WP3 | ConvertPurchaseRequestToOrderRequest + التحويل الجزئي + منع الزيادة + إعادة حساب الحالة | BE | ٢٠ بند ← ١٠ ← ١٠؛ الزيادة ٤٢٢؛ آخر بند يقفل الطلب |
| WP4 | قفل الباب الخلفي (عيب ١) + فحص الشركة | BE | طلب شركة تانية ٤٢٢/٤٠٣ |
| WP5 | إرجاع الكمية عند إلغاء الأمر (عيب ٣) | BE | ألغِ الأمر ← الكمية رجعت وأمكن تحويلها تاني |
| WP6 | الحوار + العمودين + الحالة في ٥ مواضع + الترجمات | FE | ng build أخضر |
الترتيب مقصود: WP1-2 مايغيّروش أي سلوك (بنية + إعداد مطفي). WP3 هو الميزة. WP4-5 إصلاح عيوب موجودة أصلًا لازم تسبق الواجهة، لأن التحويل الجزئي من غيرهم بيبقى قابل للالتفاف وبيحبس الكميات. WP6 آخر حاجة.
١) الجزئية على مستوى البند بس، ولا الكمية كمان؟
انت وصفتها «١٠ أصناف من ٢٠» — يعني على مستوى البند. لكن السؤال الطبيعي بعدها: ٥٠٠ من أصل ١٢٠٠
من نفس الصنف؟
ترشيحي: الكمية. التكلفة الإضافية شبه صفر (نفس العمود ونفس الحوار)، والبند بيبقى حالة
خاصة من الكمية (حوّل الكمية كلها أو صفر). ولو عملناها بالبند بس دلوقتي، إضافة الكمية بعدين
هتبقى migration تاني وتغيير في الواجهة.
٢) إلغاء أمر الشراء — الكمية ترجع للمتاح؟
ترشيحي: أيوة. لو ماترجعش، إلغاء أمر بالغلط بيحبس البضاعة في طلب مقفول للأبد —
وده بالظبط العيب ٣ الموجود دلوقتي. وأنبّه: الأمر الملغي بيفضل ظاهر في تاريخ الطلب،
الكمية بس هي اللي بترجع للمتاح.
٣) الإعداد افتراضيًا مطفي ولا شغّال؟
ترشيحي: مطفي. كل العملاء الشغالين دلوقتي مايحسّوش بأي فرق، وانت بتفتحه على نسختك
وتجرّبه. لو عايزه شغّال من الأول لكل النسخ قول وأغيّره.
البند ده مسجّل عندنا كبند مؤجَّل رقم ٢ في باك-لوج المشتريات، ومربوط بموديول مقارنة عروض الموردين (RFQ) اللي اتصمّم بالكامل ومستني: بوابة تسعير للموردين بلينك مخصوص لكل مورد، ومصفوفة مقارنة صنف×مورد، وتوليد أمر شراء لكل مورد فايز.
الموديول ده كان محتاج «طلب ← أكتر من أمر» كأساس — وهو بالظبط اللي بتطلبه دلوقتي. يعني الشغل ده مش بند معزول، ده حجر الأساس لحاجة أكبر كانت متوقفة عليه.