Files
keinasystem/改善案/issue_3_計画始動後の作付け変更_調査.md
akira 5a9b6a053b 改善案/issue_3_計画始動後の作付け変更_調査.md#L428 (line 428)
田植え計画 | 施肥と同様に対応 という決定表現が、下の詳細仕様と少しズレています。詳細では #L463 (line 463) 以降で「現時点では全件移動」と明記されているので、表も「現時点では全件移動、将来実績連携後に再設計」くらいに合わせた方が誤読されません。

改善案/issue_3_計画始動後の作付け変更_調査.md#L444 (line 444)
actual_bags = 0 は現行 services.py では null に丸められる は少し断定が強いです。現行の再集計ロジックでは未該当なら NULL になりやすい、という理解は良いのですが、将来だけでなくデータ補正や手動更新でも 0 が入り得ます。仕様書上は「未散布判定は NULL または 0 を未散布扱いとするかどうか」を明示した方が安全です。
2026-04-05 14:09:48 +09:00

495 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Issue #3 調査メモ: 計画始動後の作付け変更について
## 対象 issue
- Gitea Issue `#3`
- タイトル: `計画始動後の作付け変更について`
- 登録日: 2026-04-05
## 1. まず明らかになっている必要があること
この課題は「作付け計画を変更したい」ではなく、
「すでに計画・運搬・散布の一部が動き始めた後で、将来分だけを安全に組み替えたい」が本質。
そのため、以下を仕様として先に決めないと実装を始めると破綻しやすい。
### 1-1. どこまでを履歴として固定し、どこから先を変更対象にするか
- すでに散布実績がある `圃場 × 肥料` は履歴として固定するのか
- 固定する場合、固定対象は以下のどこまで含むのか
- 散布実績
- 実績に対応する施肥計画エントリ
- 実績に対応する運搬明細
- 在庫 USE / RESERVE
- 作業記録
- 「まだ散布していない残計画」だけを別レコードへ移すのか
- 既存計画を上書きするのではなく、履歴保持のために分割するのか
### 1-2. 品種変更を何の単位で許可するか
- 圃場単位での変更を許可するのか
- 同じ年度中に圃場の品種変更履歴を残す必要があるのか
- 変更後の圃場は新しい品種の施肥計画へ付け替えるのか
- 変更前に散布済みの肥料は「旧品種の計画に残す」のか「圃場履歴として残す」のか
### 1-3. グループ変更の意味
ユーザー確認により、issue 本文の「グループ変更」は主に圃場管理の `group_name` を指す。
- 圃場マスタの `group_name` は後から変更されうる
- 運搬計画の配送グループも散布前なら変更されうる
- ただし散布後は、運搬計画グループは基本的に変更しない前提
このため、少なくとも以下を分けて考える必要がある。
- 圃場マスタ上の現在属性としてのグループ
- 履歴として固定すべき運搬計画上のグループ
### 1-4. 不整合をどこまで許容するか
- 旧品種の施肥実績が残ったまま、新品種の作付け計画へ変更してよいか
- 「今年のその圃場は最終的に何を作ったか」と「途中で何を前提に散布したか」がズレてもよいか
- PDF や一覧画面で、旧計画分と新計画分を同時に見せる必要があるか
### 1-5. ユーザー操作として必要な単位
- 圃場 1 筆だけ切り替えたいのか
- 複数圃場をまとめて切り替えたいのか
- 施肥済み分を残しつつ、未散布分を新計画へ一括移動したいのか
- 変更理由や変更日などの監査情報が必要か
## 2. 現行実装で起きていること
### 2-1. 候補圃場は「現在の作付け計画」から再計算される
施肥計画の圃場候補は、現在の `plans.Plan(year, variety)` を見て作られている。
- [backend/apps/fertilizer/views.py](/home/akira/develop/keinasystem/backend/apps/fertilizer/views.py#L126)
- [frontend/src/app/fertilizer/_components/FertilizerEditPage.tsx](/home/akira/develop/keinasystem/frontend/src/app/fertilizer/_components/FertilizerEditPage.tsx#L180)
そのため、作付け計画で圃場の品種を変更すると、
変更前の品種に紐づく施肥計画画面では、その圃場が追加候補に出なくなる。
issue にある「足川北上が圃場追加候補に出てこない」はこの挙動と一致する。
### 2-2. 施肥計画の実績集計は `year + field + fertilizer` 単位
散布実績から `actual_bags` を再集計するとき、対象は `plan__year + field_id + fertilizer_id` で更新される。
- [backend/apps/fertilizer/services.py](/home/akira/develop/keinasystem/backend/apps/fertilizer/services.py#L11)
つまり現行実装では、
「どの施肥計画に対する実績か」ではなく、
「同年度のその圃場・その肥料の実績か」で紐づいている。
このため、同じ年度に圃場を別計画へ移したり、計画を分割したりすると、
実績が複数計画へ二重反映または意図しない再配分になる余地がある。
### 2-3. 施肥計画更新はエントリ全削除・全再作成
施肥計画更新時は、既存エントリを全削除して新しいエントリを作り直す。
- [backend/apps/fertilizer/serializers.py](/home/akira/develop/keinasystem/backend/apps/fertilizer/serializers.py#L152)
この方式だと、「散布済みの行だけ残して、未散布分だけ移す」という操作単位を持てない。
履歴保持と将来計画の分離を実現するには、今の更新方式は不足している。
### 2-4. 運搬計画は年度内の全施肥エントリを前提に集計する
運搬計画詳細の未割当圃場・利用可能肥料・全明細は、`plan__year=obj.year` の全施肥エントリから作られている。
- [backend/apps/fertilizer/serializers.py](/home/akira/develop/keinasystem/backend/apps/fertilizer/serializers.py#L291)
そのため、作付け変更に伴って施肥計画を分割・移管したい場合、
年度全体集計ベースの画面は旧計画と新計画を自然に区別できない。
### 2-5. 散布実績自体はスナップショットを保持している
散布実績明細は以下を保存している。
- 実散布袋数
- 計画袋数スナップショット
- 運搬済み袋数スナップショット
- [backend/apps/fertilizer/models.py](/home/akira/develop/keinasystem/backend/apps/fertilizer/models.py#L211)
- [backend/apps/fertilizer/serializers.py](/home/akira/develop/keinasystem/backend/apps/fertilizer/serializers.py#L437)
これは履歴保持の観点では良いが、
「どの施肥計画のどの行から発生した散布か」という計画レベルの参照は保持していない。
## 3. 影響を受ける仕様
### 3-1. 作付け計画
- 現在は `field + year` が 1 件で、その年度の最新状態のみを表す
- 途中変更の履歴を持たない
- 変更前後を区別したいなら、履歴テーブルか有効期間の考え方が必要
### 3-2. 施肥計画
- 候補圃場抽出ロジック
- 編集画面での圃場追加可否
- 既散布行と未散布行の扱い
- `actual_bags` の再集計単位
- 在庫引当の再作成ロジック
### 3-3. 圃場グループ
- 圃場マスタの `group_name` は現在値しか持っていない
- 後日変更されると、過去時点でどのグループだったかは追えない
- ただし現時点では、散布実績や施肥実績が `group_name` に直接依存している箇所は薄い
- よって圃場グループ変更は、主に表示・集計・将来計画側の問題として扱える
### 3-4. 運搬計画
- グループ割当の維持方法
- 年度全体施肥エントリを前提にした未割当圃場計算
- すでに運搬済みの明細を履歴として残しつつ、未運搬分だけ別グループへ再編できるか
### 3-5. 散布実績
- 既存実績の参照元計画が曖昧
- 変更後の計画へ実績が再集計されるかどうか
- 候補一覧生成時に、旧計画分と新計画分をどう見せ分けるか
### 3-6. 在庫管理
- 施肥計画更新時、RESERVE が全置換される
- 実績 USE は散布実績ベースで残る
- 途中変更時に「旧計画の引当を解除し、新計画へ再引当」が必要
- ただし散布済み分の USE は動かしてはいけない
### 3-7. 作業記録
- 作業記録は運搬回 / 散布実績への 1:1 参照で成立しており、履歴としては比較的安定
- 一方でタイトル等は計画名変更の影響を受けうる
## 4. この issue に対する現時点の結論
この問題は単なる「候補圃場の表示漏れ」ではない。
本質は以下の 2 点。
1. 現行システムは「現在の作付け計画」と「履歴として固定すべき施肥・運搬・散布」を分離していない
2. 施肥・運搬・散布の一部が `年度 + 圃場 + 肥料` 集計でつながっており、計画の再編単位を持っていない
したがって、候補圃場 API だけ直しても不十分で、
少なくとも「履歴固定」と「未実施分の再計画」の仕様分離が必要。
## 5. 実装前に必要な仕様決定
最低限、次の 4 点を決める必要がある。
1. 変更後も残すべき履歴の最小単位は何か
2. 未散布分をどの単位で旧計画から切り離すか
3. 品種変更後、既散布分を旧品種の施肥計画に残すのか、新品種側に見せ替えるのか
4. 圃場マスタの `group_name` 変更を履歴管理対象にするか、現在値扱いに留めるか
5. 散布前の運搬計画変更をどこまで許容するか
## 6. 推奨する実装方針の方向性
現時点では、次の方向が最も安全に見える。
### 方針A: 履歴固定 + 未実施分の再計画
- 散布実績がある `圃場 × 肥料` は旧施肥計画側に固定する
- 未散布分だけ新施肥計画へ移す
- 運搬済み明細も履歴として残し、未運搬分のみ再編対象にする
- 作付け計画の最新状態とは別に、施肥計画側で「履歴としての対象圃場集合」を保持する
- 圃場マスタの `group_name` は変更可能な現在属性として扱い、必要なら帳票側でスナップショット化を検討する
### 方針B: 候補圃場と実績参照を分離する
- 候補圃場表示は「現在の作付け計画」
- 既存計画の保持対象は「その計画に保存済みの圃場」
- 実績集計は `plan_id` またはそれに準ずる固定キーに寄せる
方針A/B を組み合わせないと、issue の A/B/C を同時には満たしにくい。
## 6-1. ユーザー確認を踏まえた補足結論
ユーザー確認により、優先順位は次のように見える。
1. 散布後の履歴固定が最優先
2. 散布前の運搬計画は変更可能
3. 圃場マスタのグループは現在値として後から変わりうる
したがって、構造上もっとも重要なのは
`作付け変更後も散布済みデータが崩れないこと`
であり、`group_name` 自体は二次的な論点。
ただし帳票や一覧で「当時のグループ」を見たい要求が出るなら、別途スナップショットが必要になる。
## 7. 次の調査・設計タスク案
1. 「既散布・未散布」「既運搬・未運搬」で分けた業務フローを図にする
2. 施肥計画エントリに履歴固定用の状態を持たせるか検討する
3. 散布実績の参照先を `year + field + fertilizer` から計画単位へ寄せる案を比較する
4. 圃場マスタ `group_name` を履歴化する必要があるかを判断する
5. UI 上で必要な操作を列挙する
6. その後に issue を「暫定対処」と「構造対応」に分割する
## 8. 追加提案に対する評価
ユーザーからの追加提案(初期):
- 変更履歴は必要
- 散布済み Entry も新品種計画へ移動する案を第一候補
- 未散布 Entry は新品種計画へ移動し、RESERVE も付け替える
- 圃場グループは対応不要
- 田植え計画も同様に移動
> **補足(最終確定)**: 散布済み Entry の扱いは後述 8-3 の検討を経て **(A) 旧計画に残す** に確定した。
> その他の方針は初期提案どおり採用。
この提案には良い点が多い一方で、現行実装のまま採ると危険な点もある。
### 8-1. 採用しやすい点
#### a. 変更履歴モデルの新設
`PlanVarietyChange` のような履歴モデル追加は妥当。
少なくとも「いつ・どの圃場の品種が・何から何に変わったか」は残すべき。
補足:
- `plan FK` だけでなく `field_id``year` を冗長保持した方が将来参照しやすい
- 変更理由 `reason` があると運用上かなり有用
- 自動移動結果の件数も履歴に残せると監査しやすい
#### b. 未散布 Entry の移動
これは方向性としてかなり自然。
「まだ実施していない将来計画」は新品種側へ寄せ、引当も付け替えるのは業務的に納得感が高い。
#### c. 田植え計画も同様に扱う
田植え計画も、候補圃場を現在の作付け計画から取っている以上、同種の問題を持つ。
施肥だけ直して田植えを放置すると整合しないため、同時に見るべきという指摘は正しい。
### 8-2. そのまま採ると危険な点
#### a. 散布済み Entry を新品種計画へ移動する案
これはもっとも議論が必要。
現行では散布実績は `SpreadingSessionItem(field, fertilizer)` にあり、
施肥計画との関係は `actual_bags` の再集計で後付けされている。
- [backend/apps/fertilizer/services.py](/home/akira/develop/keinasystem/backend/apps/fertilizer/services.py#L11)
この構造で散布済み Entry を新品種計画へ移すと、画面上は
「新品種の施肥計画に、旧品種前提で行った散布実績が載る」
ことになる。
業務的にそれを許容するなら成立するが、次の違和感が出る。
- 散布時点では旧品種前提だった履歴が、新品種計画の一部として見える
- 施肥計画 PDF や一覧で、後から見る人が経緯を誤解しやすい
- 「なぜこの新品種計画に既散布分が入っているのか」を履歴表示なしでは理解できない
したがって、散布済み Entry を移動するなら、少なくとも
`変更前品種で発生した実績である`
ことが UI で明示される必要がある。
現時点では、実装の安全性だけで見ると
`散布済み Entry は旧計画に残す`
方が素直。
#### b. Entry の plan FK 付け替えだけでは履歴の意味が弱い
提案では `FertilizationEntry.plan` / `RiceTransplantEntry.plan` を付け替えるが、
それだけでは「なぜそこへ移ったか」が DB 上から分からない。
最低でも次が欲しい。
- どの変更イベントで移動したか
- 移動前 plan
- 移動後 plan
- 自動移動日時
つまり、履歴は `PlanVarietyChange` だけでなく、
Entry 移動の監査ログも別に持つ方が安全。
#### c. 施肥計画が複数ある場合の自動集約
「最新 1 件に集約」は実装は簡単だが、業務意味が崩れやすい。
issue 本文にもあるように、同年度・同品種で複数計画がありうる。
そこへ無条件に寄せると、
- 元肥用と追肥用
- 第1回散布分と第2回散布分
- ロット違い
のような意味を壊す可能性がある。
自動選択(最新)は暫定対応としてはあり得るが、本命仕様にはしにくい。
### 8-3. 推奨と最終決定
#### 変更履歴 ✅ 採用確定
モデル設計:
```python
PlanVarietyChange
field FK(Field, PROTECT)
year int
plan FK(Plan, CASCADE)
changed_at datetime
old_variety FK(Variety, SET_NULL, null=True)
new_variety FK(Variety, SET_NULL, null=True)
reason text blank
moved_entry_count int default=0 # 自動移動した未散布エントリ数(監査用)
```
- `plan FK` だけでなく `field_id``year` を冗長保持した方が将来参照しやすい
- `reason` があると運用上かなり有用
- `moved_entry_count` で自動移動の件数を残すことで監査ログを兼ねる
#### 散布済み Entry の扱い ✅ **(A) 旧計画に残す** 確定
理由:
- 履歴解釈が明確(「にこまる用に施肥した」という文脈が旧計画に保持される)
- PDF/一覧での意味が崩れにくい
- `actual_bags` の二重反映リスクを避けられる(同 year+field+fertilizer に複数エントリを持たない)
- 将来「既散布分も移したい」要求が出た場合に後から対応できる
#### 未散布 Entry の扱い ✅ 新品種計画へ移動RESERVE付け替えあり確定
「まだ実施していない将来計画」は新品種側へ寄せる。
RESERVE 付け替えもこの方針と整合する。
#### 圃場グループ ✅ 対応不要 確定
圃場マスタの `group_name` は現在値として扱う。
帳票側でスナップショットが必要になれば別途検討。
#### 田植え計画 ✅ 対応確定(施肥とは判定軸が異なる)
施肥だけ直して田植えを放置すると整合しないため同時に対応する。
ただし田植え計画には actual_bags 相当の実績概念がないため、
**対象圃場の Entry は全件移動**(未散布判定なし)。
将来、田植え実績との連携が実装された場合は改めて設計する。
実装順は施肥の後でよい。
### 8-4. 移動先計画の選び方への見解
3案の中では、私は次を推す。
1. 本命仕様: `b. 新規作成(常に)`
2. 暫定実装: `a. 自動選択(最新)`
3. 初期段階では避けたい: `c. ユーザー選択`
理由:
- `b` は履歴が最も明確で、既存計画の意味を壊しにくい
- `a` は早く実装できるが、複数計画の意味を壊しうる
- `c` は柔軟だが、allocation 画面の操作が一気に複雑になる
実務上は、
「品種変更に伴って自動移動された分」は専用計画として分けた方が後から説明しやすい。
たとえば:
- `2026年度 たちはるか特栽 施肥計画(品種変更移動)`
- `2026年度 たちはるか特栽 田植え計画(品種変更移動)`
のような命名。
### 8-5. 実装ステップ(確定版)
散布済みEntryの扱いが確定したため、以下の順で実装する。
1. `PlanVarietyChange` モデル追加(履歴記録のみ・既存データに触らない)
2. 品種変更トリガーのサービス追加
3. `未散布 Entry のみ` 新品種計画(常に新規作成)へ移動を施肥計画で実装
4. RESERVE 付け替えと `actual_bags` 再集計を確認
5. 田植え計画へ横展開
6. allocation 画面の履歴インジケータ追加
散布済み Entry の移動は今後の要求が出た時点で別途検討する。
---
## 9. 確定仕様まとめ
> 更新日: 2026-04-05
### 9-1. 決定事項一覧
| 項目 | 決定内容 |
|---|---|
| 散布済み Entry | **旧計画に残すA確定** |
| 未散布 Entry | **新品種計画へ移動 + RESERVE付け替え** |
| 移動先計画の選び方 | **常に新規作成**(既存計画には集約しない) |
| 移動先計画の命名 | `{year}年度 {品種名} 施肥計画(品種変更移動)` |
| 変更履歴 | **PlanVarietyChange モデルを新設** |
| 圃場グループ | **対応不要**(現在値扱いのまま) |
| 田植え計画 | **現時点では全件移動**(実績概念なし)。将来の実績連携実装後に再設計(実装は施肥の後) |
### 9-2. 品種変更時の自動処理フロー
`Plan.variety``A → B` に変更されたとき:
```
1. PlanVarietyChange を記録
field, year, plan, changed_at, old_variety=A, new_variety=B, reason
2. 施肥計画エントリの移動
未散布の判定:
- actual_bags IS NULL → 未散布(移動対象)
- actual_bags IS NOT NULL かつ actual_bags < bags → 一部散布済み(移動不可・旧計画に残す)
- actual_bags >= bags → 散布完了(移動不可・旧計画に残す)
※ actual_bags = 0 の扱いは明示的に決定しておく必要がある。
現行 services.py では SUM = 0 のとき NULL に丸めるが、
データ補正や手動更新で 0 が直接セットされる可能性は排除できない。
本仕様では actual_bags IS NULL を未散布と判定し、0 は一部散布済みと同様に扱う
(移動不可・旧計画に残す)とする。
対象: FertilizationPlan.variety=A かつ year=変更年度 かつ
FertilizationEntry.field=変更圃場 かつ actual_bags IS NULL未散布
処理:
a. variety=B, year=変更年度 の新 FertilizationPlan を作成
名前: "{year}年度 {B品種名} 施肥計画(品種変更移動)"
b. 対象 FertilizationEntry の plan FK を新 plan へ付け替え
c. 旧 plan 全体の RESERVE を再生成stock_service.create_reserves_for_plan(旧plan)
※ RESERVE は plan 単位で全置換管理のため、エントリ単位ではなく plan 単位で呼び出す
d. 新 plan 全体の RESERVE を生成stock_service.create_reserves_for_plan(新plan)
e. PlanVarietyChange.moved_entry_count に移動件数を記録
非対象(旧計画に残す):
actual_bags IS NOT NULL のエントリ(一部散布済み・散布完了)
3. 田植え計画エントリの移動
田植え計画には施肥計画の actual_bags に相当する実績概念がまだない
RiceTransplantEntry は installed_seedling_boxes のみ、散布済み/未散布の区別がない)。
そのため、現時点では 対象圃場の Entry を全件移動 とする。
将来、田植え実績(田植え日・実績箱数等)との連携が実装された場合は、
「実施済み Entry は旧計画に残す」方針に揃えて再設計すること。
対象: RiceTransplantPlan.variety=A かつ year=変更年度 かつ
RiceTransplantEntry.field=変更圃場(全件)
処理:
a. variety=B, year=変更年度 の新 RiceTransplantPlan を作成
名前: "{year}年度 {B品種名} 田植え計画(品種変更移動)"
b. 対象 RiceTransplantEntry の plan FK を新 plan へ付け替え
```
### 9-3. 変更しないもの(影響なし)
- `SpreadingSessionItem` — field+fertilizer リンクのため変更不要
- `actual_bags` 集計ロジック — 現方針では再利用可能。
ただし **同一 year+field+fertilizer の FertilizationEntry が複数計画にまたがって共存しないこと** が前提。
この制約は仕様上の invariant として守る必要がある(移動処理でエントリを複製しないこと)。
- `candidate_fields` API — Plan.variety 変更後は自然に新品種で候補が返る
- `WorkRecord` — 運搬/散布実績への 1:1 参照のため影響なし
### 9-4. 未解決・将来検討
- 散布済み Entry を新品種計画側にも見せたい要求が出た場合 → 別途設計
- allocation 画面の変更履歴インジケータ実装ステップ6
- `actual_bags` 集計を `year+field+fertilizer` から `plan単位` へ変更する大規模リファクタ(中長期)