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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 12:16:42 +09:00

435 lines
13 KiB
Markdown
Raw 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.
# マスタードキュメント:施肥計画機能
> **作成**: 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 がある場合は全置換)。
---
## 将来の拡張(スコープ外)
- **配置計画**: 複数圃場分を一か所にまとめる時の置き場所割り当て(別機能として検討)
- **購入管理**: 肥料の購入・在庫管理(施肥計画の集計から購入数量を自動算出)
- **作業記録との連携**: 施肥計画の実施記録(実施日・実際の袋数)