# CODEX 実装指示書: 施肥計画連携・引当機能（Phase 1.5）

> 作成日: 2026-03-14
> 対象: `keinasystem_t02`
> 設計案: `改善案/在庫管理機能実装案.md`（セクション23が対象）
> 前提: Phase 1（セクション1〜16）は実装済み。`apps/materials` が稼働中。

---

## 0. 実装の前提と絶対ルール

### 現在のプロジェクト構造（Phase 1 実装済み）

```
keinasystem_t02/
├── backend/
│   ├── keinasystem/
│   │   ├── settings.py        # apps.materials 登録済み
│   │   └── urls.py            # /api/materials/ 登録済み
│   └── apps/
│       ├── fields/            # 圃場管理（Field モデル）
│       ├── plans/             # 作付け計画（Crop, Variety モデル）
│       ├── fertilizer/        # 施肥計画（Fertilizer, FertilizationPlan, FertilizationEntry 等）
│       │   └── models.py      # Fertilizer.material = OneToOneField(Material) 追加済み
│       └── materials/         # 在庫管理（Material, FertilizerProfile, PesticideProfile, StockTransaction）
│           └── models.py      # Phase 1 で作成済み
└── frontend/
    └── src/
        ├── types/index.ts         # Material, StockTransaction, StockSummary 定義済み
        ├── lib/api.ts             # axios インスタンス（変更不要）
        ├── components/
        │   └── Navbar.tsx         # 在庫管理メニュー追加済み
        └── app/
            ├── fertilizer/        # 施肥計画（既存）← 今回変更対象
            │   ├── page.tsx
            │   ├── [id]/edit/page.tsx
            │   └── _components/FertilizerEditPage.tsx
            └── materials/         # 在庫管理（Phase 1 で作成済み）← 今回変更対象
                ├── page.tsx
                └── _components/StockOverview.tsx
```

### 技術スタック

- Backend: Django 5.2 + Django REST Framework + PostgreSQL 16
- Frontend: Next.js 14 (App Router) + TypeScript strict + Tailwind CSS
- 認証: SimpleJWT（ヘッダー `Authorization: Bearer <token>`）
- Docker: `docker compose exec backend python manage.py ...`

### 絶対ルール

1. **既存の施肥計画 CRUD（作成・編集・削除・PDF）を壊さない**
2. **`FertilizationEntry → Fertilizer` の FK は変更しない**
3. **`Fertilizer` モデルは改名・削除しない**
4. **フロントエンドでは `alert()` / `confirm()` を使わない**（インラインバナーで表示）
5. **TypeScript strict mode に従う**
6. **Next.js 14 では `params` は通常のオブジェクト**（`use(params)` は使わない）
7. **マイグレーションは段階的に。1つのマイグレーションで複数の大きな変更をしない**

---

## 1. 実装スコープ（Phase 1.5）

### やること

1. `StockTransaction` に `reserve` タイプ追加
2. `StockTransaction` に `fertilization_plan` FK 追加（マイグレーション）
3. `FertilizationPlan` に `is_confirmed` / `confirmed_at` 追加（マイグレーション）
4. 在庫集計 API に `reserved_stock` / `available_stock` 追加
5. 施肥計画の保存時に引当（reserve）を自動作成
6. 施肥計画の削除時に引当を自動解除
7. 散布確定 API（`confirm_spreading`）
8. 肥料在庫一覧 API（施肥計画画面用）
9. フロントエンド: 在庫一覧に引当表示追加
10. フロントエンド: 施肥計画編集に在庫参照追加
11. フロントエンド: 散布確定画面
12. フロントエンド: 施肥計画一覧に確定状態表示追加

### やらないこと

- 公式データ同期（FAMIC、農水省）
- 別名辞書（MaterialAlias）
- LLM 調査支援
- 農薬散布計画の在庫連携

---

## 2. バックエンド: モデル変更

### 2.1 StockTransaction の変更 (`backend/apps/materials/models.py`)

**現在のコード**（変更が必要な箇所のみ抜粋）:

```python
class StockTransaction(models.Model):
    class TransactionType(models.TextChoices):
        PURCHASE = 'purchase', '入庫'
        USE = 'use', '使用'
        ADJUSTMENT_PLUS = 'adjustment_plus', '棚卸増'
        ADJUSTMENT_MINUS = 'adjustment_minus', '棚卸減'
        DISCARD = 'discard', '廃棄'

    INCREASE_TYPES = {
        TransactionType.PURCHASE,
        TransactionType.ADJUSTMENT_PLUS,
    }
    DECREASE_TYPES = {
        TransactionType.USE,
        TransactionType.ADJUSTMENT_MINUS,
        TransactionType.DISCARD,
    }
```

**変更後**:

```python
class StockTransaction(models.Model):
    class TransactionType(models.TextChoices):
        PURCHASE = 'purchase', '入庫'
        USE = 'use', '使用'
        RESERVE = 'reserve', '引当'          # ← 追加
        ADJUSTMENT_PLUS = 'adjustment_plus', '棚卸増'
        ADJUSTMENT_MINUS = 'adjustment_minus', '棚卸減'
        DISCARD = 'discard', '廃棄'

    INCREASE_TYPES = {
        TransactionType.PURCHASE,
        TransactionType.ADJUSTMENT_PLUS,
    }
    DECREASE_TYPES = {
        TransactionType.USE,
        TransactionType.RESERVE,             # ← 追加
        TransactionType.ADJUSTMENT_MINUS,
        TransactionType.DISCARD,
    }
```

**フィールド追加**（既存フィールドの後に追加）:

```python
    fertilization_plan = models.ForeignKey(
        'fertilizer.FertilizationPlan',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='stock_reservations',
        verbose_name='施肥計画',
    )
```

### 2.2 FertilizationPlan の変更 (`backend/apps/fertilizer/models.py`)

**フィールド追加**（既存フィールドの後に追加）:

```python
    is_confirmed = models.BooleanField(
        default=False, verbose_name='散布確定済み'
    )
    confirmed_at = models.DateTimeField(
        null=True, blank=True, verbose_name='散布確定日時'
    )
```

### 2.3 マイグレーション

#### マイグレーション1: `backend/apps/materials/migrations/0002_stocktransaction_fertilization_plan.py`

```python
import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('materials', '0001_initial'),
        ('fertilizer', '0005_fertilizer_material'),
    ]

    operations = [
        migrations.AddField(
            model_name='stocktransaction',
            name='fertilization_plan',
            field=models.ForeignKey(
                blank=True,
                null=True,
                on_delete=django.db.models.deletion.SET_NULL,
                related_name='stock_reservations',
                to='fertilizer.fertilizationplan',
                verbose_name='施肥計画',
            ),
        ),
    ]
```

注意: `TransactionType` の choices 変更はマイグレーション不要（Django は choices をDBレベルで強制しないため）。

#### マイグレーション2: `backend/apps/fertilizer/migrations/0006_fertilizationplan_confirmation.py`

```python
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('fertilizer', '0005_fertilizer_material'),
    ]

    operations = [
        migrations.AddField(
            model_name='fertilizationplan',
            name='is_confirmed',
            field=models.BooleanField(default=False, verbose_name='散布確定済み'),
        ),
        migrations.AddField(
            model_name='fertilizationplan',
            name='confirmed_at',
            field=models.DateTimeField(
                blank=True, null=True, verbose_name='散布確定日時'
            ),
        ),
    ]
```

---

## 3. バックエンド: 引当ロジック

### 3.1 引当の作成・解除ヘルパー関数

`backend/apps/materials/stock_service.py` を新規作成:

```python
from django.db import transaction
from .models import StockTransaction


@transaction.atomic
def create_reserves_for_plan(plan):
    """施肥計画の全エントリについて引当トランザクションを作成する。
    既存の引当は全削除してから再作成する（差分更新ではなく全置換）。
    """
    # 既存の引当を全削除
    StockTransaction.objects.filter(
        fertilization_plan=plan,
        transaction_type='reserve',
    ).delete()

    # plan が確定済みなら引当を作らない（use が既にある）
    if plan.is_confirmed:
        return

    for entry in plan.entries.select_related('fertilizer__material'):
        material = getattr(entry.fertilizer, 'material', None)
        if material is None:
            # Fertilizer.material が未連携の場合はスキップ
            continue
        StockTransaction.objects.create(
            material=material,
            transaction_type='reserve',
            quantity=entry.bags,
            occurred_on=plan.updated_at.date() if plan.updated_at else plan.created_at.date(),
            note=f'施肥計画「{plan.name}」からの引当',
            fertilization_plan=plan,
        )


@transaction.atomic
def delete_reserves_for_plan(plan):
    """施肥計画に紐づく全引当トランザクションを削除する。"""
    StockTransaction.objects.filter(
        fertilization_plan=plan,
        transaction_type='reserve',
    ).delete()


@transaction.atomic
def confirm_spreading(plan, actual_entries):
    """散布確定: 引当を削除し、実績数量で use トランザクションを作成する。

    actual_entries: list of dict
        [{"field_id": int, "fertilizer_id": int, "actual_bags": Decimal}, ...]
        actual_bags=0 の行は引当解除のみ（use を作成しない）
    """
    from apps.fertilizer.models import Fertilizer
    from django.utils import timezone

    # 既存の引当を全削除
    delete_reserves_for_plan(plan)

    # 実績 > 0 の行について use トランザクションを作成
    today = timezone.now().date()
    for entry_data in actual_entries:
        actual_bags = entry_data['actual_bags']
        if actual_bags <= 0:
            continue

        try:
            fertilizer = Fertilizer.objects.select_related('material').get(
                id=entry_data['fertilizer_id']
            )
        except Fertilizer.DoesNotExist:
            continue

        material = getattr(fertilizer, 'material', None)
        if material is None:
            continue

        StockTransaction.objects.create(
            material=material,
            transaction_type='use',
            quantity=actual_bags,
            occurred_on=today,
            note=f'施肥計画「{plan.name}」散布確定',
            fertilization_plan=plan,
        )

    # 計画を確定済みに更新
    plan.is_confirmed = True
    plan.confirmed_at = timezone.now()
    plan.save(update_fields=['is_confirmed', 'confirmed_at'])
```

### 3.2 施肥計画 ViewSet の変更 (`backend/apps/fertilizer/views.py`)

既存の `FertilizationPlanViewSet` に以下の変更を加える。

#### 保存時の引当自動作成

`perform_create` と `perform_update` をオーバーライドして、保存後に引当を作成する:

```python
from apps.materials.stock_service import (
    create_reserves_for_plan,
    delete_reserves_for_plan,
    confirm_spreading as confirm_spreading_service,
)

class FertilizationPlanViewSet(viewsets.ModelViewSet):
    # ... 既存コード ...

    def perform_create(self, serializer):
        instance = serializer.save()
        create_reserves_for_plan(instance)

    def perform_update(self, serializer):
        instance = serializer.save()
        create_reserves_for_plan(instance)

    def perform_destroy(self, instance):
        delete_reserves_for_plan(instance)
        instance.delete()
```

#### 散布確定アクション

```python
from rest_framework.decorators import action
from decimal import Decimal

class FertilizationPlanViewSet(viewsets.ModelViewSet):
    # ... 既存コード ...

    @action(detail=True, methods=['post'], url_path='confirm_spreading')
    def confirm_spreading(self, request, pk=None):
        plan = self.get_object()

        if plan.is_confirmed:
            return Response(
                {'detail': 'この計画は既に散布確定済みです。'},
                status=status.HTTP_400_BAD_REQUEST,
            )

        entries_data = request.data.get('entries', [])
        if not entries_data:
            return Response(
                {'detail': '実績データが空です。'},
                status=status.HTTP_400_BAD_REQUEST,
            )

        actual_entries = []
        for entry in entries_data:
            actual_entries.append({
                'field_id': entry['field_id'],
                'fertilizer_id': entry['fertilizer_id'],
                'actual_bags': Decimal(str(entry.get('actual_bags', 0))),
            })

        confirm_spreading_service(plan, actual_entries)

        serializer = self.get_serializer(plan)
        return Response(serializer.data)
```

### 3.3 施肥計画 Serializer の変更 (`backend/apps/fertilizer/serializers.py`)

`FertilizationPlanSerializer`（読み取り用）に `is_confirmed` / `confirmed_at` を追加:

```python
class FertilizationPlanSerializer(serializers.ModelSerializer):
    # ... 既存フィールド ...
    is_confirmed = serializers.BooleanField(read_only=True)
    confirmed_at = serializers.DateTimeField(read_only=True)

    class Meta:
        model = FertilizationPlan
        fields = [
            # ... 既存フィールド ...,
            'is_confirmed', 'confirmed_at',
        ]
```

---

## 4. バックエンド: 在庫集計 API の変更

### 4.1 StockSummarySerializer の変更 (`backend/apps/materials/serializers.py`)

```python
class StockSummarySerializer(serializers.Serializer):
    material_id = serializers.IntegerField()
    name = serializers.CharField()
    material_type = serializers.CharField()
    material_type_display = serializers.CharField()
    maker = serializers.CharField()
    stock_unit = serializers.CharField()
    stock_unit_display = serializers.CharField()
    is_active = serializers.BooleanField()
    current_stock = serializers.DecimalField(max_digits=10, decimal_places=3)
    reserved_stock = serializers.DecimalField(max_digits=10, decimal_places=3)   # ← 追加
    available_stock = serializers.DecimalField(max_digits=10, decimal_places=3)  # ← 追加
    last_transaction_date = serializers.DateField(allow_null=True)
```

### 4.2 StockSummaryView の変更 (`backend/apps/materials/views.py`)

在庫集計のループ内で `reserved_stock` と `available_stock` を計算する:

```python
for material in queryset:
    transactions = list(material.stock_transactions.all())
    increase = sum(
        txn.quantity for txn in transactions
        if txn.transaction_type in StockTransaction.INCREASE_TYPES
    )
    decrease = sum(
        txn.quantity for txn in transactions
        if txn.transaction_type in StockTransaction.DECREASE_TYPES
    )
    reserved = sum(
        txn.quantity for txn in transactions
        if txn.transaction_type == 'reserve'
    )
    last_date = max((txn.occurred_on for txn in transactions), default=None)

    current = increase - decrease   # 引当込みの在庫（引当分は既に引かれている）
    results.append({
        'material_id': material.id,
        'name': material.name,
        'material_type': material.material_type,
        'material_type_display': material.get_material_type_display(),
        'maker': material.maker,
        'stock_unit': material.stock_unit,
        'stock_unit_display': material.get_stock_unit_display(),
        'is_active': material.is_active,
        'current_stock': current + reserved,  # 引当を戻した「物理的な在庫」
        'reserved_stock': reserved,           # 引当中の数量
        'available_stock': current,           # 利用可能在庫（引当済み分を除く）
        'last_transaction_date': last_date,
    })
```

**在庫計算の定義**:
- `current_stock`: 物理的に倉庫にある数量（入庫 - 使用 - 廃棄 ± 調整）
- `reserved_stock`: そのうち施肥計画で引き当てられている数量
- `available_stock`: 新しい計画に使える数量（= current_stock - reserved_stock）

### 4.3 肥料在庫 API（施肥計画画面用）

`backend/apps/materials/views.py` に追加:

```python
class FertilizerStockView(generics.ListAPIView):
    """施肥計画画面用: 肥料の在庫情報を返す"""
    permission_classes = [IsAuthenticated]
    serializer_class = StockSummarySerializer

    def get_queryset(self):
        return None

    def list(self, request, *args, **kwargs):
        queryset = Material.objects.filter(
            material_type='fertilizer',
            is_active=True,
        ).prefetch_related('stock_transactions')

        results = []
        for material in queryset:
            transactions = list(material.stock_transactions.all())
            increase = sum(
                txn.quantity for txn in transactions
                if txn.transaction_type in StockTransaction.INCREASE_TYPES
            )
            decrease = sum(
                txn.quantity for txn in transactions
                if txn.transaction_type in StockTransaction.DECREASE_TYPES
            )
            reserved = sum(
                txn.quantity for txn in transactions
                if txn.transaction_type == 'reserve'
            )
            current = increase - decrease
            results.append({
                'material_id': material.id,
                'name': material.name,
                'material_type': material.material_type,
                'material_type_display': material.get_material_type_display(),
                'maker': material.maker,
                'stock_unit': material.stock_unit,
                'stock_unit_display': material.get_stock_unit_display(),
                'is_active': material.is_active,
                'current_stock': current + reserved,
                'reserved_stock': reserved,
                'available_stock': current,
                'last_transaction_date': max(
                    (t.occurred_on for t in transactions), default=None
                ),
            })

        serializer = StockSummarySerializer(results, many=True)
        return Response(serializer.data)
```

`backend/apps/materials/urls.py` に追加:

```python
urlpatterns = [
    path('', include(router.urls)),
    path('stock-summary/', views.StockSummaryView.as_view(), name='stock-summary'),
    path('fertilizer-stock/', views.FertilizerStockView.as_view(), name='fertilizer-stock'),  # ← 追加
]
```

---

## 5. フロントエンド: 型定義の変更

### 5.1 StockTransaction 型に `reserve` 追加 (`frontend/src/types/index.ts`)

**変更前**:
```typescript
transaction_type: 'purchase' | 'use' | 'adjustment_plus' | 'adjustment_minus' | 'discard';
```

**変更後**:
```typescript
transaction_type: 'purchase' | 'use' | 'reserve' | 'adjustment_plus' | 'adjustment_minus' | 'discard';
```

### 5.2 StockSummary 型に引当フィールド追加

**変更前**:
```typescript
export interface StockSummary {
  material_id: number;
  name: string;
  material_type: 'fertilizer' | 'pesticide' | 'seedling' | 'other';
  material_type_display: string;
  maker: string;
  stock_unit: string;
  stock_unit_display: string;
  is_active: boolean;
  current_stock: string;
  last_transaction_date: string | null;
}
```

**変更後**:
```typescript
export interface StockSummary {
  material_id: number;
  name: string;
  material_type: 'fertilizer' | 'pesticide' | 'seedling' | 'other';
  material_type_display: string;
  maker: string;
  stock_unit: string;
  stock_unit_display: string;
  is_active: boolean;
  current_stock: string;
  reserved_stock: string;      // ← 追加
  available_stock: string;     // ← 追加
  last_transaction_date: string | null;
}
```

### 5.3 FertilizationPlan 型に確定フィールド追加

既存の `FertilizationPlan` インターフェースに追加:

```typescript
export interface FertilizationPlan {
  // ... 既存フィールド ...
  is_confirmed: boolean;           // ← 追加
  confirmed_at: string | null;     // ← 追加
}
```

---

## 6. フロントエンド: 画面変更

### 6.1 在庫一覧の引当表示 (`frontend/src/app/materials/_components/StockOverview.tsx`)

現在庫の表示を変更:

**変更前**:
```
現在庫: 18
```

**変更後**:
```
在庫 18袋（引当 12袋）/ 利用可能 6袋
```

引当が0の場合は引当表示を省略する。

### 6.2 施肥計画編集画面の在庫参照 (`frontend/src/app/fertilizer/_components/FertilizerEditPage.tsx`)

施肥計画の編集画面（マトリクス表）で、肥料列ヘッダーに在庫情報を表示する。

**追加表示**（肥料名の下に小さく）:
```
仁井田米有機
在庫 18袋 / 計画計 24袋
```

計画合計が在庫を超える場合は赤文字で「不足 6袋」を表示する。

**データ取得**: ページ読み込み時に `GET /api/materials/fertilizer-stock/` を呼び、
`Fertilizer.material` の OneToOne 経由で material_id と紐づける。

紐づけロジック:
1. `GET /api/fertilizer/fertilizers/` で肥料一覧を取得（既存）
2. `GET /api/materials/materials/?material_type=fertilizer` で Material 一覧を取得
3. `Fertilizer.name` と `Material.name` を突き合わせる（同名で作成されているため一致する）

または、Fertilizer の serializer に `material_id` を追加して直接紐づける（推奨）。

**Fertilizer serializer への追加**（`backend/apps/fertilizer/serializers.py`）:

```python
class FertilizerSerializer(serializers.ModelSerializer):
    material_id = serializers.IntegerField(source='material.id', read_only=True, default=None)

    class Meta:
        model = Fertilizer
        fields = [
            # ... 既存フィールド ...,
            'material_id',
        ]
```

### 6.3 施肥計画一覧の確定状態表示 (`frontend/src/app/fertilizer/page.tsx`)

各計画行に確定状態を表示:

- 未確定: 通常表示 + 「散布確定」ボタン
- 確定済み: 背景色変更（例: 薄い青）+ 「確定済み ✓」バッジ + 確定日時

### 6.4 散布確定画面

**実装方法**: モーダルまたは専用ページ。施肥計画一覧の「散布確定」ボタンから起動。

**画面構成**:

```
┌─ 散布確定: 「計画名」──────────────────────────────┐
│                                                     │
│  肥料: 仁井田米有機                                  │
│  ┌─────────────┬──────┬──────────┐                  │
│  │ 圃場         │ 計画  │ 実績      │                  │
│  ├─────────────┼──────┼──────────┤                  │
│  │ 上の田       │ 3袋   │ [  3   ]  │                  │
│  │ 下の田       │ 4袋   │ [  3.5 ]  │                  │
│  │ 山の畑       │ 2袋   │ [  0   ]  │                  │
│  └─────────────┴──────┴──────────┘                  │
│                                                     │
│  肥料: 土佐勤農党                                    │
│  ┌─────────────┬──────┬──────────┐                  │
│  │ 圃場         │ 計画  │ 実績      │                  │
│  ├─────────────┼──────┼──────────┤                  │
│  │ ...          │ ...   │ [  ... ]  │                  │
│  └─────────────┴──────┴──────────┘                  │
│                                                     │
│                      [キャンセル] [一括確定]          │
└─────────────────────────────────────────────────────┘
```

**動作**:
1. 施肥計画のエントリを肥料ごとにグループ化して表示
2. 「実績」列は計画値がプリセットされた数値入力欄
3. 修正が必要な行だけ数値を変更する
4. 実績を0にした行は「未散布」として引当解除される
5. 「一括確定」で `POST /api/fertilizer/plans/{id}/confirm_spreading/` を呼ぶ

**API リクエスト**:
```json
{
  "entries": [
    {"field_id": 1, "fertilizer_id": 3, "actual_bags": 3.0},
    {"field_id": 2, "fertilizer_id": 3, "actual_bags": 3.5},
    {"field_id": 3, "fertilizer_id": 3, "actual_bags": 0}
  ]
}
```

---

## 7. API エンドポイント一覧（Phase 1.5 で追加・変更）

### 新規

| メソッド | パス | 認証 | 説明 |
|----------|------|------|------|
| POST | `/api/fertilizer/plans/{id}/confirm_spreading/` | JWT | 散布確定（reserve→use変換） |
| GET | `/api/materials/fertilizer-stock/` | JWT | 肥料在庫一覧（施肥計画画面用） |

### 変更

| メソッド | パス | 変更内容 |
|----------|------|----------|
| POST/PUT | `/api/fertilizer/plans/` | 保存後に reserve 自動作成 |
| DELETE | `/api/fertilizer/plans/{id}/` | 削除前に reserve 自動削除 |
| GET | `/api/fertilizer/plans/` | レスポンスに `is_confirmed`, `confirmed_at` 追加 |
| GET | `/api/fertilizer/fertilizers/` | レスポンスに `material_id` 追加 |
| GET | `/api/materials/stock-summary/` | レスポンスに `reserved_stock`, `available_stock` 追加 |

---

## 8. 実装順序（厳守）

### Step 1: バックエンド — モデル・マイグレーション
1. `apps/materials/models.py` に `reserve` タイプ追加、`DECREASE_TYPES` 更新、`fertilization_plan` FK 追加
2. `apps/fertilizer/models.py` に `is_confirmed`, `confirmed_at` 追加
3. `apps/materials/migrations/0002_stocktransaction_fertilization_plan.py` 作成
4. `apps/fertilizer/migrations/0006_fertilizationplan_confirmation.py` 作成

### Step 2: バックエンド — ロジック・API
5. `apps/materials/stock_service.py` 作成（引当作成・解除・散布確定ヘルパー）
6. `apps/fertilizer/views.py` の `FertilizationPlanViewSet` に `perform_create`, `perform_update`, `perform_destroy` オーバーライド追加
7. `apps/fertilizer/views.py` に `confirm_spreading` アクション追加
8. `apps/fertilizer/serializers.py` に `is_confirmed`, `confirmed_at` 追加
9. `apps/fertilizer/serializers.py` の `FertilizerSerializer` に `material_id` 追加
10. `apps/materials/serializers.py` の `StockSummarySerializer` に `reserved_stock`, `available_stock` 追加
11. `apps/materials/views.py` の `StockSummaryView` で引当集計を追加
12. `apps/materials/views.py` に `FertilizerStockView` 追加
13. `apps/materials/urls.py` に `fertilizer-stock/` パス追加

### Step 3: フロントエンド
14. `types/index.ts` に `reserve` タイプ追加、`StockSummary` に引当フィールド追加、`FertilizationPlan` に確定フィールド追加
15. `app/materials/_components/StockOverview.tsx` に引当表示追加
16. `app/materials/page.tsx` の `StockTransactionForm` に `reserve` オプション追加（手動引当は不要なら省略可）
17. `app/fertilizer/_components/FertilizerEditPage.tsx` に在庫参照表示追加
18. `app/fertilizer/page.tsx` に確定状態表示・散布確定ボタン追加
19. `app/fertilizer/_components/ConfirmSpreadingModal.tsx` 新規作成（散布確定モーダル）

---

## 9. テスト確認項目

### バックエンド
- [ ] マイグレーション適用成功（materials 0002, fertilizer 0006）
- [ ] 施肥計画を保存すると、各エントリに対応する reserve トランザクションが作成される
- [ ] 施肥計画を更新すると、古い reserve が削除され新しい reserve が作成される
- [ ] 施肥計画を削除すると、reserve が全て削除される
- [ ] `GET /api/materials/stock-summary/` で `reserved_stock` と `available_stock` が返る
- [ ] 入庫10 → 引当3 → `current_stock=10`, `reserved_stock=3`, `available_stock=7`
- [ ] `POST /api/fertilizer/plans/{id}/confirm_spreading/` で reserve が use に変換される
- [ ] 確定済み計画に再度 confirm_spreading すると 400 エラー
- [ ] actual_bags=0 の行は reserve 削除のみ（use は作成しない）
- [ ] `Fertilizer.material` が null の Fertilizer は引当をスキップする
- [ ] 既存の施肥計画 CRUD（作成・編集・削除・PDF）が壊れていない

### フロントエンド
- [ ] 在庫一覧に引当数量と利用可能在庫が表示される
- [ ] 施肥計画編集画面に肥料ごとの在庫情報が表示される
- [ ] 施肥計画一覧に確定状態（未確定/確定済み）が表示される
- [ ] 散布確定モーダルが開き、計画値がプリセットされる
- [ ] 実績を修正して一括確定できる
- [ ] 確定後、計画が「確定済み」表示に変わる
- [ ] 確定済みの計画には「散布確定」ボタンが表示されない

---

## 10. 既存コードへの変更一覧（影響範囲）

| ファイル | 変更内容 |
|----------|----------|
| `backend/apps/materials/models.py` | `StockTransaction` に `reserve` タイプ・`fertilization_plan` FK 追加 |
| `backend/apps/materials/serializers.py` | `StockSummarySerializer` に `reserved_stock`・`available_stock` 追加 |
| `backend/apps/materials/views.py` | `StockSummaryView` 集計変更、`FertilizerStockView` 追加 |
| `backend/apps/materials/urls.py` | `fertilizer-stock/` パス追加 |
| `backend/apps/materials/stock_service.py` | **新規作成** — 引当ロジック |
| `backend/apps/materials/migrations/0002_...py` | **新規作成** — fertilization_plan FK |
| `backend/apps/fertilizer/models.py` | `FertilizationPlan` に `is_confirmed`・`confirmed_at` 追加 |
| `backend/apps/fertilizer/views.py` | `perform_create/update/destroy` オーバーライド、`confirm_spreading` アクション追加 |
| `backend/apps/fertilizer/serializers.py` | `is_confirmed`・`confirmed_at`・`material_id` 追加 |
| `backend/apps/fertilizer/migrations/0006_...py` | **新規作成** — is_confirmed, confirmed_at |
| `frontend/src/types/index.ts` | `reserve` タイプ追加、引当フィールド追加、確定フィールド追加 |
| `frontend/src/app/materials/_components/StockOverview.tsx` | 引当表示追加 |
| `frontend/src/app/fertilizer/_components/FertilizerEditPage.tsx` | 在庫参照表示追加 |
| `frontend/src/app/fertilizer/page.tsx` | 確定状態表示・散布確定ボタン追加 |
| `frontend/src/app/fertilizer/_components/ConfirmSpreadingModal.tsx` | **新規作成** — 散布確定モーダル |

---

## 11. 参照すべき既存コード（実装パターンの手本）

| 目的 | 参照先 |
|------|--------|
| 施肥計画 ViewSet（perform_create の追加先） | `backend/apps/fertilizer/views.py` |
| 施肥計画 Serializer（フィールド追加先） | `backend/apps/fertilizer/serializers.py` |
| 施肥計画の @action パターン（PDF アクション） | `backend/apps/fertilizer/views.py` の `pdf` アクション |
| 在庫集計ロジック | `backend/apps/materials/views.py` の `StockSummaryView` |
| 施肥計画編集画面（マトリクス表） | `frontend/src/app/fertilizer/_components/FertilizerEditPage.tsx` |
| 施肥計画一覧画面 | `frontend/src/app/fertilizer/page.tsx` |
| モーダルパターン | `frontend/src/app/materials/_components/StockTransactionForm.tsx` |
| 在庫一覧コンポーネント | `frontend/src/app/materials/_components/StockOverview.tsx` |
