Compare commits

..

9 Commits

Author SHA1 Message Date
Akira
8de1ae70aa .codex .mcpを除外 2026-04-09 20:40:30 +09:00
akira
3901caf668 docs: reflect todo spec review feedback 2026-04-09 16:36:19 +09:00
akira
5f58c2c686 指摘を反映して 改善案/TODO管理機能仕様書案.md を更新しました。
主な修正は、Todo.year の追加、TodoCrop / TodoVariety の具体定義追加、priority の 1000 刻み再採番ルール明記、plan_links がある場合だけ対象圃場整合性を検証する形への明確化、levee_work を「MVP では計画リンクなしの work_type」として整理、plan_type + plan_id を Serializer で各 FK に変換する方針追記、admin.py と migrations の追加、見出し階層の修正です。

あわせて、実績連携フラグ名は should_link_record に寄せました。まだコミットはしていないので、必要ならこの修正分もそのままコミットします。
2026-04-09 16:34:30 +09:00
akira
83525c2f59 docs: add todo management spec draft 2026-04-09 16:27:42 +09:00
akira
627d7e4f59 docs: tighten pesticide ingredient limit consistency 2026-04-09 15:52:29 +09:00
akira
9059b2b51e 仕様書を更新しました。更新先は 農薬散布管理編.md です。
- 有効成分総使用回数の集計方式を COUNT(DISTINCT SprayEvent.id) に変更
  (農薬取締法上「1回の散布=1回の使用」の解釈に準拠、1イベント=1回で統一)
- PesticideIngredientLimit に「同一成分・同一作物であれば製品が異なっても上限値は同一」の注記を追加
- 設計判断 #5 を更新:有効成分も製品使用回数と同様に COUNT(DISTINCT SprayEvent.id) で集計する根拠を記載
- 設計判断の番号を整理(#7〜#10 → #8〜#11)
2026-04-09 15:44:44 +09:00
akira
7d2eb1ebe2 Findings
同一イベント内で同じ有効成分を含む複数製品を使った場合、総使用回数を過少計上します。
18_マスタードキュメント_農薬散布管理編.md:39 (line 39) では「同一有効成分を含む複数製品は合算カウント」と定義していますが、集計式は 同:251 (line 251) の COUNT(DISTINCT SprayEvent.id) です。これだと 1 回の散布で MEP剤A と MEP剤B を同時使用したケースが 2 回ではなく 1 回になります。1イベント=1回 は製品単位には合っても、有効成分の「複数製品合算」とは衝突しています。

SprayEventResolvedField を正源にしたはずなのに、設計判断がまだ旧仕様のままで矛盾しています。
集計の正源は 同:232 (line 232) で SprayEventResolvedField.crop_name_snapshot に統一されていますが、設計判断では 同:599 (line 599) に「作付け計画(Plan)と照合」と残っています。さらに 同:602 (line 602) では削除したはずの crop_snapshot / variety_snapshot をまだ保持対象として書いています。実装者がここを読むと旧設計に引っ張られます。

製品使用回数も、同一イベント内の重複明細をどう扱うかが未定義で、式とモデルが噛み合っていません。
集計式は 同:239 (line 239) の COUNT(DISTINCT SprayEvent.id) ですが、明細モデルには 同:213 (line 213) 以降で event + pesticide の一意制約がありません。つまり同じ農薬を同一イベントに 2 行入れられる設計なのに、集計では 1 回に潰れます。仕様として「同一イベント内で同一農薬は1回しか登録できない」を明記して一意制約を持たせるか、重複明細の意味を定義した方が安全です。

大筋ではかなり良くなっていて、特に「作物単位での法的管理」と「圃場ごとの正源を SprayEventResolvedField に寄せた」方向は明快でした。上の3点だけ揃えると、実装時の解釈ぶれがかなり減ります。
2026-04-09 15:16:45 +09:00
akira
3e2942b479 変更内容
集計ロジックの明確化(農薬取締法の要件を明示):

集計単位の説明に「農薬取締法上、使用回数は作物単位で管理する義務がある」を明記
グループ内に複数作物が混在する場合の動作を明示 → 同一イベントの農薬が水稲・大豆それぞれの回数に +1 カウントされる
集計の正源は SprayEventResolvedField.crop_name_snapshot(圃場ごと)に統一
不要フィールドの削除:

SprayEvent.crop_snapshot / variety_snapshot を削除(役割が SprayEventResolvedField に統合されたため)
APIレスポンス例からも除去
集計式の精緻化:

「1イベント = 1回」のカウント原則を明示(COUNT(DISTINCT SprayEvent.id))— グループ内に圃場が何筆あっても1散布作業は1回
2026-04-09 15:11:15 +09:00
akira
70fe3824b3 docs: refine pesticide management spec 2026-04-09 14:56:21 +09:00
3 changed files with 846 additions and 28 deletions

2
.gitignore vendored
View File

@@ -15,3 +15,5 @@ postgres_data/
nul
*.tsbuildinfo
.mcp.json
.codex

View File

@@ -59,7 +59,7 @@
### 特別栽培向け成分数集計
「節減対象農薬(`is_non_target=False`)の有効成分が何種類使われたか」を年度×作物単位でカウントする。
「節減対象農薬(`is_non_target=False`)の有効成分`is_active=True`が何種類使われたか」を年度×作物単位でカウントする。
上限はなく、報告用の集計値として表示する。
---
@@ -102,10 +102,28 @@
| 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']`
- 同一成分・同一作物であれば製品が異なっても上限値は同一(農水省登録情報の仕様)
- 保存時バリデーション: 同一 `ingredient_name + crop_name` の既存レコードと異なる `max_total_uses` を保存しようとした場合はエラーにする
- 使用回数チェック API の `ingredient_usage.max_total_uses` は、同一 `ingredient_name + crop_name` の値が一意であることを前提に単一値を返す
### PesticideProductLimit製品の使用回数上限作物別
@@ -123,6 +141,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`
@@ -152,6 +186,27 @@
| `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`
@@ -168,6 +223,7 @@
| notes | TextField | blank | 備考 |
- `pesticide` は PROTECT使用済み農薬は削除不可
- `unique_together = ['event', 'pesticide']`同一イベント内で同じ農薬を2回登録不可
---
@@ -175,35 +231,52 @@
### 集計単位
**年度 × 作物** を基本単位とする。
**年度 × 作物** を基本単位とする(農薬取締法上、使用回数は作物単位で管理する義務がある)
- `target_type=field`/`group` の場合: そのイベントの年度の作付け計画Planから作物を特定
- `target_type=crop` の場合: target_crop が直接作物
- `target_type=variety` の場合: target_variety の crop が作物
- 集計対象作物は `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行しか存在しないため、イベント単位でカウントして正確。
```
製品使用回数 = COUNT(SprayEventPesticide where pesticide=X)
で year × 作物 でフィルタしたもの
製品使用回数年度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回の散布作業イベント= 有効成分の使用回数1回。同一成分を含む複数製品を同一イベントで施用することは実務上なく、仮に混合散布しても農薬取締法上「1回の散布 = 1回の使用」と解釈される。
```
有効成分総使用回数 = COUNT(SprayEventPesticide)
where SprayEventPesticide.pesticide に
当該有効成分PesticideIngredient.nameが含まれる
かつ is_non_target=False
で year × 作物 でフィルタしたもの
有効成分総使用回数年度Y・作物C・成分名I=
COUNT(DISTINCT SprayEvent.id)
where SprayEvent に SprayEventPesticide が紐づく
かつ SprayEventPesticide.pesticide の PesticideIngredient に
name=I かつ is_active=True のものが存在する
かつ SprayEventPesticide.pesticide.is_non_target=False
かつ SprayEvent に SprayEventResolvedField(crop_name_snapshot=C) が紐づく
かつ SprayEvent.year = Y
```
`SprayEventResolvedField` は圃場ごとに複数行あるため、結合で行が増えても `DISTINCT SprayEvent.id` で 1散布作業を1回だけ数える
### 特別栽培・使用成分数の集計
```
使用成分数 = COUNT(DISTINCT PesticideIngredient.name)
where 当該年度×作物の散布イベントで使用された農薬の有効成分
かつ pesticide.is_non_target=False
使用成分数年度Y・作物C=
COUNT(DISTINCT PesticideIngredient.name)
where 上記条件年度Y・作物Cの散布イベントで使用された農薬に含まれる
かつ PesticideIngredient.is_active=True
かつ SprayEventPesticide.pesticide.is_non_target=False
```
---
@@ -243,9 +316,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 +326,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 +435,15 @@
"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": [
{
@@ -373,7 +470,8 @@
{
"year": 2026,
"crop_id": 1,
"crop_name": "稲",
"crop_name": "稲",
"crop_aliases": ["稲", "水稲"],
"product_usage": [
{
"pesticide_id": 1,
@@ -454,7 +552,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` |
**「総使用回数」テキストのパース規則:**
@@ -466,6 +564,11 @@ URL: `https://pesticide.maff.go.jp/`
正規表現: r'(\d+)回以内(?:\((.+)\))?'
```
**整合性チェック:**
- 同一 `ingredient_name + crop_name` に対して既存の `PesticideIngredientLimit.max_total_uses` と異なる値が取得された場合、その農薬の自動取込はエラーとし、手動確認を促す
- `use_timing_note` の差異は許容し、より詳細なテキストで上書きしてよい
### 実装場所
`apps/pesticide/management/commands/fetch_pesticide.py`
@@ -477,6 +580,7 @@ Django management command として実装。APIエンドポイントから呼び
- アクセスは農薬マスタ登録時の1件ずつに限定バルク取得は行わない
- 農水省サイトの内部ID`system_id`)と農薬の公式登録番号は別物
- タイムアウト: 10秒
- 適用表の作物名は `PesticideCropAlias` で内部 `Crop` と対応付ける前提で保存する
---
@@ -507,12 +611,17 @@ 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保存は別フィールド
1. **散布対象の特定**: `target_type` + 対象FK/文字列で柔軟に対応。保存時に `SprayEventResolvedField` で対象圃場と作物を確定保存する。作付け計画Planはあくまで保存時の解決に使うだけで、集計の正源ではない
2. **使用回数上限は作物別に保持**: 同一農薬でも作物ごとに上限が異なるため `PesticideProductLimit``PesticideIngredientLimit` を作物別に複数行保持する
3. **作物名の照合は別名テーブルで吸収**: 農水省表記の「稲」と内部の「水稲」のような差異を吸収するため、`PesticideCropAlias` を必須とする
4. **散布対象は保存時に確定保存する**: 後日のグループ名変更や作付け変更で過去実績の集計結果が変わらないよう、`SprayEventResolvedField` に圃場・作物をスナップショット保存する。`SprayEvent` 自体には作物情報を持たない
5. **有効成分総使用回数も「1イベント=1回」**: 同一成分を含む複数製品を同一イベントで施用することは実務上なく、仮に混合散布しても農薬取締法上「1回の散布=1回の使用」。製品使用回数と同様に `COUNT(DISTINCT SprayEvent.id)` で集計する。`SprayEventResolvedField` との結合で行が増えても `DISTINCT` で正確にカウントできる
6. **総使用回数はテキストパース**: 農水省サイトの「○○を含む農薬の総使用回数」カラムから正規表現で数値を抽出する
7. **有効成分上限の整合性は保存時に保証する**: 同一 `ingredient_name + crop_name``max_total_uses` は製品をまたいで一致している前提とし、異なる値を保存しようとした場合はエラーにする。
8. **保存はブロックしない**: 使用回数超過は警告表示のみ。農薬散布の記録は法的義務があるため、超過でも保存できるようにする。
9. **`SprayEventPesticide.pesticide` は PROTECT**: 散布記録に使用中の農薬は削除不可。
10. **成分集計は `is_active=True` のみ対象**: 「その他成分」は総使用回数・特別栽培の成分数集計に含めない。
11. **`is_spreader=True``is_non_target` 扱い**: 展着剤はカウント除外のため、展着剤フラグをセットすれば節減対象外フラグも自動的に True 扱いDB保存は別フィールド
---
@@ -520,7 +629,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 ルーティング |

View File

@@ -0,0 +1,707 @@
# TODO管理機能仕様書案
> 作成日: 2026-04-09
> 最終更新: 2026-04-09
> 対象プロジェクト: `keinasystem`
> 対象 Issue: `akira/keinasystem#17`
> 位置づけ: 実装前ドラフト(レビュー反映版)
---
## 1. 概要
繁忙期の作業を「どれから手を付けるか」の観点で整理するため、Redmine チケットライクな TODO 管理機能を追加する。
本機能は単なるメモではなく、以下の中間レイヤーとして位置付ける。
- 計画
- TODO
- 実績
将来的には、作付け計画を除く各種計画について、`計画 -> TODO -> 実績` の流れに挟める構造を目指す。
ただし MVP では、まず TODO 管理の基本機能、対象圃場の管理、計画との紐づけ、完了時の実績連携導線を整備する。
---
## 2. 背景
現状は施肥計画、田植え計画、運搬計画などの個別機能はあるが、「今日やること」「今週先に処理すべきこと」を横断的に管理する仕組みがない。
そのため、繁忙期には以下の問題が起こりやすい。
- 作業の優先順位が頭の中や紙メモに依存する
- 計画の一部だけを先に実行したい場合に管理しづらい
- 実績入力までの間に「作業待ち」「着手中」の状態を置けない
- 将来追加される作業系機能を共通の入口で扱えない
TODO 管理を導入し、計画単位ではなく「実際に動く作業単位」で優先順位と進行状態を管理できるようにする。
---
## 3. 目的
### 3.1 目指す状態
- 未着手・進行中の作業を優先順で一覧できる
- TODO は計画に紐づくものと、独立したものの両方を扱える
- 計画に紐づく TODO では、計画全体ではなく一部圃場だけを対象にできる
- 完了時に、必要なものは実績系アプリへ連携できる
- 将来増える作業系アプリでも同じ TODO 基盤を使える
### 3.2 今回の対象
- Django 新規アプリ `apps/todos`
- Next.js 画面 `frontend/src/app/todos`
- REST API `/api/todos/`
- 計画画面からの TODO 生成導線
### 3.3 今回やらないこと
- 期日通知、リマインダー、メール通知
- 複数ユーザー割り当て
- コメント、添付ファイル
- 工数見積、実績時間記録
- 完全な汎用ワークフローエンジン化
---
## 4. 基本方針
### 4.1 TODO の位置づけ
TODO は「作業指示」兼「実行待ちキュー」として扱う。
- 計画は年間またはまとまり単位の設計情報
- TODO は実際に動く単位の作業
- 実績は実際に完了した事実
### 4.2 計画との関係
- 1 計画に対して複数 TODO を紐づけられる
- 1 TODO は複数計画を参照できる
- ただし TODO の実際の対象圃場は TODO 側で明示管理する
- 計画に含まれる圃場の一部だけを TODO 対象にすることを許可する
### 4.3 実績との関係
- TODO 完了時に、実績アプリを持つ作業は実績生成の入口にする
- ただし、すべての TODO が実績アプリを持つとは限らない
- 計画なし TODO、実績なし TODO も許容する
### 4.4 圃場グループの扱い
圃場グループは独立モデル化しない。
既存の `Field.group_name` を参照用の属性として扱うにとどめ、TODO の正式な対象管理は圃場単位で保持する。
理由:
- 現状のデータモデルに独立したグループモデルが存在しない
- TODO 完了後に履歴の再現性を保つには、最終的に対象圃場を確定保持した方が安全
---
## 5. 機能スコープ
### 5.1 IN
- TODO の作成、編集、削除
- ステータス管理
- 優先順位管理
- 圃場単位の対象紐づけ
- 作物、品種の補助的な分類紐づけ
- 計画との紐づけ
- 計画画面から TODO を生成
- 完了済み、キャンセル済みの表示切り替え
- 期日の強調表示
- 並び替え API
### 5.2 OUT
- 通知
- 担当者管理
- 承認フロー
- 複数段階ステータス
- 実績アプリ未実装領域の詳細実績入力 UI
---
## 6. 用語整理
| 用語 | 意味 |
|---|---|
| TODO | 実際に着手・進行・完了する作業単位 |
| 計画リンク | TODO が参照する施肥計画、田植え計画など |
| 対象圃場 | その TODO で実際に作業対象となる圃場 |
| 実績連携 | TODO 完了時に各実績アプリへ情報を渡すこと |
---
## 7. データモデル方針
### 7.1 Todo
TODO 本体。
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| id | bigint | ✓ | PK |
| year | integer | ✓ | 年度 |
| title | varchar(200) | ✓ | タイトル |
| description | text | | 説明 |
| status | enum | ✓ | `todo / doing / done / canceled` |
| priority | integer | ✓ | 小さいほど上位 |
| due_date | date | | 期日 |
| work_type | enum | ✓ | 作業種別 |
| should_link_record | boolean | ✓ | 完了時に実績連携導線を有効にするか |
| completed_at | datetime | | 完了日時 |
| canceled_at | datetime | | キャンセル日時 |
| created_at | datetime | ✓ | |
| updated_at | datetime | ✓ | |
### 7.1.1 ステータス
- `todo`: 未着手
- `doing`: 進行中
- `done`: 完了
- `canceled`: キャンセル
### 7.1.2 並び順
- 基本は FILO とする
- 新規作成時は最上位へ入る
- `priority` は 1000 刻みの整数で保存する
- 初回作成時は最上位 TODO の `priority - 1000` を新規 TODO に割り当てる
- 一覧では `priority` 昇順で表示する
- ユーザーが並び替えた後は、表示順に 1000, 2000, 3000... と振り直して保存する
- 既存レコードの一括インクリメントや小数 priority は採用しない
- 完了、キャンセル済みも `priority` は保持する
- 一覧のデフォルト表示は `todo / doing` のみを `priority` 昇順で表示する
補足:
- 1000 刻みは API の中間挿入余地ではなく、再採番時の可読性のために採用する
- 並び順変更は常に表示対象全体を受け取って再採番する前提とする
### 7.1.3 作業種別
作業種別は「計画に対応するもの」と「計画に対応しないもの」の両方を含める。
初期案:
- `general`: 一般
- `fertilization`: 施肥
- `rice_transplant`: 田植え
- `delivery`: 運搬
- `levee_work`: 畔塗
- `pesticide`: 防除
- `other_recorded`: 計画非紐づき実績系
補足:
- 実装時点で将来の全計画種別を確定できない場合は、MVP では現行アプリに対応する種別を先行定義する
- `general` はどれにも当てはまらない作業用に必須
### 7.2 TodoTargetField
TODO が実際に対象とする圃場。
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| id | bigint | ✓ | PK |
| todo | FK(Todo) | ✓ | CASCADE |
| field | FK(fields.Field) | ✓ | PROTECT |
| field_name_snapshot | varchar(100) | ✓ | 保存時点の圃場名 |
| group_name_snapshot | varchar(50) | | 保存時点の group_name |
| created_at | datetime | ✓ | |
- `unique_together = ['todo', 'field']`
方針:
- TODO の対象管理は最終的に圃場単位で保持する
- グループ、作物、品種から一括選択する UI は許可する
- ただし保存時は対象圃場へ展開して保持する
### 7.3 TodoCrop / TodoVariety
TODO の分類補助用。
#### TodoCrop
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| id | bigint | ✓ | PK |
| todo | FK(Todo) | ✓ | CASCADE |
| crop | FK(plans.Crop) | ✓ | PROTECT |
| created_at | datetime | ✓ | |
- `unique_together = ['todo', 'crop']`
#### TodoVariety
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| id | bigint | ✓ | PK |
| todo | FK(Todo) | ✓ | CASCADE |
| variety | FK(plans.Variety) | ✓ | PROTECT |
| created_at | datetime | ✓ | |
- `unique_together = ['todo', 'variety']`
注意:
- 対象圃場の実体は `TodoTargetField` を正とする
- `Crop``Variety` だけ紐づいていて圃場が 0 件の TODO は許可する
- これにより、圃場未確定の準備作業も登録できる
### 7.4 TodoPlanLink
TODO と既存計画との紐づけ。
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| id | bigint | ✓ | PK |
| todo | FK(Todo) | ✓ | CASCADE |
| plan_type | enum | ✓ | 計画種別 |
| fertilization_plan | FK | | 施肥計画 |
| rice_transplant_plan | FK | | 田植え計画 |
| delivery_plan | FK | | 運搬計画 |
| created_at | datetime | ✓ | |
方針:
- 1 行に 1 種別のリンクだけを保持する
- `plan_type` に応じて対応する FK だけを埋める
- MVP は汎用 `GenericForeignKey` を使わず、明示 FK を優先する
- 理由は API と serializer を単純に保ちやすいため
初期対象:
- 施肥計画 `FertilizationPlan`
- 田植え計画 `RiceTransplantPlan`
- 運搬計画 `DeliveryPlan`
- 畔塗 `levee_work` は MVP では「計画リンクなしで持てる work_type」として扱う
- 将来、畔塗に計画モデルが導入された時点で `TodoPlanLink` に追加する
補足:
- 作付け計画 `Plan` は「年内の計画情報」であり、TODO 生成元としては必須ではない
- 当面は Issue 回答に合わせ、`作付け計画以外のすべての計画` を TODO の対象候補とする
### 7.5 TodoCompletionLink
完了時の実績連携先を記録する索引。
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
| id | bigint | ✓ | PK |
| todo | FK(Todo) | ✓ | TODO |
| record_type | enum | ✓ | 実績種別 |
| work_record | FK(workrecords.WorkRecord) | | 共通索引 |
| spreading_session | FK(fertilizer.SpreadingSession) | | 施肥実績 |
| rice_transplant_record_id | 将来 | | 田植え実績 |
| created_at | datetime | ✓ | |
方針:
- 完了時に何へ連携したかを TODO 側から追えるようにする
- `todo` は OneToOne に固定せず FK とする
- 理由は 1 TODO から複数実績へ分割される可能性を残すため
- 実績アプリが未実装の種別は空でよい
- 将来の田植え実績導入時に拡張できる形にする
---
## 8. API 仕様案
### 8.1 一覧
- `GET /api/todos/`
主な query:
- `status=todo,doing`
- `include_closed=true|false`
- `work_type=...`
- `due=overdue|today|upcoming`
- `year=2026`
デフォルト:
- `include_closed=false`
- `status=todo,doing`
- `priority` 昇順
### 8.2 詳細取得
- `GET /api/todos/{id}/`
返却内容:
- TODO 本体
- 対象圃場
- 作物、品種
- 計画リンク
- 完了連携状況
### 8.3 作成
- `POST /api/todos/`
作成 payload 例:
```json
{
"title": "西田エリアの追肥",
"description": "週内に先行実施",
"status": "todo",
"year": 2026,
"due_date": "2026-04-12",
"work_type": "fertilization",
"should_link_record": true,
"field_ids": [12, 18, 21],
"crop_ids": [1],
"variety_ids": [4],
"plan_links": [
{"plan_type": "fertilization", "plan_id": 8}
]
}
```
`plan_links` の吸収方針:
- API 入力は `plan_type + plan_id` の組で受ける
- Serializer で `plan_type` を見て対応 FK へ変換する
- 例:
- `fertilization` -> `fertilization_plan_id`
- `rice_transplant` -> `rice_transplant_plan_id`
- `delivery` -> `delivery_plan_id`
- DB 返却時は、フロントエンド向けに再び `plan_type + plan_id + plan_label` の形へ正規化して返す
### 8.4 更新
- `PATCH /api/todos/{id}/`
更新可能項目:
- タイトル
- 説明
- ステータス
- 期日
- 作業種別
- 実績連携フラグ
- 対象圃場
- 分類
- 計画リンク
### 8.5 削除
- `DELETE /api/todos/{id}/`
ルール案:
- 連携済み実績がある TODO は物理削除ではなく制限をかける案を優先
- MVP ではまず `done` かつ実績連携済み TODO の削除可否を要確認とする
### 8.6 並び替え
- `PATCH /api/todos/reorder/`
payload 例:
```json
{
"items": [
{"id": 31, "priority": 1000},
{"id": 27, "priority": 2000},
{"id": 42, "priority": 3000}
]
}
```
方針:
- 一括更新で保存する
- DnD が難しい場合も、矢印移動 UI から同 API を呼ぶ
### 8.7 計画から TODO 生成
- `POST /api/todos/from-plan/`
payload 例:
```json
{
"plan_type": "fertilization",
"plan_id": 8,
"title": "2026春肥の散布",
"field_ids": [12, 18],
"due_date": "2026-04-15",
"should_link_record": true
}
```
生成ルール:
- 既存計画をリンクする
- `field_ids` 未指定時は計画内の全圃場を初期対象にする
- `work_type``plan_type` から自動補完する
- タイトルは自動生成可能にする
### 8.8 完了処理
- `POST /api/todos/{id}/complete/`
方針:
- `status=done` にする専用入口を用意する
- `should_link_record=true` かつ対応実績アプリがある場合、関連画面へ遷移するための情報を返す
- MVP で自動実績作成まで行うか、完了導線のみ返すかは実装時に選べるようにする
---
## 9. UI 仕様案
### 9.1 一覧画面 `/todos`
表示内容:
- 未着手、進行中 TODO を優先表示
- タイトル
- ステータス
- 期日
- 作業種別
- 対象圃場数
- 紐づき計画
操作:
- 新規作成
- ステータス変更
- 並び替え
- 完了済み、キャンセル済み表示切り替え
- 絞り込み
視覚表現:
- 期限超過は赤系
- 当日期限は強調
- 進行中は目立つバッジ表示
### 9.2 詳細画面 `/todos/{id}`
表示・編集項目:
- タイトル
- 説明
- ステータス
- 期日
- 作業種別
- 実績連携フラグ
- 対象圃場
- 分類作物、分類品種
- 計画リンク
下部表示:
- 実績連携先
- 完了日時
- 更新日時
### 9.3 作成導線
MVP では少なくとも以下の 2 導線を持つ。
1. TODO 一覧から新規作成
2. 計画詳細または一覧から TODO 生成
### 9.4 計画画面からの導線
対象候補:
- 施肥計画
- 田植え計画
- 運搬計画
ボタン例:
- `TODOを作成`
- `この計画からTODO生成`
初期値:
- タイトル
- 作業種別
- 対象圃場候補
- `should_link_record`
---
## 10. 実績連携の考え方
### 10.1 基本原則
- TODO は実績そのものではない
- ただし、実績入力の起点にはなる
- すべての TODO が実績へ行くわけではない
### 10.2 施肥
将来像:
1. 施肥計画を作る
2. TODO を生成する
3. TODO を実施する
4. 完了時に施肥実績へつなぐ
考え方:
- 従来の `施肥計画 -> 施肥実績` に対し、間に TODO が入れるようにする
- TODO 完了時は `SpreadingSession` 作成導線へつなぐ
- 対象圃場は TODO の `TodoTargetField` を初期値として渡す
### 10.3 田植え
田植え実績アプリは今後実装予定であるため、今回の TODO 側では以下を前提にする。
- `rice_transplant` 種別の TODO を持てる
- 完了時に将来の田植え実績へ接続できるよう索引設計を残す
- MVP 時点では「完了済みだが実績アプリ未接続」の状態も許容する
### 10.4 実績アプリが無い作業
- `general` など、実績アプリに紐づかない TODO を許容する
- その場合は `status=done` のみで完了とする
---
## 11. バリデーション方針
- `done` に遷移したら `completed_at` を自動設定する
- `canceled` に遷移したら `canceled_at` を自動設定する
- `done` から `todo` または `doing` への差し戻しは MVP では許可する
- 差し戻し時も `completed_at` はクリアせず履歴値として保持する
- `plan_links` に紐づく計画の年度と TODO の利用年度が必要なら将来追加する
- `field_ids` が計画外圃場を含む場合は、`plan_links` が 1 件以上ある場合のみエラーにする
- 複数 `plan_links` がある場合は、それぞれの計画に対して対象圃場整合性を検証する
- `should_link_record=true` でも、対応実績アプリが無い場合は保存を許可する
- `TodoTargetField.field``PROTECT` を採用する
- 理由は、過去 TODO の対象圃場履歴を崩さないことを優先するため
### 11.1 レビュー反映済み判断
- `done -> todo/doing` の差し戻しは許可する
- 差し戻し後も `completed_at` は監査用の履歴値として保持する
- `TodoTargetField.field` は運用上の削除容易性より履歴保全を優先し、`PROTECT` を維持する
- 実績連携フラグ名は `should_link_record` で確定する
---
## 12. 実装方針
### 12.1 Backend
- `apps/todos/models.py`
- `apps/todos/admin.py`
- `apps/todos/serializers.py`
- `apps/todos/views.py`
- `apps/todos/urls.py`
- `apps/todos/migrations/`
- `keinasystem/settings.py` へ app 追加
- `keinasystem/urls.py``/api/todos/` 追加
### 12.2 Frontend
- `frontend/src/app/todos/page.tsx`
- `frontend/src/app/todos/[id]/page.tsx`
- `frontend/src/app/todos/new/page.tsx`
- 必要に応じて `_components` 配下に分離
- ナビゲーションへ TODO 追加
### 12.3 実装順
1. モデル、admin、serializer、migration の作成
2. TODO 一覧と CRUD API
3. TODO 一覧と詳細 UI
4. 並び替え API と UI
5. 計画から TODO 生成
6. 完了時の実績連携導線
7. `makemigrations``migrate` を実行
---
## 13. テスト観点
- TODO を新規作成できる
- 対象圃場を複数紐づけできる
- 計画の一部圃場だけを対象にできる
- 完了済み、キャンセル済みの表示切り替えができる
- 並び替え後に順番が保持される
- 計画画面から TODO を生成できる
- 実績アプリ未接続の TODO でも完了できる
- 実績連携済み TODO の挙動が壊れない
---
## 14. 未確定事項
### 14.1 work_type enum の最終一覧
今回の回答で方針は見えたが、初回実装でどこまで列挙するかは確定していない。
候補:
- 一般
- 施肥
- 田植え
- 運搬
- 畔塗
- 防除
- 計画非紐づき実績系
### 14.2 完了時の自動生成レベル
MVP で以下のどこまでやるかは実装前に決める。
- A. 完了ステータス変更のみ
- B. 実績入力画面への導線生成
- C. TODO 情報を使った実績レコード仮生成
### 14.3 削除ポリシー
実績連携後の TODO をどう扱うか。
案:
- 物理削除禁止
- 論理削除
- 参照整合性チェック付き物理削除
### 14.4 work_type と計画種別の追加ルール
MVP では以下を前提とする。
- work_type は先に定義する
- plan_link は実在する計画モデルだけを持つ
- work_type が存在しても、対応する計画 FK が未実装のことはあり得る
将来、新しい計画機能が増えたときは以下を同時に更新する。
- `Todo.work_type` choices
- `TodoPlanLink.plan_type`
- 対応 FK
- 計画から TODO 生成 API
---
## 15. 提案する MVP 決定案
実装着手しやすさを優先し、MVP では以下を採用することを提案する。
- TODO は `year` を持つ
- 対象管理は `TodoTargetField` を正とする
- `work_type``general / fertilization / rice_transplant / delivery / levee_work / pesticide` を初期採用する
- 計画リンクは明示 FK 方式で開始する
- 実績連携フラグ名は `should_link_record` を採用する
- 完了時はまず「実績入力画面への導線生成」を採用し、自動実績作成は後続検討とする
- 並び替えは API 先行、UI は DnD 優先、難しければ矢印移動で代替する