Files
keinasystem/改善案/在庫管理機能実装案.md
Akira 776a269d6d 在庫管理機能実装案.md に ## 18. 補足: 肥料公式データ同期・あいまい検索案 を追記しました。
内容は、普通肥料を主対象に公式データをローカルDB化し、検索時はローカル検索、24時間超過時だけ裏で差分同期する という方針です。FertilizerOfficialMaster のテーブル案、既存 Fertilizer との紐づけ、検索API、同期ジョブ、特殊肥料は手入力併用にする考え方まで入れています。

今回は文書更新のみで、コード変更やテスト実行はしていません。必要なら次は、この18章をもとに models.py レベルの実装草案まで起こせます。
2026-03-13 13:26:18 +09:00

30 KiB

在庫管理機能実装案

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


1. 結論

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

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

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


2. 背景

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

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

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

  • capacity_kg, nitrogen_pct, phosphorus_pct, potassium_pct を持つ
  • 施肥計画 FertilizationEntryFertilizer を直接参照している
  • 自動計算 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 構成に合わせて調整する。

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 モデルをすぐに削除したり全面改名したりしない。

理由:

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

6.2 移行方針

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

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

推奨度: 高

構成:

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

メリット:

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

デメリット:

  • 一時的に MaterialFertilizer の二重管理に見える

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

推奨度: 中

構成:

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

メリット:

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

デメリット:

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

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


7. 推奨する app 構成

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

候補:

  • backend/apps/materials
  • backend/apps/inventory

推奨:

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

理由:

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

推奨ファイル構成:

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}/

レスポンス例:

{
  "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 だけ返す
  • 必要に応じて現在庫も返す

レスポンス例:

[
  {
    "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 を追加
  • MaterialStockTransaction を作成
  • 既存 Fertilizer 1件ごとに Material(material_type='fertilizer') を作成
  • Fertilizer.material を紐づける migration を追加

フェーズ2

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

フェーズ3

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

データ移行時の注意

  • Fertilizer.nameMaterial.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 単一テーブル案

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

採用しない理由:

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

13.2 完全分離案

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

採用しない理由:

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

14. 推奨実装順

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

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

15. 最終提案

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

  • 在庫管理の主軸は MaterialStockTransaction
  • 肥料と農薬は詳細テーブルで分ける
  • 施肥計画は既存 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=

レスポンス例:

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

レスポンス例:

{
  "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 パターンで実装できる
  • 将来の購入管理や必要量算出にもつなげやすい