Get a quote

أتمتة إدارة المخزون: تكامل الباركود مع أنظمة نقاط البيع

إدارة المخزون اليدوية في بيئة التجزئة تتسبب في أخطاء التسعير والفقدان الخفي وتوقف المبيعات بسبب نفاد المخزون غير المتوقع. هذا المقال يشرح كيفية بناء نظام مخزون متكامل مع الباركود وأنظمة نقاط البيع للعمليات التجارية في لبنان والمنطقة.

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

هيكل قاعدة البيانات الذي يجعل المخزون المباشر ممكناً

أهم قرار في تصميم المخطط هو نمذجة المخزون كسجل حركات بدلاً من كمية واحدة قابلة للتعديل لكل منتج:

CREATE TABLE stock_levels (
  product_id      UUID NOT NULL REFERENCES products(id),
  location_id     UUID NOT NULL REFERENCES inventory_locations(id),
  quantity        DECIMAL(12,3) NOT NULL DEFAULT 0,
  last_updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  PRIMARY KEY(product_id, location_id)
);

CREATE TABLE stock_movements (
  id               UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id        UUID NOT NULL,
  product_id       UUID NOT NULL REFERENCES products(id),
  from_location_id UUID REFERENCES inventory_locations(id),
  to_location_id   UUID REFERENCES inventory_locations(id),
  quantity         DECIMAL(12,3) NOT NULL,
  movement_type    TEXT NOT NULL,
  reference_id     UUID,
  occurred_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

كل عملية بيع تكتب في stock_movements كحركة من موقع التخزين إلى العميل. كل استلام بضاعة يكتب كحركة من المورد إلى المستودع. مستوى المخزون الحالي في stock_levels هو المجموع المُجسَّد لجميع الحركات، يُحدَّث بشكل ذري مع كل كتابة حركة.

هذا التصميم يعني أن الكمية الحالية قابلة للاستعلام دائماً في O(1)، بينما تاريخ الحركات الكامل متاح للمطابقة والتدقيق والتحليل بدون أي تحديثات مدمّرة.

خصم المخزون عند البيع: النمط الذري

عند اكتمال عملية بيع على نقطة البيع، يجب خصم المخزون لكل صنف مباع في نفس معاملة قاعدة البيانات مثل كتابة سجل البيع. كتابتان منفصلتان (أولاً تسجيل البيع، ثم تحديث المخزون) عرضة للفشل الجزئي:

func (s *InventoryService) DeductForSale(ctx context.Context, saleID uuid.UUID, items []SaleItem) error {
    return s.db.WithTransaction(ctx, func(tx pgx.Tx) error {
        for _, item := range items {
            result, err := tx.Exec(ctx, `
                UPDATE stock_levels
                SET quantity = quantity - $1, last_updated_at = NOW()
                WHERE product_id = $2 AND location_id = $3
                  AND quantity >= $1
            `, item.Quantity, item.ProductID, item.LocationID)
            if err != nil || result.RowsAffected() == 0 {
                return fmt.Errorf("insufficient stock for product %s", item.ProductID)
            }
            tx.Exec(ctx, `
                INSERT INTO stock_movements
                  (tenant_id, product_id, from_location_id, quantity, movement_type, reference_id)
                VALUES ($1, $2, $3, $4, 'sale', $5)
            `, item.TenantID, item.ProductID, item.LocationID, item.Quantity, saleID)
        }
        return nil
    })
}

شرط quantity >= $1 في استعلام UPDATE يمنع المخزون السلبي بدون race condition منفصل. إذا لم يتحقق الشرط، يُرجع RowsAffected() صفراً، تُرجع الخدمة خطأً، تُرجع المعاملة للخلف، ويُرفض البيع بشكل نظيف.

البحث بالباركود ورؤية المخزون على نقطة البيع

عندما يمسح الكاشير باركود منتج، يُرسل تطبيق نقطة البيع الباركود إلى API ويستقبل تفاصيل المنتج والمخزون المتاح الحالي في موقع الجهاز.

فهرسة عمود الباركود لكل مستأجر للبحث السريع:

CREATE UNIQUE INDEX products_tenant_barcode_idx ON products(tenant_id, barcode);

بحث بالباركود مع فلتر المستأجر يُصل إلى هذا الفهرس ويُرجع في أقل من ميلي ثانية على نسخة PostgreSQL دافئة.

تنبيهات المخزون المنخفض: مدفوعة بالأحداث ومجدولة

تنبيهات المخزون المنخفض تحتاج إلى مسارين: تنبيه فوري عند انخفاض المخزون دون الحد الأدنى أثناء البيع، وتقرير صباحي مجدول يسرد جميع المنتجات عند الحد الأدنى أو أدناه.

للتقرير المجدول، نفّذ استعلام PostgreSQL يومياً:

SELECT p.name, p.barcode, sl.quantity, p.low_stock_threshold, il.name as location
FROM stock_levels sl
JOIN products p ON sl.product_id = p.id
JOIN inventory_locations il ON sl.location_id = il.id
WHERE sl.quantity <= p.low_stock_threshold
  AND p.tenant_id = $1
ORDER BY (sl.quantity / NULLIF(p.low_stock_threshold, 0)) ASC;

الترتيب بنسبة الكمية الحالية إلى الحد يُبرز المنتجات الأكثر نضوباً في أعلى التقرير.

اعتبارات خاصة بعمليات التجزئة في المنطقة

الاتصال المتقطع. أجهزة نقاط البيع في لبنان تعمل كثيراً خلال انقطاعات الإنترنت. يجب تخزين أحداث مسح الباركود محلياً ومزامنتها مع نظام المخزون المركزي عند عودة الاتصال.

المنتجات متعددة الوحدات. التجزئة في المنطقة تشمل منتجات تُباع بالكيلوغرام وبالقطعة وبالعبوة. يجب أن يدعم مخطط المخزون الكميات الكسرية وتحويلات وحدة القياس.

الدروس الأساسية من الإنتاج

نمذجة المخزون كحركات مع مستوى حالي مُجسَّد، وليس كرقم قابل للتقليل. استخدام نمط UPDATE مع quantity >= $1 لمنع المخزون السلبي. كتابة البيع وخصم المخزون في معاملة واحدة. تسليم تنبيهات المخزون المنخفض فورياً عند الخصم وعبر تقرير يومي مجدول.

العودة إلى المدونة
Chat on WhatsApp