ISS-2026-9104 · تحليل مرحلة أولى (بحث بلا كود)

وحدات القياس عبر البرنامج — تحليل الفجوة وكشف المشاكل Units of Measure across Moon ERP — Gap Analysis & Problem Report

النطاق: كل الموديولات الخطورة: عالية — [FIN] مخزون/تكلفة الحالة: موجود كبيانات · غير مُستخدَم فعليًا التاريخ: 2026-07-15

1 المشكلة

طلبت مرجعية واضحة لوحدات القياس: منتج له وحدة جرام / كيلو / طن — إزاي تتعمل في البرنامج؟ موجودة فعلًا ولا ناقصة؟ وفين بالظبط بتُستخدم الوحدة في كل البرنامج، وهل بتُستخدم صح؟ مع تحليل تفصيلي وكشف المشاكل.

الخلاصة في سطر: نظام الوحدات موجود بالكامل كبنية بيانات وشاشات إدخال (فئات + وحدات بمعامل تحويل + وحدات متعددة للصنف الواحد بباركود وسعر لكل وحدة)، لكن القطعة اللي بتخلّيه «يشتغل» — التحويل الفعلي — مش موجودة نهائيًا. كل المعاملات بتخزّن الوحدة بس بتتعامل مع الكمية كأنها بوحدة الأساس، والرصيد نفسه مالوش عمود وحدة. النتيجة: لو اخترت وحدة غير وحدة الأساس (كرتونة/طن) → الرصيد والتكلفة والتسعير كلها تطلع غلط.

2 الوضع الحالي (ما هو موجود) — مُتتبَّع بالكود

أ) محرك الوحدات في Core — بنية بيانات ممتازة

العنصرالوصفالمرجع
فئات الوحدات unit_groupsمجموعة (وزن، عدد، حجم…) باسم عربي/إنجليزيCore/…/create_unit_groups_table.php
الوحدات unitsاسم + رمز + conversion_factor(15,6) + is_base + الفئةCore/…/create_units_table.php
وحدات الصنف product_unitsلكل منتج: وحدة + معامل تحويل + باركود + سعر شراء/بيع + is_purchase/is_saleCore/…/create_product_units_table.php
وحدة أساس الصنفproducts.base_unit_id (+ وراثة default_unit_id من فئة المنتج)Product.php · CreateProduct.php:44-49
شاشات الإدخال (FE)شاشة الوحدات + شاشة الفئات + قسم «وحدات بديلة» في فورم المنتج — كلها تشتغل للإدخالfeatures/units · unit-groups · products.component.html:655-691
إجابة «هل موجودة؟»: نعم — بنية «جرام/كيلو/طن للصنف الواحد» مبنية بالكامل، والمستخدم يقدر يدخّلها اليوم من الواجهة.

ب) الوحدة على سطور المستندات — مُلتقَطة

كل جداول سطور المستندات فيها عمود unit_id (مرتبط بجدول units): المبيعات (عرض سعر/أمر/فاتورة/إذن تسليم/مرتجع)، المشتريات (طلب/أمر/GRN/فاتورة/مرتجع)، حركات المخزون (استلام/صرف/تحويل/جرد/تسوية)، والإنتاج (مكوّنات BOM + مواد + ناتج). فالواجهة بتسمح للمستخدم يختار وحدة على السطر.

3 المطلوب

4 الفجوة (GAP) — الموجود مقابل المطلوب

الجانبالموجود اليومالمطلوبالحالة
بنية الوحدات + وحدات الصنفكاملة (فئات/وحدات/معامل تحويل/باركود/سعر لكل وحدة)نفسهموجود
دالة التحويل (تحويل كمية بالمعامل)غير موجودة إطلاقًاconversion_factor بيانات ميتةدالة/خدمة تحويل مشتركةناقص
وحدة على الرصيد + حركات المخزونلا يوجد عمود وحدة على inventory_stock_balances ولا inventory_movementsالرصيد دائمًا بوحدة الأساس + عرض الوحدةناقص
خصم/إضافة المخزونبالكمية الخام بلا تحويل (StockService بلا بارامتر وحدة)تحويل الكمية لوحدة الأساس قبل تحريك الرصيدغلط
التسعير بالوحدةدائمًا سعر الأساس — product_units.sale_price/purchase_price لا يُقرأسعر الوحدة المختارةغلط
باركود الوحدة في الكاشيرPOS يقرأ products.barcode فقط — يتجاهل باركود الوحدةمسح باركود الكرتونة/العبوةناقص
قائمة الوحدات في السطر (FE)تعرض كل وحدات الشركة مش وحدات الصنف؛ ومفيش معالج تغيير/تحويلوحدات الصنف فقط + إعادة تسعير/تحويل عند التغييرغلط
فحص التوافر / FIFO / الحجز / MRPيقارن كمية بوحدة السطر مع رصيد بوحدة الأساس؛ طبقات التكلفة والحجز بلا وحدةالكل بوحدة الأساسغلط
التحقق (Validation)conversion_factor يقبل 0؛ لا «أساس واحد لكل فئة»؛ وحدة الأساس nullableمعامل > 0 + أساس واحد + توافق الفئةضعيف
البيانات الافتراضية (Seeding)شركة جديدة = صفر وحدات (يبنيها المستخدم يدويًا)وحدات افتراضية جاهزةناقص
وحدات المعمل (LIS)lab_units نظام منفصل تمامًا بلا تحويل — ازدواجية(توحيد مستقبلي — خارج النطاق)ملاحظة
المشكلة الجوهرية (بالأدلة): الرصيد الحالي مُخزّن في inventory_stock_balances.quantity بلا وحدة، وكل مَن يكتب فيه (StockService) بيثق إن المُدخَل بوحدة الأساس — وكل المُنادِين بيمرّروا كمية السطر الخام بالوحدة اللي اختارها المستخدم بلا ضرب في معامل التحويل. يعني الرصيد صحيح «بالصدفة» فقط لو المستخدم صادف وأدخل بوحدة الأساس. لو أدخلت استلام بـ«كرتونة» (معامل 12) → الرصيد يزيد 12 مش 144.
PostSalesInvoice.php:223,249 · PostPurchaseBill.php:630 · ApproveReceipt.php:185 · ApproveIssue.php:100,200,232 · StockService.php:40,51,125,134 · IssueMaterials.php:243

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

Backend (Laravel)

  • Core/app/Models/Unit.php · ProductUnit.php · UnitGroup.php · Product.php — إضافة دالة تحويل.
  • Inventory/app/Services/StockService.php — نقطة الحقيقة لتحريك الرصيد (بلا وحدة اليوم).
  • Inventory/app/Actions/ApproveReceipt.php · ApproveIssue.php
  • Sales/app/Actions/PostSalesInvoice.php · ConfirmDeliveryNote.php · PostSalesReturn.php
  • Purchases/app/Actions/PostPurchaseBill.php
  • POS/…/POSSaleController.php · POSProductController.php
  • Production/app/Actions/IssueMaterials.php · Backflush… · Stage…
  • Core/…/Requests/{Store,Update}{Unit,ProductUnit}Request.php — التحقق.
  • Migrations جديدة: عمود وحدة/الأساس على الرصيد والحركات (لو اخترنا العرض).

Frontend (Angular)

  • core/services/*unit* · store/units · store/unit-groups
  • features/products/products.component.* — الوحدات البديلة + التحقق.
  • TransactionLineItemsComponent + descriptors — عمود الوحدة على السطر (اليوم يعرض كل الوحدات، مقفول في doc-config).
  • sales/* · purchases/* · POS — معالج تغيير الوحدة (تحويل + تسعير).
  • features/*/stock-balances · reports — عرض رمز الوحدة.
  • i18n: ar.json/en.json مفاتيح UNIT.*.

الاعتماديّة الحرجة [FIN]: أي تحويل بيمسّ كمية المخزون → يمسّ التكلفة (FIFO/المتوسط) وقيمة المخزون وقيود WIP وربحية المبيعات. لازم يتبني خلف إعداد افتراضي = سلوك اليوم بالحرف، ويتراجع عليه Codex + استشاري مالي.

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

الحالةالمعالجة المقترحة
وحدة مختارة مش ضمن وحدات الصنف (لا يوجد معامل)نقصر قائمة السطر على وحدات الصنف + الأساس؛ ولو وحدة مجهولة → معامل=1 مع تحذير/منع.
معامل تحويل = 0 أو سالبتحقق > 0 على مستوى الطلب + قيد DB.
الصنف بلا وحدة أساس (base_unit_id فارغ)نعامل الكمية كوحدة أساس ضمنيًا (سلوك اليوم) لو الإعداد OFF؛ ولو ON نطلب وحدة أساس.
أكثر من وحدة أساس في نفس الفئةتحقق «أساس واحد لكل فئة».
كسور بعد التحويل (144.000000)الرصيد decimal(15,3) — نثبّت سياسة تقريب واضحة على وحدة الأساس.
مرتجع/إلغاء لمستند اتعمل بوحدة غير الأساسنفس معامل التحويل المخزَّن وقت السطر يُعكَس (نثبّت المعامل على السطر لتفادي انجراف لو اتغيّر لاحقًا).
تحويل بين مخازن بوحدات مختلفةالطرفان بوحدة الأساس داخليًا → لا مشكلة بعد التوحيد.
بيانات قديمة أُدخلت بوحدة غير الأساس قبل الإصلاحقرار مالك: تصحيح بأثر رجعي أم إيقاف الجديد فقط (على moonui2 كله تجريبي).

7 خطة التنفيذ (WPs) — مرحلية، خلف إعداد

المبدأ: إعداد inventory.enable_unit_conversion افتراضي OFF = سلوك اليوم بايت-بايت (صفر انحدار). التفعيل اختياري للشركة اللي محتاجة وحدات متعددة.
WPالنطاقRepo[FIN]Migration
WP1 — محرك التحويل + نقطة واحدةخدمة UnitConversionService::toBase(product, unitId, qty) تقرأ product_units.conversion_factor؛ تُستدعى عند كل حدّ مستند→مخزون فيحُوّل لوحدة الأساس قبل StockService؛ تثبيت المعامل على السطر.BEمحتمل (عمود معامل على السطر)
WP2 — تصحيح فحص التوافر/FIFO/الحجز/MRPكلها تعمل بوحدة الأساس بعد WP1 (إزالة المقارنة عبر وحدات مختلفة).BE
WP3 — واجهة السطرقصر قائمة الوحدات على وحدات الصنف؛ معالج تغيير الوحدة (تحويل الكمية المعروضة + إعادة التسعير)؛ فكّ قفل عمود الوحدة بشكل مضبوط.FE
WP4 — التسعير بالوحدة + باركود الكاشيرقراءة product_units.sale_price/purchase_price؛ POS يمسح باركود الوحدة ويحمّل وحدات الصنف.BE+FE
WP5 — العرض + Seeding + التحققعرض رمز الوحدة على الرصيد/الحركات/التقارير/الطباعة؛ Seeder وحدات افتراضية؛ تشديد التحقق (معامل>0، أساس واحد، توافق الفئة)؛ إضافة destroy لوحدة الصنف.BE+FEمحتمل

مؤجَّل صراحةً: توحيد lab_units مع Core (ازدواجية تاريخية) · الرصيد لكل تشغيلة×وحدة (per-lot) — مبادرات منفصلة.

8 قرارات تحتاج المالك

ق1 — مستوى الطموح: نبني المحرك الكامل الشغّال (تحويل فعلي عبر كل المستندات) مرحليًا؟ أم نكتفي مؤقتًا بـإيقاف/إخفاء اختيار الوحدات غير الأساس لحد ما نبني المحرك (يمنع البيانات الغلط بأقل تكلفة)؟
التوصية: المحرك الكامل مرحليًا (WP1→WP5) خلف إعداد.
ق2 — التفعيل الافتراضي: إعداد enable_unit_conversion افتراضي OFF (سلوك اليوم بالحرف) وتفعّله الشركة المحتاجة؟
التوصية: نعم — OFF افتراضيًا، صفر انحدار.
ق3 — التسعير عند اختيار وحدة أكبر: السعر ييجي من سعر الوحدة المُدخَل (product_units.sale_price)؟ أم سعر الأساس × معامل التحويل (محسوب تلقائيًا)؟
التوصية: سعر الوحدة المُدخَل إن وُجد، وإلا سعر الأساس×المعامل كاحتياطي.
ق4 — وحدة تخزين الرصيد: نثبّت إن الرصيد يُخزَّن دائمًا بوحدة الأساس (المعياري عالميًا)، والعرض يحوّل للوحدة المطلوبة؟
التوصية: نعم — الرصيد بوحدة الأساس؛ العرض فقط يحوّل.
ق5 — البيانات القائمة: فيه أصناف متعددة الوحدات أُدخلت فعليًا واتعملها معاملات بوحدة غير الأساس (تحتاج تصحيح بأثر رجعي)؟ أم نكتفي بمنع الغلط الجديد؟
التوصية: على moonui2 كله تجريبي → لا تصحيح؛ للعملاء نوفّر أمر فحص/تصحيح لو لزم.

9 معاينة الواجهة المتوقّعة

التغيير الأبرز على الواجهة = عمود الوحدة على سطر المستند: النهارده يعرض كل وحدات الشركة بلا أي تحويل؛ بعد التنفيذ يعرض وحدات الصنف فقط، وعند تغيير الوحدة يحوّل الكمية ويعيد التسعير تلقائيًا.

قبل — سطر فاتورة اليوم (الوحدة ديكور فقط)
فاتورة بيع — سطر الأصناف
الصنفالكميةالوحدةالسعرالإجمالي
سكر ناعم2كرتونة ▾ (كل وحدات الشركة)25.00050.000
⚠️ يخصم من المخزون 2 فقط (مش 2×12)، والسعر سعر الأساس — الرصيد والربح غلط.
بعد — سطر فاتورة بعد التنفيذ (تحويل + تسعير)
فاتورة بيع — سطر الأصناف
الصنفالكميةالوحدةالسعر/وحدةالإجمالييُخصَم من المخزون
سكر ناعم2كرتونة ▾ (كيلو · كرتونة)300.000600.00024 كيلو (2×12)
✅ الوحدة من وحدات الصنف فقط · الكمية تتحوّل لوحدة الأساس (كيلو) للمخزون والتكلفة · السعر من سعر الكرتونة.
شاشة أرصدة المخزون — عرض الوحدة (WP5)
أرصدة المخزون
الصنفالمخزنالرصيدالوحدة (الأساس)
سكر ناعمالرئيسي1,240.000كيلو
الرصيد دائمًا بوحدة الأساس؛ الرمز يظهر بجانب الكمية.

تقرير مرحلة أولى — لا يُكتب أي كود قبل موافقتك على النطاق أعلاه. بعد الموافقة تُنفَّذ WP1→WP5 بالترتيب مع مراجعة مستقلة واختبارات لكل حزمة، والإعداد OFF يضمن عدم تأثّر أي عميل حالي.