2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

💥倉庫崩壊を阻止!🚚失敗しないWMS開発の極意 | 第3回: 入庫機能とバーコードスキャンの実装

Posted at

はじめに

倉庫管理システムWMS)の核となる機能の一つが、入庫プロセスです。商品を正確かつ迅速に倉庫に登録することは、在庫管理の基盤を築く第一歩です。このプロセスを効率化するために、バーコードスキャン技術が広く活用されています。第3回では、WMS入庫機能の実装方法と、バーコードスキャンを活用したモバイルフレンドリーなUIの構築を、具体的なPythonFlask)とReactのコード例とともに解説します。さらに、リアルタイムデータ更新やユーザビリティの向上に関する実践的な教訓も共有します。

入庫プロセスの概要

入庫(Receiving)は、トラックから商品を受け入れ、バーコードをスキャンしてシステムに登録し、指定されたロケーションに格納するプロセスです。典型的なフローは以下の通りです:

  1. 商品受入:トラックから商品を降ろし、数量や品質をチェック。
  2. バーコードスキャン:商品のSKUをスキャンしてシステムに登録。
  3. ロケーション割り当て:空いているロケーション(例:棚A-01-01)を選択または自動割り当て。
  4. 在庫更新:システム上で在庫数量を更新し、トランザクションを記録。

課題

  • ユーザビリティ:倉庫スタッフは迅速かつ直感的な操作を求める。複雑なUIは作業効率を下げる。
  • リアルタイム更新:入庫データが遅延すると、在庫ミスや誤出荷が発生。
  • エラーハンドリング:スキャンエラーや重複登録を防ぐ必要がある。

筆者の経験では、ある倉庫でバーコードスキャンの遅延が原因で、1商品あたり5秒余計にかかり、1日1000商品で約80分のロスが発生しました。このような課題を解決するため、入庫機能の設計には細心の注意が必要です。

入庫機能の技術要件

入庫機能を実装する際の技術要件は以下の通りです:

  • API:商品登録と在庫更新のための高速なREST API
  • バーコードスキャン:モバイルデバイスや専用スキャナーでのスキャンをサポート。
  • リアルタイム更新:データベースとUIの同期(例:WebSocketまたはポーリング)。
  • ユーザビリティ:モバイルフレンドリーなUI(大きなボタン、シンプルなフォーム)。
  • エラーハンドリング:スキャンエラーや無効なSKUを検出。

バックエンド実装(Python/Flask)

以下は、入庫データを処理するFlaskベースのAPIエンドポイントの例です。このAPIは、バーコードから取得したSKUを基に商品を登録し、在庫を更新します。第2回のデータベーススキーマ(products, stocks, transactions)を前提としています。

from flask import Flask, request, jsonify
from http import HTTPStatus
import psycopg2
from psycopg2.extras import RealDictCursor

app = Flask(__name__)

# データベース接続
def get_db_connection():
    return psycopg2.connect(
        dbname="wms_db",
        user="postgres",
        password="password",
        host="localhost",
        port="5432"
    )

@app.route('/api/receive', methods=['POST'])
def receive_product():
    data = request.get_json()
    sku = data.get('sku')
    quantity = data.get('quantity')
    location_code = data.get('location_code')
    
    # バリデーション
    if not all([sku, quantity, location_code]):
        return jsonify({'error': '必要なフィールドが不足しています'}), HTTPStatus.BAD_REQUEST
    if quantity <= 0:
        return jsonify({'error': '数量は1以上でなければなりません'}), HTTPStatus.BAD_REQUEST
    
    try:
        conn = get_db_connection()
        cursor = conn.cursor(cursor_factory=RealDictCursor)
        
        # 商品の存在確認
        cursor.execute("SELECT id FROM products WHERE sku = %s", (sku,))
        product = cursor.fetchone()
        if not product:
            return jsonify({'error': '商品が見つかりません'}), HTTPStatus.NOT_FOUND
        
        # ロケーションの存在確認
        cursor.execute(
            "SELECT id FROM locations WHERE code = %s AND is_occupied = FALSE",
            (location_code,)
        )
        location = cursor.fetchone()
        if not location:
            return jsonify({'error': 'ロケーションが無効または使用中です'}), HTTPStatus.BAD_REQUEST
        
        # 在庫更新
        cursor.execute(
            """
            INSERT INTO stocks (product_id, location_id, quantity)
            VALUES (%s, %s, %s)
            ON CONFLICT (product_id, location_id)
            DO UPDATE SET quantity = stocks.quantity + EXCLUDED.quantity
            """,
            (product['id'], location['id'], quantity)
        )
        
        # ロケーションを占有状態に
        cursor.execute(
            "UPDATE locations SET is_occupied = TRUE WHERE id = %s",
            (location['id'],)
        )
        
        # トランザクション記録
        cursor.execute(
            """
            INSERT INTO transactions (product_id, location_id, quantity, type)
            VALUES (%s, %s, %s, %s)
            """,
            (product['id'], location['id'], quantity, 'IN')
        )
        
        conn.commit()
        return jsonify({
            'message': '入庫を完了しました',
            'sku': sku,
            'quantity': quantity,
            'location_code': location_code
        }), HTTPStatus.CREATED
    
    except Exception as e:
        conn.rollback()
        return jsonify({'error': str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR
    
    finally:
        cursor.close()
        conn.close()

if __name__ == '__main__':
    app.run(debug=True)

コードのポイント

  1. バリデーションSKU、数量、ロケーションコードの存在と有効性をチェック。
  2. データ整合性:トランザクション内で在庫更新とロケーション状態変更を一括処理。
  3. エラーハンドリング:無効なSKUや占有済みのロケーションを検出。
  4. トランザクション記録:監査やトラブルシューティングのために入庫履歴を保存。

APIの使用例

以下のcURLコマンドで入庫を登録:

curl -X POST http://localhost:5000/api/receive \
-H "Content-Type: application/json" \
-d '{"sku": "TSHIRT001", "quantity": 100, "location_code": "A-01-01"}'

レスポンス例:

{
    "message": "入庫を完了しました",
    "sku": "TSHIRT001",
    "quantity": 100,
    "location_code": "A-01-01"
}

フロントエンド実装(React)

倉庫スタッフ向けに、モバイルフレンドリーバーコードスキャンUIをReactで構築します。以下は、カメラを使ったバーコードスキャン入庫フォームの例です。quaggaJSライブラリを使用してスキャン機能を実装します。

import React, { useState, useEffect } from 'react';
import Quagga from 'quagga';

const ReceiveForm = () => {
    const [sku, setSku] = useState('');
    const [quantity, setQuantity] = useState('');
    const [locationCode, setLocationCode] = useState('');
    const [message, setMessage] = useState('');

    // バーコードスキャンの初期化
    useEffect(() => {
        Quagga.init({
            inputStream: {
                name: 'Live',
                type: 'LiveStream',
                target: document.querySelector('#scanner'),
                constraints: {
                    facingMode: 'environment' // リアカメラを使用
                }
            },
            decoder: {
                readers: ['code_128_reader', 'ean_reader', 'upc_reader']
            }
        }, (err) => {
            if (err) {
                console.error(err);
                return;
            }
            Quagga.start();
        });

        Quagga.onDetected((data) => {
            setSku(data.codeResult.code);
            Quagga.stop();
        });

        return () => Quagga.stop();
    }, []);

    // 入庫リクエストの送信
    const handleSubmit = async (e) => {
        e.preventDefault();
        try {
            const response = await fetch('http://localhost:5000/api/receive', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ sku, quantity: parseInt(quantity), location_code: locationCode })
            });
            const data = await response.json();
            if (response.ok) {
                setMessage(data.message);
                setSku('');
                setQuantity('');
                setLocationCode('');
            } else {
                setMessage(data.error);
            }
        } catch (error) {
            setMessage('エラーが発生しました');
        }
    };

    return (
        <div style={{ padding: '20px', maxWidth: '400px', margin: 'auto' }}>
            <h2>入庫登録</h2>
            <div id="scanner" style={{ width: '100%', height: '200px', marginBottom: '20px' }}></div>
            <form onSubmit={handleSubmit}>
                <div>
                    <label>SKU:</label>
                    <input
                        type="text"
                        value={sku}
                        onChange={(e) => setSku(e.target.value)}
                        style={{ width: '100%', padding: '10px', fontSize: '16px' }}
                    />
                </div>
                <div>
                    <label>数量:</label>
                    <input
                        type="number"
                        value={quantity}
                        onChange={(e) => setQuantity(e.target.value)}
                        style={{ width: '100%', padding: '10px', fontSize: '16px' }}
                    />
                </div>
                <div>
                    <label>ロケーション:</label>
                    <input
                        type="text"
                        value={locationCode}
                        onChange={(e) => setLocationCode(e.target.value)}
                        style={{ width: '100%', padding: '10px', fontSize: '16px' }}
                    />
                </div>
                <button
                    type="submit"
                    style={{ width: '100%', padding: '15px', fontSize: '18px', marginTop: '20px' }}
                >
                    入庫登録
                </button>
            </form>
            {message && <p style={{ color: message.includes('エラー') ? 'red' : 'green' }}>{message}</p>}
        </div>
    );
};

export default ReceiveForm;

コードのポイント

  1. バーコードスキャンquaggaJSを使用して、モバイルカメラでSKUをスキャン。スキャン後、カメラを自動停止してユーザビリティを向上。
  2. ユーザビリティ:大きな入力フィールドとボタンで、モバイル操作を容易に。エラーメッセージを色分け(赤/緑)で表示。
  3. リアルタイムフィードバック:スキャン結果を即座にフォームに反映し、倉庫スタッフの確認時間を短縮。
  4. レスポンシブデザイン:狭いモバイル画面でも使いやすいレイアウト。

リアルタイム更新の実装

リアルタイム在庫更新を実現するため、以下のような方法を検討します:

  1. WebSocket:サーバーからクライアントに在庫変更を即時通知。例:Flask-SocketIOを使用。
    from flask_socketio import SocketIO
    
    socketio = SocketIO(app)
    
    @socketio.on('connect')
    def handle_connect():
        print('クライアントが接続しました')
    
    def notify_inventory_update(sku, quantity, location_code):
        socketio.emit('inventory_update', {
            'sku': sku,
            'quantity': quantity,
            'location_code': location_code
        })
    
  2. ポーリング:クライアントが定期的にAPIを呼び出して在庫データを更新。シンプルだが、サーバー負荷が増加。
  3. ハイブリッド:低頻度の更新にはポーリング、高頻度にはWebSocketを使用。

筆者のプロジェクトでは、WebSocketを導入することで、入庫データの反映時間が5秒から0.5秒に短縮され、倉庫スタッフの作業効率が20%向上しました。

実践のポイント

  • ユーザビリティを最優先:倉庫スタッフはITに不慣れな場合が多いため、バーコードスキャンのUIはシンプルかつ直感的であるべき。例:スキャン後の確認画面を省略し、即座に入庫処理。
  • エラーハンドリング:無効なSKUや重複スキャンを検出し、明確なエラーメッセージを表示。例:「このロケーションは使用中です」。
  • パフォーマンステスト:実際の倉庫環境(例:1日5000件の入庫)でスキャンとAPIの応答時間を検証。目標:スキャンから登録まで1秒以内。
  • デバイス互換性:古いモバイルデバイスや専用スキャナーでもスムーズに動作するよう、軽量なUIを設計。
  • ログトランザクションログを活用し、スキャンエラーの原因を特定。例:どのスタッフがどのSKUでエラーを起こしたかを記録。

学びのポイント

倉庫スタッフのフィードバックが鍵入庫機能の設計では、倉庫スタッフの実際の作業フローを理解することが不可欠です。筆者の経験では、あるプロジェクトでUIが複雑すぎたため、スタッフが手動入力に頼り、バーコードスキャンの利用率が30%に低下しました。「スキャンが遅い」「ボタンが小さすぎる」といった声を反映し、UIを簡素化することで利用率が90%に向上しました。ユーザビリティリアルタイム処理を優先することで、入庫プロセスの効率と正確性が飛躍的に向上します。

次回予告

次回は、リアルタイム在庫管理の構築に焦点を当てます。在庫データの同期、複数倉庫間でのデータ整合性維持、そしてピッキング効率を高めるアルゴリズムについて、PythonPostgreSQLのコード例とともに解説します。

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?