# マスタードキュメント:施肥計画機能 > **作成**: 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 `)が必要。 ### 肥料マスタ | メソッド | 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('') // 圃場・肥料 const [selectedFields, setSelectedFields] = useState([]) const [planFertilizers, setPlanFertilizers] = useState([]) // 自動計算設定(肥料ごと) const [calcSettings, setCalcSettings] = useState([]) // CalcSetting: { fertilizer_id, method: 'per_tan'|'even'|'nitrogen', param: string } // マトリクス(fieldId → fertilizerId → 袋数文字列) const [matrix, setMatrix] = useState>>({}) ``` --- ## ファイル構成 ### 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 がある場合は全置換)。 --- ## 将来の拡張(スコープ外) - **配置計画**: 複数圃場分を一か所にまとめる時の置き場所割り当て(別機能として検討) - **購入管理**: 肥料の購入・在庫管理(施肥計画の集計から購入数量を自動算出) - **作業記録との連携**: 施肥計画の実施記録(実施日・実際の袋数)