施肥計画のマスタードキュメントを追加

document/13_マスタードキュメント_施肥計画編.md を新規作成
データモデル・全API仕様・自動計算ロジック・フロントエンド画面・ファイル構成・注意点を網羅

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Akira
2026-03-01 12:16:42 +09:00
parent f207f5de27
commit 8ac3a00737

View File

@@ -0,0 +1,434 @@
# マスタードキュメント:施肥計画機能
> **作成**: 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. **保存**: 「保存」ボタンで entries を一括送信
#### State 構成
```typescript
// 基本情報
const [planName, setPlanName] = useState('')
const [planYear, setPlanYear] = useState(currentYear)
const [varietyId, setVarietyId] = useState<number | ''>('')
// 圃場・肥料
const [selectedFields, setSelectedFields] = useState<FieldInfo[]>([])
const [planFertilizers, setPlanFertilizers] = useState<Fertilizer[]>([])
// 自動計算設定(肥料ごと)
const [calcSettings, setCalcSettings] = useState<CalcSetting[]>([])
// CalcSetting: { fertilizer_id, method: 'per_tan'|'even'|'nitrogen', param: string }
// マトリクスfieldId → fertilizerId → 袋数文字列)
const [matrix, setMatrix] = useState<Record<number, Record<number, string>>>({})
```
---
## ファイル構成
### 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 がある場合は全置換)。
---
## 将来の拡張(スコープ外)
- **配置計画**: 複数圃場分を一か所にまとめる時の置き場所割り当て(別機能として検討)
- **購入管理**: 肥料の購入・在庫管理(施肥計画の集計から購入数量を自動算出)
- **作業記録との連携**: 施肥計画の実施記録(実施日・実際の袋数)