document/13_マスタードキュメント_施肥計画編.md を新規作成 データモデル・全API仕様・自動計算ロジック・フロントエンド画面・ファイル構成・注意点を網羅 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
435 lines
13 KiB
Markdown
435 lines
13 KiB
Markdown
# マスタードキュメント:施肥計画機能
|
||
|
||
> **作成**: 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 がある場合は全置換)。
|
||
|
||
---
|
||
|
||
## 将来の拡張(スコープ外)
|
||
|
||
- **配置計画**: 複数圃場分を一か所にまとめる時の置き場所割り当て(別機能として検討)
|
||
- **購入管理**: 肥料の購入・在庫管理(施肥計画の集計から購入数量を自動算出)
|
||
- **作業記録との連携**: 施肥計画の実施記録(実施日・実際の袋数)
|