دراسة الوضع الحالي + خطة تنفيذ + عقد نطاق — تذكرة ISS-2026-0186 (نوع: تحسين، بحجم فيتشر)
اليوم، عند عمل جرد لازم تبحث عن كل منتج وتضيفه سطرًا-سطرًا. للمخازن الكبيرة ده بطيء ومُنهِك، وسهل تنسى منتجًا.
العميل عايز وضع جرد ثانٍ («جرد محسّن / تفصيلي») بزرار منفصل: نفس شاشة الجرد، لكن أول ما تختار المخزن تُعرَض كل منتجات النظام تلقائيًا — المنتج بدون رصيد يظهر بصفر، والمنتج له رصيد يظهر أمامه «الكمية قبل» (استرشادية) — وتدخل الكمية المعدودة. لازم يراعي الأداء (ممكن آلاف المنتجات). عند الحفظ = نفس الجرد العادي تمامًا؛ التحسين في شكل/سهولة الإدخال فقط.
تتبّعنا التدفق كاملًا بالكود:
| الجزء | ما هو موجود | المرجع |
|---|---|---|
| شاشة الجرد | ديالوج «جرد جديد» — تضيف المنتجات واحدًا-واحدًا بالبحث عبر جدول مشترك <app-transaction-line-items> مبني على FormArray | inventory-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/counts | inventory-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 |
| الموجود اليوم | المطلوب | ما الذي يجب بناؤه |
|---|---|---|
| إضافة منتج بالبحث سطرًا-سطرًا | كل المنتجات معروضة تلقائيًا لمخزن | Endpoint قراءة جديد: كل المنتجات LEFT JOIN الأرصدة لمخزن، مرقّم، يُرجّع لكل منتج: id/اسم/كود، unit_id، before_quantity (0 لو لا رصيد)، average_cost |
| «قبل» تُحسب بنداء منفصل لكل منتج | «قبل» عمود جاهز لكل صف | يأتي من نفس الـendpoint الجديد (لا نداءات N) |
جدول FormArray (سطر = FormGroup) | آلاف الصفوف قابلة للتحرير | جدول افتراضي التمرير على signal<Row[]> بنمط محرّر الأسعار (لا FormArray) |
| الحفظ يُنشئ بندًا لكل منتج أضفته | الحفظ = نفس الجرد | نفس الحمولة/الخدمة بلا تغيير — نحوّل الصفوف المُدخَلة للشكل نفسه |
productsForWarehouse($warehouse) (أو كنترولر/خدمة مخصّصة) + مسار في routes/api.php.products وinventory_stock_balances (على product+variant+warehouse، مقيّد بالشركة) — يعيد الكمية والتكلفة وbase_unit_id. مرقّم بسقف per_page على نمط ProductController:70 (حتى 100/200).store/finalize) وFinalizeCount بلا أي تغيير.(onChange) على قائمة المخزن (غير موجود اليوم) لتشغيل التحميل.create/approve تُعاد بلا تغيير.INVENTORY.* (NEW_COUNT_ENHANCED، QUANTITY_BEFORE، LOAD_ALL_PRODUCTS) في ar.json/en.json ~3163.لو عرضنا كل المنتجات وحقل «المعدودة» يبدأ بـصفر، فأي منتج له رصيد 100 ولم تلمسه → فرقه = 0−100 = −100 → تسوية تصفّره. النتيجة: الحفظ يمسح مخزون كل منتج لم تعُدّه يدويًا. هذا يجب منعه بالتصميم (القرار #1 بالأسفل).
| الحالة | المعالجة المقترحة |
|---|---|
| منتج بلا رصيد (لم يُخزَّن قط) | يظهر بـ«قبل» = 0 (الـendpoint يزوّد unit_id من الوحدة الأساسية للمنتج) |
| آلاف المنتجات (أداء) | تمرير افتراضي (يُرسم المرئي فقط) + بحث/فلتر فئة — نمط محرّر الأسعار |
| تغيير المخزن بعد الإدخال | تحذير «سيُلغى ما أدخلته» قبل إعادة التحميل |
| منتجات بمتغيّرات (variants) | صف لكل متغيّر (مفتاح product+variant، كما في الأرصدة) |
| منتجات دفعات/سيريال | الجرد كمّي فقط (لا أعمدة دفعة/انتهاء على بند الجرد) — بلا تغيير على هذا |
| وحدة القياس | تأتي من الوحدة الأساسية للمنتج (base_unit_id) لكل صف |
| WP | النطاق | الطرف | اختبار |
|---|---|---|---|
| WP1 حَرِج | Endpoint جديد: كل المنتجات LEFT JOIN الأرصدة لمخزن، مرقّم، يعيد {id, اسم, كود, unit_id, before_quantity(0 لو لا رصيد), average_cost}. مقيّد بالشركة + سقف per_page. | BE | Pest: يعيد منتجات بلا رصيد بـ0؛ يحترم الشركة/الترقيم |
| WP2 | وضع الجرد المحسّن FE: جدول افتراضي، «قبل» للقراءة، «المعدودة» قابلة للتحرير بالسلوك المتفق (قرار #1)، بحث/فلتر. الحفظ عبر الخدمة القائمة بلا تغيير. | FE | ng build + تجربة يدوية |
| WP3 | زرار «جرد جديد محسّن» + هوك تغيير المخزن + i18n (ar/en). | FE | ng build |
| WP4 | حارس الأمان: عند الحفظ تُرسَل فقط الصفوف الحقيقية (حسب قرار #1) — والتحقق أن منتجًا غير مَلموس لا يُنتج تسوية. | FE/BE | Pest + تجربة |
المنطق المحاسبي والمخزوني (FinalizeCount + التسويات) لا يتغيّر إطلاقًا — الفيتشر طبقة إدخال بيانات فقط. لذلك المخاطر محصورة في تجربة الإدخال، وأهمها القرار #1.
لتفادي تصفير المخزن بالخطأ، عندنا خياران آمنان:
| الخيار | السلوك | الأنسب لـ |
|---|---|---|
| (أ) الافتراضي = «قبل» + حفظ الفروق فقط موصى به | حقل «المعدودة» يبدأ مساويًا لـ«قبل»؛ المنتج الذي لا تلمسه = فرق صفر = بلا تسوية. يُحفظ فقط ما اختلفت كميته فعلًا. | الأمان + السرعة (تعدّل المختلف فقط) |
| (ب) جرد شامل حقيقي | تُدخِل عدًّا لكل منتج؛ يُحفظ الكل كسجل جرد كامل (قد يكون آلاف البنود). | جرد سنوي «حائط-لحائط» يريد سجلًا لكل صنف |
التوصية: الخيار (أ) — «المعدودة» تبدأ بقيمة «قبل»، ولا يُحفظ إلا ما غيّرته. يمنع الكارثة ويطابق «قبل ← ثم تضع المعدودة».
التوصية: تحميل الكل مرة واحدة + تمرير افتراضي (نمط محرّر الأسعار المثبَت). الترقيم من الخادم يُضيّع ما أدخلته عند تغيير الصفحة — سيّئ لجرد. التمرير الافتراضي يرسم المرئي فقط فيتحمّل الآلاف.
التوصية: كل المنتجات الفعّالة افتراضيًا، مع بحث + فلتر فئة (يخفّف الآلاف عمليًا). هل تريد استبعاد المنتجات المؤرشفة/غير القابلة للتخزين؟
التوصية: endpoint واحد بالباك (استعلام LEFT JOIN، نداء واحد) — أسرع وأنظف من تحميل كل المنتجات + كل الأرصدة على الواجهة ودمجهما. (البديل: دمج بالواجهة بلا تغيير بالباك، لكنه نداءات أكثر.)
| # | المنتج | الوحدة | الكمية المعدودة | تكلفة |
|---|---|---|---|---|
| 1 | 🔍 ابحث عن منتج… | الوحدة | 0.00 | 0.00 |
تبحث وتضيف كل منتج يدويًا — بطيء للمخازن الكبيرة.
| المنتج | الكود | الوحدة | الكمية قبل | الكمية المعدودة |
|---|---|---|---|---|
| منتج 1000 | PRD-17151 | قطعة | 120 | 120 |
| منتج 1001 | PRD-17152 | علبة | 0 | 0 |
| منتج 1002 | PRD-17153 | قطعة | 45 | 43 |
| منتج 1003 | PRD-17154 | كرتونة | 0 | 0 |
| منتج 1004 | PRD-17155 | قطعة | 8 | 8 |
فيتشر منخفض المخاطر تقنيًا (طبقة إدخال فقط؛ الترحيل والتسويات بلا تغيير) لكن فيه قرار واحد حَرِج (#1) يمنع تصفير المخزن بالخطأ. البناء = endpoint قراءة واحد بالباك + وضع جدول افتراضي بالواجهة؛ لا migration، لا صلاحية جديدة. الحفظ يُعيد استخدام نفس مسار الجرد الحالي بالكامل.
مطلوب قبل الكود: موافقتك على القرارات الأربعة (خصوصًا #1) وعلى شكل الشاشة أعلاه.