Files
keinasystem/document/13_マスタードキュメント_施肥計画編.md
Akira 13c21ed7de ローカル更新済み:
13_マスタードキュメント_施肥計画編.md — 散布実績セクション整備、在庫連携・集計ルール・WorkRecord自動生成・前年度コピーのセクション追加、旧「散布確定モーダル」記述削除、型定義・ファイル構成・将来の拡張を更新
14_マスタードキュメント_分配計画編.md — 散布実績との連携・WorkRecord自動生成のセクション追加
CLAUDE.md — データモデル(SpreadingSession/Item, WorkRecord, actual_bags)追加、プロジェクト構造にfertilizer/workrecordsアプリ追加、実装状況に散布実績・作業記録索引を追記、更新履歴に2026-03-17エントリ追加
2026-03-17 20:31:22 +09:00

752 lines
26 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
> **最終更新**: 2026-03-17
> **対象機能**: 施肥計画(年度×品種単位のマトリクス管理・在庫引当・散布実績記録)
> **実装状況**: 実装完了・本番稼働中(散布実績連携追加)
---
## 概要
農業生産者が「年度 × 品種」単位で施肥計画を立てる機能。
複数圃場 × 複数肥料 × 袋数をマトリクス形式で管理し、PDF出力・在庫引当・散布実績記録・作業記録索引生成まで一連で扱う。
### 機能スコープIN / OUT
| IN実装済み | OUT対象外 |
|---|---|
| 肥料マスタ管理 | 肥料購入管理 |
| 施肥計画の作成・編集・削除 | 運搬計画(→ `14_マスタードキュメント_分配計画編.md` 参照) |
| 3方式の自動計算 | 運搬便ごとの散布充当追跡 |
| 作付け計画からの圃場自動取得 | 相手先ごとのPDF様式実装 |
| PDF出力圃場×肥料マトリクス表 | 残肥返却・再入庫管理 |
| 在庫引当・引当解除 | |
| 散布実績記録(日付単位・運搬済み肥料ベース) | |
| 作業記録索引WorkRecord自動生成 | |
| 在庫USE連携散布実績保存時 | |
| 施肥計画進捗表示(未散布/一部散布/完了/計画超過) | |
---
## データモデル
### 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 |
| is_confirmed | bool | default=False | ~~散布確定済みフラグ~~deprecated: 新UIでは使用しない |
| confirmed_at | datetime | nullable | ~~散布確定日時~~deprecated: 新UIでは使用しない |
| created_at | datetime | auto | |
| updated_at | datetime | auto | |
#### 表示用計算項目APIレスポンスに含まれる
| 項目 | 型 | 説明 |
|---|---|---|
| spread_status | string | `unspread` / `partial` / `completed` / `over_applied` |
| planned_total_bags | decimal | 計画袋数合計全entries.bagsの合計 |
| spread_total_bags | decimal | 散布済み袋数合計全entries.actual_bagsの合計 |
| remaining_total_bags | decimal | 残袋数planned_total_bags - spread_total_bags |
### FertilizationEntry施肥エントリ圃場×肥料×袋数
| フィールド | 型 | 制約 | 説明 |
|---|---|---|---|
| id | int | PK | |
| plan | FK(FertilizationPlan) | CASCADE | |
| field | FK(fields.Field) | CASCADE | |
| fertilizer | FK(Fertilizer) | **PROTECT** | 施肥計画で使用中の肥料は削除不可 |
| bags | decimal(8,2) | required | 袋数(計画値) |
| actual_bags | decimal(10,4) | nullable | 散布実績集計値SpreadingSessionItemから自動集計 |
- `unique_together = ['plan', 'field', 'fertilizer']`
- 順序: `field__display_order, field__id, fertilizer__name`
### SpreadingSession散布実績セッション
| フィールド | 型 | 制約 | 説明 |
|---|---|---|---|
| id | int | PK | |
| year | int | required | 年度フィルタ用 |
| date | DateField | required | 散布日 |
| name | varchar(100) | required | セッション名(必須) |
| notes | text | blank | 備考 |
| created_at | datetime | auto | |
| updated_at | datetime | auto | |
- `year + date` の一意制約は付けない(同日に午前・午後やエリア別で複数記録可能)
### SpreadingSessionItem散布実績明細
| フィールド | 型 | 制約 | 説明 |
|---|---|---|---|
| id | int | PK | |
| session | FK(SpreadingSession) | CASCADE | |
| field | FK(fields.Field) | PROTECT | |
| fertilizer | FK(Fertilizer) | PROTECT | |
| actual_bags | decimal(10,4) | required | 実散布袋数 |
| planned_bags_snapshot | decimal(10,4) | required | 表示時点の計画値 |
| delivered_bags_snapshot | decimal(10,4) | required | 表示時点の運搬済み合計 |
| created_at | datetime | auto | |
| updated_at | datetime | auto | |
- `unique_together = ['session', 'field', 'fertilizer']`
### WorkRecord作業記録索引
別アプリ `apps/workrecords/` で管理。施肥・運搬の作業を日付順に一覧するための索引テーブル。
詳細の本体は各業務テーブル側DeliveryTrip / SpreadingSessionに持つ。
| フィールド | 型 | 制約 | 説明 |
|---|---|---|---|
| id | int | PK | |
| work_date | DateField | required | 作業日 |
| work_type | varchar | required | `fertilizer_delivery` / `fertilizer_spreading` |
| title | varchar(200) | required | 一覧表示名 |
| year | int | required | 年度フィルタ補助 |
| auto_created | bool | default=True | 自動生成フラグ |
| delivery_trip | OneToOne FK(DeliveryTrip) | nullable | 運搬由来 |
| spreading_session | OneToOne FK(SpreadingSession) | nullable | 散布由来 |
| created_at | datetime | auto | |
| updated_at | datetime | auto | |
---
## 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}/` | 削除 |
| POST | `/api/fertilizer/plans/{id}/confirm_spreading/` | ~~散布確定~~deprecated: UI上で廃止、バックエンドは互換維持 |
| POST | `/api/fertilizer/plans/{id}/unconfirm/` | ~~散布確定取消~~deprecated: UI上で廃止、バックエンドは互換維持 |
| GET | `/api/fertilizer/plans/{id}/pdf/` | PDF出力application/pdf |
一覧レスポンス例FertilizationPlan:
```json
{
"id": 1,
"name": "2025年コシヒカリ施肥計画",
"year": 2025,
"variety": 3,
"variety_name": "コシヒカリ",
"crop_name": "米",
"is_confirmed": false,
"confirmed_at": null,
"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 を省略した場合は既存を維持。
### 散布実績(新規)
| メソッド | URL | 説明 |
|---|---|---|
| GET | `/api/fertilizer/spreading/?year={year}` | 年度別一覧 |
| POST | `/api/fertilizer/spreading/` | 新規作成 |
| GET | `/api/fertilizer/spreading/{id}/` | 詳細 |
| PUT | `/api/fertilizer/spreading/{id}/` | 更新 |
| DELETE | `/api/fertilizer/spreading/{id}/` | 削除 |
| GET | `/api/fertilizer/spreading/candidates/?year={year}` | 散布候補一覧 |
散布候補一覧レスポンス例:
```json
[
{
"field": 5,
"field_name": "田中上",
"fertilizer": 1,
"fertilizer_name": "電気炉さい",
"planned_bags": "4.0000",
"delivered_bags": "4.0000",
"spread_bags": "1.5000",
"remaining_bags": "2.5000",
"remaining_plan_bags": "2.5000",
"delivery_gap": "0.0000"
}
]
```
散布実績 POST リクエスト例:
```json
{
"year": 2026,
"date": "2026-04-15",
"name": "午前・田中エリア",
"notes": "",
"items": [
{
"field": 5,
"fertilizer": 1,
"actual_bags": "2.5000",
"planned_bags_snapshot": "4.0000",
"delivered_bags_snapshot": "4.0000"
}
]
}
```
### 作業記録(新規・別アプリ)
| メソッド | URL | 説明 |
|---|---|---|
| GET | `/api/workrecords/?year={year}` | 一覧 |
| GET | `/api/workrecords/{id}/` | 詳細(元レコードへのリンク情報を返す) |
### 圃場候補取得
```
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出力・編集・削除
- ヘッダー: 「肥料マスタ」「新規作成」ボタン
- 進捗表示: `未散布` / `一部散布 3.5 / 8.0袋` / `散布完了` / `計画超過`
- 計画値と実績値を並べて表示
### 肥料マスタ(`/fertilizer/masters`
- 肥料一覧テーブル(名前・メーカー・容量・窒素・リン酸・カリ・備考)
- インライン行編集EditRow コンポーネント)
- 新規追加フォーム
- 削除ボタン押下で即削除を試みるconfirm ダイアログなし)
- 施肥計画で使用中の場合は PROTECT によりバックエンドが 500 を返し、赤いインラインバナーで理由を表示
### 施肥計画編集(`/fertilizer/new` / `/fertilizer/[id]/edit`
`FertilizerEditPage.tsx``fertilizer/_components/`)を共有コンポーネントとして使用。
#### 操作フロー
1. **計画基本情報入力**: 計画名・年度・品種(ドロップダウン)
2. **圃場選択**: 品種選択後に候補圃場が自動取得(`candidate_fields` API。チップ形式で追加/解除。候補外の圃場は「全圃場から追加」で手動選択
3. **肥料追加**: 「+肥料を追加」で肥料マスタからドロップダウン選択
4. **自動計算**: 各肥料に方式per_tan/even/nitrogenとパラメータを設定し「計算」ボタンでマトリクスに反映上書き確認あり
5. **四捨五入**: 肥料列ヘッダーの `≈` ボタン(青)を押すと袋数を整数に丸める。押した後は `↩` ボタン(琥珀色)に変わり、押すと元の計算値に戻る
6. **手動調整**: マトリクス表のセルを直接編集
7. **保存**: 「保存」ボタンで entries を一括送信
#### 在庫連携・実績表示
- 肥料列ヘッダーに在庫 / 利用可能在庫 / 計画計 / 不足数を表示
- マトリクス表で `bags`(計画値)を編集可能、`actual_bags`(実績値)は読み取り専用で参照表示
- 散布実績画面(`/fertilizer/spreading`)へのリンクを表示
#### マトリクスの表示仕様
- 自動計算直後: セルに計算値(小数)がそのまま表示される(編集可)
- `≈` ボタン押下後: セルの入力値が整数に丸められ、元の計算値が薄いグレーで参照表示される
- `↩` ボタン押下: 整数値を破棄し、元の計算値に戻る(参照グレー表示も消える)
- 編集中に計算を再実行すると、その肥料列の `adjusted``roundedColumns` がリセットされる
### 散布実績画面(`/fertilizer/spreading`
- 年度セレクタlocalStorage `fertilizerYear` と連動)
- 散布日入力DateField
- セッション名入力(必須)
- 運搬済み・未散布候補一覧を表示(`candidates` APIから取得
- 圃場単位で選択可能(全部または一部)
- 実績袋数の編集
- 差異がある場合はインライン警告表示
- 保存時に在庫USE連携・WorkRecord自動生成・FertilizationEntry.actual_bags再集計を実行
### 作業記録画面(`/workrecords`
- 年度セレクタ
- 日付・作業種別・タイトルの一覧表示
- 元データ(運搬回 / 散布セッション)への遷移リンク
#### State 構成(施肥計画編集画面)
```typescript
// 基本情報
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, SpreadingSession, SpreadingSessionItem
├── serializers.py # FertilizerSerializer, FertilizationPlanSerializer/WriteSerializer, SpreadingSessionSerializer
├── services.py # actual_bags再集計、WorkRecord自動生成、在庫USE連携
├── views.py # FertilizerViewSet, FertilizationPlanViewSet, SpreadingSessionViewSet, CandidateFieldsView, CalculateView
├── urls.py # DefaultRouter + candidate_fields/ + calculate/ + spreading/
├── migrations/
│ ├── 0001_initial.py
│ ├── 0002_alter_fertilizationentry_fertilizer.py # CASCADE → PROTECT
│ └── ... # SpreadingSession, SpreadingSessionItem, actual_bags 追加
└── 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 # 肥料マスタ管理
├── spreading/
│ └── ... # 散布実績画面(一覧・作成・編集)
└── _components/
└── FertilizerEditPage.tsx # 新規/編集共通コンポーネント(複雑)
frontend/src/app/workrecords/
└── ... # 作業記録画面(一覧・詳細)
```
### 変更されたファイル
| ファイル | 変更内容 |
|---|---|
| `backend/keinasystem/settings.py` | `INSTALLED_APPS``'apps.fertilizer'`, `'apps.workrecords'` を追加 |
| `backend/keinasystem/urls.py` | `api/fertilizer/`, `api/workrecords/` を追加 |
| `backend/apps/materials/models.py` | `StockTransaction.spreading_item` FK 追加(`on_delete=SET_NULL` |
| `backend/apps/workrecords/` | 作業記録索引アプリWorkRecord モデル・API・services |
| `frontend/src/types/index.ts` | 施肥・散布・作業記録の型を追加 |
| `frontend/src/components/Navbar.tsx` | Sprout アイコン + 施肥計画メニューを追加 |
---
## 在庫連携
### RESERVE施肥計画保存時
- 従来どおり計画値 `bags` ベースで維持
- 施肥計画の entries 保存時に RESERVE トランザクションを作成
### USE散布実績保存時
- `SpreadingSessionItem` ごとに USE を1件作成
- `material`: `item.fertilizer.material`
- `quantity`: `actual_bags`
- `occurred_on`: `session.date`
- `note`: `散布実績「{session.name or session.date}」`
### StockTransaction 追加フィールド
- `spreading_item = FK(SpreadingSessionItem, null=True, blank=True, on_delete=SET_NULL)`
### 更新・削除
- 散布実績更新時: その session に紐づく USE を全置換で作り直す
- 散布実績削除時: 対応 USE を削除する
### RESERVE と USE の整合
- RESERVE は計画値 `bags` ベース
- USE は散布実績 `actual_bags` ベース
- 計画値と実績値は併存する
---
## 集計ルール
### planned_total圃場×肥料×年度
`FertilizationEntry.bags` の合計
### delivered_total圃場×肥料×年度
`DeliveryTrip.date != null``DeliveryTripItem.bags` 合計
### spread_total圃場×肥料×年度
`SpreadingSessionItem.actual_bags` の合計
### actual_bags 再集計ルール
- `SUM(SpreadingSessionItem.actual_bags)` を同一 year, field, fertilizer で集計
- 散布実績の保存・更新・削除時に該当する `FertilizationEntry.actual_bags` を即時再計算
- `SUM(...) = 0` の場合は `actual_bags = null`
### remaining_bags表示用の残量
`delivered_total - spread_total`
### remaining_plan_bags計画進捗用の残量
`planned_total - spread_total`
### 差異の扱い
- `remaining_bags < 0`: 運搬実績不足
- `remaining_plan_bags < 0`: 計画超過
- 圃場+肥料単位で差異が分かることを優先する
---
## WorkRecord 自動生成ルール
### 運搬fertilizer_delivery
- `DeliveryTrip.date` 保存時に upsert
- `title = 肥料運搬: {delivery_plan.name} {n}回目`
- 日付削除時は対応 WorkRecord を削除
### 散布fertilizer_spreading
- `SpreadingSession` 保存時に upsert
- `title = 肥料散布: {session.name or session.date}`
- 削除時は対応 WorkRecord を削除
### 実装方針
自動生成は view に直書きせず、サービス層(`services.py`)で idempotent に実装する。
---
## 前年度コピー
`copy_from_previous_year` で前年度の `FertilizationEntry` をコピーする際のルール:
- `actual_bags` がある場合: `actual_bags` を新年度の `bags` 初期値として使用
- `actual_bags``null` の場合: 従来どおり `bags` をコピー
前年度に実際に散布した量を次年度計画の初期値として再利用できる。
---
## 型定義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;
actual_bags: string | null; // 散布実績集計値
}
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[];
spread_status: 'unspread' | 'partial' | 'completed' | 'over_applied';
planned_total_bags: string;
spread_total_bags: string;
remaining_total_bags: string;
}
```
---
## 注意点・既知の問題
### エラー表示方針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_kg``nitrogen_pct`
両方登録されていないと 400 エラーになる。肥料マスタ登録時にユーザーへ案内が必要。
### 袋数の精度
袋数は `decimal(8,2)`小数点以下2桁。0.01 刻みで四捨五入。
自動計算も `Decimal.quantize(Decimal('0.01'))` で丸める。
### entries の更新方式
PUT 時は entries を全削除→再作成する「全置換」方式。
部分更新は非対応PATCH でも entries がある場合は全置換)。
### 散布実績の在庫連携
- 施肥計画保存時: `RESERVE`(計画値 `bags` ベース)
- 散布実績保存時: `USE`(実績値 `actual_bags` ベース)
- `RESERVE``USE` は併存する(計画値と実績値は別管理)
- 散布実績更新時は `session` に紐づく `USE` を全置換で作り直す
- 散布実績削除時は対応 `USE` を削除する(`StockTransaction.spreading_item``SET_NULL`
- `perform_destroy` で明示的に `StockTransaction` を削除してから `session.delete()` を呼ぶ
### 散布セッション名は必須
`SpreadingSession.name` は必須フィールド。WorkRecord のタイトル生成や一覧表示に使用するため、
空文字での保存は許可しない。
### useSearchParams と SuspenseNext.js 14
散布実績画面(`/fertilizer/spreading`)では `useSearchParams()` を使用するため、
`Suspense` boundary でラップする必要がある(本番ビルドで必須)。
### Next.js ホットリロードが効かない問題Windows + Docker
Windows 環境では Docker ボリュームマウント経由のファイル変更が inotify で検知されず、
フロントエンドのホットリロードが動かない。
**対策**: `docker-compose.yml` の frontend 環境変数に `WATCHPACK_POLLING: "true"` を追加。
ポーリング方式に切り替えることでファイル変更を検知できるようになる。
---
## 将来の拡張(スコープ外)
- **相手先別PDF様式**: 客先ごとの提出資料フォーマット(元データは散布実績から取得可能)
- **残肥返却・再入庫管理**: 散布後の残りを在庫に戻す処理
- **SpreadingAllocation**: 運搬便単位の散布充当追跡(現状は集計ベースで十分)
- **購入管理**: 肥料の購入・在庫管理(施肥計画の集計から購入数量を自動算出)
- **配置計画**: 複数圃場分を一か所にまとめる時の置き場所割り当て(別機能として検討)