From eba62674956d14e1117a5392dba94d3b06205261 Mon Sep 17 00:00:00 2001 From: Akira Date: Mon, 16 Mar 2026 16:05:46 +0900 Subject: [PATCH] =?UTF-8?q?=E5=A4=89=E6=9B=B4=E3=81=97=E3=81=9F=E3=83=89?= =?UTF-8?q?=E3=82=AD=E3=83=A5=E3=83=A1=E3=83=B3=E3=83=88=20=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=A4=E3=83=AB=09=E5=A4=89=E6=9B=B4=E5=86=85?= =?UTF-8?q?=E5=AE=B9=2014=5F=E3=83=9E=E3=82=B9=E3=82=BF=E3=83=BC=E3=83=89?= =?UTF-8?q?=E3=82=AD=E3=83=A5=E3=83=A1=E3=83=B3=E3=83=88=5F=E5=88=86?= =?UTF-8?q?=E9=85=8D=E8=A8=88=E7=94=BB=E7=B7=A8.md=09=E5=85=A8=E9=9D=A2?= =?UTF-8?q?=E6=94=B9=E8=A8=82:=20=E6=97=A7=E3=80=8C=E5=88=86=E9=85=8D?= =?UTF-8?q?=E8=A8=88=E7=94=BB=E3=80=8D=E2=86=92=20=E6=96=B0=E3=80=8C?= =?UTF-8?q?=E9=81=8B=E6=90=AC=E8=A8=88=E7=94=BB=E3=80=8D=E3=80=82=E3=83=87?= =?UTF-8?q?=E3=83=BC=E3=82=BF=E3=83=A2=E3=83=87=E3=83=AB5=E3=83=86?= =?UTF-8?q?=E3=83=BC=E3=83=96=E3=83=AB=E3=80=81API=E4=BB=95=E6=A7=98?= =?UTF-8?q?=E3=80=81=E7=94=BB=E9=9D=A2UI=E6=93=8D=E4=BD=9C=E3=80=81PDF?= =?UTF-8?q?=E3=83=95=E3=82=A9=E3=83=BC=E3=83=9E=E3=83=83=E3=83=88=E3=82=92?= =?UTF-8?q?=E8=A8=98=E8=BC=89=20CLAUDE.md=09=E3=83=87=E3=83=BC=E3=82=BF?= =?UTF-8?q?=E3=83=A2=E3=83=87=E3=83=AB=E6=A6=82=E8=A6=81=EF=BC=88Distribut?= =?UTF-8?q?ion*=20=E2=86=92=20Delivery*=20=E3=81=AB=E5=B7=AE=E3=81=97?= =?UTF-8?q?=E6=9B=BF=E3=81=88=EF=BC=89=E3=80=81=E5=AE=9F=E8=A3=85=E7=8A=B6?= =?UTF-8?q?=E6=B3=81=E3=82=BB=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E3=80=81?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=B1=A5=E6=AD=B4=E3=82=92=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=2013=5F=E3=83=9E=E3=82=B9=E3=82=BF=E3=83=BC=E3=83=89=E3=82=AD?= =?UTF-8?q?=E3=83=A5=E3=83=A1=E3=83=B3=E3=83=88=5F=E6=96=BD=E8=82=A5?= =?UTF-8?q?=E8=A8=88=E7=94=BB=E7=B7=A8.md=09OUT=20=E3=82=B9=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=83=97=E3=81=AE=E3=80=8C=E5=9C=83=E5=A0=B4=E3=81=B8?= =?UTF-8?q?=E3=81=AE=E9=85=8D=E7=BD=AE=E8=A8=88=E7=94=BB=E3=80=8D=E3=82=92?= =?UTF-8?q?=E3=80=8C=E9=81=8B=E6=90=AC=E8=A8=88=E7=94=BB=E3=80=8D=E3=81=B8?= =?UTF-8?q?=E3=81=AE=E5=8F=82=E7=85=A7=E3=81=AB=E4=BF=AE=E6=AD=A3=20?= =?UTF-8?q?=E5=86=85=E5=AE=B9=E3=82=92=E7=A2=BA=E8=AA=8D=E3=81=97=E3=81=A6?= =?UTF-8?q?=E3=80=81=E5=95=8F=E9=A1=8C=E3=81=AA=E3=81=91=E3=82=8C=E3=81=B0?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=E3=81=AB=E9=80=B2=E3=81=BF=E3=81=BE=E3=81=99?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.json | 3 +- CLAUDE.md | 52 ++- .../13_マスタードキュメント_施肥計画編.md | 2 +- .../14_マスタードキュメント_分配計画編.md | 374 ++++++++++++------ 4 files changed, 303 insertions(+), 128 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index a571a6e..706647a 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -64,7 +64,8 @@ "Bash(TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzczNTY4MTAxLCJpYXQiOjE3NzM0ODE3MDEsImp0aSI6ImM1N2EyNzVjMzkyNTRmMjFiZmUxMzA4ZmU4ZjQ3ZWExIiwidXNlcl9pZCI6Mn0.LOZIpPOHN48YZuf7UBaDrPJfxb12hIm15QRnUPc9sYM\" __NEW_LINE_0b7fd53b80bd968a__ echo \"=== Stock summary \\(should show reserved\\) ===\" curl -s http://localhost:8000/api/materials/stock-summary/?material_type=fertilizer -H \"Authorization: Bearer $TOKEN\")", "Read(//c/Users/akira/Develop/keinasystem_t02/**)", "Bash(TOKEN=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzczNTY4MTAxLCJpYXQiOjE3NzM0ODE3MDEsImp0aSI6ImM1N2EyNzVjMzkyNTRmMjFiZmUxMzA4ZmU4ZjQ3ZWExIiwidXNlcl9pZCI6Mn0.LOZIpPOHN48YZuf7UBaDrPJfxb12hIm15QRnUPc9sYM\" __NEW_LINE_74a785697e4cd919__ echo \"=== After confirm: stock summary ===\" curl -s http://localhost:8000/api/materials/stock-summary/?material_type=fertilizer -H \"Authorization: Bearer $TOKEN\")", - "Bash(git diff:*)" + "Bash(git diff:*)", + "mcp__serena__find_symbol" ], "additionalDirectories": [ "C:\\Users\\akira\\AppData\\Local\\Temp", diff --git a/CLAUDE.md b/CLAUDE.md index adb12ef..b7f403e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -191,22 +191,37 @@ FertilizationEntry (施肥エントリ・中間テーブル) ├── bags(袋数、Decimal) └── unique_together = ['plan', 'field', 'fertilizer'] -DistributionPlan (分配計画) -├── fertilization_plan (FK to FertilizationPlan, CASCADE) +DeliveryPlan (運搬計画) ← 旧 DistributionPlan を置き換え(2026-03-16 再設計) +├── year(年度)← 施肥計画へのFK廃止、年度ベースで全施肥計画を横断 ├── name(計画名) -└── groups → DistributionGroup +├── groups → DeliveryGroup +└── trips → DeliveryTrip -DistributionGroup (分配グループ) -├── distribution_plan (FK to DistributionPlan, CASCADE) +DeliveryGroup (配送先グループ) +├── delivery_plan (FK to DeliveryPlan, CASCADE) ├── name(グループ名) ├── order(表示順) -└── unique_together = ['distribution_plan', 'name'] +└── unique_together = ['delivery_plan', 'name'] -DistributionGroupField (グループ圃場割り当て) -├── distribution_plan (FK to DistributionPlan, CASCADE) ← 一意制約用 -├── group (FK to DistributionGroup, CASCADE) +DeliveryGroupField (グループ圃場割り当て) +├── delivery_plan (FK to DeliveryPlan, CASCADE) ← 一意制約用 +├── group (FK to DeliveryGroup, CASCADE) ├── field (FK to fields.Field, PROTECT) -└── unique_together = ['distribution_plan', 'field'] ← 1圃場=1グループ/1計画 +└── unique_together = ['delivery_plan', 'field'] ← 1圃場=1グループ/1計画 + +DeliveryTrip (運搬回) +├── delivery_plan (FK to DeliveryPlan, CASCADE) +├── order(何回目) +├── name(任意の名前) +├── date(運搬日、nullable、デフォルト=1回目の日付) +└── items → DeliveryTripItem + +DeliveryTripItem (運搬明細) +├── trip (FK to DeliveryTrip, CASCADE) +├── field (FK to fields.Field, PROTECT) +├── fertilizer (FK to Fertilizer, PROTECT) +├── bags(袋数、Decimal) +└── unique_together = ['trip', 'field', 'fertilizer'] ``` ### 重要な設計判断 @@ -343,12 +358,15 @@ DistributionGroupField (グループ圃場割り当て) - 自動計算3方式: 反当袋数(per_tan)、均等配分(even)、反当チッソ(nitrogen) - フロントエンド: `/fertilizer/`(一覧), `/fertilizer/new`・`/fertilizer/[id]/edit`(編集・マトリクス表), `/fertilizer/masters/`(肥料マスタ) - スコープ外(将来): 購入管理 -11. **分配計画機能**(2026-03-02 実装): - - Django `apps/fertilizer` アプリに3モデル追加(DistributionPlan, DistributionGroup, DistributionGroupField) - - API(JWT認証): `GET/POST /api/fertilizer/distribution/?year=`, `GET/PUT/DELETE /api/fertilizer/distribution/{id}/`, `GET /api/fertilizer/distribution/{id}/pdf/` - - 施肥計画を元に圃場をカスタムグループに割り当て、グループ×肥料の集計表を生成 - - PDF出力(A4横向き・グループ合計行★+圃場サブ行) - - フロントエンド: `/distribution/`(一覧), `/distribution/new`・`/distribution/[id]/edit`(編集) +11. **運搬計画機能**(旧・分配計画、2026-03-16 再設計中): + - 旧 DistributionPlan/Group/GroupField → 新 DeliveryPlan/Group/GroupField/Trip/TripItem に移行 + - 施肥計画への直接FK廃止 → 年度ベースで全施肥計画を横断 + - 「軽トラ1回分」を基本単位とする運搬回(DeliveryTrip)を追加 + - 運搬明細(DeliveryTripItem)で圃場×肥料単位の袋数を管理 + - 運搬回ごとの日付記録(作業記録としても機能) + - API(JWT認証): `/api/fertilizer/delivery/` 配下 + - PDF出力(A4横向き・回ごとに1ページ) + - フロントエンド: `/distribution/`(一覧・編集) - マスタードキュメント: `document/14_マスタードキュメント_分配計画編.md` ### 🚧 既知の課題・技術的負債 @@ -503,6 +521,8 @@ docker-compose exec backend python manage.py migrate ## 📝 更新履歴 +- 2026-03-16: 分配計画を「運搬計画」に再設計。実運用のワークフロー(軽トラ複数回・複数施肥計画混在・肥料指定)に合わせ、DeliveryPlan/Trip/TripItem モデルへ移行。施肥計画へのFK廃止→年度ベース。マスタードキュメント14を全面改訂 + - 2026-03-05: メール通知機能を更新。MailEmail.account を xserver1〜xserver6 で識別可能に変更。Windmill mail_filter に To ヘッダー宛先補正を追加し、Gmail先行取り込みでも Xserver 宛先ラベルが崩れないよう修正。マスタードキュメント/仕様書を同期。 - 2026-02-28: Cursor連携を廃止。Claude Code 単独運用に変更。`document/20_Cursor_Claude連携ガイド.md` を削除 diff --git a/document/13_マスタードキュメント_施肥計画編.md b/document/13_マスタードキュメント_施肥計画編.md index ddcafe6..39debfe 100644 --- a/document/13_マスタードキュメント_施肥計画編.md +++ b/document/13_マスタードキュメント_施肥計画編.md @@ -17,7 +17,7 @@ | IN(実装済み) | OUT(対象外) | |---|---| | 肥料マスタ管理 | 肥料購入管理 | -| 施肥計画の作成・編集・削除 | 圃場への配置計画(置き場所割り当て) | +| 施肥計画の作成・編集・削除 | 運搬計画(→ `14_マスタードキュメント_分配計画編.md` 参照) | | 3方式の自動計算 | 個別作業日報の詳細管理 | | 作付け計画からの圃場自動取得 | | | PDF出力(圃場×肥料マトリクス表) | | diff --git a/document/14_マスタードキュメント_分配計画編.md b/document/14_マスタードキュメント_分配計画編.md index 536c1dc..5823a09 100644 --- a/document/14_マスタードキュメント_分配計画編.md +++ b/document/14_マスタードキュメント_分配計画編.md @@ -1,65 +1,140 @@ -# マスタードキュメント:分配計画機能 +# マスタードキュメント:運搬計画機能(旧・分配計画) > **作成**: 2026-03-02 -> **最終更新**: 2026-03-02 -> **対象機能**: 分配計画(施肥計画の圃場をグループ化し配置場所単位で集計) -> **実装状況**: 実装完了 +> **最終更新**: 2026-03-16 +> **対象機能**: 運搬計画(施肥計画の肥料を軽トラで運ぶ単位で計画・記録する) +> **実装状況**: 再設計中(旧分配計画から運搬計画へ移行) --- ## 概要 -施肥計画(FertilizationPlan)で決めた圃場ごとの袋数を、**実際に肥料を配置する場所の単位**でまとめる機能。 -例:「田中エリアにはA肥料12袋・B肥料6袋を持っていく」という単位で計画・PDF出力できる。 +施肥計画で決めた圃場ごとの肥料袋数を、**軽トラ1回分の積載単位**で運搬計画にまとめる機能。 +実際の作業では一度に全部運べないため、「何回目にどのグループのどの肥料を何袋運ぶか」を計画・記録する。 + +### 旧設計(分配計画)からの変更理由 + +旧設計は「1つの施肥計画の圃場をグループ分けする」だけだった。 +実運用で以下のギャップが判明(2026-03-16): + +1. **複数の施肥計画が混在する** - 軽トラには品種をまたいで積む +2. **単一の施肥計画が分割される** - 1回で運びきれない +3. **全肥料を一度に運ぶわけではない** - 運ぶ肥料を指定する必要がある +4. **圃場単位の合計袋数は不要** - グループ×肥料の合計が重要 +5. **同じグループの圃場を回ごとに分割する** - 載りきらないときは次の回に +6. **作業記録でもある** - 運搬した日付を記録したい ### 機能スコープ -| IN(実装済み) | OUT(対象外) | +| IN(実装対象) | OUT(対象外) | |---|---| -| 施肥計画を元に圃場をカスタムグループに割り当て | 購入管理 | -| グループ×肥料の集計表(画面表示) | 実施記録 | -| PDF出力(グループ合計行+圃場サブ行) | | -| グループの順序変更・名前変更 | | +| 年度単位の運搬計画作成 | 購入管理 | +| 配送先グループへの圃場割り当て | 肥料の在庫管理 | +| 運搬回ごとの圃場×肥料割り当て | ルート最適化 | +| 回ごとの積載合計リアルタイム表示 | | +| 圃場を回の間で移動する操作 | | +| 「残り全部」一括割り当て | | +| 回ごとの運搬日記録 | | +| PDF出力(回ごとに1ページ) | | --- ## データモデル -### DistributionPlan(分配計画) +### 旧モデルからの移行 + +| 旧(削除) | 新(追加) | 備考 | +|---|---|---| +| DistributionPlan | DeliveryPlan | FK(FertilizationPlan) 廃止 → year ベース | +| DistributionGroup | DeliveryGroup | ほぼ同等 | +| DistributionGroupField | DeliveryGroupField | ほぼ同等 | +| (なし) | DeliveryTrip | 新規:運搬回 | +| (なし) | DeliveryTripItem | 新規:運搬明細(圃場×肥料単位) | + +### DeliveryPlan(運搬計画) | フィールド | 型 | 制約 | 説明 | |---|---|---|---| | id | int | PK | | -| fertilization_plan | FK(FertilizationPlan) | CASCADE | | +| year | int | required | 年度 | | name | varchar(200) | required | 計画名 | | created_at / updated_at | datetime | auto | | -- `ordering = ['-fertilization_plan__year', 'name']` -- 1つの施肥計画に対して複数の分配計画を作れる(OneToOneではなくFK) +- `ordering = ['-year', 'name']` +- 施肥計画への直接FK なし(年度ベースで全施肥計画を横断) -### DistributionGroup(分配グループ) +### DeliveryGroup(配送先グループ) | フィールド | 型 | 制約 | 説明 | |---|---|---|---| | id | int | PK | | -| distribution_plan | FK(DistributionPlan) | CASCADE | | -| name | varchar(100) | required | グループ名 | +| delivery_plan | FK(DeliveryPlan) | CASCADE | | +| name | varchar(100) | required | グループ名(例: キウイ, 足川北) | | order | PositiveIntegerField | default=0 | 表示順 | -- `unique_together = [['distribution_plan', 'name']]` → 同一計画内でグループ名重複不可 +- `unique_together = [['delivery_plan', 'name']]` - `ordering = ['order', 'id']` -### DistributionGroupField(グループ圃場割り当て) +### DeliveryGroupField(グループ圃場割り当て) | フィールド | 型 | 制約 | 説明 | |---|---|---|---| | id | int | PK | | -| distribution_plan | FK(DistributionPlan) | CASCADE | 一意制約のために冗長保持 | -| group | FK(DistributionGroup) | CASCADE | | -| field | FK(fields.Field) | PROTECT | 圃場 | +| delivery_plan | FK(DeliveryPlan) | CASCADE | 一意制約用 | +| group | FK(DeliveryGroup) | CASCADE | | +| field | FK(fields.Field) | PROTECT | | -- `unique_together = [['distribution_plan', 'field']]` → 1圃場=1グループ/1計画 -- `ordering = ['field__display_order', 'field__id']` +- `unique_together = [['delivery_plan', 'field']]` → 1圃場=1グループ/1計画 + +### DeliveryTrip(運搬回) + +| フィールド | 型 | 制約 | 説明 | +|---|---|---|---| +| id | int | PK | | +| delivery_plan | FK(DeliveryPlan) | CASCADE | | +| order | PositiveIntegerField | default=0 | 何回目(表示順) | +| name | varchar(100) | blank | 任意の名前(例: "たちはるか電気炉さい") | +| date | DateField | nullable | 運搬日(デフォルト: 1回目の日付を引き継ぎ) | + +- `ordering = ['order', 'id']` + +### DeliveryTripItem(運搬明細) + +| フィールド | 型 | 制約 | 説明 | +|---|---|---|---| +| id | int | PK | | +| trip | FK(DeliveryTrip) | CASCADE | | +| field | FK(fields.Field) | PROTECT | | +| fertilizer | FK(Fertilizer) | PROTECT | | +| bags | Decimal(10,4) | required | 袋数 | + +- `unique_together = [['trip', 'field', 'fertilizer']]` +- bags は施肥計画の FertilizationEntry から自動計算で初期値を設定するが、手動上書きも可能 + +### ER図(概念) + +``` +DeliveryPlan (運搬計画) +├── year, name +│ +├── groups → DeliveryGroup (配送先グループ) +│ ├── name, order +│ └── fields → DeliveryGroupField → Field +│ +└── trips → DeliveryTrip (運搬回) + ├── order, name, date + └── items → DeliveryTripItem + ├── field → Field + ├── fertilizer → Fertilizer + └── bags +``` + +### 袋数の算出ルール + +1. 運搬計画作成時、年度の全 FertilizationEntry を参照して「グループ×肥料→圃場×袋数」を自動算出 +2. ユーザーが運搬回に圃場を割り当てると、該当する FertilizationEntry の bags が DeliveryTripItem.bags にコピーされる +3. 手動で bags を上書きすることも可能(施肥計画との差異は許容) +4. 「残り全部」操作: 施肥計画の合計 − 既に割り当て済みの回の合計 = 残り --- @@ -69,143 +144,209 @@ | メソッド | URL | 説明 | |---|---|---| -| GET | `/api/fertilizer/distribution/?year={year}` | 一覧(年度フィルタ) | -| POST | `/api/fertilizer/distribution/` | 新規作成 | -| GET | `/api/fertilizer/distribution/{id}/` | 詳細(groups/entries/unassigned込み) | -| PUT | `/api/fertilizer/distribution/{id}/` | 更新(groups全置換) | -| DELETE | `/api/fertilizer/distribution/{id}/` | 削除 | -| GET | `/api/fertilizer/distribution/{id}/pdf/` | PDF出力(application/pdf) | +| GET | `/api/fertilizer/delivery/?year={year}` | 一覧(年度フィルタ) | +| POST | `/api/fertilizer/delivery/` | 新規作成 | +| GET | `/api/fertilizer/delivery/{id}/` | 詳細(groups/trips/items 込み) | +| PUT | `/api/fertilizer/delivery/{id}/` | 更新(groups・trips 全置換) | +| DELETE | `/api/fertilizer/delivery/{id}/` | 削除 | +| GET | `/api/fertilizer/delivery/{id}/pdf/` | PDF出力 | -### 一覧レスポンス(DistributionPlanListSerializer) +### 一覧レスポンス ```json { "id": 1, - "name": "2025年コシヒカリ 分配計画", - "fertilization_plan_id": 3, - "fertilization_plan_name": "2025年コシヒカリ施肥計画", - "year": 2025, - "variety_name": "コシヒカリ", - "crop_name": "米", - "group_count": 3, - "field_count": 12, + "year": 2026, + "name": "2026春 肥料運搬", + "group_count": 5, + "trip_count": 3, "created_at": "...", "updated_at": "..." } ``` -### 詳細レスポンス(DistributionPlanReadSerializer) +### 詳細レスポンス ```json { "id": 1, - "name": "2025年コシヒカリ 分配計画", - "fertilization_plan": { - "id": 3, - "name": "2025年コシヒカリ施肥計画", - "year": 2025, - "variety_name": "コシヒカリ", - "crop_name": "米", - "fertilizers": [{"id": 1, "name": "一発肥料"}], - "entries": [{"field": 5, "fertilizer": 1, "bags": "2.40"}] - }, + "year": 2026, + "name": "2026春 肥料運搬", "groups": [ { "id": 10, - "name": "田中エリア", + "name": "キウイ", "order": 0, - "fields": [{"id": 5, "name": "田中上", "area_tan": "1.2000"}] + "fields": [ + {"id": 5, "name": "キウイ畑1", "area_tan": "1.2000"} + ] } ], - "unassigned_fields": [{"id": 7, "name": "未割り当て圃場", "area_tan": "0.5000"}] + "trips": [ + { + "id": 1, + "order": 0, + "name": "1回目 たちはるか電気炉さい", + "date": "2026-03-16", + "items": [ + {"field": 5, "fertilizer": 1, "bags": "4.00"} + ] + } + ], + "unassigned_fields": [], + "available_fertilizers": [ + {"id": 1, "name": "電気炉さい"}, + {"id": 2, "name": "ミネラルホウ素"} + ] } ``` +- `available_fertilizers`: 該当年度の全施肥計画で使われている肥料の一覧 +- `unassigned_fields`: グループに割り当てられていない圃場 + ### 書き込みリクエスト(POST/PUT) ```json { - "name": "2025年コシヒカリ 分配計画", - "fertilization_plan_id": 3, + "year": 2026, + "name": "2026春 肥料運搬", "groups": [ - {"name": "田中エリア", "order": 0, "field_ids": [5, 6]}, - {"name": "奥地エリア", "order": 1, "field_ids": [7]} + {"name": "キウイ", "order": 0, "field_ids": [5, 6]} + ], + "trips": [ + { + "order": 0, + "name": "1回目", + "date": "2026-03-16", + "items": [ + {"field_id": 5, "fertilizer_id": 1, "bags": "4.00"} + ] + } ] } ``` -PUT は groups を全削除→再作成する全置換方式。 +PUT は groups・trips を全削除→再作成する全置換方式。 --- ## フロントエンド画面 -### 分配計画一覧 `/distribution` +### 運搬計画一覧 `/distribution` - 年度セレクタ(`localStorage distributionYear` で保持) -- テーブル: 計画名・施肥計画・作物/品種・グループ数・圃場数 +- テーブル: 計画名・グループ数・回数 - アクション: PDF・編集・削除 -- 削除エラー: インラインバナー(確認なし・失敗したらバナー表示) -### 分配計画編集 `/distribution/new` / `/distribution/[id]/edit` +### 運搬計画編集 `/distribution/new` / `/distribution/[id]/edit` -**共通コンポーネント**: `frontend/src/app/distribution/_components/DistributionEditPage.tsx` +#### 画面レイアウト -#### State構成 +``` +[計画名: ________________] [年度: 2026] -```typescript -// 基本情報 -const [name, setName] = useState('') -const [fertilizationPlanId, setFertilizationPlanId] = useState('') +━━━ グループ定義 ━━━━━━━━━━━━━━━━━━━ + (既存の方式: グループ追加・圃場割り当て・並び替え) -// 施肥計画詳細(施肥計画選択後に取得) -const [fertPlanDetail, setFertPlanDetail] = useState(null) +━━━ 対象肥料 ━━━━━━━━━━━━━━━━━━━━━ + ☑電気炉さい ☑ミネラルホウ素 ☐有機100号 ... + (年度の施肥計画に含まれる肥料をチェックボックスで選択) -// ローカルグループ(tempId で管理、保存時にサーバーへ送信) -const [groups, setGroups] = useState([]) -// LocalGroup = { tempId: string, name: string, order: number, fieldIds: number[], isRenamingName?: string } +━━━ 未割り当て ━━━━━━━━━━━━━━━━━━━━ + ★ キウイ (小計: 電気炉さい 4, ミネラルホウ素 5) + 圃場A 電気炉さい:2 ミネラルホウ素:3 [→1回目 ▼] + 圃場B 電気炉さい:2 ミネラルホウ素:2 [→1回目 ▼] + ★ 足川北 (小計: 電気炉さい 12, ミネラルホウ素 6) + 圃場D ... + +━━━ 1回目 (2026-03-16) ━━━ 積載: 46袋 ━━━ + 日付: [2026-03-16] 名前: [たちはるか電気炉さい] + ★ たちはるか (小計: 電気炉さい 46) + 圃場X 電気炉さい:10 [←戻す] + 圃場Y 電気炉さい:12 [←戻す] + ... + +━━━ 2回目 (2026-03-16) ━━━ 積載: 39袋 ━━━ + 日付: [2026-03-16] 名前: [____________] + ★ キウイ (小計: 電気炉さい 4, ミネラルホウ素 5) + ... + +[+回を追加] [残り全部→新しい回] [保存] ``` -#### UI構成 +#### 主要な操作 -1. **計画基本情報**: 計画名テキスト + 施肥計画セレクタ -2. **グループ割り当て**: - - 新規グループ追加(名前入力 + 追加ボタン) - - グループカード(↑↓順序変更・鉛筆名前変更・×削除) - - グループ内圃場(×解除)+ 肥料別袋数をインライン表示 - - 未割り当て圃場セクション(グループ選択ドロップダウンで割り当て) -3. **集計プレビュー**: グループ×肥料マトリクス(リアルタイム・サーバー通信なし) +| 操作 | 方法 | 説明 | +|---|---|---| +| 圃場を回に割り当て | 圃場行の「→N回目」ドロップダウン | 未割り当て→指定回に移動 | +| 圃場を回から戻す | 圃場行の「←戻す」ボタン | 回→未割り当てに移動 | +| 圃場を別の回に移動 | 戻す→再割り当て、または直接ドロップダウン | 回の間で移動 | +| 残り全部を一括割り当て | 「残り全部→新しい回」ボタン | 未割り当て全圃場を新しい回に追加 | +| 回の追加 | 「+回を追加」ボタン | 空の回を追加 | +| 回の削除 | 回ヘッダーの「×」ボタン | 回を削除、中の圃場は未割り当てに戻る | +| 回の日付設定 | 日付入力フィールド | デフォルトは1回目の日付 | +| 対象肥料の絞り込み | チェックボックス | 選択した肥料だけ表示 | + +#### 積載合計のリアルタイム表示 + +各回のヘッダーに、その回の肥料ごとの合計袋数と総袋数を表示。 +圃場を追加・削除するたびに即時再計算(サーバー通信なし)。 --- ## PDF 出力 -`GET /api/fertilizer/distribution/{id}/pdf/` +`GET /api/fertilizer/delivery/{id}/pdf/` -- WeasyPrint(既存施肥計画PDFと同じ仕組み) -- テンプレート: `backend/apps/fertilizer/templates/fertilizer/distribution_pdf.html` -- フォーマット: A4横向き -- 内容: - - ★グループ合計行(太字・緑背景) - - 圃場サブ行(小フォント・灰色背景) - - 肥料列合計・総合計 -- ファイル名: `distribution_{year}_{plan_id}.pdf` +### フォーマット + +- WeasyPrint、A4横向き +- **回ごとに1ページ**(1回目=1ページ目、2回目=2ページ目...) + +### 各ページの内容 + +``` +━━━ 2回目 2026-03-16 ━━━━━━━━━━━━━━━ + 電気炉さい ミネラルホウ素 +★ キウイ 4 5 + 圃場A 2 3 + 圃場B 2 2 +★ 池田さんちの前 2 2 + 圃場C 2 2 +★ 足川北 12 6 + 圃場D 4 2 + 圃場E 4 2 + 圃場F 4 2 +★ 出祥邸 - 8 + 圃場G - 4 + 圃場H - 4 +───────────────────────────────────── + 合計 18 21 +``` + +- ★行: グループ小計(肥料ごと)、太字・緑背景 +- 圃場行: 各圃場の肥料ごとの袋数(**合計列なし**) +- 最下行: 回全体の肥料ごと合計 +- 日付を各ページのヘッダーに記載 +- ファイル名: `delivery_{year}_{plan_id}.pdf` --- -## ファイル構成 +## ファイル構成(予定) ### Backend ``` backend/apps/fertilizer/ -├── models.py # DistributionPlan/Group/GroupField 追加(migration 0003) -├── serializers.py # Distribution* シリアライザ追加 -├── views.py # DistributionPlanViewSet 追加 -├── urls.py # router.register('distribution', ...) 追加 -├── admin.py # DistributionPlan/Group の admin 登録 +├── models.py # DeliveryPlan/Group/GroupField/Trip/TripItem +├── serializers.py # Delivery* シリアライザ +├── views.py # DeliveryPlanViewSet +├── urls.py # router.register('delivery', ...) +├── admin.py # DeliveryPlan 等の admin 登録 +├── migrations/ +│ └── 000X_delivery_*.py # 旧Distribution → 新Delivery マイグレーション └── templates/fertilizer/ - └── distribution_pdf.html # A4横 PDF テンプレート + └── delivery_pdf.html # 回ごと1ページ PDF テンプレート ``` ### Frontend @@ -215,27 +356,40 @@ frontend/src/app/distribution/ ├── page.tsx # 一覧ページ ├── new/page.tsx # 新規作成(ラッパー) ├── [id]/edit/page.tsx # 編集(ラッパー) -└── _components/DistributionEditPage.tsx # 編集共通コンポーネント +└── _components/DeliveryEditPage.tsx # 編集共通コンポーネント ``` --- +## マイグレーション方針 + +### 旧モデル(Distribution*)の扱い + +1. 新モデル(Delivery*)を追加するマイグレーションを作成 +2. 旧モデル(Distribution*)は削除マイグレーションで除去 +3. 旧データは少量のため、データ移行は行わない(手動で再作成) + +### マイグレーション順序 + +1. `000X_add_delivery_models.py` - DeliveryPlan, DeliveryGroup, DeliveryGroupField, DeliveryTrip, DeliveryTripItem を追加 +2. `000Y_remove_distribution_models.py` - DistributionPlan, DistributionGroup, DistributionGroupField を削除 + +--- + ## 注意点 -### 集計は全クライアントサイド計算 +### 施肥計画との関係 -集計プレビューは API を呼ばず、`fertPlanDetail.entries` と `groups.fieldIds` からクライアントで計算する。 +- 運搬計画は施肥計画への直接FKを持たない +- 年度ベースで、その年度の全 FertilizationEntry を参照して圃場×肥料の袋数を取得する +- 施肥計画を変更すると、未割り当ての圃場の袋数は自動で反映される +- 既に運搬回に割り当て済みの DeliveryTripItem.bags は変わらない(コピー済み) + +### 集計はクライアントサイド計算 + +画面上の集計(グループ小計・回の積載合計)は API を呼ばずクライアントで計算。 PDF生成時のみサーバーサイドで同じ計算を実施。 -### PUT の全置換方式 - -PUT 時は `groups.all().delete()` → 再作成。部分更新は非対応。 - -### 未割り当て圃場の扱い - -- 施肥計画に含まれる圃場のうちグループに割り当てられていないものは「未割り当て」として表示 -- PDF にも「未割り当て」グループとして出力される(ゼロの場合は出力なし) - ### エラー表示方針 施肥計画機能と同じく alert/confirm 廃止・インラインバナーに統一。