# 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 `) - 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` |