Files
keinasystem/改善案/在庫管理機能実装案.md
Akira f1512febde 在庫管理機能実装案.md に、電気炉さい ミネラルホウ素 リン酸グアノ も追記しました。
追加した内容は、各資材についての袋表示・保証票ベースの情報、実装上の扱い、alias の例です。あわせて最後のまとめも更新して、5件を次の3パターンに整理しました。

現場名と保証票名が違う: 仁井田米有機, 土佐勤農党, 電気炉さい
現場名と表示名がほぼ一致する: リン酸グアノ
公式マスタよりローカル資材管理向き: ミネラルホウ素
今回は文書更新のみで、コード変更やテストはしていません。必要なら次に、この5件を前提にした MaterialAlias の Django モデル案まで具体化します。
2026-03-13 13:50:24 +09:00

1367 lines
39 KiB
Markdown

# 在庫管理機能実装案
> 作成日: 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`
- `unit`
- `occurred_on`
- `reference_type`
- `reference_id`
- `note`
- `created_by`
- `created_at`
`transaction_type` の候補:
- `purchase`
- `use`
- `adjustment_plus`
- `adjustment_minus`
- `inventory_count`
- `discard`
`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', '棚卸減'
INVENTORY_COUNT = 'inventory_count', '棚卸記録'
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, null=True)
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
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/fertilizers`
- 肥料マスタ管理
- `/materials/pesticides`
- 農薬マスタ管理
- `/materials/stock`
- 在庫一覧
- `/materials/stock/new`
- 入出庫登録
### 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つを組み合わせることで、正式名称ベースの整合性と、
現場名称ベースの使いやすさを両立できる。