فحص للوضع الحالي — بالكود وبالبيانات الفعلية

لما تحذف منتج… إيه اللي بيحصل بالظبط؟

الخبر المطمئن: الحذف مؤقت مش نهائي — الصف بيفضل في قاعدة البيانات، والمخزون والقيود المحاسبية ما بيتلمسوش.

الخبر اللي محتاج قرارك: النظام بيسمح بالحذف من غير أي سؤال — حتى لو المنتج له رصيد وحركة وفواتير. وبعد الحذف، اسم المنتج بيختفي من كارت الصنف وحركات المخزون وبتفضل أرقام بلا اسم.

الحذف: مؤقت (قابل للاسترجاع) الفواتير: سليمة كارت الصنف: الاسم بيضيع ٩ أغسطس ٢٠٢٦

١ الإجابة المختصرة

السؤالالإجابة
بيتحذف نهائي؟ لأ. حذف مؤقت — الصف بيفضل في قاعدة البيانات وبيتوسم بتاريخ الحذف بس، وقابل للاسترجاع.
بيفضل في كل حتة؟ بيفضل في قاعدة البيانات، بس بيختفي من الشاشات — وبشكل غير متسق. فيه شاشات بتعرضه وشاشات لأ.
الفواتير؟ سليمة تمامًا. السطور والمبالغ والإجماليات ما بتتغيّرش، والقيود المحاسبية ما بتتلمسش.
المخزون؟ الأرقام سليمة، بس بقت بلا اسم. الرصيد وسجل الحركات موجودين بالكامل — والمنتج نفسه اختفى من كارت الصنف.
النظام بيمنعني لو المنتج له حركة؟ لأ — ولا سؤال ولا تحذير. الحذف بيتم على طول.

٢ اللي بيحصل تحت لما تدوس «حذف»

تدوس حذف مفيش أي فحص يتوسم بتاريخ الحذف كل البيانات التانية زي ما هي

عمليًا بيحصل حاجة واحدة بس: النظام بيكتب تاريخ الحذف على صف المنتج. وبعد كده أي شاشة بتسأل عن المنتجات بتتجاهله تلقائيًا.

✅ اللي مبيتلمسش خالص

يعني مفيش أي فقدان بيانات، ومفيش رقم بيتغيّر.

٣ بس فيه مشكلة حقيقية — الاسم بيضيع من نص الشاشات

ده اللي طلع من فحص الكود، وهو عدم اتساق مش قرار تصميمي:

المكانبعد الحذفالنتيجة
سطور الفواتيرالاسم يختفيالسطر موجود بكمياته ومبالغه — بلا اسم صنف
أذون الصرف والاستلامالاسم يختفينفس الحكاية
كارت الصنفمالوش وجودالحركات بتفضل مسجّلة والصفحة نفسها مش هتفتح
أرصدة المخزونيختفي من القايمةالرصيد موجود في قاعدة البيانات وغير ظاهر
التقاريرحسب التقريربعضها بيعرضه وبعضها لأ

🔴 والتفصيلة اللي بتوضّح إن ده سهو مش تصميم

النظام محمي فعلًا ضد نفس المشكلة — بس لـألوان ومقاسات المنتج فقط.

فيه تعليق مكتوب في الكود بيشرح ده بالحرف: سطر المستند «سجل تاريخي»، وإخفاء لون اتشال من الكتالوج بعدين «ما يصحّش يعيد كتابة اللي المستند بيقوله إنه حصل». ومكتوب إن من غير الحماية دي «الاسم بيرجع فاضي والمستند بيتطبع من غير اللون خالص».

والحماية دي متطبّقة على اللون… ومش متطبّقة على المنتج نفسه — في ٧ أماكن، كلهم بنفس الشكل.

٧
موديل محمي فيه اللون
٠
موديل محمي فيه المنتج

يعني نفس السطر في الفاتورة: اللون بيفضل ظاهر، واسم الصنف بيختفي. نفس المنطق بالظبط اتطبّق على نص المشكلة بس.

الشكل قدام المستخدم

قبل الحذف
#الصنفاللونالكميةالسعرالإجمالي
1خام بودرة ميلامينأبيض12048.005,760
بعد حذف المنتج — المبالغ سليمة، بس مين ده؟
#الصنفاللونالكميةالسعرالإجمالي
1— (فاضي)أبيض12048.005,760
⚠ اللون فضل واسم الصنف راح — نفس السطر

٤ ده حصل فعلًا على نسختكم

فحصت قاعدة البيانات: فيه ٩ منتجات محذوفة بالفعل، ومنهم منتجات ليها حركة مخزنية:

المنتجتاريخ الحذفعدد الحركات المتبقيةالرصيد وقتها
MFG Smoke RM2026-06-12٧ حركاتصفر
MFG Smoke FG2026-06-12٥ حركاتصفر
Melamine Powder Raw -AUD22026-06-12٣ حركاتصفر
ORDINARY LACTIC ACID 5 % 32026-08-09بلا حركةصفر

والخبر الحلو إن كلهم كان رصيدهم صفر وقت الحذف — يعني لحد دلوقتي مفيش حالة اتحذف فيها منتج وهو له رصيد قايم. المشكلة النظرية لسه ما حصلتش عمليًا.

٥ السيناريو اللي يوجع فعلًا

منتج له رصيد ٥٠٠ وحدة، وحد حذفه بالغلط. اللي هيحصل:

ودي أخطر من ضياع الاسم: ضياع الاسم بيبوّظ الشكل — أما ده بيخفي بضاعة موجودة فعلًا.

٦ رأيي في اللي المفروض يتعمل

١ — النظام يسأل قبل الحذف (الأهم)

لو المنتج له رصيد أو حركة أو فواتير، الحذف يترفض ويقول السبب: «المنتج ده له رصيد ٥٠٠ وحدة و١٢ حركة — استخدم إيقاف التنشيط بدل الحذف».

ده أهم إصلاح، ورخيص: فحص واحد قبل الحذف.

٢ — اسم الصنف يفضل ظاهر في المستندات القديمة

نفس الحماية المطبّقة على اللون تتطبّق على المنتج. فاتورة قديمة لازم تفضل مقروءة للأبد — ده مستند محاسبي، مش شاشة عرض.

الإصلاح سطر واحد في ٧ موديلات، والنمط والتوثيق موجودين خلاص في نفس الملفات.

٣ — فرق واضح بين «إيقاف» و«حذف»

النظام فيه خاصية تنشيط/إيقاف للمنتج. ودي هي المطلوبة في ٩٩٪ من الحالات: الصنف بيبطّل يظهر في الشاشات الجديدة، وتاريخه كله بيفضل مقروء.

الحذف يفضل للمنتجات اللي اتضافت بالغلط ومالهاش أي حركة.

الخلاصة

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

بس فيه بابين مفتوحين: إن حد يحذف منتج له رصيد فيختفي من الشاشات وهو موجود في المخزن، وإن أسماء الأصناف بتضيع من المستندات القديمة بينما ألوانها بتفضل.

والاتنين إصلاحهم صغير: فحص قبل الحذف، وسطر واحد في ٧ موديلات. قول وأعملهم.

٧ اتنفّذ — ٩ أغسطس ٢٠٢٦

الاتنين اتعملوا

١) الحارس قبل الحذف. ProductController::destroy() بقى بيرفض بـ422 في حالتين، كل واحدة برسالة بتقول السبب: الصنف لسه له رصيد (والرسالة فيها الكمية)، أو عليه حركات مخزنية (والرسالة فيها العدد). الرصيد بيتفحص الأول لأنه الإجابة اللي المستخدم يقدر يعمل بيها حاجة. الفحصين الاتنين مقصورين على شركة الصنف.

٢) الاسم على المستندات. المستند اللي بيسمّي صنف اتحذف بقى يطبع اسمه بدل خانة فاضية. بس التصميم اتغيّر في النص — اقرا الكارت الأحمر تحت، ده أهم جزء في الصفحة.

كوميتات: 89752aa7f (الحارس) · 6e6bc117d (إعادة تصميم النقطة ٢) · 306744925 (توصيل مسارات العرض للـ١٩ موديل) · الواجهة 6a0298d26. اتعملهم فول بوش يوم ٩ أغسطس — الريبوهين على a6eca4ef1 (BE) و7bd181b2c (FE)، والفرع وmain على نفس الكوميت.

الاختبارات: ProductDeleteGuardTest ١٢ نجحوا · ProductActiveSearchFilterTest ٢ نجحوا · pest Modules/Inventory ٨٠٧ نجحوا / ٤ فشلوا = الـbaseline الموثّق بالظبط · Modules/Sales + Modules/Purchases ٩٤٠ نجحوا / ١٩ فشلوا — ملاحظة أمانة: مقارنة الـbaseline للرقم ده اتوقفت بقرار المالك توفيرًا للوقت، فالـ١٩ دول مفترَض إنهم موجودين من قبل مش متأكَّد منهم بالتشغيل. التعديل إضافي بحت (الـfallback مابيشتغلش غير لما القيمة تكون null أصلًا، ومفيش eager-load اتغيّر)، وده اللي بيخلّي الافتراض ده معقول — بس مش برهان.

⚠ غلطة مني اتصلّحت قبل ما تخرج — تستاهل تتقري

أول تنفيذ للنقطة ٢ كان ->withTrashed() على علاقة product() نفسها في ٧ موديلات. عملت جرد قبلها للمستهلكين، بس الجرد ده كان بيدوّر على قيود الاستعلام (whereHas وأخواتها) — وفات عليه نمط تاني بالكامل:

if (! $item->product || ! $item->product->track_inventory) { continue; }

الشرط ! $item->product ده بيعمل شغلانتين: «السطر مالوش منتج» و — النهارده — «منتج السطر اتحذف». توسيع العلاقة بيخلّي السطور دي تتحل، فتبطّل تتخطّى وتبدأ تحرّك مخزون وتعمل قيود. المواقع المؤكدة: PostSalesInvoice، PostPurchaseBill، CreateRemainingIssue، CommissionService، وIssueMaterials:430,470 (بوابة اللوطات المتتبّعة بالباتش).

جرد تاني على الاتناشر موديل الباقيين لقى نفس الشكل تاني في PostGrnGrniAccrual، وPostPurchaseReturn (ده if/else — المبلغ مابيختفيش، بيتنقل بين حساب المخزون وحساب المصروف)، وPostSalesReturn، وConfirm/CancelDeliveryNote، وReserve/DeductStoreOrderStock، وLoyaltyPointsService.

جردين، وكل واحد فاته صنف مختلف من المواقع. يبقى أي تصميم أمانه متوقّف على إني لقيت كل المستهلكين هو تصميم غلط من أساسه هنا. فالتصميم اتغيّر:

product() — ما اتلمستش. سلوك الترحيل مثبَّت بالبرهان في كل مكان، من غير ما أعدّ أي حاجة.
productWithTrashed() — علاقة جديدة من trait مشترك (NamesADeletableProduct). مسارات العرض بس هي اللي بتستخدمها، وبتستخدمها صراحةً.

الفرق اللي بيحسم: موقع عرض يفوتني ⟵ اسم فاضي، يعني الوضع الحالي زي ما هو. موقع حارس يفوتني ⟵ مخزون بيتحرّك وقيد بيتكتب. الاتنين مش قابلين للمقارنة.

والاختبار بقى بيثبّت الاتجاهين لكل موديل: productWithTrashed() بيرجّع الصنف المحذوف، وproduct() لازم مايرجّعوش.

سؤال مفتوح — محتاج قرارك (مش مستعجل)

السلوك الحالي، ومش اتغيّر: السطر اللي منتجه محذوف بيتخطّى وقت الترحيل — مفيش حركة مخزن ولا طرف قيد — ومن غير أي تحذير في أي مكان. ده اللي بيحصل النهارده وقبل التعديل ده، وأنا حافظت عليه بالظبط. لكن هل ده المقصود؟ دي حتة تانية خالص.

ولاحظ إنها مش مسألة تاريخية هتخلص: الحارس الجديد بيفحص الرصيد والحركات، مش السطور في المستندات المسودّة — يعني صنف متعملش عليه حركة لسه ينفع يتحذف وهو مكتوب على عرض سعر أو أمر، والمستندات دي ممكن تترحّل بعدين. فالسؤال هيفضل شغّال.

أ) إيقاف التنشيط بقى ليه معنى — اتنفّذ

المشكلة اللي ظهرت: رسالة الرفض بتقول «أوقف تنشيط الصنف بدل الحذف»، وإيقاف التنشيط ماكانش بيخفي الصنف من مربعات الأصناف. الجرد طلّع إن السويتش ده كان بيتحترم في نقطة البيع بس (POSProductController بيفلتر is_active في السيرفر) — وعلى كل المستندات التانية كان بيتجاهَل تمامًا.

الإصلاح في الطبقة المشتركة: product-search.component.ts — المنتقي اللي كل شبكات السطور بتستخدمه — بقى يبعت is_active=true افتراضيًا، فالتغيير الواحد ده بيغطي كل مستندات البيع والشراء (عبر TransactionLineItemsComponent) وشاشات تخطيط الإنتاج كمان.

استثناء مقصود: إذن الصرف وإذن التوريد بيمرّروا [includeInactive]="true". الصنف الموقوف ممكن يكون لسه في المخزن، ولازم أمين المخزن يقدر يطلّع الباقي منه — إخفاؤه هناك كان هيحبس البضاعة. وسطوره بتيجي وعليها علامة «غير نشط» علشان ما يتعرضش بالساكت.

مش متأثر: شاشة المنتجات نفسها (لسه بتعرض كل حاجة — هي المكان اللي بترجّع منه صنف للخدمة)، والسطور المحفوظة على مستند قديم (بتتحل بالـid مش بالبحث).

كوميت الواجهة 6a0298d26 · كوميت الاختبار 1b4b5c044ProductActiveSearchFilterTest بيثبّت إن is_active بيتركّب مع البحث (كتلة البحث مجموعة orWhere؛ لو المجموعة اتكسرت الفلتر بيبطل يشتغل من غير أي خطأ).

ب) الاتناشر مستند التاني

فيه ١٢ نوع مستند تاني بنفس المشكلة. جرد كامل لعلاقات product() في المشروع طلع ٤٩ موديل من غير withTrashed()؛ أغلبها بيانات أساسية (أسعار، وحدات، BOM) وصح إنها تفضل كده. لكن دول سطور مستندات بتتطبع: SalesOrderItem, SalesQuotationItem, SalesDeliveryNoteItem, SalesReturnItem, PurchaseOrderItem, PurchaseRequestItem, PurchaseGrnItem, PurchaseReturnItem, MfgMaterialIssueLine, MfgGoodsReceiptLine, StoreOrderItem, StoreOrderReturnItem.

دول كمان بقى فيهم الـtrait، فكلهم بيقدروا يطبعوا اسم الصنف المحذوف — بنفس التصميم الآمن اللي في الكارت الأحمر فوق، من غير ما product() تتلمس في أي واحد فيهم.