生成AIを使ってDjangoで在庫管理システム作ってみた

## はじめに
今回は、ChatGPTを使って在庫管理システムを作成してみようと思います。
使うのは、```python```で扱うことのできる```Django```を使っていきます。
## 要件定義
まず在庫管理システムにどんな機能を付けるかを明確にしておきます。
ChatGPTに出力してもらいましょう。
### **1. システム概要**

倉庫内の商品や資材の在庫を効率的に管理するためのシステム。入出庫管理、在庫状況の可視化、アラート通知、レポート出力などを通じて、在庫管理の正確性と作業効率の向上を目指す。


### **2. 主要機能**

1. **入庫・出庫管理**
    - 新規入庫・出庫の登録
    - 在庫数の自動更新
2. **在庫状況の表示**
    - 商品ごとの在庫数表示
    - 最終更新日時の表示

## システム開発
では、さっそく作っていきましょう。
まず```Django```のインストール、プロジェクト・アプリケーション作成を行っていきます。
```:Djangoインストール、プロジェクト・アプリケーション作成
pip install django
django-admin startproject inventory_manager
cd inventory_manager
python manage.py startapp inventory_manager_app
```
これで、```Django```のインストールとプロジェクト・アプリケーションの作成が完了です。
次に作成したアプリケーションを使うために設定を行います。inventory_manager/inventory_manager内のsettings.pyにアプリケーションの登録を行います。
```python:inventory_manager/settings.py
...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'inventory_manager_app.apps.InventoryManagerAppConfig',#追加
]
...
LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_TZ = True
...
```
ここまで、できたらいよいよ要件定義で決めた機能の実行を行っていきます。

---
## モデル作成
```Django```はモデルとビューを使用してwebアプリケーションを作成していきます。モデル作成では、数値や文字などの値のフィールドを作っていきます。
具体的には、アプリケーションを作成したときに生成された```inventory_manager_app```フォルダの中の```models.py```を編集することでモデルの実装を行うことができます。
今回は、以下の4つのモデルを作成していこうと思います。
1. カテゴリモデル(Category)
2. ロケーションモデル(Location)
3. 商品モデル(Product)
4. 在庫モデル(StockTransaction)

各モデルの細かい要素は、コードを見ながら確認しましょう。
```python:inventory_manager_app/models.py
from django.db import models

#カテゴリモデル
class Category(models.Model):
    name = models.CharField(max_length=20,verbose_name='カテゴリ名')
    created_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return self.name

#ロケーションモデル
class Location(models.Model):
    name = models.CharField(max_length=50,verbose_name='保管場所')
    description = models.TextField(verbose_name='保管場所住所')
    
    def __str__(self):
        return self.name

#商品モデル
class Product(models.Model):
    name = models.CharField(max_length=50,verbose_name='商品名')
    sku = models.CharField(max_length=50,verbose_name='商品識別用コード')
    description = models.TextField(verbose_name='商品説明')
    category = models.ForeignKey(Category,on_delete=models.CASCADE,verbose_name='カテゴリ')
    location = models.ForeignKey(Location,on_delete=models.CASCADE,verbose_name='保管場所')
    stock = models.PositiveIntegerField(default=0,verbose_name='在庫数')
    min_stock_level = models.IntegerField(default=0,verbose_name='最小在庫数')
    created_at = models.DateTimeField(auto_now_add=True,verbose_name='登録日時')
    updated_at = models.DateTimeField(auto_now=True,verbose_name='更新日時')
    
    def __str__(self):
        return self.name

#在庫モデル
class StockTransaction(models.Model):
    IN = 'IN'
    OUT = 'OUT'
    TRANSACTION_CHOICES = [
        (IN,'入庫'),
        (OUT,'出庫')
    ]
    
    product = models.ForeignKey(Product,on_delete=models.CASCADE,verbose_name='商品')
    quantity = models.PositiveIntegerField(verbose_name='入出庫数')
    transaction_type = models.CharField(max_length=3,choices=TRANSACTION_CHOICES,verbose_name='取引タイプ(IN or OUT)')
    note = models.TextField(blank=True,null=True,verbose_name='備考')
    last_updated = models.DateTimeField(auto_now=True,verbose_name='最終更新日')
    
    def __str__(self):
        return f"{self.product.name} - {self.quantity} ({self. transaction_type})"
```
上記のコードのように```IntegerField```や```CharField```などの整数値を扱うフィールド、文字列を扱うフィールドを設定していきます。引数値の```verbose_name```をうまく使うことでこのフィールドはどんな値を示しているのかがわかりやすくなります。
```Field```と記載がない```ForeignKey```はリレーションを行うフィールドで、リレーションとはデータベースのテーブル同士を関連付けることです。
商品モデルの```
category = models.ForeignKey(Category,on_delete=models.CASCADE,verbose_name="カテゴリ")```例にすると、商品モデルの```category```という変数にカテゴリモデルの要素である```name```と```description```を参照しているということです。

そしたら、記述したモデルをデータベースに適用していきましょう。マイグレーションと呼ばれるデータベース構造に反映させる仕組みを使います。やり方はすごく簡単です。
```
python manage.py makemigrations #変更点を検出し、マイグレーションファイルを作成
python manage.py migrate        #データベースに適用
```
---
### ビューの作成
まずは簡単に、商品の一覧を表示するページを作成してみましょう。

ビューの作成はアプリケーションを作成したときに生成されたinventory_manager_appフォルダの中のviews.pyを編集することで行うことができます。

```python:inventory_manager_app/views.py
from django.shortcuts import render,redirect
from django.views.generic import ListView,View
from .models import Product,StockTransaction

class ProductListView(ListView):
    def get(self,request):
        product_list = Product.objects.all()
        return render(request,'inventory_manager_app/product_list.html',{'product_list':product_list})

product_list = ProductListView.as_view()
```
```product_list```変数で商品モデルのすべてのオブジェクトを取得し、テンプレート(```product_list.html```)に渡しています。
では、```product_list.html```を作成していきます。
＊テンプレートはアプリケーションフォルダ(inventory_manager_app)内に```templates/inventory_manager_app```フォルダを作成してその中に格納します。
```html:templates/inventory_manager_app/product_list.html
{% extends "base.html" %}
{% block content %}

<h2 class="product_list">商品一覧</h2>
<table>
    <thead>
        <tr>
            <th>商品</th>
            <th>カテゴリ</th>
            <th>在庫</th>
            <th>更新日時</th>
            <th>備考</th>
        </tr>
    </thead>
    <tbody>
        {% for product in product_list %}
        <tr>
            <td>{{ product.name }}</td>
            <td>{{ product.category }}</td>
            <td>{{ product.stock }}</td>
            <td>{{ product.updated_at }}</td>
            <td>{{ product.note }}</td>
        </tr>
        {% empty %}
        <tr>
            <td colspan="5">履歴がありません。</td>
        </tr>
        {% endfor %}
    </tbody>
</table>
{% endblock %}
```
上記のコードの最初```{% extends "base.html" %}```は、```base.html```を読み込むコードであり、テンプレートファイルで、同じ記述をする部分を記述しておくことで、何回も同じ記述をすることを避けることができます。

作成した```ProductListView```はただオブジェクトを表示するだけのものなので、商品の登録ができません。そこで、```admin.py```を編集して、管理者画面から商品登録を行ってみようと思います。
```python:inventory_manager_app/admin.py
from django.contrib import admin
from .models import Category,Location,Product,StockTransaction

#モデル登録
admin.site.register(Category)
admin.site.register(Location)
admin.site.register(Product)
admin.site.register(StockTransaction)
```
まずはこれだけ記述しておけば大丈夫でしょう。
サーバーを動かして、アプリケーションを使用するためにプロジェクトファイルの```urls.py```にパス設定を行います。
```python:inventory_manager/urls.py
from django.contrib import admin
from django.urls import path,include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('',include('inventory_manager_app.urls')), #追加
]
```
これで、サーバーを動かすと管理者画面にアクセスすることができます。管理者画面はログインが必要なので、スーパーユーザーを作成してからアクセスします。
```:管理者画面にアクセス
python manage.py createsuperuser
python manage.py runserver
```
```createsuperuser```でスーパーユーザーを作成し、```runserver```でサーバーを起動します。```runserver```を行うと
```http://127.0.0.1:8000/```
というリンクが出てくるので、この後に続けて、```admin```と入力すると管理者画面にアクセスでき、商品の登録を行うことができます。
商品の登録を行うと先ほど作成したビューに登録した商品が表示されるはずです。```inventory_manager_app/urls.py```を作成して、URL設定を行うとサーバー上でテンプレートファイルを表示することができます。
```python:inventory_manager_app/urls.py
from django.urls import path
from .views import product_list
urlpatterns = [
    path('product_list',product_list,name='product_list'),
]
```
このように記述することで、サーバーを起動後に、```http://127.0.0.1:8000/product_list```とURLを打つと表示されます。
### 在庫数の自動更新
次に在庫数の自動更新機能を付けます。今のままだと商品の入出庫ができません。そのための在庫の変動を管理することができないため、入出庫に合わせて自動で在庫を更新する機能をつけます。
まず```inventory_manager_app```フォルダに```forms.py```を作成して、入出庫をフォーム入力できるようにしていきます。
```python:inventory_manager_app/forms.py
from django import forms
from .models import StockTransaction,Product

class StockInForm(forms.ModelForm):
    class Meta:
        model = StockTransaction 
        fields = ['product','quantity','note']
    
    def __init__(self,*args,**kwargs):
        super().__init__(*args,**kwargs)
        self.fields['quantity'].widget.attrs['min'] = 0 #最小値を0に設定
    
    def save(self,commit=True):
        instance = super().save(commit=False)
        instance.transaction_type = StockTransaction.IN #transaction_typeをINに固定
        
        #在庫を増やす
        product = instance.product
        product.stock += instance.quantity
        product.save() #在庫情報更新

        if commit:
            instance.save()
        return instance

class StockOutForm(forms.ModelForm):
    class Meta:
        model = StockTransaction
        fields = ['product','quantity','note']
    
    def __init__(self,*args,**kwargs):
        super().__init__(*args,**kwargs)
        self.fields['quantity'].widget.attrs['min'] = 0 #最小値を0に固定
    
    #フォームバリデーション
    def clean_quantity(self):
        quantity = self.cleaned_data.get("quantity")
        if quantity is None or quantity < 0:
            raise forms.ValidationError("在庫数は0以上である必要があります。")
        return quantity
    
    def save(self,commit=True):
        instance = super().save(commit=False)
        instance.transaction_type = StockTransaction.OUT #transaction_typeをOUTに固定 
        
        #在庫を減らす
        product = instance.product
        if product.stock >= instance.quantity:
            product.stock -= instance.quantity
            product.save() #商品の在庫数を更新
        else:
            raise ValueError("在庫が不足しています。(現在の在庫：{})".format(product.stock))
        
        if commit:
            instance.save() #出庫情報を保存
        return instance
```
```StockInForm```と```StockOutForm```と入庫と出庫別々のフォームを作成しました。
内容としては、在庫モデルで作成した```product```,```quantity```,```note```フィールドを入力するフォームを作成して、```save```関数の部分で、在庫の増減を計算して、在庫数を更新しています。
このフォームを表示するためのビューがこちらです。
```python:inventory_manager_app/views.py
class StockTrasaction(View):
    def get(self,request):
        inform =  StockInForm()
        outform = StockOutForm()
        return render(request,'inventory_manager_app/stocktransaction.html',{'inform':inform,'outform':outform})

    def post(self,request):
        inform = StockInForm(request.POST)
        outform = StockOutForm(request.POST)
    # フォームの種類を判別するための隠しフィールドを追加することをお勧めします
        if 'form_type' in request.POST:
            if request.POST['form_type'] == 'in':
                if inform.is_valid():
                    inform.save()
                    return redirect('stocktransaction_list')
            else:
                if outform.is_valid():
                    stock_transaction = outform.save()
                    product = stock_transaction.product
                    if product.stock <= product.min_stock_level:
                        messages.warning(request, f"⚠ {product.name} の在庫が最低レベルを下回りました！")
                    return redirect('stocktransaction_list')
              
        return render(request, 'inventory_manager_app/stocktransaction.html', 
                 {'inform': inform, 'outform': outform})

stocktransaction = StockTrasaction.as_view()
```
フォーム入力されると```post```のほうが動き、フォーム内容の確認を行い、在庫数の更新を行っています。```forms.py```のほうでもフォームバリデーションを記述しているので、```views.py```のほうで記述する必要はないですが、このような記述もできるということで記述しました。
ビューの作成ができたら、テンプレートを作成していきます。
テンプレートは以下になります。
```html:templates/stocktransaction.html
{% extends "base.html" %}

{% block content %}
<div class="transaction-forms">
    <div class="form-section">
        <h3>入庫登録</h3>
        <form method="post" action="{% url 'stocktransaction' %}">
            {% csrf_token %}
            <input type="hidden" name="form_type" value="in">
            {% for field in inform %}
            <div class="form-group">
                <label for="{{ field.id_for_label }}">{{ field.label }}</label>
                {{ field }}
                {% if field.errors %}
                <div class="error-message">
                    {% for error in field.errors %}
                    <p>{{ error }}</p>
                    {% endfor %}
                </div>
                {% endif %}
            </div>
            {% endfor %}
            <button type="submit">入庫を登録</button>
        </form>
    </div>

    <div class="form-section">
        <h3>出庫登録</h3>
        <form method="post" action="{% url 'stocktransaction' %}">
            {% csrf_token %}
            <input type="hidden" name="form_type" value="out">
            {% for field in outform %}
            <div class="form-group">
                <label for="{{ field.id_for_label }}">{{ field.label }}</label>
                {{ field }}
                {% if field.errors %}
                <div class="error-message">
                    {% for error in field.errors %}
                    <p>{{ error }}</p>
                    {% endfor %}
                </div>
                {% endif %}
            </div>
            {% endfor %}
            <button type="submit">出庫を登録</button>
        </form>
    </div>
</div>
{% endblock %}
```
```{% for field in inform %}```がフォームの部分で、後々```css```でUIを整えやすいように各フィールドのを```field```に格納して、表示しています。もしフォームでエラーが起きた場合、エラーメッセージが表示されるように```{% if field.errors %}```のように```if```文を使用して、エラーメッセージが表示されるようにしています。

そして、先ほどと同様にURL設定をします。
```python:inventory_manager_app/urls.py
from django.urls import path
from .views import product_list,stocktransaction
urlpatterns = [
    path('product_list',product_list,name='product_list'),
    path('stocktransaction',stocktransaction,name='stocktransaction'),
]
```
以上で、
1. **入庫・出庫管理**
    - 新規入庫・出庫の登録
    - 在庫数の自動更新
2. **在庫状況の表示**
    - 商品ごとの在庫数表示
    - 最終更新日時の表示

を満たすシステムができました。プログラムは```ChatGPT```に大まかを作成してもらい、エラー箇所を修正するといった方法で、充分できます。
あとは、デザイン部分である```css```を丸投げしてみたり、検索機能、csvファイルのダウンロード機能などを付けてみても面白いかなと思います！！