Inventory & MSIالمخزون.
إزاي Magento بيعرف عنده كام قطعة، وإزاي بيمنع بيع حاجة مش موجودة. الـ Sources والـ Stocks، الـ Reservations، الفرق بين الكميات، والتعامل معاه في الكود.
المخزون في Magento 2 بنظام MSI: الـ Source مكان فيزيائي فيه كميات حقيقية، والـ Stock تجميعة منطقية لمصادر مربوطة بـ website. الكمية القابلة للبيع هي مجموع المصادر ناقص الـ reservations، وهي أسطر بالسالب بتتسجّل عند الأوردر وبتتعوّض عند الشحن أو الإلغاء. السلة مابتحجزش، والفحص في الكود بيتم بـ GetProductSalableQtyInterface على الـ stock.
قبل MSI وبعده
ليه النظام اتغيّر أصلاً.
النظام القديم
رقم واحد لكل منتج: qty. مخزن واحد، وخصم مباشر عند الشراء.
MSI
كميات في أماكن متعددة، وحسبة بتراعي الطلبات اللي لسه ما اتشحنتش.
- أكتر من مخزن — بضاعة موزّعة على أماكن مختلفة.
- البيع من الفروع — الفرع نفسه مخزن.
- الطلب من المورّد مباشرة — dropshipping.
- أسواق مختلفة — كل website بيبيع من مخازن معيّنة.
اختصار Multi-Source Inventory — مخزون متعدد المصادر. مفعّل افتراضياً في النسخ الحديثة، وحتى لو عندك مخزن واحد، أنت بتستخدمه (بالمصدر والمخزون الافتراضيين).
Sources و Stocks
المفهومان الأساسيان — والخلط بينهم بيعطّل الفهم كله.
Magento بييجي بـ Default Source وDefault Stock. لو مضفتش غيرهم، كل حاجة بتمشي عليهم — وده الوضع في معظم المشاريع البسيطة.
أنواع الكميات
تلات أرقام مختلفة — والخلط بينهم بيسبب بيع زايد.
مخزن القاهرة: 10 قطع
مخزن جدة: 5 قطع
──────
Aggregated: 15 قطعة # الموجود فعلياً
طلبات لسه ما اتشحنتش: 3
──────
Salable: 12 قطعة # اللي ينفع تبيعه
# الـ 3 دول لسه في المخزن فيزيائياً
# بس محجوزين لعملاء اشتروهم
الـ Reservations
أذكى فكرة في النظام — وأكترها إساءة فهم.
عندك قطعة واحدة، وعشرة عملاء ضغطوا "اشتري" في نفس الثانية. لو كل واحد بيقرا الكمية ويخصم منها، ممكن العشرة يقروا "1" قبل ما حد يخصم — وتبيع عشرة وعندك واحدة.
تقفل الصف في قاعدة البيانات لحد ما الأول يخلص. بس ده بيخلّي الطلبات تستنى في طابور — وفي عروض الفلاش ده بيوقّع الموقع.
بدل ما نعدّل الكمية، نضيف سطر حجز بقيمة سالبة. الإضافة أسرع بكتير من التعديل ومابتحتاجش قفل، وعمليات الإضافة مابتتعارضش مع بعض.
inventory_reservation ────────────────────────────────────── id │ stock │ sku │ quantity 1 │ 1 │ SKU-001 │ -2 # أوردر 2 │ 1 │ SKU-001 │ -1 # أوردر تاني 3 │ 1 │ SKU-001 │ +2 # اتشحن، الحجز اتلغى # المحصلة: -1 → قطعة واحدة محجوزة
- العميل يطلب → يتسجّل حجز بالسالب. الكمية الفعلية ماتغيّرتش.
- الـ salable بتقل فوراً — فمحدش تاني يقدر ياخد نفس القطع.
- الطلب يتشحن → الكمية الفعلية بتقل، والحجز بيتلغى بسطر موجب.
- أو يتلغى الطلب → الحجز بيتلغى والكمية بترجع متاحة.
دورة حياة المخزون
من السلة للشحن — إيه بيتغيّر امتى.
خوارزميات التوزيع
من أنهي مخزن نشحن؟
الطلب فيه 5 قطع، وعندك 3 في القاهرة و4 في جدة. من فين تشحن؟ الـ Source Selection Algorithm بيجاوب.
بيتحدد من Stores → Inventory → Stocks، وبتسحب المصادر بالترتيب اللي عايزه. الترتيب ده قرار تشغيلي — عادة الأكبر أول عشان تقلّل عدد الشحنات، أو الأقرب أول عشان تقلّل التكلفة.
الإعدادات المهمة
اللي بتأثر على السلوك فعلياً.
المخزون في الكود
الواجهات الصح للقراءة والكتابة.
use Magento\InventorySalesApi\Api\ GetProductSalableQtyInterface; use Magento\InventorySalesApi\Api\ StockResolverInterface; use Magento\InventorySalesApi\Api\Data\ SalesChannelInterface; class StockChecker { public function __construct( private readonly GetProductSalableQtyInterface $getSalableQty, private readonly StockResolverInterface $stockResolver ) {} public function getQty( string $sku, string $websiteCode ): float { // نحوّل الـ website لـ stock $stock = $this->stockResolver->execute( SalesChannelInterface::TYPE_WEBSITE, $websiteCode ); // الكمية القابلة للبيع فعلاً return $this->getSalableQty->execute( $sku, (int) $stock->getStockId() ); } }
الـ stock resolver بيحوّل "أنا بابيع في website مصر" لـ "يبقى المخزون رقم 2". وبعدين الـ salable qty بيحسب المتاح في المخزون ده. لازم الخطوتين — لأن نفس المنتج ليه كميات مختلفة حسب الـ website.
use Magento\InventorySalesApi\Api\ IsProductSalableInterface; // لو عايز تعرف بس "متاح ولا لأ" $canSell = $this->isProductSalable->execute( $sku, $stockId ); // أو بكمية محددة use Magento\InventorySalesApi\Api\ IsProductSalableForRequestedQtyInterface; $result = $this->isSalableForQty->execute( $sku, $stockId, $requestedQty ); if (!$result->isSalable()) { // بيرجّع أسباب الرفض foreach ($result->getErrors() as $error) { // $error->getMessage() } }
use Magento\InventoryApi\Api\SourceItemsSaveInterface; use Magento\InventoryApi\Api\Data\SourceItemInterfaceFactory; use Magento\InventoryApi\Api\Data\SourceItemInterface; $sourceItem = $this->sourceItemFactory->create(); $sourceItem->setSku('SKU-001'); $sourceItem->setSourceCode('cairo_warehouse'); $sourceItem->setQuantity(50); $sourceItem->setStatus( SourceItemInterface::STATUS_IN_STOCK ); // بياخد مصفوفة — أسرع للتحديث الجماعي $this->sourceItemsSave->execute([$sourceItem]);
فيه واجهة لإضافة حجوزات يدوياً، بس ماتستخدمهاش إلا لو فاهم النتائج كويس. الحجوزات بتتدار تلقائياً مع دورة حياة الأوردر، وإضافة حجز يدوي من غير ما تلغيه بيخلّي كمية محجوزة للأبد.
التشخيص وإصلاح الأخطاء
لما الأرقام تطلع غلط.
# يكشف الحجوزات المعلّقة الغلط bin/magento inventory:reservation:list-inconsistencies # يولّد أوامر التصحيح (من غير تنفيذ) bin/magento inventory:reservation:create-compensations \ --raw # التنفيذ الفعلي — بعد المراجعة bin/magento inventory:reservation:create-compensations
-- الكميات الفعلية في المصادر SELECT source_code, quantity, status FROM inventory_source_item WHERE sku = 'SKU-001'; -- الحجوزات القائمة SELECT stock_id, SUM(quantity) AS reserved FROM inventory_reservation WHERE sku = 'SKU-001' GROUP BY stock_id; -- أوردرات قديمة لسه محجوزة (مؤشر مشكلة) SELECT o.increment_id, o.status, o.created_at FROM sales_order o WHERE o.status IN ('pending', 'processing') AND o.created_at < NOW() - INTERVAL 30 DAY;
- الكمية الفعلية صح؟ — شوف inventory_source_item.
- الحجوزات منطقية؟ — قارنها بالأوردرات المفتوحة.
- فيه أوردرات عالقة؟ — أوردر قديم مش متشحن ولا ملغي بيفضل حاجز.
- الفهارس متحدّثة؟ — indexer:status.
- الكاش اتمسح؟ — الكميات بتتكاش في الصفحات.
الأنواع المركّبة
المخزون في الـ configurable والـ bundle.
// الأب مالوش مخزون أصلاً $qty = $this->getSalableQty->execute( $configurableProduct->getSku(), $stockId ); // النتيجة مالهاش معنى
$children = $product->getTypeInstance() ->getUsedProducts($product); $total = 0; foreach ($children as $child) { $total += $this->getSalableQty->execute( $child->getSku(), $stockId ); } // أو للتوفّر بس — أسرع $isAvailable = $product->isSalable();
الفخاخ الشائعة
اقرا دي حتى لو مش هتقرا حاجة تانية.
- قراءة qty بدل الـ salable — بتسمح ببيع قطع محجوزة. الفخ الأول.
- افتراض إن السلة بتحجز — مابتحجزش. الحجز عند الطلب.
- الفحص على الأب في الـ configurable — الأب مالوش مخزون.
- تعديل جدول الحجوزات يدوياً — بيخرّب الحسبة بشكل صعب التتبّع.
- أوردرات عالقة في pending — بتفضل حاجزة كمية للأبد.
- تحديث المخزون واحد واحد — بطيء جداً في المزامنة. ابعت مصفوفة.
- نسيان الـ website في حساب الكمية — نفس المنتج له كميات مختلفة لكل مخزون.
- الاستغراب من salable بالسالب — طبيعي لو الـ backorders مفعّلة.
- تعديل الـ Default Stock — له قيود خاصة. اعمل stock جديد.
- تشغيل التصحيح من غير مراجعة — بيخفي مشكلة حقيقية في الكود.
أسئلة الإنترفيو
من أكتر المواضيع اللي بتميّز السنيور.
Inventory & MSI ✓
دلوقتي فاهم بنية الـ sources والـ stocks، الفرق بين الكميات، فكرة الحجوزات وليه موجودة، ودورة حياة المخزون والتشخيص.
الخلاصة: الـ salable هي اللي بتحدد البيع مش الكمية الفعلية، والحجوزات بتحل السباق بالإضافة بدل التعديل، والسلة مابتحجزش.
خلّصت الدرس؟علّمه عشان تتابع تقدّمك في الكورس.
أسئلة شائعة
إيه الفرق بين Source و Stock في MSI؟
الـ Source مكان فيزيائي فيه بضاعة حقيقية زي مخزن أو فرع، والكميات الفعلية بتتخزّن فيه في جدول inventory_source_item. الـ Stock تجميعة منطقية لمصادر مربوطة بـ websites، ومفيهاش كميات بل بتجمّع من مصادرها. Magento بييجي بـ Default Source و Default Stock، ولو هتعمل نظام متعدد المخازن اعمل stocks جديدة بدل ما تعدّل الافتراضي.
إيه الفرق بين Quantity و Salable Quantity في Magento 2؟
الـ Quantity هي الكمية الفعلية في المصدر، والـ Aggregated مجموعها في كل مصادر الـ stock. الـ Salable Quantity هي المتاح للبيع فعلًا: المجموع ناقص الـ reservations بتاعة الأوردرات اللي لسه مااتشحنتش. أي فحص توفّر لازم يكون على الـ salable، لأن قراءة qty العادية بتسمح ببيع قطع محجوزة لعملاء تانيين.
يعني إيه Reservations في MSI وليه موجودة؟
سجل تراكمي في جدول inventory_reservation بيسجّل الكميات المطلوبة اللي لسه مااتشحنتش كأسطر بالسالب، ولما الأوردر يتشحن أو يتلغي بيتسجّل سطر موجب يعوّضها. بتحل مشكلة السباق: الإضافة أسرع من التعديل ومابتحتاجش قفل، فعشرة عملاء بيشتروا آخر قطعة في نفس اللحظة مابيتعارضوش. الكمية الفعلية مابتتغيّرش إلا عند الشحن.
هل إضافة المنتج للسلة بتحجزه من المخزون؟
لأ. السلة بتشيّك على التوفّر عند الإضافة بس. الحجز بيتسجّل عند إنشاء الأوردر، والكمية الفعلية بتتخصم عند الشحن، ولو الأوردر اتلغى الحجز بيتعوّض والقطع بترجع متاحة. ده تصميم مقصود عشان السلال المهجورة ماتجمّدش المخزون، بس معناه إن آخر قطعة في سلة عميل ممكن عميل تاني يشتريها قبل ما يدفع.
إزاي أقرا الكمية القابلة للبيع في الكود؟
حوّل الـ website لـ stock بـ StockResolverInterface::execute(SalesChannelInterface::TYPE_WEBSITE, $websiteCode)، وبعدين GetProductSalableQtyInterface::execute($sku, $stockId). لو محتاج تعرف متاح ولا لأ بس، IsProductSalableInterface أسرع، وIsProductSalableForRequestedQtyInterface بيرجّع أسباب الرفض لكمية معيّنة. في الـ configurable افحص على SKU الابن لأن الأب مالوش كمية.
إزاي أحدّث كميات المخزون برمجيًا في Magento 2؟
اعمل SourceItemInterface بالـ sku و source_code و quantity و status، وابعته لـ SourceItemsSaveInterface::execute([$sourceItem]). الميثود بتاخد مصفوفة، فلو بتزامن من ERP ابعت كل الأصناف مرة واحدة مش واحد واحد لأن كل نداء بيطلق إعادة فهرسة. ومتضيفش reservations يدويًا، لأنها بتتدار تلقائيًا مع دورة حياة الأوردر.
كمية المنتج طالعة غلط في Magento 2، أشخّص إزاي؟
بالترتيب: الكمية الفعلية في inventory_source_item، وبعدين مجموع الحجوزات في inventory_reservation وقارنه بالأوردرات المفتوحة، وبعدين دوّر على أوردرات قاعدة في pending من شهور لأنها بتفضل حاجزة كمية. الأمر inventory:reservation:list-inconsistencies بيكشف التعارضات، وcreate-compensations بيصلّحها بعد ما تراجع النتيجة. وفي الآخر الفهرس والكاش.