الخبر المطمئن: الحذف مؤقت مش نهائي — الصف بيفضل في قاعدة البيانات،
والمخزون والقيود المحاسبية ما بيتلمسوش.
الخبر اللي محتاج قرارك: النظام بيسمح بالحذف من غير أي سؤال — حتى لو
المنتج له رصيد وحركة وفواتير. وبعد الحذف، اسم المنتج بيختفي من كارت الصنف وحركات المخزون
وبتفضل أرقام بلا اسم.
| السؤال | الإجابة |
|---|---|
| بيتحذف نهائي؟ | لأ. حذف مؤقت — الصف بيفضل في قاعدة البيانات وبيتوسم بتاريخ الحذف بس، وقابل للاسترجاع. |
| بيفضل في كل حتة؟ | بيفضل في قاعدة البيانات، بس بيختفي من الشاشات — وبشكل غير متسق. فيه شاشات بتعرضه وشاشات لأ. |
| الفواتير؟ | سليمة تمامًا. السطور والمبالغ والإجماليات ما بتتغيّرش، والقيود المحاسبية ما بتتلمسش. |
| المخزون؟ | الأرقام سليمة، بس بقت بلا اسم. الرصيد وسجل الحركات موجودين بالكامل — والمنتج نفسه اختفى من كارت الصنف. |
| النظام بيمنعني لو المنتج له حركة؟ | لأ — ولا سؤال ولا تحذير. الحذف بيتم على طول. |
عمليًا بيحصل حاجة واحدة بس: النظام بيكتب تاريخ الحذف على صف المنتج. وبعد كده أي شاشة بتسأل عن المنتجات بتتجاهله تلقائيًا.
يعني مفيش أي فقدان بيانات، ومفيش رقم بيتغيّر.
ده اللي طلع من فحص الكود، وهو عدم اتساق مش قرار تصميمي:
| المكان | بعد الحذف | النتيجة |
|---|---|---|
| سطور الفواتير | الاسم يختفي | السطر موجود بكمياته ومبالغه — بلا اسم صنف |
| أذون الصرف والاستلام | الاسم يختفي | نفس الحكاية |
| كارت الصنف | مالوش وجود | الحركات بتفضل مسجّلة والصفحة نفسها مش هتفتح |
| أرصدة المخزون | يختفي من القايمة | الرصيد موجود في قاعدة البيانات وغير ظاهر |
| التقارير | حسب التقرير | بعضها بيعرضه وبعضها لأ |
النظام محمي فعلًا ضد نفس المشكلة — بس لـألوان ومقاسات المنتج فقط.
فيه تعليق مكتوب في الكود بيشرح ده بالحرف: سطر المستند «سجل تاريخي»، وإخفاء لون اتشال من الكتالوج بعدين «ما يصحّش يعيد كتابة اللي المستند بيقوله إنه حصل». ومكتوب إن من غير الحماية دي «الاسم بيرجع فاضي والمستند بيتطبع من غير اللون خالص».
والحماية دي متطبّقة على اللون… ومش متطبّقة على المنتج نفسه — في ٧ أماكن، كلهم بنفس الشكل.
يعني نفس السطر في الفاتورة: اللون بيفضل ظاهر، واسم الصنف بيختفي. نفس المنطق بالظبط اتطبّق على نص المشكلة بس.
| # | الصنف | اللون | الكمية | السعر | الإجمالي |
|---|---|---|---|---|---|
| 1 | خام بودرة ميلامين | أبيض | 120 | 48.00 | 5,760 |
| # | الصنف | اللون | الكمية | السعر | الإجمالي |
|---|---|---|---|---|---|
| 1 | — (فاضي) | أبيض | 120 | 48.00 | 5,760 |
فحصت قاعدة البيانات: فيه ٩ منتجات محذوفة بالفعل، ومنهم منتجات ليها حركة مخزنية:
| المنتج | تاريخ الحذف | عدد الحركات المتبقية | الرصيد وقتها |
|---|---|---|---|
| MFG Smoke RM | 2026-06-12 | ٧ حركات | صفر |
| MFG Smoke FG | 2026-06-12 | ٥ حركات | صفر |
| Melamine Powder Raw -AUD2 | 2026-06-12 | ٣ حركات | صفر |
| ORDINARY LACTIC ACID 5 % 3 | 2026-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 · كوميت الاختبار 1b4b5c044 —
ProductActiveSearchFilterTest بيثبّت إن is_active بيتركّب مع البحث
(كتلة البحث مجموعة orWhere؛ لو المجموعة اتكسرت الفلتر بيبطل يشتغل من غير أي خطأ).
فيه ١٢ نوع مستند تاني بنفس المشكلة. جرد كامل لعلاقات
product() في المشروع طلع ٤٩ موديل من غير withTrashed()؛ أغلبها بيانات أساسية
(أسعار، وحدات، BOM) وصح إنها تفضل كده. لكن دول سطور مستندات بتتطبع:
SalesOrderItem, SalesQuotationItem, SalesDeliveryNoteItem,
SalesReturnItem, PurchaseOrderItem, PurchaseRequestItem,
PurchaseGrnItem, PurchaseReturnItem, MfgMaterialIssueLine,
MfgGoodsReceiptLine, StoreOrderItem, StoreOrderReturnItem.
دول كمان بقى فيهم الـtrait، فكلهم بيقدروا يطبعوا اسم الصنف المحذوف — بنفس التصميم الآمن
اللي في الكارت الأحمر فوق، من غير ما product() تتلمس في أي واحد فيهم.