Files
keinasystem/CODEX.md
Akira 42b11a5df8 在庫管理 Phase 1.5(引当・散布確定)の設計を追記し、CODEX指示書を更新
- 在庫管理機能実装案.md: セクション23(引当・散布確定ワークフロー)を追加
- CODEX.md: Phase 1完了を受け、Phase 1.5実装指示に全面書き換え

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 17:29:05 +09:00

32 KiB
Raw Blame History

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. StockTransactionreserve タイプ追加
  2. StockTransactionfertilization_plan FK 追加(マイグレーション)
  3. FertilizationPlanis_confirmed / confirmed_at 追加(マイグレーション)
  4. 在庫集計 API に reserved_stock / available_stock 追加
  5. 施肥計画の保存時に引当reserveを自動作成
  6. 施肥計画の削除時に引当を自動解除
  7. 散布確定 APIconfirm_spreading
  8. 肥料在庫一覧 API施肥計画画面用
  9. フロントエンド: 在庫一覧に引当表示追加
  10. フロントエンド: 施肥計画編集に在庫参照追加
  11. フロントエンド: 散布確定画面
  12. フロントエンド: 施肥計画一覧に確定状態表示追加

やらないこと

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

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

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

現在のコード(変更が必要な箇所のみ抜粋):

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,
    }

変更後:

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,
    }

フィールド追加(既存フィールドの後に追加):

    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)

フィールド追加(既存フィールドの後に追加):

    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

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

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 を新規作成:

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_createperform_update をオーバーライドして、保存後に引当を作成する:

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()

散布確定アクション

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 を追加:

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)

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_stockavailable_stock を計算する:

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 に追加:

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 に追加:

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)

変更前:

transaction_type: 'purchase' | 'use' | 'adjustment_plus' | 'adjustment_minus' | 'discard';

変更後:

transaction_type: 'purchase' | 'use' | 'reserve' | 'adjustment_plus' | 'adjustment_minus' | 'discard';

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

変更前:

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;
}

変更後:

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 インターフェースに追加:

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.nameMaterial.name を突き合わせる(同名で作成されているため一致する)

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

Fertilizer serializer への追加backend/apps/fertilizer/serializers.py:

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 リクエスト:

{
  "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.pyreserve タイプ追加、DECREASE_TYPES 更新、fertilization_plan FK 追加
  2. apps/fertilizer/models.pyis_confirmed, confirmed_at 追加
  3. apps/materials/migrations/0002_stocktransaction_fertilization_plan.py 作成
  4. apps/fertilizer/migrations/0006_fertilizationplan_confirmation.py 作成

Step 2: バックエンド — ロジック・API

  1. apps/materials/stock_service.py 作成(引当作成・解除・散布確定ヘルパー)
  2. apps/fertilizer/views.pyFertilizationPlanViewSetperform_create, perform_update, perform_destroy オーバーライド追加
  3. apps/fertilizer/views.pyconfirm_spreading アクション追加
  4. apps/fertilizer/serializers.pyis_confirmed, confirmed_at 追加
  5. apps/fertilizer/serializers.pyFertilizerSerializermaterial_id 追加
  6. apps/materials/serializers.pyStockSummarySerializerreserved_stock, available_stock 追加
  7. apps/materials/views.pyStockSummaryView で引当集計を追加
  8. apps/materials/views.pyFertilizerStockView 追加
  9. apps/materials/urls.pyfertilizer-stock/ パス追加

Step 3: フロントエンド

  1. types/index.tsreserve タイプ追加、StockSummary に引当フィールド追加、FertilizationPlan に確定フィールド追加
  2. app/materials/_components/StockOverview.tsx に引当表示追加
  3. app/materials/page.tsxStockTransactionFormreserve オプション追加(手動引当は不要なら省略可)
  4. app/fertilizer/_components/FertilizerEditPage.tsx に在庫参照表示追加
  5. app/fertilizer/page.tsx に確定状態表示・散布確定ボタン追加
  6. app/fertilizer/_components/ConfirmSpreadingModal.tsx 新規作成(散布確定モーダル)

9. テスト確認項目

バックエンド

  • マイグレーション適用成功materials 0002, fertilizer 0006
  • 施肥計画を保存すると、各エントリに対応する reserve トランザクションが作成される
  • 施肥計画を更新すると、古い reserve が削除され新しい reserve が作成される
  • 施肥計画を削除すると、reserve が全て削除される
  • GET /api/materials/stock-summary/reserved_stockavailable_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 StockTransactionreserve タイプ・fertilization_plan FK 追加
backend/apps/materials/serializers.py StockSummarySerializerreserved_stockavailable_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 FertilizationPlanis_confirmedconfirmed_at 追加
backend/apps/fertilizer/views.py perform_create/update/destroy オーバーライド、confirm_spreading アクション追加
backend/apps/fertilizer/serializers.py is_confirmedconfirmed_atmaterial_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. 参照すべき既存コード(実装パターンの手本)

目的 参照先
施肥計画 ViewSetperform_create の追加先) backend/apps/fertilizer/views.py
施肥計画 Serializerフィールド追加先 backend/apps/fertilizer/serializers.py
施肥計画の @action パターンPDF アクション) backend/apps/fertilizer/views.pypdf アクション
在庫集計ロジック backend/apps/materials/views.pyStockSummaryView
施肥計画編集画面(マトリクス表) 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