الجرد المحسّن — عرض كل منتجات المخزن دفعة واحدة

دراسة الوضع الحالي + خطة تنفيذ + عقد نطاق — تذكرة ISS-2026-0186 (نوع: تحسين، بحجم فيتشر)

🏭 موديول المخزون — الجرد 🖥️ شاشة + Endpoint جديد ⚠️ قرار حَرِج على الأرصدة 🔁 الحفظ = نفس الجرد الحالي
مبني على فحص كود مباشر (وكيلا BE + FE، opus) + قاعدة المعرفة (موضوع inventory-count-zero-balance-fix / ISS-2026-0004). تحليل فقط — صفر كود قبل الموافقة.

1 المشكلة / المطلوب

اليوم، عند عمل جرد لازم تبحث عن كل منتج وتضيفه سطرًا-سطرًا. للمخازن الكبيرة ده بطيء ومُنهِك، وسهل تنسى منتجًا.

العميل عايز وضع جرد ثانٍ («جرد محسّن / تفصيلي») بزرار منفصل: نفس شاشة الجرد، لكن أول ما تختار المخزن تُعرَض كل منتجات النظام تلقائيًا — المنتج بدون رصيد يظهر بصفر، والمنتج له رصيد يظهر أمامه «الكمية قبل» (استرشادية) — وتدخل الكمية المعدودة. لازم يراعي الأداء (ممكن آلاف المنتجات). عند الحفظ = نفس الجرد العادي تمامًا؛ التحسين في شكل/سهولة الإدخال فقط.

2 الوضع الحالي (ما هو موجود فعلًا)

تتبّعنا التدفق كاملًا بالكود:

الجزءما هو موجودالمرجع
شاشة الجردديالوج «جرد جديد» — تضيف المنتجات واحدًا-واحدًا بالبحث عبر جدول مشترك <app-transaction-line-items> مبني على FormArrayinventory-counts.component.ts:152-157, 514-523
«الكمية قبل» موجودة أصلًالكل سطر بعد اختيار المنتج، يُحسب رصيد النظام عبر stockBalanceService.listFiltered ويظهر كـbadge (يتلوّن أحمر لو صفر) — سابقة جاهزة للعمود الاسترشاديinventory-counts.component.ts:571-596
حمولة الحفظ{warehouse_id, date, items:[{product_id, unit_id, counted_quantity, unit_cost?, ...}]}POST inventory/countsinventory-counts.component.ts:324-366
منطق الترحيلFinalizeCount: يعمل firstOrCreate لصف الرصيد لكل بند (إصلاح ISS-2026-0004)، ثم ينشئ تسوية للبنود ذات الفرق ≠ 0 فقطActions/FinalizeCount.php:42-115
🔴 عرض «كل المنتجات» لمخزنغير موجود. endpoints الأرصدة (byWarehouse/index) تقرأ من صفوف الأرصدة فقط → المنتجات بدون رصيد غائبة تمامًا، وسقفها 25 صف/صفحةStockBalanceController.php:607-630, 56-156
سرد المنتجاتProductController::index يدعم per_page حتى 100، لكن بدون ربط بالأرصدة/التكلفة (لا يوجد عمود تكلفة على المنتج — التكلفة لكل مخزن في inventory_stock_balances)ProductController.php:57-74
سابقة جدول ضخم قابل للتحريرمحرّر أسعار المعمل (LIS price-editor): يحمّل آلاف الصفوف مرة واحدة (listAll(2000)) في p-table بـ[virtualScroll] والتحرير بـ[(ngModel)] مباشرة (لا FormArray) — النمط الوحيد الذي يتحمّل الآلافlis-price-editor.component.ts:144,79 · .html:75-76
الصلاحياتinventory.counts.create للحفظ + inventory.stock.view لتحميل المنتجات/الأرصدة — لا حاجة لصلاحية جديدةRolePermissionSeeder.php:681-687

3 المطلوب (بالتفصيل)

4 الفجوة (GAP)

الموجود اليومالمطلوبما الذي يجب بناؤه
إضافة منتج بالبحث سطرًا-سطرًاكل المنتجات معروضة تلقائيًا لمخزنEndpoint قراءة جديد: كل المنتجات LEFT JOIN الأرصدة لمخزن، مرقّم، يُرجّع لكل منتج: id/اسم/كود، unit_id، before_quantity (0 لو لا رصيد)، average_cost
«قبل» تُحسب بنداء منفصل لكل منتج«قبل» عمود جاهز لكل صفيأتي من نفس الـendpoint الجديد (لا نداءات N)
جدول FormArray (سطر = FormGroup)آلاف الصفوف قابلة للتحريرجدول افتراضي التمرير على signal<Row[]> بنمط محرّر الأسعار (لا FormArray)
الحفظ يُنشئ بندًا لكل منتج أضفتهالحفظ = نفس الجردنفس الحمولة/الخدمة بلا تغيير — نحوّل الصفوف المُدخَلة للشكل نفسه

5 الملفات المتأثرة والاعتماديات

الباك (BE)

الواجهة (FE)

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

⚠️ الأخطر — المنتج الذي لم يُعَد عدّه (قرار حَرِج على الأرصدة)

لو عرضنا كل المنتجات وحقل «المعدودة» يبدأ بـصفر، فأي منتج له رصيد 100 ولم تلمسه → فرقه = 0−100 = −100 → تسوية تصفّره. النتيجة: الحفظ يمسح مخزون كل منتج لم تعُدّه يدويًا. هذا يجب منعه بالتصميم (القرار #1 بالأسفل).

الحالةالمعالجة المقترحة
منتج بلا رصيد (لم يُخزَّن قط)يظهر بـ«قبل» = 0 (الـendpoint يزوّد unit_id من الوحدة الأساسية للمنتج)
آلاف المنتجات (أداء)تمرير افتراضي (يُرسم المرئي فقط) + بحث/فلتر فئة — نمط محرّر الأسعار
تغيير المخزن بعد الإدخالتحذير «سيُلغى ما أدخلته» قبل إعادة التحميل
منتجات بمتغيّرات (variants)صف لكل متغيّر (مفتاح product+variant، كما في الأرصدة)
منتجات دفعات/سيريالالجرد كمّي فقط (لا أعمدة دفعة/انتهاء على بند الجرد) — بلا تغيير على هذا
وحدة القياستأتي من الوحدة الأساسية للمنتج (base_unit_id) لكل صف

7 خطة التنفيذ (WPs)

WPالنطاقالطرفاختبار
WP1 حَرِجEndpoint جديد: كل المنتجات LEFT JOIN الأرصدة لمخزن، مرقّم، يعيد {id, اسم, كود, unit_id, before_quantity(0 لو لا رصيد), average_cost}. مقيّد بالشركة + سقف per_page.BEPest: يعيد منتجات بلا رصيد بـ0؛ يحترم الشركة/الترقيم
WP2وضع الجرد المحسّن FE: جدول افتراضي، «قبل» للقراءة، «المعدودة» قابلة للتحرير بالسلوك المتفق (قرار #1)، بحث/فلتر. الحفظ عبر الخدمة القائمة بلا تغيير.FEng build + تجربة يدوية
WP3زرار «جرد جديد محسّن» + هوك تغيير المخزن + i18n (ar/en).FEng build
WP4حارس الأمان: عند الحفظ تُرسَل فقط الصفوف الحقيقية (حسب قرار #1) — والتحقق أن منتجًا غير مَلموس لا يُنتج تسوية.FE/BEPest + تجربة

ملاحظة مهمّة

المنطق المحاسبي والمخزوني (FinalizeCount + التسويات) لا يتغيّر إطلاقًا — الفيتشر طبقة إدخال بيانات فقط. لذلك المخاطر محصورة في تجربة الإدخال، وأهمها القرار #1.

8 قرارات تحتاج المالك/العميل

القرار #1 (الأهم) — ماذا يفعل منتج لم تُعِد عدّه؟

لتفادي تصفير المخزن بالخطأ، عندنا خياران آمنان:

الخيارالسلوكالأنسب لـ
(أ) الافتراضي = «قبل» + حفظ الفروق فقط موصى بهحقل «المعدودة» يبدأ مساويًا لـ«قبل»؛ المنتج الذي لا تلمسه = فرق صفر = بلا تسوية. يُحفظ فقط ما اختلفت كميته فعلًا.الأمان + السرعة (تعدّل المختلف فقط)
(ب) جرد شامل حقيقيتُدخِل عدًّا لكل منتج؛ يُحفظ الكل كسجل جرد كامل (قد يكون آلاف البنود).جرد سنوي «حائط-لحائط» يريد سجلًا لكل صنف

التوصية: الخيار (أ) — «المعدودة» تبدأ بقيمة «قبل»، ولا يُحفظ إلا ما غيّرته. يمنع الكارثة ويطابق «قبل ← ثم تضع المعدودة».

القرار #2 — الأداء: تحميل الكل + تمرير افتراضي، أم ترقيم صفحات من الخادم؟

التوصية: تحميل الكل مرة واحدة + تمرير افتراضي (نمط محرّر الأسعار المثبَت). الترقيم من الخادم يُضيّع ما أدخلته عند تغيير الصفحة — سيّئ لجرد. التمرير الافتراضي يرسم المرئي فقط فيتحمّل الآلاف.

القرار #3 — نطاق المنتجات المعروضة

التوصية: كل المنتجات الفعّالة افتراضيًا، مع بحث + فلتر فئة (يخفّف الآلاف عمليًا). هل تريد استبعاد المنتجات المؤرشفة/غير القابلة للتخزين؟

القرار #4 — Endpoint جديد بالباك أم دمج بالواجهة؟

التوصية: endpoint واحد بالباك (استعلام LEFT JOIN، نداء واحد) — أسرع وأنظف من تحميل كل المنتجات + كل الأرصدة على الواجهة ودمجهما. (البديل: دمج بالواجهة بلا تغيير بالباك، لكنه نداءات أكثر.)

9 معاينة الواجهة المتوقّعة (قبل ← بعد)

قبل — الجرد الحالي (إضافة سطرًا-سطرًا)

جرد جديد
المخزن *مخزن جده ▾التاريخ *2026-07-14
#المنتجالوحدةالكمية المعدودةتكلفة
1🔍 ابحث عن منتج…الوحدة0.000.00
+ إضافة صنف

تبحث وتضيف كل منتج يدويًا — بطيء للمخازن الكبيرة.

بعد — الجرد المحسّن (كل المنتجات معروضة)

جرد جديد محسّن
المخزن *مخزن جده ▾التاريخ *2026-07-14🔍 بحث / فلتر فئة…
المنتجالكودالوحدةالكمية قبلالكمية المعدودة
منتج 1000PRD-17151قطعة120120
منتج 1001PRD-17152علبة00
منتج 1002PRD-17153قطعة4543
منتج 1003PRD-17154كرتونة00
منتج 1004PRD-17155قطعة88
⇕ تمرير افتراضي — كل منتجات المخزن معروضة (يُرسم المرئي فقط)
حفظإلغاء

ملاحظات على المعاينة

  • عمود «الكمية قبل» للقراءة فقط (رمادي)؛ الصفر يظهر بأحمر.
  • عمود «الكمية المعدودة» قابل للتحرير (أخضر)، يبدأ مساويًا لـ«قبل» حسب التوصية #1(أ) — تعدّل ما اختلف فقط (لاحظ منتج 1002: قبل 45 / معدودة 43).
  • شريط بحث/فلتر فئة أعلى الجدول لتضييق الآلاف.
  • المكوّن المتأثر: features/inventory-counts/ (وضع جديد)؛ الحفظ عبر inventory-count.service.ts بلا تغيير.

الخلاصة

فيتشر منخفض المخاطر تقنيًا (طبقة إدخال فقط؛ الترحيل والتسويات بلا تغيير) لكن فيه قرار واحد حَرِج (#1) يمنع تصفير المخزن بالخطأ. البناء = endpoint قراءة واحد بالباك + وضع جدول افتراضي بالواجهة؛ لا migration، لا صلاحية جديدة. الحفظ يُعيد استخدام نفس مسار الجرد الحالي بالكامل.

مطلوب قبل الكود: موافقتك على القرارات الأربعة (خصوصًا #1) وعلى شكل الشاشة أعلاه.