diff --git a/document/18_マスタードキュメント_農薬散布管理編.md b/document/18_マスタードキュメント_農薬散布管理編.md index e1241f0..bf5c2d1 100644 --- a/document/18_マスタードキュメント_農薬散布管理編.md +++ b/document/18_マスタードキュメント_農薬散布管理編.md @@ -59,7 +59,7 @@ ### 特別栽培向け成分数集計 -「節減対象農薬(`is_non_target=False`)の有効成分が何種類使われたか」を年度×作物単位でカウントする。 +「節減対象農薬(`is_non_target=False`)の有効成分(`is_active=True`)が何種類使われたか」を年度×作物単位でカウントする。 上限はなく、報告用の集計値として表示する。 --- @@ -102,10 +102,25 @@ | 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', 'name']` +- `unique_together = ['pesticide', 'ingredient_name', 'crop_name']` ### PesticideProductLimit(製品の使用回数上限:作物別) @@ -123,6 +138,22 @@ - `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` @@ -139,6 +170,8 @@ | target_group | CharField(50) | blank | 対象が圃場グループの場合(group_name) | | target_crop | FK(plans.Crop) | null=True, PROTECT | 対象が作物の場合 | | target_variety | FK(plans.Variety) | null=True, PROTECT | 対象が品種の場合 | +| crop_snapshot | CharField(100) | blank | 保存時点の集計対象作物名 | +| variety_snapshot | CharField(100) | blank | 保存時点の対象品種名 | | notes | TextField | blank | 備考 | | created_at | DateTimeField | auto | | | updated_at | DateTimeField | auto | | @@ -152,6 +185,27 @@ | `crop` | target_crop | 特定の作物に対して散布(作付け計画と照合) | | `variety` | target_variety | 特定の品種に対して散布(作付け計画と照合) | +- 保存時に `crop_snapshot` / `variety_snapshot` を自動設定し、後日の作付け変更やグループ名変更があっても過去実績の集計結果が変わらないようにする + +### 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` @@ -177,9 +231,9 @@ **年度 × 作物** を基本単位とする。 -- `target_type=field`/`group` の場合: そのイベントの年度の作付け計画(Plan)から作物を特定 -- `target_type=crop` の場合: target_crop が直接作物 -- `target_type=variety` の場合: target_variety の crop が作物 +- 集計対象作物は `SprayEvent.crop_snapshot` と `SprayEventResolvedField.crop_name_snapshot` を正とする +- `target_type=field`/`group`/`crop`/`variety` の違いにかかわらず、保存時に解決済み対象圃場と作物スナップショットを作成する +- 使用回数上限の照合は、内部 `Crop` から `PesticideCropAlias` を介して農水省表記へ変換して行う ### 製品使用回数の集計 @@ -194,6 +248,7 @@ 有効成分総使用回数 = COUNT(SprayEventPesticide) where SprayEventPesticide.pesticide に 当該有効成分(PesticideIngredient.name)が含まれる + かつ PesticideIngredient.is_active=True かつ is_non_target=False で year × 作物 でフィルタしたもの ``` @@ -203,6 +258,7 @@ ``` 使用成分数 = COUNT(DISTINCT PesticideIngredient.name) where 当該年度×作物の散布イベントで使用された農薬の有効成分 + かつ PesticideIngredient.is_active=True かつ pesticide.is_non_target=False ``` @@ -243,9 +299,7 @@ "id": 1, "name": "MEP", "concentration": "50.0%", - "is_active": true, - "max_total_uses": 3, - "use_timing_note": "種もみへの処理は1回以内、育苗箱散布は1回以内、本田では2回以内" + "is_active": true } ], "product_limits": [ @@ -255,6 +309,23 @@ "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 + } ] } ``` @@ -347,6 +418,17 @@ "target_type": "group", "target_group": "田中エリア", "target_display": "田中エリア(グループ)", + "crop_snapshot": "水稲", + "variety_snapshot": "", + "resolved_fields": [ + { + "field": 5, + "field_name_snapshot": "田中上", + "group_name_snapshot": "田中エリア", + "crop_name_snapshot": "水稲", + "variety_name_snapshot": "コシヒカリ" + } + ], "notes": "曇り、風弱し", "pesticides": [ { @@ -373,7 +455,8 @@ { "year": 2026, "crop_id": 1, - "crop_name": "稲", + "crop_name": "水稲", + "crop_aliases": ["稲", "水稲"], "product_usage": [ { "pesticide_id": 1, @@ -454,7 +537,7 @@ URL: `https://pesticide.maff.go.jp/` | 作物名 | 作物名 | `PesticideProductLimit.crop_name` | | 本剤の使用回数 | 「N回以内」から N を抽出 | `PesticideProductLimit.max_uses` | | 使用時期 | テキストそのまま | `PesticideProductLimit.use_timing_note` | -| `{成分名}を含む農薬の総使用回数` | 「N回以内(...)」から N と補足を抽出 | `PesticideIngredient.max_total_uses` / `use_timing_note` | +| `{成分名}を含む農薬の総使用回数` | 「N回以内(...)」から N と補足を抽出 | `PesticideIngredientLimit.max_total_uses` / `use_timing_note` | **「総使用回数」テキストのパース規則:** @@ -477,6 +560,7 @@ Django management command として実装。APIエンドポイントから呼び - アクセスは農薬マスタ登録時の1件ずつに限定(バルク取得は行わない) - 農水省サイトの内部ID(`system_id`)と農薬の公式登録番号は別物 - タイムアウト: 10秒 +- 適用表の作物名は `PesticideCropAlias` で内部 `Crop` と対応付ける前提で保存する --- @@ -508,11 +592,14 @@ Django management command として実装。APIエンドポイントから呼び ## 設計判断と制約 1. **散布対象の特定**: `target_type` + 対象FK/文字列で柔軟に対応。作物ごとの集計は作付け計画(Plan)と照合。 -2. **使用回数上限は作物別に保持**: 同一農薬でも稲と野菜で上限が異なるため `PesticideProductLimit` を作物別に複数行保持。 -3. **総使用回数はテキストパース**: 農水省サイトの「○○を含む農薬の総使用回数」カラムから正規表現で数値を抽出。 -4. **保存はブロックしない**: 使用回数超過は警告表示のみ。農薬散布の記録は法的義務があるため、超過でも保存できるようにする。 -5. **`SprayEventPesticide.pesticide` は PROTECT**: 散布記録に使用中の農薬は削除不可。 -6. **`is_spreader=True` は `is_non_target` 扱い**: 展着剤はカウント除外のため、展着剤フラグをセットすれば節減対象外フラグも自動的に True 扱い(DB保存は別フィールド)。 +2. **使用回数上限は作物別に保持**: 同一農薬でも作物ごとに上限が異なるため `PesticideProductLimit` と `PesticideIngredientLimit` を作物別に複数行保持する。 +3. **作物名の照合は別名テーブルで吸収**: 農水省表記の「稲」と内部の「水稲」のような差異を吸収するため、`PesticideCropAlias` を必須とする。 +4. **散布対象は保存時に確定保存する**: 後日のグループ名変更や作付け変更で過去実績の集計結果が変わらないよう、`SprayEventResolvedField` と `crop_snapshot` / `variety_snapshot` を保持する。 +5. **総使用回数はテキストパース**: 農水省サイトの「○○を含む農薬の総使用回数」カラムから正規表現で数値を抽出する。 +6. **保存はブロックしない**: 使用回数超過は警告表示のみ。農薬散布の記録は法的義務があるため、超過でも保存できるようにする。 +7. **`SprayEventPesticide.pesticide` は PROTECT**: 散布記録に使用中の農薬は削除不可。 +8. **成分集計は `is_active=True` のみ対象**: 「その他成分」は総使用回数・特別栽培の成分数集計に含めない。 +9. **`is_spreader=True` は `is_non_target` 扱い**: 展着剤はカウント除外のため、展着剤フラグをセットすれば節減対象外フラグも自動的に True 扱い(DB保存は別フィールド)。 --- @@ -520,7 +607,7 @@ Django management command として実装。APIエンドポイントから呼び | ファイル | 説明 | |---|---| -| `backend/apps/pesticide/models.py` | Pesticide, PesticideIngredient, PesticideProductLimit, SprayEvent, SprayEventPesticide | +| `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 ルーティング |