# マスタードキュメント:農薬散布管理機能 > **作成**: 2026-04-09 > **最終更新**: 2026-04-09 > **対象機能**: 農薬散布管理(農薬マスタ・散布記録・使用回数チェック・特別栽培向け成分数集計) > **実装状況**: 未着手(仕様確定済み) > **Gitea Issue**: akira/keinasystem#18 --- ## 概要 農業生産者が散布した農薬を記録・管理し、農薬取締法に基づく使用基準(製品ごと・有効成分ごとの使用回数制限)への適合確認と、特別栽培認証用の成分数集計を行う機能。 ### 機能スコープ(IN / OUT) | IN(実装対象) | OUT(対象外) | |---|---| | 農薬マスタ管理(CRUD) | 農薬の在庫管理・購入管理 | | 農林水産省サイトからの農薬情報自動取得 | 農薬費用の管理 | | 散布イベント記録(圃場/グループ/作物/品種対象) | 希釈液の量管理 | | 製品ごとの使用回数チェック(年度×作物) | 農薬の廃棄記録 | | 有効成分ごとの総使用回数チェック(年度×作物) | 農薬散布マップ(GIS) | | 特別栽培用:節減対象農薬の使用成分数集計 | 農薬の処方箋・防除暦の自動作成 | | 回数超過アラート表示 | | --- ## 使用回数カウントのルール 農薬の使用回数は **製品単位** と **有効成分単位** の2軸で管理する。 ### ルール1:製品ごとの使用回数 農薬製品(例: 住化スミチオン乳剤)を1シーズンに使用した回数 ≤ 登録情報の「本剤の使用回数」上限。 ### ルール2:有効成分ごとの総使用回数 同一有効成分を含む複数製品を使用した場合、その有効成分の総使用回数として合算カウントする。 ``` 例)「MEP乳剤A(上限3回)」と「MEP乳剤B(上限3回)」、MEP成分の総上限3回 → A剤2回 + B剤1回 = 合計3回 → OK → A剤2回 + B剤2回 = 合計4回 → 超過! ``` ### ルール3:使用時期別カウント 育苗期・本圃期など時期別に別カウントになる場合がある(登録情報のテキストとして記録)。 システムでは現フェーズで時期別の自動判定は行わず、登録情報テキストを参照情報として表示する。 ### カウント対象外農薬(節減対象外) 以下の農薬は使用回数・成分数のカウントから除外する(`is_non_target` フラグで管理): - 展着剤(`is_spreader` フラグでも管理) - 有機JAS別表2に掲げる農薬(除虫菊乳剤・硫黄剤・天敵生物農薬・性フェロモン剤等) - 化学合成でないと認められた農薬(カスガマイシン剤・ポリオキシン剤・バリダマイシン剤等) ### 特別栽培向け成分数集計 「節減対象農薬(`is_non_target=False`)の有効成分(`is_active=True`)が何種類使われたか」を年度×作物単位でカウントする。 上限はなく、報告用の集計値として表示する。 --- ## データモデル ### Pesticide(農薬マスタ) **アプリ**: `apps/pesticide` **テーブル名**: `pesticide_pesticide` | フィールド | 型 | 制約 | 説明 | |---|---|---|---| | id | BigAutoField | PK | | | name | CharField(200) | required | 農薬名(例: 住化スミチオン乳剤) | | pesticide_type | CharField(100) | blank | 農薬の種類(例: MEP乳剤) | | registration_number | CharField(20) | blank | 農薬登録番号(公式登録番号) | | system_id | CharField(20) | blank | 農水省サイトの内部ID(詳細URLに使用) | | purpose | CharField(100) | blank | 用途(例: 殺虫剤) | | formulation | CharField(100) | blank | 剤型(例: 乳剤) | | toxicity | CharField(20) | blank | 製剤毒性(普/毒/劇等) | | is_spreader | BooleanField | default=False | 展着剤フラグ | | is_non_target | BooleanField | default=False | 節減対象外フラグ(カウント除外) | | notes | TextField | blank | 備考 | | fetched_at | DateTimeField | null=True | 農水省サイトからの最終取得日時 | | created_at | DateTimeField | auto | | | updated_at | DateTimeField | auto | | - `name` は unique 制約なし(同名で複数登録番号が存在しうる) - `is_spreader=True` の場合、`is_non_target` も自動的に `True` 扱いとする ### PesticideIngredient(有効成分) **テーブル名**: `pesticide_pesticideingredient` | フィールド | 型 | 制約 | 説明 | |---|---|---|---| | id | BigAutoField | PK | | | pesticide | FK(Pesticide) | CASCADE | | | name | CharField(200) | required | 成分名称(例: MEP) | | concentration | CharField(100) | blank | 含有濃度(例: 50.0%) | | is_active | BooleanField | default=True | 有効成分かどうか(False = その他成分) | - `unique_together = ['pesticide', 'name']` ### PesticideIngredientLimit(有効成分の総使用回数上限:作物別) **テーブル名**: `pesticide_pesticideingredientlimit` 農水省の「○○を含む農薬の総使用回数」は作物ごとに異なりうるため、有効成分本体とは分離して作物別に保持する。 | フィールド | 型 | 制約 | 説明 | |---|---|---|---| | id | BigAutoField | PK | | | pesticide | FK(Pesticide) | CASCADE | 取得元農薬 | | ingredient_name | CharField(200) | required | 成分名称(例: MEP) | | crop_name | CharField(200) | required | 作物名(農水省登録情報の表記、例: 稲) | | max_total_uses | IntegerField | null=True | この成分を含む農薬の総使用回数上限 | | use_timing_note | TextField | blank | 使用時期別制限のテキスト(例: 種もみへの処理は1回以内、…) | - `unique_together = ['pesticide', 'ingredient_name', 'crop_name']` ### PesticideProductLimit(製品の使用回数上限:作物別) **テーブル名**: `pesticide_pesticideproductlimit` 農水省の適用表は作物ごとに上限が異なるため、作物名をキーとして保存する。 | フィールド | 型 | 制約 | 説明 | |---|---|---|---| | id | BigAutoField | PK | | | pesticide | FK(Pesticide) | CASCADE | | | crop_name | CharField(200) | required | 作物名(農水省登録情報の表記、例: 稲) | | max_uses | IntegerField | required | 本剤の使用回数上限 | | use_timing_note | TextField | blank | 使用時期・条件の補足テキスト | - `unique_together = ['pesticide', 'crop_name']` ### PesticideCropAlias(農水省作物名と内部作物の対応) **テーブル名**: `pesticide_pesticidecropalias` 農水省の適用表上の作物名と、内部 `plans.Crop` の作物を対応付けるための正規化テーブル。 | フィールド | 型 | 制約 | 説明 | |---|---|---|---| | id | BigAutoField | PK | | | crop | FK(plans.Crop) | PROTECT | 内部作物 | | alias_name | CharField(200) | required, unique | 農水省登録情報の作物名(例: 稲, 水稲) | | is_primary | BooleanField | default=False | 代表表記かどうか | - 使用回数チェック時は `crop_id` から本テーブルを逆引きし、`PesticideProductLimit.crop_name` / `PesticideIngredientLimit.crop_name` と照合する - 初期データ例: `Crop=水稲` に対し `alias_name=稲`, `alias_name=水稲` を登録 ### SprayEvent(散布イベント) **テーブル名**: `pesticide_sprayevent` 1回の散布作業を1件として記録する。 | フィールド | 型 | 制約 | 説明 | |---|---|---|---| | id | BigAutoField | PK | | | year | IntegerField | required | 年度(集計フィルタ用) | | date | DateField | required | 散布日 | | target_type | CharField(20) | required | 対象種別: `field` / `group` / `crop` / `variety` | | target_field | FK(fields.Field) | null=True, PROTECT | 対象が圃場の場合 | | target_group | CharField(50) | blank | 対象が圃場グループの場合(group_name) | | target_crop | FK(plans.Crop) | null=True, PROTECT | 対象が作物の場合 | | target_variety | FK(plans.Variety) | null=True, PROTECT | 対象が品種の場合 | | notes | TextField | blank | 備考 | | created_at | DateTimeField | auto | | | updated_at | DateTimeField | auto | | #### target_type 別のバリデーション | target_type | 必須フィールド | 意味 | |---|---|---| | `field` | target_field | 特定の圃場1筆に散布 | | `group` | target_group | 同一 group_name の全圃場に散布 | | `crop` | target_crop | 特定の作物に対して散布(作付け計画と照合) | | `variety` | target_variety | 特定の品種に対して散布(作付け計画と照合) | - 保存時に全対象圃場を `SprayEventResolvedField` として確定保存し、後日の作付け変更やグループ名変更があっても過去実績の集計結果が変わらないようにする ### SprayEventResolvedField(散布イベント対象圃場スナップショット) **テーブル名**: `pesticide_sprayeventresolvedfield` `target_type=group` / `crop` / `variety` のように複数圃場へ展開される散布について、保存時点で対象圃場を確定保存する。 | フィールド | 型 | 制約 | 説明 | |---|---|---|---| | id | BigAutoField | PK | | | event | FK(SprayEvent) | CASCADE | | | field | FK(fields.Field) | PROTECT | 対象圃場 | | field_name_snapshot | CharField(100) | required | 保存時点の圃場名 | | group_name_snapshot | CharField(50) | blank | 保存時点のグループ名 | | crop_name_snapshot | CharField(100) | required | 保存時点の作物名 | | variety_name_snapshot | CharField(100) | blank | 保存時点の品種名 | - `unique_together = ['event', 'field']` - `target_type=field` の場合も 1 行作成しておくと、集計ロジックを統一しやすい ### SprayEventPesticide(散布農薬明細) **テーブル名**: `pesticide_sprayeventpesticide` 1つの散布イベントに複数農薬を紐づける。 | フィールド | 型 | 制約 | 説明 | |---|---|---|---| | id | BigAutoField | PK | | | event | FK(SprayEvent) | CASCADE | | | pesticide | FK(Pesticide) | PROTECT | 使用農薬 | | dilution_ratio | CharField(50) | blank | 希釈倍率(例: 1000倍) | | amount_used | CharField(50) | blank | 使用量(例: 500mL、単位込みで自由記述) | | notes | TextField | blank | 備考 | - `pesticide` は PROTECT(使用済み農薬は削除不可) - `unique_together = ['event', 'pesticide']`(同一イベント内で同じ農薬を2回登録不可) --- ## 使用回数集計の仕組み ### 集計単位 **年度 × 作物** を基本単位とする(農薬取締法上、使用回数は作物単位で管理する義務がある)。 - 集計対象作物は `SprayEventResolvedField.crop_name_snapshot` を正とする(圃場ごとに記録) - `target_type=field`/`group`/`crop`/`variety` の違いにかかわらず、保存時に全対象圃場の `SprayEventResolvedField` を作成し、各圃場の作物をスナップショットとして保持する - **グループ内に複数作物が混在する場合**、同一の散布イベント・散布農薬でも作物ごとに使用回数がカウントされる。例:グループ内に「水稲」3筆・「大豆」1筆が含まれる場合、そのイベントの農薬は水稲の回数にも大豆の回数にも +1 される - 使用回数上限の照合は、`SprayEventResolvedField.crop_name_snapshot` → `PesticideCropAlias` → `PesticideProductLimit` / `PesticideIngredientLimit` の順に行う ### 製品使用回数の集計 1イベント = 1散布作業 = 1回。`unique_together=['event', 'pesticide']` により同一イベント内で同一農薬は1行しか存在しないため、イベント単位でカウントして正確。 ``` 製品使用回数(年度Y・作物C・農薬P)= COUNT(DISTINCT SprayEvent.id) where SprayEvent に SprayEventPesticide(pesticide=P) が紐づく かつ SprayEvent に SprayEventResolvedField(crop_name_snapshot=C) が紐づく かつ SprayEvent.year = Y ``` ※ 1イベントで複数圃場に散布しても「1回」とカウントする(1イベント=1散布作業) ### 有効成分総使用回数の集計 有効成分の総使用回数は「製品」単位でカウントする(農薬取締法の「○○を含む農薬の総使用回数」の定義に従う)。1イベントでMEP剤AとMEP剤Bを同時使用した場合、MEP成分は **2回** カウントされる。 ``` 有効成分総使用回数(年度Y・作物C・成分名I)= COUNT(SprayEventPesticide) where SprayEventPesticide.pesticide の PesticideIngredient に name=I かつ is_active=True のものが存在する かつ SprayEventPesticide.pesticide.is_non_target=False かつ SprayEventPesticide.event に SprayEventResolvedField(crop_name_snapshot=C) が紐づく かつ SprayEventPesticide.event.year = Y ``` ※ `unique_together=['event', 'pesticide']` により、同一イベント内で同一農薬は重複しないため単純な COUNT で正確に合算できる ### 特別栽培・使用成分数の集計 ``` 使用成分数(年度Y・作物C)= COUNT(DISTINCT PesticideIngredient.name) where 上記条件(年度Y・作物C)の散布イベントで使用された農薬に含まれる かつ PesticideIngredient.is_active=True かつ SprayEventPesticide.pesticide.is_non_target=False ``` --- ## API エンドポイント すべて JWT 認証(`Authorization: Bearer `)が必要。 ### 農薬マスタ | メソッド | URL | 説明 | |---|---|---| | GET | `/api/pesticide/pesticides/` | 一覧取得 | | POST | `/api/pesticide/pesticides/` | 新規作成 | | GET | `/api/pesticide/pesticides/{id}/` | 詳細取得 | | PUT/PATCH | `/api/pesticide/pesticides/{id}/` | 更新 | | DELETE | `/api/pesticide/pesticides/{id}/` | 削除(使用中は 400) | | POST | `/api/pesticide/pesticides/fetch/` | 農水省サイトから情報取得 | 農薬マスタ レスポンス例: ```json { "id": 1, "name": "住化スミチオン乳剤", "pesticide_type": "MEP乳剤", "registration_number": "4962", "system_id": "4962", "purpose": "殺虫剤", "formulation": "乳剤", "toxicity": "普", "is_spreader": false, "is_non_target": false, "notes": "", "fetched_at": "2026-04-09T10:00:00Z", "ingredients": [ { "id": 1, "name": "MEP", "concentration": "50.0%", "is_active": true } ], "product_limits": [ { "id": 1, "crop_name": "稲", "max_uses": 2, "use_timing_note": "収穫21日前まで" } ], "ingredient_limits": [ { "id": 1, "ingredient_name": "MEP", "crop_name": "稲", "max_total_uses": 3, "use_timing_note": "種もみへの処理は1回以内、育苗箱散布は1回以内、本田では2回以内" } ], "crop_aliases": [ { "crop": 1, "crop_name": "水稲", "alias_name": "稲", "is_primary": true } ] } ``` #### `POST /api/pesticide/pesticides/fetch/` 農水省農薬登録情報提供システムから農薬情報を取得してマスタに保存する。 取得に失敗した場合は `fetch_error` を返し、手動入力に切り替える。 リクエスト: ```json { "name": "スミチオン" } ``` レスポンス(成功): ```json { "status": "ok", "candidates": [ { "system_id": "4962", "name": "住化スミチオン乳剤", "pesticide_type": "MEP乳剤", "registration_number": "4962" }, { "system_id": "4991", "name": "ホクコースミチオン乳剤", "pesticide_type": "MEP乳剤", "registration_number": "4991" } ] } ``` 候補が複数ある場合はフロントで選択させ、選択後に詳細取得リクエストを投げる: ```json { "system_id": "4962" } ``` レスポンス(失敗): ```json { "status": "error", "message": "農林水産省サイトへの接続に失敗しました。手動で入力してください。" } ``` ### 散布イベント | メソッド | URL | 説明 | |---|---|---| | GET | `/api/pesticide/events/?year={year}` | 年度別一覧 | | POST | `/api/pesticide/events/` | 新規作成 | | GET | `/api/pesticide/events/{id}/` | 詳細取得 | | PUT/PATCH | `/api/pesticide/events/{id}/` | 更新 | | DELETE | `/api/pesticide/events/{id}/` | 削除 | 散布イベント POST リクエスト例(圃場グループを対象に複数農薬散布): ```json { "year": 2026, "date": "2026-05-10", "target_type": "group", "target_group": "田中エリア", "notes": "曇り、風弱し", "pesticides": [ { "pesticide": 1, "dilution_ratio": "1000倍", "amount_used": "500mL" }, { "pesticide": 3, "dilution_ratio": "2000倍", "amount_used": "200mL" } ] } ``` 散布イベント レスポンス例: ```json { "id": 10, "year": 2026, "date": "2026-05-10", "target_type": "group", "target_group": "田中エリア", "target_display": "田中エリア(グループ)", "resolved_fields": [ { "field": 5, "field_name_snapshot": "田中上", "group_name_snapshot": "田中エリア", "crop_name_snapshot": "水稲", "variety_name_snapshot": "コシヒカリ" } ], "notes": "曇り、風弱し", "pesticides": [ { "id": 15, "pesticide": 1, "pesticide_name": "住化スミチオン乳剤", "dilution_ratio": "1000倍", "amount_used": "500mL" } ], "created_at": "2026-04-09T10:00:00Z", "updated_at": "2026-04-09T10:00:00Z" } ``` ### 使用回数チェック #### `GET /api/pesticide/usage-summary/?year={year}&crop_id={crop_id}` 年度×作物単位で使用回数の集計・チェック結果を返す。 レスポンス例: ```json { "year": 2026, "crop_id": 1, "crop_name": "水稲", "crop_aliases": ["稲", "水稲"], "product_usage": [ { "pesticide_id": 1, "pesticide_name": "住化スミチオン乳剤", "used_count": 2, "max_uses": 2, "remaining": 0, "is_over": false } ], "ingredient_usage": [ { "ingredient_name": "MEP", "used_count": 2, "max_total_uses": 3, "remaining": 1, "is_over": false, "products_used": ["住化スミチオン乳剤"] } ], "component_count": 2, "has_violation": false } ``` --- ## 農水省サイトスクレイピング仕様 ### 対象サイト 農林水産省 農薬登録情報提供システム URL: `https://pesticide.maff.go.jp/` ### アクセスフロー ``` 1. GET /agricultural-chemicals/name-search/ → JSESSIONID クッキー + CSRF トークン(フォーム埋め込み)取得 2. POST /agricultural-chemicals/name-search Content-Type: application/x-www-form-urlencoded Body: _csrf=&agriculturalChemicalsName=<農薬名>&agriculturalChemicalsType= → 302 リダイレクト先: /agricultural-chemicals/list 3. GET /agricultural-chemicals/list → 検索結果一覧 HTML → からリンク抽出 4. GET /agricultural-chemicals/details/{system_id} → 詳細ページ HTML → 下記データをパース ``` ### 詳細ページ パース項目 **基本情報テーブル(`th[scope=col]` + `td` ペア):** | th テキスト | 取得項目 | 保存先 | |---|---|---| | 登録番号 | 登録番号 | `registration_number` | | 農薬の種類 | 種類名 | `pesticide_type` | | 農薬の名称 | 農薬名 | `name` | | 用途 | 用途 | `purpose` | | 剤型 | 剤型 | `formulation` | | 製剤毒性 | 毒性区分 | `toxicity` | **有効成分テーブル:** - 「有効成分」行: `is_active=True`、成分名・含有濃度を取得 - 「その他成分」行: `is_active=False` **適用表(作物×病害虫ごとの行):** 各行のカラム(`data-label` 属性でカラム識別): | data-label | 取得項目 | 保存先 | |---|---|---| | 作物名 | 作物名 | `PesticideProductLimit.crop_name` | | 本剤の使用回数 | 「N回以内」から N を抽出 | `PesticideProductLimit.max_uses` | | 使用時期 | テキストそのまま | `PesticideProductLimit.use_timing_note` | | `{成分名}を含む農薬の総使用回数` | 「N回以内(...)」から N と補足を抽出 | `PesticideIngredientLimit.max_total_uses` / `use_timing_note` | **「総使用回数」テキストのパース規則:** ``` 入力例: "3回以内(種もみへの処理は1回以内、育苗箱散布は1回以内、本田では2回以内)" → max_total_uses = 3 → use_timing_note = "種もみへの処理は1回以内、育苗箱散布は1回以内、本田では2回以内" 正規表現: r'(\d+)回以内(?:\((.+)\))?' ``` ### 実装場所 `apps/pesticide/management/commands/fetch_pesticide.py` Django management command として実装。APIエンドポイントから呼び出す。 ### 注意事項 - セッション(`requests.Session`)を使用し、クッキーとCSRFを維持する - アクセスは農薬マスタ登録時の1件ずつに限定(バルク取得は行わない) - 農水省サイトの内部ID(`system_id`)と農薬の公式登録番号は別物 - タイムアウト: 10秒 - 適用表の作物名は `PesticideCropAlias` で内部 `Crop` と対応付ける前提で保存する --- ## 画面仕様 ### 農薬マスタ画面(`/pesticide/`) - 登録済み農薬の一覧表示 - 農薬名で検索 → 農水省サイトから候補を取得 → 選択して詳細取得 → 保存 - 取得失敗時は手動入力フォームに切り替え - 展着剤フラグ・節減対象外フラグの編集 ### 散布記録入力画面(`/pesticide/events/new`) - 散布日・年度入力 - 対象種別(圃場/グループ/作物/品種)選択 → 対象を選択 - 農薬を追加(複数可): 農薬マスタから選択 + 希釈倍率 + 使用量 - 保存時に使用回数チェックを実行し、超過がある場合は警告を表示(保存はブロックしない) ### 使用回数チェック画面(`/pesticide/usage`) - 年度・作物でフィルタ - **製品使用回数テーブル**: 農薬名 / 使用回数 / 上限 / 残回数(超過時は赤表示) - **有効成分総使用回数テーブル**: 成分名 / 使用回数 / 上限 / 残回数 / 使用製品一覧(超過時は赤表示) - **特別栽培欄**: 節減対象農薬の使用成分数(報告用) --- ## 設計判断と制約 1. **散布対象の特定**: `target_type` + 対象FK/文字列で柔軟に対応。保存時に `SprayEventResolvedField` で対象圃場と作物を確定保存する。作付け計画(Plan)はあくまで保存時の解決に使うだけで、集計の正源ではない。 2. **使用回数上限は作物別に保持**: 同一農薬でも作物ごとに上限が異なるため `PesticideProductLimit` と `PesticideIngredientLimit` を作物別に複数行保持する。 3. **作物名の照合は別名テーブルで吸収**: 農水省表記の「稲」と内部の「水稲」のような差異を吸収するため、`PesticideCropAlias` を必須とする。 4. **散布対象は保存時に確定保存する**: 後日のグループ名変更や作付け変更で過去実績の集計結果が変わらないよう、`SprayEventResolvedField` に圃場・作物をスナップショット保存する。`SprayEvent` 自体には作物情報を持たない。 5. **有効成分総使用回数は `SprayEventPesticide` 単位で集計する**: 1イベントでMEP剤AとMEP剤Bを同時使用した場合、MEP成分は2回とカウントする。`unique_together=['event', 'pesticide']` で重複行を防ぎ、単純な COUNT で正確に計算できる。 6. **総使用回数はテキストパース**: 農水省サイトの「○○を含む農薬の総使用回数」カラムから正規表現で数値を抽出する。 7. **保存はブロックしない**: 使用回数超過は警告表示のみ。農薬散布の記録は法的義務があるため、超過でも保存できるようにする。 8. **`SprayEventPesticide.pesticide` は PROTECT**: 散布記録に使用中の農薬は削除不可。 9. **成分集計は `is_active=True` のみ対象**: 「その他成分」は総使用回数・特別栽培の成分数集計に含めない。 10. **`is_spreader=True` は `is_non_target` 扱い**: 展着剤はカウント除外のため、展着剤フラグをセットすれば節減対象外フラグも自動的に True 扱い(DB保存は別フィールド)。 --- ## ソースファイル索引(実装後に更新) | ファイル | 説明 | |---|---| | `backend/apps/pesticide/models.py` | Pesticide, PesticideIngredient, PesticideIngredientLimit, PesticideProductLimit, PesticideCropAlias, SprayEvent, SprayEventResolvedField, SprayEventPesticide | | `backend/apps/pesticide/serializers.py` | DRF シリアライザ | | `backend/apps/pesticide/views.py` | ViewSet | | `backend/apps/pesticide/urls.py` | URL ルーティング | | `backend/apps/pesticide/management/commands/fetch_pesticide.py` | 農水省スクレイパー | | `frontend/src/app/pesticide/page.tsx` | 農薬マスタ一覧・散布記録 | | `frontend/src/app/pesticide/usage/page.tsx` | 使用回数チェック画面 | | `frontend/src/lib/types.ts` | 型定義(Pesticide, SprayEvent 等) |