# マスタードキュメント：施肥計画機能

> **作成**: 2026-03-01
> **対象機能**: 施肥計画（年度×品種単位のマトリクス管理）
> **実装状況**: 実装完了（commit f207f5d）

---

## 概要

農業生産者が「年度 × 品種」単位で施肥計画を立てる機能。
複数圃場 × 複数肥料 × 袋数をマトリクス形式で管理し、PDF出力する。

### 機能スコープ（IN / OUT）

| IN（実装済み） | OUT（対象外） |
|---|---|
| 肥料マスタ管理 | 肥料購入管理 |
| 施肥計画の作成・編集・削除 | 圃場への配置計画（置き場所割り当て） |
| 3方式の自動計算 | 施肥作業の実績記録 |
| 作付け計画からの圃場自動取得 | |
| PDF出力（圃場×肥料マトリクス表） | |

---

## データモデル

### Fertilizer（肥料マスタ）

| フィールド | 型 | 制約 | 説明 |
|---|---|---|---|
| id | int | PK | |
| name | varchar(100) | unique, required | 肥料名 |
| maker | varchar(100) | nullable | メーカー |
| capacity_kg | decimal(8,3) | nullable | 1袋重量(kg) ← nitrogen計算に必須 |
| nitrogen_pct | decimal(5,2) | nullable | 窒素含有率(%) ← nitrogen計算に必須 |
| phosphorus_pct | decimal(5,2) | nullable | リン酸含有率(%) |
| potassium_pct | decimal(5,2) | nullable | カリ含有率(%) |
| notes | text | nullable | 備考 |

### FertilizationPlan（施肥計画）

| フィールド | 型 | 制約 | 説明 |
|---|---|---|---|
| id | int | PK | |
| name | varchar(200) | required | 計画名（ユーザーが自由入力） |
| year | int | required | 年度 |
| variety | FK(plans.Variety) | PROTECT | 品種（≠NULL） |
| created_at | datetime | auto | |
| updated_at | datetime | auto | |

### FertilizationEntry（施肥エントリ：圃場×肥料×袋数）

| フィールド | 型 | 制約 | 説明 |
|---|---|---|---|
| id | int | PK | |
| plan | FK(FertilizationPlan) | CASCADE | |
| field | FK(fields.Field) | CASCADE | |
| fertilizer | FK(Fertilizer) | CASCADE | |
| bags | decimal(8,2) | required | 袋数 |

- `unique_together = ['plan', 'field', 'fertilizer']`
- 順序: `field__display_order, field__id, fertilizer__name`

---

## API エンドポイント

すべて JWT 認証（`Authorization: Bearer <token>`）が必要。

### 肥料マスタ

| メソッド | URL | 説明 |
|---|---|---|
| GET | `/api/fertilizer/fertilizers/` | 一覧取得 |
| POST | `/api/fertilizer/fertilizers/` | 新規作成 |
| GET | `/api/fertilizer/fertilizers/{id}/` | 詳細取得 |
| PUT/PATCH | `/api/fertilizer/fertilizers/{id}/` | 更新 |
| DELETE | `/api/fertilizer/fertilizers/{id}/` | 削除 |

レスポンス例（Fertilizer）:
```json
{
  "id": 1,
  "name": "コシヒカリ専用一発肥料",
  "maker": "JA",
  "capacity_kg": "20.000",
  "nitrogen_pct": "14.00",
  "phosphorus_pct": "12.00",
  "potassium_pct": "12.00",
  "notes": null
}
```

### 施肥計画

| メソッド | URL | 説明 |
|---|---|---|
| GET | `/api/fertilizer/plans/?year={year}` | 年度別一覧 |
| POST | `/api/fertilizer/plans/` | 新規作成（entries 含む） |
| GET | `/api/fertilizer/plans/{id}/` | 詳細取得（entries 含む） |
| PUT | `/api/fertilizer/plans/{id}/` | 更新（entries 全置換） |
| DELETE | `/api/fertilizer/plans/{id}/` | 削除 |
| GET | `/api/fertilizer/plans/{id}/pdf/` | PDF出力（application/pdf） |

一覧レスポンス例（FertilizationPlan）:
```json
{
  "id": 1,
  "name": "2025年コシヒカリ施肥計画",
  "year": 2025,
  "variety": 3,
  "variety_name": "コシヒカリ",
  "crop_name": "米",
  "field_count": 12,
  "fertilizer_count": 2,
  "entries": [
    {
      "id": 1,
      "field": 5,
      "field_name": "田中上",
      "field_area_tan": "1.2000",
      "fertilizer": 1,
      "fertilizer_name": "コシヒカリ専用一発肥料",
      "bags": "2.40"
    }
  ],
  "created_at": "2025-03-01T10:00:00Z",
  "updated_at": "2025-03-01T10:00:00Z"
}
```

POST/PUT リクエスト例:
```json
{
  "name": "2025年コシヒカリ施肥計画",
  "year": 2025,
  "variety": 3,
  "entries": [
    {"field_id": 5, "fertilizer_id": 1, "bags": 2.4},
    {"field_id": 6, "fertilizer_id": 1, "bags": 1.6}
  ]
}
```

PUT 時は entries が全置換（削除→再作成）。entries を省略した場合は既存を維持。

### 圃場候補取得

```
GET /api/fertilizer/candidate_fields/?year={year}&variety_id={variety_id}
```

作付け計画（Planモデル）から year + variety で圃場を検索して返す。

レスポンス例:
```json
[
  {"id": 5, "name": "田中上", "area_tan": "1.2000", "area_m2": 1200, "group_name": "田中"},
  {"id": 6, "name": "田中下", "area_tan": "0.8000", "area_m2": 800, "group_name": "田中"}
]
```

### 自動計算

```
POST /api/fertilizer/calculate/
```

計算結果を返すのみ（DB保存なし）。

#### 方式 1: per_tan（反当袋数）

```json
{
  "method": "per_tan",
  "param": 2.0,
  "field_ids": [5, 6]
}
```

計算式: `bags = Sa × A`（Sa: 反当袋数, A: 圃場面積[反]）

#### 方式 2: even（均等配分）

```json
{
  "method": "even",
  "param": 50,
  "field_ids": [5, 6]
}
```

計算式: `bags = (SS / ΣA) × A`（SS: 総袋数, A: 圃場面積[反]）

#### 方式 3: nitrogen（反当チッソ成分量）

```json
{
  "method": "nitrogen",
  "param": 3.0,
  "fertilizer_id": 1,
  "field_ids": [5, 6]
}
```

計算式: `bags = (Nr / (C × Nd/100)) × A`
- Nr: 反当チッソ成分量(kg/反)
- C: 1袋重量(kg) ← Fertilizer.capacity_kg 必須
- Nd: 窒素含有率(%) ← Fertilizer.nitrogen_pct 必須
- A: 圃場面積[反]

nitrogen 方式は capacity_kg・nitrogen_pct が未設定の肥料に対してはエラー（400）。

レスポンス（共通）:
```json
[
  {"field_id": 5, "bags": 2.40},
  {"field_id": 6, "bags": 1.60}
]
```

---

## 品種・作物の取得

品種一覧は既存の plans アプリの CropViewSet を使用:

```
GET /api/plans/crops/
```

レスポンス例:
```json
[
  {
    "id": 1,
    "name": "米",
    "base_temp": "0.0",
    "varieties": [
      {"id": 1, "name": "コシヒカリ", "crop": 1},
      {"id": 2, "name": "ヒノヒカリ", "crop": 1}
    ]
  }
]
```

**注意**: plans アプリの DefaultRouter が `r''` に登録されているため、
`/api/plans/get-crops-with-varieties/` のようなカスタムパスは 404 になる（URLルーティング競合）。
`/api/plans/crops/` を使うこと。

---

## PDF 出力

`GET /api/fertilizer/plans/{id}/pdf/`

- WeasyPrint を使用（reports アプリと同パターン）
- テンプレート: `backend/apps/fertilizer/templates/fertilizer/pdf.html`
- フォーマット: A4横向き
- 内容: 圃場（行）× 肥料（列）のマトリクス表、行合計・列合計・総合計
- ファイル名: `fertilization_{year}_{plan_id}.pdf`

---

## フロントエンド画面

### 施肥計画一覧（`/fertilizer`）

- 年度セレクタ（localStorage `fertilizerYear` で保持）
- 計画カード一覧: 計画名・作物/品種・圃場数・肥料数
- 操作ボタン: PDF出力・編集・削除
- ヘッダー: 「肥料マスタ」「新規作成」ボタン

### 肥料マスタ（`/fertilizer/masters`）

- 肥料一覧テーブル（名前・メーカー・容量・窒素・リン酸・カリ・備考）
- インライン行編集（EditRow コンポーネント）
- 新規追加フォーム
- 削除確認ダイアログ

### 施肥計画編集（`/fertilizer/new` / `/fertilizer/[id]/edit`）

`FertilizerEditPage.tsx`（`fertilizer/_components/`）を共有コンポーネントとして使用。

#### 操作フロー

1. **計画基本情報入力**: 計画名・年度・品種（ドロップダウン）
2. **圃場選択**: 品種選択後に候補圃場が自動取得（`candidate_fields` API）。チップ形式で追加/解除。候補外の圃場は「全圃場から追加」で手動選択
3. **肥料追加**: 「＋肥料を追加」で肥料マスタからドロップダウン選択
4. **自動計算**: 各肥料に方式（per_tan/even/nitrogen）とパラメータを設定し「計算」ボタンでマトリクスに反映（上書き確認あり）
5. **四捨五入**: 肥料列ヘッダーの `≈` ボタン（青）を押すと袋数を整数に丸める。押した後は `↩` ボタン（琥珀色）に変わり、押すと元の計算値に戻る
6. **手動調整**: マトリクス表のセルを直接編集
7. **保存**: 「保存」ボタンで entries を一括送信

#### マトリクスの表示仕様

- 自動計算直後: セルに計算値（小数）がそのまま表示される（編集可）
- `≈` ボタン押下後: セルの入力値が整数に丸められ、元の計算値が薄いグレーで参照表示される
- `↩` ボタン押下: 整数値を破棄し、元の計算値に戻る（参照グレー表示も消える）
- 編集中に計算を再実行すると、その肥料列の `adjusted` と `roundedColumns` がリセットされる

#### State 構成

```typescript
// 基本情報
const [name, setName] = useState('')
const [year, setYear] = useState(currentYear)
const [varietyId, setVarietyId] = useState<number | ''>('')

// 圃場・肥料
const [selectedFields, setSelectedFields] = useState<Field[]>([])
const [planFertilizers, setPlanFertilizers] = useState<Fertilizer[]>([])

// 自動計算設定（肥料ごと）
const [calcSettings, setCalcSettings] = useState<CalcSetting[]>([])
// CalcSetting: { fertilizer_id, method: 'per_tan'|'even'|'nitrogen', param: string }

// マトリクス 2層構成（fieldId → fertilizerId → 袋数文字列）
const [calcMatrix, setCalcMatrix] = useState<Matrix>({})   // 自動計算値（参照用・変更不可表示）
const [adjusted, setAdjusted] = useState<Matrix>({})       // ユーザー確定値（保存対象）
const [roundedColumns, setRoundedColumns] = useState<Set<number>>(new Set())  // ↩ トグル管理

// effectiveValue(fieldId, fertId) で保存値を決定:
//   adjusted[field][fert] があればそれを優先、なければ calcMatrix[field][fert]
```

---

## ファイル構成

### Backend

```
backend/apps/fertilizer/
├── __init__.py
├── admin.py                   # Django admin 登録
├── apps.py                    # FertilizerConfig
├── models.py                  # Fertilizer, FertilizationPlan, FertilizationEntry
├── serializers.py             # FertilizerSerializer, FertilizationPlanSerializer/WriteSerializer
├── views.py                   # FertilizerViewSet, FertilizationPlanViewSet, CandidateFieldsView, CalculateView
├── urls.py                    # DefaultRouter + candidate_fields/ + calculate/
├── migrations/
│   └── 0001_initial.py
└── templates/
    └── fertilizer/
        └── pdf.html           # WeasyPrint テンプレート（A4横向き）
```

### Frontend

```
frontend/src/app/fertilizer/
├── page.tsx                   # 施肥計画一覧
├── new/
│   └── page.tsx               # 新規作成（FertilizerEditPage をラップ）
├── [id]/
│   └── edit/
│       └── page.tsx           # 編集（FertilizerEditPage をラップ）
├── masters/
│   └── page.tsx               # 肥料マスタ管理
└── _components/
    └── FertilizerEditPage.tsx  # 新規/編集共通コンポーネント（複雑）
```

### 変更されたファイル

| ファイル | 変更内容 |
|---|---|
| `backend/keinasystem/settings.py` | `INSTALLED_APPS` に `'apps.fertilizer'` を追加 |
| `backend/keinasystem/urls.py` | `path('api/fertilizer/', include('apps.fertilizer.urls'))` を追加 |
| `frontend/src/types/index.ts` | `Fertilizer`, `FertilizationEntry`, `FertilizationPlan` 型を追加 |
| `frontend/src/components/Navbar.tsx` | Sprout アイコン + 施肥計画メニューを追加 |

---

## 型定義（TypeScript）

```typescript
// frontend/src/types/index.ts

export interface Fertilizer {
  id: number;
  name: string;
  maker: string | null;
  capacity_kg: string | null;
  nitrogen_pct: string | null;
  phosphorus_pct: string | null;
  potassium_pct: string | null;
  notes: string | null;
}

export interface FertilizationEntry {
  id: number;
  field: number;
  field_name: string;
  field_area_tan: string;
  fertilizer: number;
  fertilizer_name: string;
  bags: string;
}

export interface FertilizationPlan {
  id: number;
  name: string;
  year: number;
  variety: number;
  variety_name: string;
  crop_name: string;
  field_count: number;
  fertilizer_count: number;
  entries: FertilizationEntry[];
}
```

---

## 注意点・既知の問題

### URL ルーティング競合（解決済み）

plans アプリの `DefaultRouter(r'', PlanViewSet)` が `plans/get-crops-with-varieties/` を
`{pk}/` パターンとして解釈して 404 になる問題があった。
`/api/plans/crops/`（CropViewSet）を使うことで回避。

### nitrogen 計算の前提条件

反当チッソ成分量方式（nitrogen）は、指定した肥料に `capacity_kg` と `nitrogen_pct` が
両方登録されていないと 400 エラーになる。肥料マスタ登録時にユーザーへ案内が必要。

### 袋数の精度

袋数は `decimal(8,2)`（小数点以下2桁）。0.01 刻みで四捨五入。
自動計算も `Decimal.quantize(Decimal('0.01'))` で丸める。

### entries の更新方式

PUT 時は entries を全削除→再作成する「全置換」方式。
部分更新は非対応（PATCH でも entries がある場合は全置換）。

### Next.js ホットリロードが効かない問題（Windows + Docker）

Windows 環境では Docker ボリュームマウント経由のファイル変更が inotify で検知されず、
フロントエンドのホットリロードが動かない。

**対策**: `docker-compose.yml` の frontend 環境変数に `WATCHPACK_POLLING: "true"` を追加。
ポーリング方式に切り替えることでファイル変更を検知できるようになる。

---

## 将来の拡張（スコープ外）

- **配置計画**: 複数圃場分を一か所にまとめる時の置き場所割り当て（別機能として検討）
- **購入管理**: 肥料の購入・在庫管理（施肥計画の集計から購入数量を自動算出）
- **作業記録との連携**: 施肥計画の実施記録（実施日・実際の袋数）
