Files
keinasystem/document/13_マスタードキュメント_施肥計画編.md
Akira 50d5fdcbb3 CLAUDE.md・施肥計画マスタードキュメントを最終更新
- FertilizationEntry.fertilizer PROTECT化を反映
- alert/confirm廃止・インラインバナー方針を記録
- 実装状況セクションに施肥計画機能を追加
- マスタードキュメント index に13を追加

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

15 KiB
Raw Blame History

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

作成: 2026-03-01 最終更新: 2026-03-01 対象機能: 施肥計画(年度×品種単位のマトリクス管理) 実装状況: 実装完了・本番稼働中(最終 commit deb03ef


概要

農業生産者が「年度 × 品種」単位で施肥計画を立てる機能。 複数圃場 × 複数肥料 × 袋数をマトリクス形式で管理し、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) PROTECT 施肥計画で使用中の肥料は削除不可
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:

{
  "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:

{
  "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 リクエスト例:

{
  "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 で圃場を検索して返す。

レスポンス例:

[
  {"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反当袋数

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

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

方式 2: even均等配分

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

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

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

{
  "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

レスポンス(共通):

[
  {"field_id": 5, "bags": 2.40},
  {"field_id": 6, "bags": 1.60}
]

品種・作物の取得

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

GET /api/plans/crops/

レスポンス例:

[
  {
    "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 コンポーネント)
  • 新規追加フォーム
  • 削除ボタン押下で即削除を試みるconfirm ダイアログなし)
  • 施肥計画で使用中の場合は PROTECT によりバックエンドが 500 を返し、赤いインラインバナーで理由を表示

施肥計画編集(/fertilizer/new / /fertilizer/[id]/edit

FertilizerEditPage.tsxfertilizer/_components/)を共有コンポーネントとして使用。

操作フロー

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

マトリクスの表示仕様

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

State 構成

// 基本情報
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
│   └── 0002_alter_fertilizationentry_fertilizer.py  # CASCADE → PROTECT
└── 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

// 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[];
}

注意点・既知の問題

エラー表示方針confirm/alert を使わない)

ブラウザが alert() / confirm() をブロックすると操作が無音で失敗する問題を受けて、 施肥機能全体で alert/confirm を廃止し、React インラインバナーに統一した。

  • 保存失敗: saveError state → ヘッダー直下に赤いバナー(✕で閉じられる)
  • 肥料削除失敗: deleteError state → テーブル上部に赤いバナー
  • 計画削除失敗: deleteError state → 一覧上部に赤いバナー
  • 肥料列の除去: ローカルstate操作のみ失敗しない

URL ルーティング競合(解決済み)

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

nitrogen 計算の前提条件

反当チッソ成分量方式nitrogenは、指定した肥料に capacity_kgnitrogen_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" を追加。 ポーリング方式に切り替えることでファイル変更を検知できるようになる。


将来の拡張(スコープ外)

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