# 在庫管理機能実装案

> 作成日: 2026-03-13
> 対象プロジェクト: `keinasystem_t02`
> 目的: 肥料と農薬を含む資材在庫管理機能を追加し、施肥計画から参照できるようにする

---

## 1. 結論

本プロジェクトの在庫管理機能は、以下の構成で実装することを推奨する。

- 共通の資材マスタ `Material` を新設する
- 肥料専用情報は `FertilizerProfile` として分離する
- 農薬専用情報は `PesticideProfile` として分離する
- 在庫数は共通の入出庫履歴 `StockTransaction` で管理する
- 施肥計画は従来どおり「肥料のみ」を対象とし、農薬は混在させない

つまり、設計方針は「共通化するのは在庫管理の土台だけ、業務利用は肥料と農薬で分ける」である。

---

## 2. 背景

既存実装では、`backend/apps/fertilizer/models.py` の `Fertilizer` モデルが以下の役割を持っている。

- 肥料マスタ
- 施肥計画エントリの参照先
- 窒素量計算の対象
- 分配計画の集計対象

特に、以下の点から `Fertilizer` はすでに肥料専用モデルになっている。

- `capacity_kg`, `nitrogen_pct`, `phosphorus_pct`, `potassium_pct` を持つ
- 施肥計画 `FertilizationEntry` が `Fertilizer` を直接参照している
- 自動計算 API が `Fertilizer` の窒素成分を使っている

このため、既存の `Fertilizer` を単純に「資材」へ拡張すると、以下の問題が起きやすい。

- 農薬に不要な肥料属性が大量にぶら下がる
- 施肥画面で農薬が見えてしまう
- 今後の農薬散布機能で肥料向けロジックが混ざる
- フロントエンドと API の条件分岐が増える

---

## 3. 要件整理

今回の要件は大きく分けて次の2つである。

### 3.1 共通でほしいこと

- 肥料と農薬を「在庫」という観点で一元管理したい
- 入庫、出庫、棚卸、調整の履歴を残したい
- 現在庫を一覧で見たい
- 将来的に種苗など他資材にも拡張できるようにしたい

### 3.2 分けておきたいこと

- 施肥計画では肥料だけを使いたい
- 農薬散布計画や散布履歴では農薬だけを使いたい
- 肥料特有の計算項目と農薬特有の項目は混在させたくない

---

## 4. 推奨アーキテクチャ

### 4.1 モデル分離の考え方

以下の3層で整理する。

1. 共通資材層
2. 種別別の詳細層
3. 在庫履歴層

### 4.2 推奨モデル構成

#### Material

資材共通マスタ。在庫管理の主キーになる。

想定フィールド:

- `id`
- `name` 資材名
- `material_type` 資材種別
- `maker` メーカー
- `stock_unit` 在庫単位
- `is_active` 使用可否
- `notes` 備考
- `created_at`
- `updated_at`

`material_type` の候補:

- `fertilizer`
- `pesticide`
- `seedling`
- `other`

`stock_unit` の候補:

- `bag`
- `bottle`
- `kg`
- `liter`
- `piece`

#### FertilizerProfile

肥料専用の属性を持つ。

想定フィールド:

- `id`
- `material` `OneToOneField(Material)`
- `capacity_kg` 1袋重量
- `nitrogen_pct`
- `phosphorus_pct`
- `potassium_pct`
- `dilution_note` 任意、必要なら

#### PesticideProfile

農薬専用の属性を持つ。

想定フィールド:

- `id`
- `material` `OneToOneField(Material)`
- `registration_no` 農薬登録番号
- `formulation` 剤型
- `usage_unit` 使用単位
- `dilution_ratio` 希釈倍率
- `active_ingredient` 有効成分
- `category` 殺菌剤、除草剤、殺虫剤など
- `remark_for_use` 使用上メモ

#### StockTransaction

在庫の増減を管理する履歴テーブル。現在庫はこの集計で算出する。

想定フィールド:

- `id`
- `material` `ForeignKey(Material)`
- `transaction_type`
- `quantity`
- `occurred_on`
- `note`
- `created_at`

以下のフィールドは初期実装では **除外** する:

- ~~`unit`~~ → `Material.stock_unit` から取得すれば十分。トランザクションごとに単位を持つと不整合の元になる
- ~~`reference_type`~~ / ~~`reference_id`~~ → Generic FK パターンは複雑化の元。将来の施肥計画連携時に必要なら追加する
- ~~`created_by`~~ → シングルユーザーシステムのため不要。マルチユーザー対応時に追加する

`transaction_type` の候補:

- `purchase`
- `use`
- `adjustment_plus`
- `adjustment_minus`
- `discard`

以下は初期実装では除外する:

- ~~`inventory_count`~~ → `adjustment_plus` / `adjustment_minus` で棚卸差異を十分表現できる

`quantity` は正の数で持ち、増減方向は `transaction_type` で判定する方式を推奨する。

#### MaterialStockSnapshot

初期フェーズでは不要。パフォーマンス課題が出た場合のみ追加する。

- 日次や月次の在庫スナップショット
- 一覧高速化用の集計テーブル

---

## 5. Django モデル案

以下はイメージであり、実装時には既存 app 構成に合わせて調整する。

```python
from django.conf import settings
from django.db import models


class Material(models.Model):
    class MaterialType(models.TextChoices):
        FERTILIZER = 'fertilizer', '肥料'
        PESTICIDE = 'pesticide', '農薬'
        SEEDLING = 'seedling', '種苗'
        OTHER = 'other', 'その他'

    class StockUnit(models.TextChoices):
        BAG = 'bag', '袋'
        BOTTLE = 'bottle', '本'
        KG = 'kg', 'kg'
        LITER = 'liter', 'L'
        PIECE = 'piece', '個'

    name = models.CharField(max_length=100, verbose_name='資材名')
    material_type = models.CharField(max_length=20, choices=MaterialType.choices, verbose_name='資材種別')
    maker = models.CharField(max_length=100, blank=True, null=True, verbose_name='メーカー')
    stock_unit = models.CharField(max_length=20, choices=StockUnit.choices, verbose_name='在庫単位')
    is_active = models.BooleanField(default=True, verbose_name='使用中')
    notes = models.TextField(blank=True, null=True, verbose_name='備考')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['material_type', 'name']
        constraints = [
            models.UniqueConstraint(fields=['material_type', 'name'], name='uniq_material_type_name'),
        ]


class FertilizerProfile(models.Model):
    material = models.OneToOneField(Material, on_delete=models.CASCADE, related_name='fertilizer_profile')
    capacity_kg = models.DecimalField(max_digits=8, decimal_places=3, blank=True, null=True)
    nitrogen_pct = models.DecimalField(max_digits=5, decimal_places=2, blank=True, null=True)
    phosphorus_pct = models.DecimalField(max_digits=5, decimal_places=2, blank=True, null=True)
    potassium_pct = models.DecimalField(max_digits=5, decimal_places=2, blank=True, null=True)


class PesticideProfile(models.Model):
    material = models.OneToOneField(Material, on_delete=models.CASCADE, related_name='pesticide_profile')
    registration_no = models.CharField(max_length=100, blank=True, null=True)
    formulation = models.CharField(max_length=100, blank=True, null=True)
    usage_unit = models.CharField(max_length=50, blank=True, null=True)
    dilution_ratio = models.CharField(max_length=100, blank=True, null=True)
    active_ingredient = models.CharField(max_length=200, blank=True, null=True)
    category = models.CharField(max_length=100, blank=True, null=True)


class StockTransaction(models.Model):
    class TransactionType(models.TextChoices):
        PURCHASE = 'purchase', '入庫'
        USE = 'use', '使用'
        ADJUSTMENT_PLUS = 'adjustment_plus', '棚卸増'
        ADJUSTMENT_MINUS = 'adjustment_minus', '棚卸減'
        DISCARD = 'discard', '廃棄'

    material = models.ForeignKey(Material, on_delete=models.PROTECT, related_name='stock_transactions')
    transaction_type = models.CharField(max_length=30, choices=TransactionType.choices)
    quantity = models.DecimalField(max_digits=10, decimal_places=3)
    occurred_on = models.DateField()
    note = models.TextField(blank=True, default='')
    created_at = models.DateTimeField(auto_now_add=True)
```

---

## 6. 既存肥料機能とのつなぎ方

### 6.1 重要方針

既存の `Fertilizer` モデルをすぐに削除したり全面改名したりしない。

理由:

- 施肥計画機能がすでに本番稼働中
- `FertilizationEntry` が `Fertilizer` を直接参照している
- 分配計画、PDF、計算 API まで連鎖的に影響する

### 6.2 移行方針

段階的には次のどちらかが現実的である。

#### 案A: 既存 `Fertilizer` を残しつつ `Material` を追加する

推奨度: 高

構成:

- `Material` を新設
- 既存 `Fertilizer` に `material = OneToOneField(Material)` を追加
- 施肥計画は当面 `Fertilizer` を使い続ける
- 在庫管理は `Material` を使う

メリット:

- 既存施肥機能への影響が最小
- 農薬機能をあとから追加しやすい
- データ移行が安全

デメリット:

- 一時的に `Material` と `Fertilizer` の二重管理に見える

#### 案B: `Fertilizer` を廃止して `Material + FertilizerProfile` に完全移行する

推奨度: 中

構成:

- `Fertilizer` の役割を `Material + FertilizerProfile` に移す
- `FertilizationEntry` は `FertilizerProfile` または `Material` を参照するよう変更

メリット:

- モデル構造がきれいになる

デメリット:

- 既存コード改修範囲が大きい
- 移行難易度が高い
- 本番稼働機能への影響が大きい

現時点では案Aを採用し、運用が安定してから必要なら案Bへ寄せるのがよい。

---

## 7. 推奨する app 構成

在庫管理を `apps/fertilizer` に混ぜ込まず、新規 app として切り出すことを推奨する。

候補:

- `backend/apps/materials`
- `backend/apps/inventory`

推奨:

- 共通マスタも在庫履歴も含めて `backend/apps/materials`

理由:

- 肥料専用 app に農薬や種苗を混ぜないため
- 将来の資材計画、散布履歴、購入管理の拡張がしやすいため

推奨ファイル構成:

```text
backend/apps/materials/
├── models.py
├── serializers.py
├── views.py
├── urls.py
├── admin.py
└── migrations/
```

---

## 8. API 実装案

### 8.1 資材マスタ API

- `GET /api/materials/materials/`
- `POST /api/materials/materials/`
- `GET /api/materials/materials/{id}/`
- `PUT /api/materials/materials/{id}/`
- `DELETE /api/materials/materials/{id}/`

主なクエリ:

- `?material_type=fertilizer`
- `?material_type=pesticide`
- `?active=true`

### 8.2 在庫履歴 API

- `GET /api/materials/stock-transactions/`
- `POST /api/materials/stock-transactions/`
- `GET /api/materials/stock-transactions/{id}/`

主なクエリ:

- `?material_id=`
- `?material_type=`
- `?date_from=`
- `?date_to=`

### 8.3 在庫集計 API

- `GET /api/materials/stock-summary/`
- `GET /api/materials/stock-summary/{material_id}/`

レスポンス例:

```json
{
  "material_id": 12,
  "name": "コシヒカリ専用一発肥料",
  "material_type": "fertilizer",
  "stock_unit": "bag",
  "current_stock": "18.00",
  "last_transaction_date": "2026-03-10"
}
```

### 8.4 施肥計画からの参照 API

施肥計画画面では、以下のような肥料在庫参照 API を追加する。

- `GET /api/materials/fertilizer-stock-options/`

用途:

- 施肥計画で使う肥料一覧を返す
- `material_type=fertilizer` だけ返す
- 必要に応じて現在庫も返す

レスポンス例:

```json
[
  {
    "material_id": 12,
    "fertilizer_id": 3,
    "name": "コシヒカリ専用一発肥料",
    "current_stock": "18.00",
    "stock_unit": "bag"
  }
]
```

ここで重要なのは、施肥計画画面に農薬を返さない専用 API を用意することである。

---

## 9. フロントエンド実装案

### 9.1 新規画面

- `/materials`
  - 在庫一覧（メイン画面）。在庫集計表 + 入出庫登録モーダル
- `/materials/masters`
  - 資材マスタ管理（肥料/農薬/その他をタブ切り替え、インライン編集テーブル）

**設計判断**: 入出庫登録は独立ページではなくモーダル方式にする。
理由: 既存の肥料マスタ画面（`/fertilizer/masters`）のインライン編集パターンに合わせ、
画面遷移を減らして操作効率を上げるため。
肥料マスタと農薬マスタを別ページに分けず、1画面でタブ切り替えにする。

### 9.2 一覧画面の基本列

- 資材名
- 種別
- メーカー
- 在庫単位
- 現在庫
- 最終入出庫日
- 使用中フラグ

### 9.3 施肥計画画面での使い方

既存の肥料選択 UI は維持しつつ、以下の補助情報だけ追加する。

- 肥料名の横に現在庫を表示
- 在庫不足が見込まれる場合は注意表示
- 必要であれば、計画合計と在庫差分を表示

例:

- `コシヒカリ専用一発肥料 在庫18袋`
- `計画必要数 24袋 / 不足 6袋`

これにより、施肥計画の UX を壊さずに在庫参照を実現できる。

---

## 10. 在庫計算ロジック

### 10.1 基本ルール

現在庫は `StockTransaction` の累積で求める。

計算式:

`現在庫 = 入庫合計 - 出庫合計 + 調整増合計 - 調整減合計`

### 10.2 推奨ルール

- 在庫は更新値を直接持たず、履歴を正とする
- 誤登録時は履歴修正または取消履歴で対応する
- 初期在庫は `adjustment_plus` で登録する
- 棚卸差異は `adjustment_plus` / `adjustment_minus` で表現する

### 10.3 将来連携

将来的には以下と自動連携できる。

- 施肥計画確定時に肥料必要量を引当
- 施肥実績登録時に肥料を出庫
- 農薬散布実績登録時に農薬を出庫

ただし初期フェーズでは「計画から参照のみ」に留め、在庫自動減算はまだ行わない方が安全である。

---

## 11. データ移行案

### フェーズ1

- `apps/materials` を追加
- `Material` と `StockTransaction` を作成
- 既存 `Fertilizer` 1件ごとに `Material(material_type='fertilizer')` を作成
- `Fertilizer.material` を紐づける migration を追加

### フェーズ2

- 在庫一覧画面と入出庫登録画面を実装
- 施肥計画画面で肥料在庫参照を追加

### フェーズ3

- `PesticideProfile` を追加
- 農薬マスタ管理画面を実装
- 将来の農薬散布計画または散布履歴と連携

### データ移行時の注意

- `Fertilizer.name` と `Material.name` の不整合を防ぐ
- 肥料削除制約と `Material` の削除制約を揃える
- 既存 API を壊さない
- 既存フロント型定義を急に置き換えない

---

## 12. 画面・業務フロー案

### 12.1 資材登録

1. 資材種別を選ぶ
2. 共通項目を入力する
3. 肥料なら肥料専用項目を入力する
4. 農薬なら農薬専用項目を入力する

### 12.2 在庫登録

1. 資材を選ぶ
2. 入庫 or 出庫 or 棚卸調整を選ぶ
3. 数量と日付を入力する
4. メモを残す

### 12.3 施肥計画での利用

1. 従来どおり肥料を選ぶ
2. 肥料ごとの在庫を横に表示する
3. 計画全体の必要量と現在庫の差分を見る
4. 必要なら購入判断につなげる

---

## 13. 採用しない案

### 13.1 単一テーブル案

`Fertilizer` を `Material` に改名し、肥料も農薬も1テーブルで管理する案。

採用しない理由:

- nullable 項目が増えすぎる
- `material_type` 条件分岐が画面や API に広がる
- 施肥機能に農薬が混ざりやすい
- モデル名と実務概念がずれて保守性が落ちる

### 13.2 完全分離案

肥料在庫テーブルと農薬在庫テーブルを別々に作る案。

採用しない理由:

- 入出庫、棚卸、在庫集計ロジックを二重実装することになる
- 将来の資材種追加に弱い
- 一覧画面や集計の統一感がなくなる

---

## 14. 推奨実装順

最も安全な進め方は以下である。

1. `apps/materials` を追加する
2. `Material` と `StockTransaction` を実装する
3. 既存 `Fertilizer` に `material` を紐づける
4. 肥料だけを対象に在庫一覧を表示する
5. 施肥計画画面に在庫参照を追加する
6. その後、農薬マスタと農薬在庫を追加する

---

## 15. 最終提案

本案件では、次の方針を正式案とすることを推奨する。

- 在庫管理の主軸は `Material` と `StockTransaction`
- 肥料と農薬は詳細テーブルで分ける
- 施肥計画は既存 `Fertilizer` 中心のまま維持する
- まずは「在庫参照」まで実装し、自動出庫連携は後続フェーズに回す

この方針であれば、既存の施肥計画機能を壊さず、農薬対応と将来の資材拡張に耐える構成になる。

---

## 16. 次アクション案

次に着手するなら、以下の順で進めるとよい。

1. `apps/materials` の Django モデルを確定する
2. migration 方針を決める
3. API 設計を確定する
4. フロントの画面導線を決める
5. 施肥計画画面に出す在庫情報の粒度を決める

必要であれば次の段階として、上記案をベースに

- Django モデル実装
- migration 作成方針
- API 詳細仕様
- 画面ワイヤー案

まで具体化できる。

---

## 17. 補足: 農薬公式データ同期・あいまい検索案

農薬マスタについては、ユーザーが毎回 `農薬登録番号` や正式名称を手入力するのではなく、
FAMIC / 農水省の公式データを取り込んで補完できるようにすることを推奨する。

### 17.1 目的

- 農薬登録番号を手で調べる手間を減らす
- 名称ゆれがあっても候補を見つけやすくする
- 公式データをもとにした登録に寄せる
- 画面の操作感は速く保つ

### 17.2 基本方針

検索時に毎回外部サイトへ直接問い合わせるのではなく、以下の流れを採用する。

1. まず FAMIC / 農水省の公式データを取得してローカルDB化する
2. 農薬マスタ画面でのあいまい検索はローカルDBに対して行う
3. 画面表示時または検索時に、最終同期から24時間以上経過していれば更新ジョブを起動する
4. 更新ジョブでは公式データを再取得し、差分だけローカルDBに反映する
5. 更新完了後、検索候補を再読み込みする

この方式なら、検索体験を外部サイトの応答速度に依存させず、最新性もある程度保てる。

### 17.3 ユーザー操作フロー

ユーザー操作:

- 農薬名を入力する

画面:

- すぐローカルDBで候補表示する

裏側:

- `最終同期が24時間以上前` なら更新ジョブを実行する

更新完了後:

- 候補データを再読み込みする

補足:

- 同期中であっても検索自体は継続可能とする
- 同期失敗時もローカルDBの候補表示は継続する
- 最新同期時刻を画面に小さく表示すると安心感が高い

### 17.4 追加テーブル案

#### PesticideOfficialMaster

公式農薬データの取り込み先となるローカル参照テーブル。

想定フィールド:

- `id`
- `registration_no` 農薬登録番号
- `name` 農薬名称
- `generic_name` 種類名または一般名称
- `holder_name` 登録を有する者の名称
- `formulation` 剤型
- `active_ingredient` 有効成分
- `crop_scope_text` 適用作物の要約文字列
- `pest_scope_text` 適用病害虫・雑草の要約文字列
- `raw_payload` 元データ保存用 JSON
- `source_updated_at` 元データ上の更新日時
- `last_synced_at` ローカル同期日時
- `is_active` 有効フラグ

用途:

- あいまい検索の検索元
- 農薬マスタ登録時の候補ソース
- 将来の適用作物・病害虫チェックの基礎データ

#### OfficialDataSyncStatus

外部同期の状態管理テーブル。

想定フィールド:

- `id`
- `source_name` 例: `famic_pesticide`
- `last_checked_at`
- `last_succeeded_at`
- `last_failed_at`
- `last_source_version`
- `last_error_message`
- `is_running`

用途:

- 24時間ルールの判定
- 同時実行防止
- 管理画面での状況確認

### 17.5 農薬マスタとの紐づけ案

`PesticideProfile` には、公式参照用として以下の項目を追加する。

- `official_master = ForeignKey(PesticideOfficialMaster, null=True, blank=True, on_delete=SET_NULL)`
- `registration_no`
- `official_name`
- `holder_name`

運用方針:

- 公式候補を選んだときは `official_master` を紐づける
- その時点の登録番号や名称は `PesticideProfile` 側にもコピー保持する
- 後で公式マスタの表記が少し変わっても、登録済みデータが勝手に変わらないようにする

### 17.6 同期ジョブ案

実装方法の候補:

- Django management command
- バックエンド API から非同期ジョブ起動
- cron や定期タスクでの夜間同期

初期実装では以下を推奨する。

- `python manage.py sync_pesticide_official_master`
- 必要時に API から起動できるようにする

同期処理の流れ:

1. `OfficialDataSyncStatus` を確認する
2. 同期実行中なら二重起動しない
3. 公式データを取得する
4. 取得データを正規化する
5. `registration_no` を主キー相当として差分比較する
6. 新規追加、更新、失効を反映する
7. 同期結果と件数を記録する

### 17.7 差分更新ルール

差分更新は以下の基準を推奨する。

- `registration_no` が未登録なら新規追加
- `registration_no` が既存で、名称や保持者などが変わっていれば更新
- 公式データから消えた場合は即削除せず `is_active=False` にする

理由:

- 物理削除すると、過去に登録済みの農薬マスタとの整合が崩れやすい
- 失効農薬も履歴参照上は残しておいた方がよい

### 17.8 あいまい検索 API 案

- `GET /api/materials/pesticide-official-search/?q=`

レスポンス例:

```json
{
  "items": [
    {
      "id": 1234,
      "registration_no": "12345",
      "name": "サンプルフロアブル",
      "holder_name": "サンプル株式会社",
      "formulation": "フロアブル",
      "is_active": true
    }
  ],
  "sync": {
    "last_succeeded_at": "2026-03-13T08:10:00Z",
    "refresh_started": true
  }
}
```

API の考え方:

- 検索結果は必ずローカルDBから返す
- 同時に同期必要性を判定する
- 同期が必要ならバックグラウンド更新を開始する
- API レスポンスには同期開始有無だけ含める

### 17.9 フロントエンド動作案

農薬マスタ画面での候補検索 UI は以下の流れを推奨する。

1. ユーザーが 2〜3 文字入力する
2. 300ms 程度の debounce 後に検索 API を呼ぶ
3. ローカルDBの候補を即表示する
4. `refresh_started=true` の場合は「公式データ更新中」と小さく表示する
5. 数秒後に再検索する、または「再読込」ボタンを出す
6. 候補選択時に登録番号、正式名称、保持者名をフォームへ自動入力する

UI 表示例:

- `候補はローカル保存済みの公式データから表示しています`
- `公式データを更新中です`
- `最終同期: 2026-03-13 17:10`

### 17.10 失敗時のフォールバック

外部取得に失敗した場合も、画面は止めないことを原則とする。

想定動作:

- 検索結果は直近のローカルDBをそのまま返す
- 同期失敗メッセージは管理者向けログに残す
- 一般ユーザーには必要最小限の表示に留める
- 必要なら管理画面で「最終同期失敗」を確認できるようにする

### 17.11 初期実装のスコープ

初期フェーズでは、以下までで十分である。

- 公式農薬マスタのローカルDB化
- 24時間単位の同期判定
- ローカルDBに対するあいまい検索
- 候補選択による登録番号などの自動入力

初期フェーズでは見送ってよいもの:

- 適用作物・病害虫の厳密な構造化
- リアルタイム同期
- 画面上での詳細ラベル比較
- 農薬使用可否の自動判定

### 17.12 この案のメリット

- ユーザーが登録番号を自分で調べる負担が減る
- 検索が速い
- 公式データの更新にも追従しやすい
- 外部サイト障害に強い
- 将来の農薬散布記録や適用チェックに発展させやすい

---

## 18. 補足: 肥料公式データ同期・あいまい検索案

肥料についても、農薬と同様に公式データをローカルDBへ取り込み、
肥料マスタ画面であいまい検索できるようにすることは可能である。

ただし、農薬と違って肥料は制度上の区分があるため、初期実装では
`普通肥料を主対象` とし、`特殊肥料や独自資材は手入力を許容する`
方針が現実的である。

### 18.1 目的

- 肥料名や登録番号を手で調べる手間を減らす
- 公式登録銘柄を候補から選べるようにする
- 既存の肥料マスタ登録を速く、正確にする
- 施肥計画で使う肥料名の表記ゆれを減らす

### 18.2 基本方針

農薬と同じく、検索時に毎回外部サイトへ直接問い合わせるのではなく、
ローカルDB検索を主とした構成を採用する。

1. 農水省の肥料登録銘柄データを取得してローカルDB化する
2. 肥料マスタ画面でのあいまい検索はローカルDBに対して行う
3. 画面表示時または検索時に、最終同期から24時間以上経過していれば更新ジョブを起動する
4. 更新ジョブでは公式データを再取得し、差分だけローカルDBに反映する
5. 更新完了後、候補データを再読み込みする

### 18.3 適用範囲

初期対象:

- 普通肥料
- 登録銘柄検索システムで取得できる肥料

初期対象外または要個別対応:

- 特殊肥料
- 堆肥
- 土壌改良資材
- 地域独自資材
- 登録制度外の取扱資材

このため、肥料マスタ画面では以下の2方式を共存させるのがよい。

- `公式候補から選ぶ`
- `手入力で登録する`

### 18.4 追加テーブル案

#### FertilizerOfficialMaster

公式肥料データの取り込み先となるローカル参照テーブル。

想定フィールド:

- `id`
- `registration_no` 登録番号
- `name` 銘柄名
- `fertilizer_type` 普通肥料区分
- `brand_name` 商品名に相当する表記
- `holder_name` 登録業者名
- `guaranteed_nitrogen_pct`
- `guaranteed_phosphorus_pct`
- `guaranteed_potassium_pct`
- `guaranteed_other_text`
- `source_updated_at`
- `last_synced_at`
- `is_active`
- `raw_payload`

用途:

- 肥料あいまい検索の検索元
- 肥料マスタ作成時の候補データ
- 将来の施肥計画での成分初期値補完

#### OfficialDataSyncStatus

既に 17章で定義した同期管理テーブルを共用する。

`source_name` の例:

- `famic_pesticide`
- `maff_fertilizer`

### 18.5 肥料マスタとの紐づけ案

既存 `Fertilizer` モデルには、公式参照用として以下の項目追加を検討する。

- `official_master = ForeignKey(FertilizerOfficialMaster, null=True, blank=True, on_delete=SET_NULL)`
- `registration_no`
- `official_name`
- `holder_name`

また、既存の以下の項目は、公式候補選択時に初期値として自動入力できる。

- `name`
- `maker`
- `nitrogen_pct`
- `phosphorus_pct`
- `potassium_pct`

運用方針:

- 公式候補を選んだら、その時点の名称や登録番号を `Fertilizer` 側にもコピー保持する
- 後で公式マスタの表記が変わっても、登録済みの肥料マスタは勝手に変えない
- ユーザーが手で微修正したい場合は上書きを許可する

### 18.6 同期ジョブ案

初期実装では以下を推奨する。

- `python manage.py sync_fertilizer_official_master`

同期処理の流れ:

1. `OfficialDataSyncStatus(source_name='maff_fertilizer')` を確認する
2. 実行中なら二重起動しない
3. 公式データを取得する
4. 取得データを正規化する
5. `registration_no` を主キー相当として差分比較する
6. 新規追加、更新、失効を反映する
7. 同期結果を記録する

### 18.7 差分更新ルール

差分更新は以下の基準を推奨する。

- `registration_no` が未登録なら新規追加
- `registration_no` が既存で、名称、業者名、保証成分値などが変わっていれば更新
- 公式データから消えた場合は即削除せず `is_active=False` にする

補足:

- 実運用では、過去に使っていた肥料も履歴上残したい可能性が高い
- そのため、物理削除ではなく論理無効化が安全である

### 18.8 あいまい検索 API 案

- `GET /api/materials/fertilizer-official-search/?q=`

レスポンス例:

```json
{
  "items": [
    {
      "id": 5678,
      "registration_no": "生第12345号",
      "name": "コシヒカリ専用一発肥料",
      "holder_name": "サンプル肥料株式会社",
      "guaranteed_nitrogen_pct": "14.00",
      "guaranteed_phosphorus_pct": "12.00",
      "guaranteed_potassium_pct": "12.00",
      "is_active": true
    }
  ],
  "sync": {
    "last_succeeded_at": "2026-03-13T08:10:00Z",
    "refresh_started": true
  }
}
```

API の考え方:

- 検索結果は必ずローカルDBから返す
- 同時に同期必要性を判定する
- 同期が必要ならバックグラウンド更新を開始する
- API レスポンスには同期状態を含める

### 18.9 フロントエンド動作案

肥料マスタ画面での検索 UI は以下の流れを推奨する。

1. ユーザーが 2〜3 文字入力する
2. 300ms 程度の debounce 後に検索 API を呼ぶ
3. ローカルDBの候補を即表示する
4. `refresh_started=true` の場合は「公式データ更新中」と小さく表示する
5. 候補選択時に、名称、登録番号、メーカー、成分値をフォームへ自動入力する
6. 必要なら `capacity_kg` だけユーザーが補完する

理由:

- 肥料の 1袋重量は流通単位や運用上の入力が必要な場合がある
- 公式データだけでは既存施肥計画に必要な値が必ず揃うとは限らない

### 18.10 失敗時のフォールバック

外部取得に失敗しても、画面は止めない。

想定動作:

- 検索結果は直近のローカルDBをそのまま返す
- 同期失敗はログと同期状態テーブルに記録する
- ユーザーは手入力登録へ切り替えられる

### 18.11 初期実装のスコープ

初期フェーズでは、以下までで十分である。

- 公式肥料マスタのローカルDB化
- 24時間単位の同期判定
- ローカルDBに対するあいまい検索
- 候補選択による名称、登録番号、成分値の自動入力

初期フェーズでは見送ってよいもの:

- 特殊肥料の網羅的取り込み
- 施肥計画との自動引当
- 肥料成分の高度な比較ロジック
- 取扱単位の自動変換

### 18.12 この案のメリット

- 肥料マスタ登録が速くなる
- 肥料名や成分の入力ミスを減らせる
- 施肥計画で使う肥料情報の標準化が進む
- 農薬と同じ UX パターンで実装できる
- 将来の購入管理や必要量算出にもつなげやすい

---

## 19. 補足: 別名辞書(alias)設計案

実運用では、現場で使っている名称と保証票上の正式名称が一致しないことが多い。

例:

- 現場名: `仁井田米有機`
- 保証票名: `複合肥料 DH85号`

- 現場名: `土佐勤農党`
- 保証票名: `新・土佐勤農党・U`

このようなケースに対応するため、公式データ同期・あいまい検索に加えて、
`別名辞書` を持つことを強く推奨する。

### 19.1 目的

- 現場の通称やブランド名で検索できるようにする
- 正式名称へのひも付けを一度覚えたら、次回以降すぐ使えるようにする
- 手入力のたびに同じ確認作業を繰り返さないようにする

### 19.2 追加テーブル案

#### MaterialAlias

共通資材向けの別名辞書。

想定フィールド:

- `id`
- `material_type` `fertilizer` / `pesticide`
- `alias_name` 現場の通称、略称、ブランド名
- `alias_type` `brand` / `local_name` / `abbreviation` / `kana_variation`
- `material` 既存登録済み資材への紐づけ
- `official_fertilizer_master` `ForeignKey(FertilizerOfficialMaster, null=True, blank=True)`
- `official_pesticide_master` `ForeignKey(PesticideOfficialMaster, null=True, blank=True)`
- `maker_hint`
- `note`
- `created_at`
- `updated_at`

運用上は、以下のどちらかで使えるようにする。

- 公式マスタ候補へのショートカット
- 既に登録済みの社内資材マスタへのショートカット

### 19.3 検索優先順位

検索時の流れは以下を推奨する。

1. まず別名辞書を検索する
2. 一致があれば、それに紐づく公式候補または登録済み資材を優先表示する
3. 一致がなければ、通常の公式マスタあいまい検索を行う
4. ユーザーが正式候補を確定したら、必要に応じて別名辞書を学習登録する

### 19.4 自動学習の考え方

初期フェーズでは手動登録だけでも十分だが、将来的には以下も可能である。

- ユーザーが `仁井田米有機` で検索し、`複合肥料 DH85号` を選択したら
  `MaterialAlias` に候補登録する
- 同じ入力が複数回出たら、管理者承認付きで正式 alias に昇格する

### 19.5 この設計のメリット

- 現場名と正式名が違っても使いやすい
- ローカル運用に最適化できる
- 公式同期の精度不足を現場知識で補える
- 農薬・肥料の両方で同じ仕組みを使える

---

## 20. 補足: 現場肥料名と保証票名の具体例

以下は、今回共有された実物ラベル写真から読み取れる内容をもとにした、
`現場名` と `保証票上の名称` の対応例である。

### 20.1 仁井田米有機

現場呼称:

- `仁井田米有機`

保証票上の情報:

- 肥料区分: `指定配合肥料`
- 肥料の名称: `複合肥料 DH85号`
- 生産業者: `大東肥料株式会社`
- 正味重量: `20キログラム`
- 保証成分量:
  - `窒素全量 8.0`
  - `りん酸全量 5.0`
  - `加里全量 3.0`
  - `内水溶性加里 2.2`

実装上の扱い:

- `Fertilizer.name`: `仁井田米有機`
- `Fertilizer.official_name`: `複合肥料 DH85号`
- `Fertilizer.maker`: `大東肥料株式会社`
- `Fertilizer.capacity_kg`: `20`
- `Fertilizer.nitrogen_pct`: `8.0`
- `Fertilizer.phosphorus_pct`: `5.0`
- `Fertilizer.potassium_pct`: `3.0`

alias 例:

- `MaterialAlias.alias_name = '仁井田米有機'`
- `official_fertilizer_master -> 複合肥料 DH85号`

### 20.2 土佐勤農党

現場呼称:

- `土佐勤農党`

保証票上の情報:

- 肥料区分: `指定配合肥料`
- 肥料の名称: `新・土佐勤農党・U`
- 生産業者: `株式会社 古田産業`
- 生産した事業場: `株式会社古田産業タナスカ工場`
- 正味重量: `20キログラム`
- 保証成分量:
  - `窒素全量 17.0`
  - `内アンモニア性窒素 7.5`
  - `可溶性りん酸 15.0`
  - `内水溶性りん酸 12.5`
  - `水溶性加里 15.0`

実装上の扱い:

- `Fertilizer.name`: `土佐勤農党`
- `Fertilizer.official_name`: `新・土佐勤農党・U`
- `Fertilizer.maker`: `株式会社 古田産業`
- `Fertilizer.capacity_kg`: `20`
- `Fertilizer.nitrogen_pct`: `17.0`
- `Fertilizer.phosphorus_pct`: `15.0`
- `Fertilizer.potassium_pct`: `15.0`

alias 例:

- `MaterialAlias.alias_name = '土佐勤農党'`
- `official_fertilizer_master -> 新・土佐勤農党・U`

### 20.3 電気炉さい

現場呼称:

- `電気炉さい`

保証票上の情報:

- 登録番号: `生第87006号`
- 肥料の種類: `鉱さいけい酸質肥料`
- 肥料の名称: `粒状スーパー珪鉄1号`
- 生産業者: `株式会社 古田産業`
- 生産した事業場: `株式会社古田産業東孕工場`
- 正味重量: `20キログラム`
- 保証成分量:
  - `可溶性けい酸 24.0`
  - `アルカリ分 37.0`
  - `く溶性苦土 3.0`

実装上の扱い:

- `Fertilizer.name`: `電気炉さい`
- `Fertilizer.official_name`: `粒状スーパー珪鉄1号`
- `Fertilizer.registration_no`: `生第87006号`
- `Fertilizer.maker`: `株式会社 古田産業`
- `Fertilizer.capacity_kg`: `20`
- `Fertilizer.notes`: `鉱さいけい酸質肥料。現場では電気炉さいと呼称`

補足:

- この資材は、N/P/K 中心の複合肥料とは異なり、けい酸質肥料として管理する性格が強い
- そのため、施肥計画で使う場合は `notes` や追加属性で区分を保持した方がよい

alias 例:

- `MaterialAlias.alias_name = '電気炉さい'`
- `official_fertilizer_master -> 粒状スーパー珪鉄1号`

### 20.4 ミネラルホウ素

現場呼称:

- `ミネラルホウ素`

袋表示上の情報:

- 表示名: `複合ミネラル有機JAS適合資材`
- JAS登録番号: `JASOM-130419`
- 特長表示:
  - `多種の微量要素を含有`
  - `珪酸の吸収を助ける`
  - `有害カビ類に対し抑止力効果`
- 適用量例:
  - `水稲 60〜100kg`
  - `露地野菜 60〜120kg`
  - `施設園芸 100〜140kg`
  - `花卉 60〜100kg`
  - `特用作物・牧草 80〜120kg`
- 成分表（分析例）:
  - `珪酸 60.16`
  - `粘土 18.45`
  - `酸化鉄 5.55`
  - `加里 3.36`
  - `苦土 2.17`
  - `石灰 2.68`
  - `りん酸 0.22`
  - `マンガン 0.09`
  - `ほう素 0.005`

実装上の扱い:

- `Fertilizer.name`: `ミネラルホウ素`
- `Fertilizer.official_name`: `複合ミネラル有機JAS適合資材`
- `Fertilizer.capacity_kg`: 袋表示要確認
- `Fertilizer.notes`: `微量要素資材。袋表示上は複合ミネラル有機JAS適合資材`

補足:

- 袋表示からは一般的な N/P/K 保証票というより、分析例と資材特長が前面に出ている
- このため、通常の普通肥料公式マスタにそのまま一致しない可能性がある
- 初期実装では `手入力 + alias辞書` で扱い、後から公式対応可否を判定するのが安全

alias 例:

- `MaterialAlias.alias_name = 'ミネラルホウ素'`
- `material -> 自社登録のミネラルホウ素材材`

### 20.5 リン酸グアノ

現場呼称:

- `リン酸グアノ`

袋表示上の情報:

- 表示名: `純天然有機質リン酸肥料`
- 商品表示名: `リン酸グアノ`
- 正味重量: `20kg`
- 原産国表示: `MADE IN INDONESIA`
- 成分表（分析例）:
  - `リン酸全量 25.73`
  - `く溶性リン酸 17.47`
  - `石灰 31.69`
- 施肥基準量例:
  - `水稲 40kg〜60kg`
  - `葉菜・芝草類 60kg〜100kg`
  - `根菜・果菜・果樹類 80kg〜120kg`

実装上の扱い:

- `Fertilizer.name`: `リン酸グアノ`
- `Fertilizer.official_name`: `リン酸グアノ`
- `Fertilizer.capacity_kg`: `20`
- `Fertilizer.phosphorus_pct`: `25.73` または用途に応じて `く溶性リン酸 17.47` を備考保持
- `Fertilizer.notes`: `純天然有機質リン酸肥料。石灰成分を多く含む`

補足:

- `リン酸グアノ` は商品表示名そのものが現場呼称と一致しており、今回の例では alias なしでも扱いやすい
- ただし、同名類似品がある場合はメーカー名や原産国も保持できると識別しやすい

alias 例:

- `MaterialAlias.alias_name = 'リン酸グアノ'`
- `official_fertilizer_master -> リン酸グアノ`

### 20.6 この具体例から分かること

今回の5例を見ると、現場資材の扱いは大きく3パターンに分かれる。

- `現場名と保証票名が違う`
  - 例: `仁井田米有機`, `土佐勤農党`, `電気炉さい`
- `現場名と表示名がほぼ一致する`
  - 例: `リン酸グアノ`
- `公式マスタよりローカル資材管理向き`
  - 例: `ミネラルホウ素`

このため、肥料公式データ同期を実装する場合でも、実運用では次の3つが必要になる。

- 公式データ同期
- ローカルDBあいまい検索
- 別名辞書(alias)

この3つを組み合わせることで、正式名称ベースの整合性と、
現場名称ベースの使いやすさを両立できる。

---

## 21. 補足: 外部サブスクLLM活用案

### 21.1 基本方針

恵菜システム自身が LLM API を呼び出す方式は採用しない。

理由:

- API 従量課金が発生する
- 継続利用時の運用コストが読みにくい
- 現場で既に契約済みのサブスク LLM を活用した方が現実的である

このため、恵菜システムは `LLMを呼ぶシステム` ではなく、
`外部LLMを使った調査作業を支援するシステム` として設計する。

### 21.2 目指す形

恵菜システムで提供するのは、次の3点である。

- 調査用プロンプト
- 調査手順
- LLM の回答結果を貼り付けて反映する機能

つまり、ユーザーは ChatGPT などのサブスク LLM で調査し、
その結果だけを恵菜システムに持ち込む。

### 21.3 利用フロー

1. ユーザーが肥料マスタ画面で `LLM調査を使う` を押す
2. 恵菜システムが `調査手順` と `プロンプト` を表示する
3. ユーザーは袋写真と通称を使って、外部サブスク LLM で調査する
4. LLM の回答を恵菜システムの貼り付け欄へ貼る
5. 恵菜システムが回答を項目ごとに分解する
6. フォームへ自動反映する
7. ユーザーが確認して保存する

### 21.4 画面イメージ

肥料マスタ画面に、通常の手入力フォームとは別に以下の UI を追加する。

- `LLM調査手順を見る`
- `調査用プロンプトをコピー`
- `LLM回答を貼り付け`
- `貼り付け結果を反映`
- `aliasとして保存`

表示イメージ:

- 左側: 通常の肥料マスタ入力フォーム
- 右側またはモーダル: LLM調査支援パネル

### 21.5 恵菜システムが提供する調査手順

画面上に表示する手順例:

1. 肥料袋の表面・裏面・保証票を撮影する
2. `調査用プロンプトをコピー` する
3. ChatGPT などの外部サブスク LLM に写真と一緒に貼り付ける
4. 返答結果をそのままコピーする
5. 恵菜システムの `LLM回答貼り付け欄` に貼り付ける
6. 項目を確認して肥料マスターへ保存する

補足:

- 写真は複数枚に分けてよい
- 読みづらい場合は表面・裏面・保証票を別々に投げる
- 推定内容はそのまま確定せず、人が確認する

### 21.6 肥料調査用プロンプト案

恵菜システムが表示・コピーできるプロンプトの例:

```text
この肥料について、写真や表示内容をもとに、肥料マスタ登録のための情報を整理してください。

必ず次のルールに従ってください。
- 袋や保証票に明記されている内容と、推定内容を混ぜない
- 読み取れない項目は「不明」と書く
- 推定した内容は必ず「推定事項」にまとめる
- 出力形式は崩さない

出力形式:
通称:
正式名称:
メーカー:
肥料区分:
登録番号:
正味重量kg:
窒素%:
りん酸%:
加里%:
その他成分:
別名候補:
根拠:
推定事項:
```

### 21.7 農薬調査用プロンプト案

同じ仕組みは農薬にも転用できる。

```text
この農薬について、写真や表示内容をもとに、農薬マスタ登録のための情報を整理してください。

必ず次のルールに従ってください。
- ラベルに明記されている内容と、推定内容を混ぜない
- 読み取れない項目は「不明」と書く
- 推定した内容は必ず「推定事項」にまとめる
- 出力形式は崩さない

出力形式:
通称:
正式名称:
メーカー:
農薬登録番号:
剤型:
内容量:
有効成分:
使用単位:
別名候補:
根拠:
推定事項:
```

### 21.8 貼り付け結果の取り込み方式

恵菜システム側では、LLM の回答を `項目名: 値` 形式で受け取り、
シンプルなルールベースで分解する。

例:

```text
通称: 仁井田米有機
正式名称: 複合肥料 DH85号
メーカー: 大東肥料株式会社
肥料区分: 指定配合肥料
登録番号: 不明
正味重量kg: 20
窒素%: 8.0
りん酸%: 5.0
加里%: 3.0
その他成分: 内水溶性加里 2.2
別名候補: 仁井田米有機
根拠: 袋の保証票に「複合肥料 DH85号」「大東肥料株式会社」「20キログラム」と記載あり
推定事項: 登録番号は写真から確認できない
```

取り込み後は、以下のようにフォームへ流し込む。

- `通称` → `Fertilizer.name`
- `正式名称` → `Fertilizer.official_name`
- `メーカー` → `Fertilizer.maker`
- `正味重量kg` → `Fertilizer.capacity_kg`
- `窒素%` → `Fertilizer.nitrogen_pct`
- `りん酸%` → `Fertilizer.phosphorus_pct`
- `加里%` → `Fertilizer.potassium_pct`
- `別名候補` → `MaterialAlias.alias_name`
- `根拠` と `推定事項` → `notes` または確認欄

### 21.9 この方式のメリット

- API 費用がかからない
- ユーザーが契約済みのサブスク LLM を使える
- 画像ベースの調査ができる
- 政府公式データに載っていない資材も扱える
- 恵菜システム側の実装が軽い

### 21.10 注意点

- LLM の回答をそのまま自動確定しない
- 保存前に必ず人が確認する
- `根拠` と `推定事項` を分けて表示する
- 重要な項目が不明なら、保存前に警告を出す

### 21.11 本案件における位置づけ

今回の現場肥料の実態を見ると、主軸として最も現実的なのは以下である。

- 主軸: `外部サブスクLLMを使った調査支援`
- 補助: `公式データ同期`
- 補助: `alias辞書`

この構成であれば、コストを抑えつつ、現場名・ブランド名・保証票名の差異にも対応しやすい。

---

## 22. レビュー記録（2026-03-14）

既存コードとの整合性レビューを実施し、以下の修正を反映した。

### 22.1 StockTransaction フィールドの簡素化

以下のフィールドを初期実装から除外した。

| 除外フィールド | 理由 |
|----------------|------|
| `unit` | `Material.stock_unit` から取得すれば十分。トランザクションごとに単位を持つと不整合の元 |
| `reference_type` / `reference_id` | Generic FK パターンは複雑化の元。将来の施肥計画連携時に必要なら追加 |
| `created_by` | シングルユーザーシステムのため不要。マルチユーザー対応時に追加 |
| `inventory_count` | `adjustment_plus` / `adjustment_minus` で棚卸差異を十分表現可能 |

### 22.2 フロントエンド画面構成の変更

- 入出庫登録を独立ページ (`/materials/stock/new`) → モーダル方式に変更
- 肥料マスタ・農薬マスタを別ページ → 1画面タブ切り替え (`/materials/masters`) に統合
- 理由: 既存の `/fertilizer/masters` のインライン編集パターンに合わせ、画面遷移を減らす

### 22.3 実装フェーズの整理

- セクション1〜16: Phase 1（初期実装）→ 実装済み
- セクション17〜21: Phase 3（公式データ同期、alias辞書、LLM調査支援）
- セクション23: Phase 1.5（施肥計画連携・引当機能）→ 次の実装対象

---

## 23. 施肥計画連携: 引当(reserve)・散布確定フロー

### 23.1 背景と要件

施肥計画を立てるとき、現在の利用可能在庫を見ながら計画を立てたい。
しかし「計画確定 = 即在庫消費」にすると、実際の散布量が計画と異なった場合に
差異調整が毎回必要になる。

そこで「引当 → 散布確定」の2段階方式を採用する。

要件:

- 施肥計画確定時に、計画数量を「引当」として在庫から仮押さえする
- 実際に散布した後、実績数量で「使用確定」に変換する
- 差分は自動で在庫に戻す
- 計画削除・変更時は引当を自動解除する
- 施肥計画画面で「利用可能在庫」を表示し、計画策定の判断材料にする

### 23.2 在庫の3つの状態

```
総在庫 = 入庫 + 調整増 - 使用確定 - 調整減 - 廃棄 - 引当中
利用可能在庫 = 総在庫のうち引当を除いた分
             = 入庫 + 調整増 - 使用確定 - 調整減 - 廃棄
引当中 = reserve トランザクションの合計（未確定の施肥計画分）
```

施肥計画画面では「利用可能在庫」を表示する。
在庫一覧画面では「総在庫（引当中 X を含む）」のように表示する。

### 23.3 StockTransaction への変更

#### transaction_type に `reserve` を追加

```python
class TransactionType(models.TextChoices):
    PURCHASE = 'purchase', '入庫'
    USE = 'use', '使用'                        # 散布確定済み
    RESERVE = 'reserve', '引当'                 # ← 新規追加
    ADJUSTMENT_PLUS = 'adjustment_plus', '棚卸増'
    ADJUSTMENT_MINUS = 'adjustment_minus', '棚卸減'
    DISCARD = 'discard', '廃棄'
```

`reserve` は `use` と同じく在庫を減少させる方向で集計する。

#### fertilization_plan FK を追加

```python
fertilization_plan = models.ForeignKey(
    'fertilizer.FertilizationPlan',
    on_delete=models.SET_NULL,
    null=True,
    blank=True,
    related_name='stock_reservations',
    verbose_name='施肥計画',
)
```

当初除外した `reference_type` / `reference_id`（Generic FK）ではなく、
施肥計画への明示的な FK を採用する。理由:

- 現時点で在庫連携が必要なのは施肥計画のみ
- Generic FK は Django の管理上も ORM 上も扱いにくい
- 将来、農薬散布計画が加わった場合は `pesticide_plan` FK を追加すればよい

#### INCREASE_TYPES / DECREASE_TYPES の更新

```python
INCREASE_TYPES = {
    TransactionType.PURCHASE,
    TransactionType.ADJUSTMENT_PLUS,
}
DECREASE_TYPES = {
    TransactionType.USE,
    TransactionType.RESERVE,        # ← 追加
    TransactionType.ADJUSTMENT_MINUS,
    TransactionType.DISCARD,
}
```

### 23.4 操作フロー

#### フロー1: 施肥計画の確定（引当作成）

```
ユーザー: 施肥計画を保存
システム: 計画内の各 FertilizationEntry について
          → 対応する Fertilizer.material を特定
          → StockTransaction(type='reserve', material=...,
             quantity=bags, fertilization_plan=plan) を作成
          → 利用可能在庫が減少
```

注意: `Fertilizer.material` が null の場合（未連携の古いデータ）は引当を作成しない。

#### フロー2: 散布確定（引当 → 使用変換）

```
ユーザー: 施肥計画画面で「散布確定」ボタンを押す
システム: 確定前の調整画面を表示

  ┌─────────────┬──────┬──────┬────────┐
  │ 圃場         │ 計画  │ 実績  │ 状態    │
  ├─────────────┼──────┼──────┼────────┤
  │ 上の田       │ 3袋   │ 3    │ ✓      │
  │ 下の田       │ 4袋   │ 3.5  │ 修正    │
  │ 山の畑       │ 2袋   │ 0    │ 未散布  │
  └─────────────┴──────┴──────┴────────┘

ユーザー: 各行の実績を確認・修正し、「一括確定」を押す
システム:
  - 実績 > 0 の行: reserve を削除 → use(quantity=実績) を作成
  - 実績 = 0 の行: reserve を削除（引当解除、在庫に戻る）
  - 差分は自動計算（ユーザーの追加操作不要）
```

普段は計画値がそのまま実績にプリセットされるので、何も修正せず「一括確定」するだけ。
アクシデントがあった圃場だけ数量を修正する。

#### フロー3: 計画の削除

```
ユーザー: 施肥計画を削除
システム: その計画に紐づく全 reserve トランザクションを削除
          → 引当が解除され、在庫に戻る
```

#### フロー4: 計画の変更（エントリ追加・削除・数量変更）

```
ユーザー: 施肥計画を編集・保存
システム: その計画に紐づく既存 reserve を全削除
          → 新しいエントリに基づいて reserve を再作成
```

注意: reserve の差分更新は複雑になるため、「全削除 → 再作成」方式を推奨する。

### 23.5 施肥計画画面の在庫表示

施肥計画の編集画面（マトリクス表）で、肥料ごとの在庫情報を表示する。

表示位置: 肥料列ヘッダーの下、または肥料名の横

表示例:

```
仁井田米有機
在庫 18袋 / 計画計 24袋 / 不足 6袋
```

API:

- 既存の `GET /api/materials/stock-summary/` を流用
- または施肥計画用の専用エンドポイントで肥料在庫のみ返す

### 23.6 在庫一覧画面の表示変更

在庫一覧（`/materials`）では、引当中の数量も表示する。

表示例:

```
仁井田米有機: 在庫 18袋（うち引当 12袋）/ 利用可能 6袋
```

StockSummary API のレスポンスに以下を追加:

```json
{
  "material_id": 3,
  "name": "仁井田米有機",
  "current_stock": "18.000",
  "reserved_stock": "12.000",
  "available_stock": "6.000",
  "last_transaction_date": "2026-03-14"
}
```

### 23.7 施肥計画の状態管理

施肥計画に散布確定状態を持たせる。

FertilizationPlan に以下のフィールドを追加:

```python
is_confirmed = models.BooleanField(default=False, verbose_name='散布確定済み')
confirmed_at = models.DateTimeField(null=True, blank=True, verbose_name='散布確定日時')
```

状態遷移:

- `is_confirmed=False`: 計画中（編集可能、引当あり）
- `is_confirmed=True`: 散布確定済み（引当 → 使用に変換済み）

確定済みの計画は:

- 編集不可（またはバナーで注意喚起して編集許可）
- 再確定はしない
- 削除は可能（use トランザクションはそのまま残す）

### 23.8 API 変更・追加

#### 既存 API への影響

| API | 変更内容 |
|-----|----------|
| `POST/PUT /api/fertilizer/plans/` | 保存時に reserve 自動作成（全削除→再作成） |
| `DELETE /api/fertilizer/plans/{id}/` | 削除時に reserve 自動削除 |

#### 新規 API

| メソッド | パス | 説明 |
|----------|------|------|
| `POST` | `/api/fertilizer/plans/{id}/confirm_spreading/` | 散布確定（reserve→use変換） |
| `GET` | `/api/materials/fertilizer-stock/` | 肥料在庫一覧（施肥計画画面用、利用可能在庫含む） |

散布確定 API のリクエスト例:

```json
{
  "entries": [
    {"field_id": 1, "fertilizer_id": 3, "actual_bags": 3.0},
    {"field_id": 2, "fertilizer_id": 3, "actual_bags": 3.5},
    {"field_id": 3, "fertilizer_id": 3, "actual_bags": 0}
  ]
}
```

`actual_bags=0` の行は引当解除として処理する。

### 23.9 マイグレーション計画

#### materials 0002: reserve タイプ追加 + fertilization_plan FK

```python
# StockTransaction に以下を追加:
# - transaction_type の choices に 'reserve' を追加（choices 変更はマイグレーション不要）
# - fertilization_plan FK を追加
migrations.AddField(
    model_name='stocktransaction',
    name='fertilization_plan',
    field=models.ForeignKey(
        blank=True,
        null=True,
        on_delete=django.db.models.deletion.SET_NULL,
        related_name='stock_reservations',
        to='fertilizer.fertilizationplan',
        verbose_name='施肥計画',
    ),
)
```

#### fertilizer 0006: is_confirmed, confirmed_at 追加

```python
migrations.AddField(
    model_name='fertilizationplan',
    name='is_confirmed',
    field=models.BooleanField(default=False, verbose_name='散布確定済み'),
),
migrations.AddField(
    model_name='fertilizationplan',
    name='confirmed_at',
    field=models.DateTimeField(blank=True, null=True, verbose_name='散布確定日時'),
),
```

### 23.10 フロントエンド変更

#### 施肥計画一覧 (`/fertilizer`)

- 各計画に確定状態アイコンを追加（未確定 / 確定済み）
- 確定済みの計画は背景色で視覚的に区別

#### 施肥計画編集 (`/fertilizer/[id]/edit`)

- 肥料列ヘッダーに在庫情報を表示
- 確定済みの場合は編集不可バナーを表示

#### 散布確定画面（新規）

- 施肥計画一覧から「散布確定」ボタンで遷移 or モーダル
- 施肥計画編集画面と同じく「圃場 = 行」「肥料 = 列」のマトリクス表で表示
- 各セルは薄いグレーの計画値 + 実績入力欄の2段表示にし、編集時の視線移動を揃える
- 行末に圃場ごとの実績合計、フッターに肥料別合計と総合計を表示する
- 「一括確定」ボタンで POST

#### 在庫一覧 (`/materials`)

- `reserved_stock` と `available_stock` を表示に追加

### 23.11 実装順序

1. StockTransaction に `reserve` タイプ追加 + `fertilization_plan` FK（マイグレーション）
2. FertilizationPlan に `is_confirmed` / `confirmed_at` 追加（マイグレーション）
3. StockSummary API に `reserved_stock` / `available_stock` 追加
4. 施肥計画の保存処理に reserve 自動作成ロジック追加
5. 施肥計画の削除処理に reserve 自動削除ロジック追加
6. 散布確定 API（`confirm_spreading`）実装
7. 肥料在庫 API（`fertilizer-stock`）実装
8. フロントエンド: 在庫一覧に引当表示追加
9. フロントエンド: 施肥計画編集に在庫参照追加
10. フロントエンド: 散布確定画面実装
11. フロントエンド: 施肥計画一覧に確定状態表示追加

### 23.12 この設計のメリット

- 計画作成時に利用可能在庫を見ながら計画を立てられる
- 普段は一括確定するだけなので操作は軽い
- アクシデント時は個別に実績数量を修正できる
- 計画の削除・変更で引当が自動解除されるため、在庫が宙に浮かない
- 在庫一覧で「引当中」が見えるため、在庫状況が透明
