# 施肥散布実績連携変更実装仕様

> 作成日: 2026-03-17
> 改訂日: 2026-03-17
> 対象プロジェクト: `keinasystem_t02`
> 位置づけ: 実運用反映版・MVP仕様

---

## 1. 結論

今回の変更では、施肥データの流れを以下の一気通貫に揃える。

1. 施肥計画を作る
2. 施肥計画を元に運搬計画を作る
3. 運搬済み肥料を元に散布実績を記録する
4. 散布実績を元に作業記録として参照できる
5. 将来、必要な相手先提出資料へ変換できる

MVPの方針は以下とする。

- 施肥計画の `散布確定` ボタンは廃止する
- 散布実績は `SpreadingSession` と `SpreadingSessionItem` で管理する
- `SpreadingAllocation` は作らない
- 在庫の `USE` は散布実績保存時に発生させる
- `WorkRecord` は作るが、明細の二重管理はしない
- `WorkRecord` は `DeliveryTrip` と `SpreadingSession` への索引として使う
- `FertilizationEntry.bags` は計画値として維持し、`actual_bags` を実績集計値として別保持する
- 客先提出PDFは今回実装しない
- ただし、将来どの様式にも変換できるよう、元データを引き出せる構造にする

---

## 2. 背景

現状実装では、`FertilizationPlan` に「散布確定」ボタンがあり、施肥計画単位で一括確定する流れになっている。

しかし実運用では、実際に必要なのは以下である。

- どの圃場に
- どの肥料を
- どれだけ散布したか
- それがいつ行われたか

また、データは以下のように流用できる必要がある。

- 施肥計画作成
- 運搬計画作成
- 散布実績記録
- 作業記録参照
- 相手先提出資料への転用

この流れが切れると、従来どおり別DBや別表へ転記が必要になり、システム化の価値が大きく下がる。

---

## 3. 今回の判断

### 3.1 採用するもの

- `SpreadingSession` と `SpreadingSessionItem`
- `WorkRecord` の新設
- 散布保存時の在庫 `USE` 登録
- `FertilizationEntry.actual_bags` の集計反映
- 施肥計画一覧からの `散布確定` UI 廃止
- 施肥計画進捗の自動集計表示

### 3.2 今回は見送るもの

- `SpreadingAllocation`
- `DeliveryTripItem.fertilization_entry` FK
- 客先提出PDFの固定様式実装

### 3.3 理由

- `SpreadingAllocation` が必要になる運用は今後も発生しない前提である
- `WorkRecord` は必要だが、詳細を二重保持するのではなく索引で十分である
- 在庫は実際に減るため、散布実績と同時に管理すべきである
- 提出資料は相手先ごとに違うため、今固定様式を作るより、元データ抽出可能性を優先すべきである
- 運搬計画は年度ベースで複数施肥計画を横断するため、`DeliveryTripItem` に単一の `fertilization_entry` FK を持たせるのは不安がある

---

## 4. 機能スコープ

### 4.1 IN

- 施肥計画の `散布確定` ボタン廃止
- 散布実績モデル追加
- 作業記録索引モデル追加
- 在庫 `USE` 連携
- `FertilizationEntry.actual_bags` 集計
- 散布実績一覧・作成・更新・削除 API
- 散布候補集計 API
- 施肥計画進捗表示
- 前年度コピー時の `actual_bags → bags` 初期化反映
- 作業記録一覧から運搬・散布の実績を参照可能にすること

### 4.2 OUT

- 運搬便ごとの散布充当追跡
- 相手先ごとのPDF様式実装
- 残肥返却・再入庫管理
- カレンダーUIの完成版
- 栽培管理全体を包含した汎用作業日誌の完成版

---

## 5. 新しい業務フロー

### 5.1 施肥計画

1. 施肥計画を作成する
2. 計画値として保持する
3. この段階では散布確定しない

### 5.2 運搬計画

1. 施肥計画を元に運搬計画を作成する
2. `DeliveryTrip.date` に運搬日を入れる
3. 日付が入った運搬回を `運搬済み` とみなす
4. 運搬日が保存されたら `WorkRecord` を自動生成または更新する

### 5.3 散布実績

1. ユーザーは散布日を選ぶ
2. システムはその年度の `運搬済み` データを集計する
3. `未散布残` がある圃場×肥料を候補として表示する
4. ユーザーは全部または一部の圃場を選ぶ
5. 実際に散布した袋数を入力して保存する
6. 保存時に在庫 `USE` を作成する
7. 保存時に `WorkRecord` を自動生成または更新する

---

## 6. データモデル

### 6.1 `FertilizationPlan`

既存の `is_confirmed` / `confirmed_at` は中心機能ではなくなる。

#### 方針

- DBカラムは当面残す
- 新UIでは `confirm_spreading` / `unconfirm` を使わない
- 一覧では散布進捗を計算表示する

#### 追加する表示用項目

- `spread_status`: `unspread | partial | completed | over_applied`
- `planned_total_bags`
- `spread_total_bags`
- `remaining_total_bags`

#### `FertilizationEntry.actual_bags` の追加

`FertilizationEntry` に以下のフィールドを追加する。

- `actual_bags = DecimalField(max_digits=10, decimal_places=4, null=True, blank=True)`

#### 役割

- `bags`: 計画値
- `actual_bags`: 散布実績の集計値

`bags` はユーザーが立てた計画として保持し、散布後も自動で上書きしない。
実績は `actual_bags` に集約する。

#### `actual_bags` の更新契機

- `SpreadingSessionItem` 作成時
- `SpreadingSessionItem` 更新時
- `SpreadingSessionItem` 削除時

上記のたびに、同一年度・圃場・肥料の散布実績合計を再集計して反映する。

### 6.2 `DeliveryTrip` / `DeliveryTripItem`

MVPではモデル追加は行わない。

#### 前提

- `DeliveryTrip.date != null` の明細のみを `運搬済み` とみなす
- `DeliveryTripItem` は今までどおり `field + fertilizer + bags` を保持する
- `fertilization_entry` FK は追加しない

### 6.3 `SpreadingSession`（新規）

散布日の親レコード。

| フィールド | 型 | 制約 | 説明 |
|---|---|---|---|
| id | int | PK | |
| year | int | required | 年度フィルタ用 |
| date | DateField | required | 散布日 |
| name | varchar(100) | blank | 任意名 |
| notes | text | blank | 備考 |
| created_at / updated_at | datetime | auto | |

#### 制約

- `year + date` の一意制約は付けない

同日に午前・午後やエリア別で複数散布記録を分けられるようにする。

### 6.4 `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 / updated_at | datetime | auto | |

#### 制約

- `unique_together = [['session', 'field', 'fertilizer']]`

#### 補足

- `fertilization_entry` FK は持たない
- 圃場+肥料単位の事実記録を優先する

### 6.5 `WorkRecord`（新規）

作業記録参照用の索引テーブル。

#### 目的

- 日付順に作業を一覧できるようにする
- 将来、播種・農薬散布・収穫なども同じ入口で見られるようにする
- ただし、詳細の本体は各業務テーブル側に持つ

| フィールド | 型 | 制約 | 説明 |
|---|---|---|---|
| id | int | PK | |
| work_date | DateField | required | 作業日 |
| work_type | CharField | 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 / updated_at | datetime | auto | |

#### 方針

- `WorkRecord` 自体には圃場別明細を持たない
- 明細参照時は `DeliveryTrip` / `SpreadingSession` 側を開く
- 実体ではなく索引として使う

---

## 7. 在庫連携

### 7.1 変更前

- 施肥計画保存時に `RESERVE`
- 施肥計画の `散布確定` で `USE`

### 7.2 変更後

- 施肥計画保存時に `RESERVE` を継続
- 散布実績保存時に `USE`
- `USE.occurred_on` は `SpreadingSession.date`

### 7.3 `StockTransaction` 追加推奨フィールド

- `spreading_item = FK(SpreadingSessionItem, null=True, blank=True, on_delete=SET_NULL)`

### 7.4 `USE` 作成ルール

- `SpreadingSessionItem` ごとに `USE` を1件作る
- `material` は `item.fertilizer.material`
- `quantity` は `actual_bags`
- `occurred_on` は `session.date`
- `note` は `散布実績「{session.name or session.date}」`

### 7.5 更新・削除

- 散布実績更新時は、その `session` に紐づく `USE` を全置換で作り直す
- 散布実績削除時は対応 `USE` を削除する

### 7.6 `RESERVE` との整合

- `RESERVE` は従来どおり計画値 `bags` ベースで維持する
- `USE` は散布実績 `actual_bags` ベースで発生する
- 計画値と実績値は併存する

つまり、

- 計画: `bags`
- 実績集計: `actual_bags`
- 引当: `RESERVE`
- 使用: `USE`

を分けて持つ。

---

## 8. 集計ルール

`SpreadingAllocation` を持たないため、残量は集計で求める。

### 8.1 計画値

`planned_total(field, fertilizer, year)`

- `FertilizationEntry` の合計

### 8.2 運搬済み量

`delivered_total(field, fertilizer, year)`

- `DeliveryTrip.date != null` の `DeliveryTripItem.bags` 合計

### 8.3 散布済み量

`spread_total(field, fertilizer, year)`

- `SpreadingSessionItem.actual_bags` の合計

### 8.4 `FertilizationEntry.actual_bags` 集計ルール

`actual_bags(field, fertilizer, year)`

- `SUM(SpreadingSessionItem.actual_bags)`
- 対象条件は `同一 year, field, fertilizer`

実装上は、散布実績保存・更新・削除時に、該当する `FertilizationEntry` を再計算して更新する。

### 8.5 表示用の残量

`remaining_bags = delivered_total - spread_total`

### 8.6 計画進捗用の残量

`remaining_plan_bags = planned_total - spread_total`

### 8.7 差異の扱い

運搬数量と散布数量がずれることは許容する。

- `remaining_bags < 0` の場合: `運搬実績不足`
- `remaining_plan_bags < 0` の場合: `計画超過`

まずは `圃場+肥料単位で差異が分かること` を優先する。

---

## 9. API方針

### 9.1 施肥計画 API

#### UI上で廃止するもの

- `POST /api/fertilizer/plans/{id}/confirm_spreading/`
- `POST /api/fertilizer/plans/{id}/unconfirm/`

#### 追加する読み取り項目

- `spread_status`
- `planned_total_bags`
- `spread_total_bags`
- `remaining_total_bags`
- 各 `FertilizationEntry` の `actual_bags`

### 9.2 散布実績 API（新規）

| メソッド | 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}` | 散布候補一覧 |

### 9.3 作業記録 API（新規）

| メソッド | URL | 説明 |
|---|---|---|
| GET | `/api/workrecords/?year={year}` | 一覧 |
| GET | `/api/workrecords/{id}/` | 詳細 |

詳細では元レコードへのリンク情報を返すだけでよい。

### 9.4 候補一覧レスポンス例

```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"
  }
]
```

### 9.5 保存時の基本バリデーション

- `actual_bags > 0` を必須
- 同一 `session` 内で `field + fertilizer` の重複禁止
- `remaining_plan_bags` を大きく超える入力はエラー
- `remaining_bags` を超える入力は警告を出した上で保存は許容してよい

### 9.6 `actual_bags` 同期APIルール

- 散布実績保存後、対象となる `FertilizationEntry.actual_bags` を即時再集計する
- `SUM(...) = 0` の場合は `actual_bags = null` としてよい
- 一覧・編集画面では、再計算後の値を返す

---

## 10. フロントエンド変更

### 10.1 施肥計画一覧 `/fertilizer`

- `散布確定` ボタンを削除
- `ConfirmSpreadingModal.tsx` は削除対象
- カードに進捗状態を表示
- 計画値と実績値を並べて表示できるようにする

表示例:

- `未散布`
- `一部散布 3.5 / 8.0袋`
- `散布完了`
- `計画超過`
- `計画 4.0 → 実績 3.5`

### 10.2 散布実績画面（新規）

- `/fertilizer/spreading`

#### 画面要件

- 散布日入力
- 年度選択
- 運搬済み・未散布候補一覧
- 圃場単位選択
- 実績袋数編集
- 差異のインライン警告

### 10.4 施肥計画編集画面

- `bags` を計画値として表示
- `actual_bags` を実績値として参照表示
- 編集時にも `計画 / 実績` の差が分かるようにする

`actual_bags` は散布実績からの集計値なので、編集画面で直接入力はしない。

### 10.3 作業記録画面（新規または将来画面の土台）

最低限、以下を一覧できること。

- 日付
- 作業種別
- タイトル
- 元データへの遷移

---

## 11. 将来の資料生成に向けた要件

今回、固定形式のPDFは実装しない。

ただし、将来どの相手先様式にも対応できるよう、散布実績から少なくとも以下を引き出せる必要がある。

- 散布日
- 年度
- 圃場
- 肥料
- 散布袋数
- 備考

また、資料生成時に計画値と実績値を比較できるよう、少なくとも以下の対比を維持する。

- `bags`（計画）
- `actual_bags`（実績）

つまり今回の要件は、`PDFを作ること` ではなく、`後で資料化できる元データを崩さず持つこと` である。

---

## 12. 前年度コピーへの影響

将来 `copy_from_previous_year` で前年度の `FertilizationEntry` をコピーする際は、以下のルールを適用する。

- `actual_bags` がある場合: `actual_bags` を新年度の `bags` 初期値として使う
- `actual_bags` が `null` の場合: 従来どおり `bags` をコピーする

この方針により、前年度に実際に散布した量を次年度計画の初期値として再利用できる。

---

## 13. WorkRecord 自動生成ルール

### 12.1 運搬

- `DeliveryTrip.date` 保存時に upsert
- `title = 肥料運搬: {delivery_plan.name} {n}回目`
- 日付削除時は対応 `WorkRecord` を削除

### 12.2 散布

- `SpreadingSession` 保存時に upsert
- `title = 肥料散布: {session.name or session.date}`
- 削除時は対応 `WorkRecord` を削除

### 12.3 実装方針

自動生成は view に直書きせず、サービス層で idempotent に実装する。

---

## 14. 移行方針

### 13.1 既存の `is_confirmed`

- 既存カラムは残す
- 新UIでは参照しない
- 旧方式の確定データは `旧散布確定データ` として扱う

### 13.2 既存 `confirm_spreading` API

- まずフロントから呼ばない状態にする
- しばらく互換維持
- 新散布実績画面へ完全移行後に削除を検討する

---

## 15. 実装ステップ

1. `SpreadingSession` / `SpreadingSessionItem` を追加
2. `apps/workrecords/` を追加
3. `FertilizationEntry.actual_bags` を追加
4. `StockTransaction.spreading_item` を追加
5. 散布実績 API を追加
6. 作業記録 API を追加
7. 散布候補集計 API を追加
8. 散布保存時の `actual_bags` 再集計を追加
9. 施肥計画一覧に進捗表示を追加
10. 施肥計画一覧から `散布確定` ボタンを外す
11. `ConfirmSpreadingModal.tsx` を撤去
12. 散布実績画面を追加
13. WorkRecord 自動生成を追加
14. 散布保存時の在庫 `USE` 連携を追加
15. `copy_from_previous_year` の `actual_bags → bags` 反映を追加

---

## 16. 影響ファイル（想定）

### Backend

- `backend/apps/fertilizer/models.py`
- `backend/apps/fertilizer/serializers.py`
- `backend/apps/fertilizer/views.py`
- `backend/apps/fertilizer/urls.py`
- `backend/apps/fertilizer/admin.py`
- `backend/apps/fertilizer/migrations/`
- `backend/apps/fertilizer/services.py` または同等の集計ロジック配置先
- `backend/apps/materials/models.py`
- `backend/apps/materials/stock_service.py`
- `backend/apps/materials/migrations/`
- `backend/apps/workrecords/`

### Frontend

- `frontend/src/app/fertilizer/page.tsx`
- `frontend/src/app/fertilizer/_components/FertilizerEditPage.tsx`
- `frontend/src/app/fertilizer/_components/ConfirmSpreadingModal.tsx`
- `frontend/src/app/fertilizer/spreading/`
- `frontend/src/app/workrecords/`
- `frontend/src/types/index.ts`

### ドキュメント

- `document/13_マスタードキュメント_施肥計画編.md`
- `document/14_マスタードキュメント_分配計画編.md`
- `CLAUDE.md`

---

## 17. 受け入れ条件

- 施肥計画一覧に `散布確定` ボタンが表示されない
- 日付単位の散布実績を登録できる
- 散布対象は全部または一部の圃場を選べる
- 運搬回の順番に依存せず、運搬済みデータを元に散布できる
- 実際の散布袋数を入力できる
- 散布保存時に在庫 `USE` が作成される
- 散布保存・更新・削除時に `FertilizationEntry.actual_bags` が再集計される
- 作業記録一覧から運搬と散布を参照できる
- 施肥計画一覧で `未散布 / 一部散布 / 完了 / 計画超過` が分かる
- 施肥計画一覧・編集画面で `計画値 / 実績値` の両方が分かる
- 前年度コピー時に `actual_bags` があればそれを次年度 `bags` 初期値として使える
- 将来、散布実績データから資料化に必要な情報を取り出せる

---

## 18. 将来拡張

今回見送るが、将来必要になれば以下を追加できる。

- `SpreadingAllocation` による便単位追跡
- 相手先別PDFテンプレート群
- 残肥返却や再入庫
- 栽培管理全体を包含した作業記録詳細
