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

831 lines
32 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. 参照すべき既存コード(実装パターンの手本)
| 目的 | 参照先 |
|------|--------|
| 施肥計画 ViewSetperform_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` |