# マスタードキュメント:運搬計画機能(旧・分配計画) > **作成**: 2026-03-02 > **最終更新**: 2026-03-16 > **対象機能**: 運搬計画(施肥計画の肥料を軽トラで運ぶ単位で計画・記録する) > **実装状況**: 本番稼働中 --- ## 概要 施肥計画で決めた圃場ごとの肥料袋数を、**軽トラ1回分の積載単位**で運搬計画にまとめる機能。 実際の作業では一度に全部運べないため、「何回目にどのグループのどの肥料を何袋運ぶか」を計画・記録する。 ### 旧設計(分配計画)からの変更理由 旧設計は「1つの施肥計画の圃場をグループ分けする」だけだった。 実運用で以下のギャップが判明(2026-03-16): 1. **複数の施肥計画が混在する** - 軽トラには品種をまたいで積む 2. **単一の施肥計画が分割される** - 1回で運びきれない 3. **全肥料を一度に運ぶわけではない** - 運ぶ肥料を指定する必要がある 4. **圃場単位の合計袋数は不要** - グループ×肥料の合計が重要 5. **同じグループの圃場を回ごとに分割する** - 載りきらないときは次の回に 6. **作業記録でもある** - 運搬した日付を記録したい ### 機能スコープ | IN(実装対象) | OUT(対象外) | |---|---| | 年度単位の運搬計画作成 | 購入管理 | | 配送先グループへの圃場割り当て | 肥料の在庫管理 | | 運搬回ごとの圃場×肥料割り当て | ルート最適化 | | 回ごとの積載合計リアルタイム表示 | | | 圃場を回の間で移動する操作 | | | 「残り全部」一括割り当て | | | 回ごとの運搬日記録 | | | PDF出力(回ごとに1ページ) | | --- ## データモデル ### 旧モデルからの移行 | 旧(削除) | 新(追加) | 備考 | |---|---|---| | DistributionPlan | DeliveryPlan | FK(FertilizationPlan) 廃止 → year ベース | | DistributionGroup | DeliveryGroup | ほぼ同等 | | DistributionGroupField | DeliveryGroupField | ほぼ同等 | | (なし) | DeliveryTrip | 新規:運搬回 | | (なし) | DeliveryTripItem | 新規:運搬明細(圃場×肥料単位) | ### DeliveryPlan(運搬計画) | フィールド | 型 | 制約 | 説明 | |---|---|---|---| | id | int | PK | | | year | int | required | 年度 | | name | varchar(200) | required | 計画名 | | created_at / updated_at | datetime | auto | | - `ordering = ['-year', 'name']` - 施肥計画への直接FK なし(年度ベースで全施肥計画を横断) ### DeliveryGroup(配送先グループ) | フィールド | 型 | 制約 | 説明 | |---|---|---|---| | id | int | PK | | | delivery_plan | FK(DeliveryPlan) | CASCADE | | | name | varchar(100) | required | グループ名(例: キウイ, 足川北) | | order | PositiveIntegerField | default=0 | 表示順 | - `unique_together = [['delivery_plan', 'name']]` - `ordering = ['order', 'id']` ### DeliveryGroupField(グループ圃場割り当て) | フィールド | 型 | 制約 | 説明 | |---|---|---|---| | id | int | PK | | | delivery_plan | FK(DeliveryPlan) | CASCADE | 一意制約用 | | group | FK(DeliveryGroup) | CASCADE | | | field | FK(fields.Field) | PROTECT | | - `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. 「残り全部」操作: 施肥計画の合計 − 既に割り当て済みの回の合計 = 残り --- ## API エンドポイント すべて JWT 認証(`Authorization: Bearer `)。 | メソッド | URL | 説明 | |---|---|---| | 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出力 | ### 一覧レスポンス ```json { "id": 1, "year": 2026, "name": "2026春 肥料運搬", "group_count": 5, "trip_count": 3, "created_at": "...", "updated_at": "..." } ``` ### 詳細レスポンス ```json { "id": 1, "year": 2026, "name": "2026春 肥料運搬", "groups": [ { "id": 10, "name": "キウイ", "order": 0, "fields": [ {"id": 5, "name": "キウイ畑1", "area_tan": "1.2000"} ] } ], "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 { "year": 2026, "name": "2026春 肥料運搬", "groups": [ {"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・trips を全削除→再作成する全置換方式。 --- ## フロントエンド画面 ### 運搬計画一覧 `/distribution` - 年度セレクタ(`localStorage distributionYear` で保持) - テーブル: 計画名・グループ数・回数 - アクション: PDF・編集・削除 ### 運搬計画編集 `/distribution/new` / `/distribution/[id]/edit` #### 画面レイアウト ``` [計画名: ________________] [年度: 2026] ━━━ グループ定義 ━━━━━━━━━━━━━━━━━━━ (既存の方式: グループ追加・圃場割り当て・並び替え) ━━━ 対象肥料 ━━━━━━━━━━━━━━━━━━━━━ ☑電気炉さい ☑ミネラルホウ素 ☐有機100号 ... (年度の施肥計画に含まれる肥料をチェックボックスで選択) ━━━ 未割り当て ━━━━━━━━━━━━━━━━━━━━ ★ キウイ (小計: 電気炉さい 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) ... [+回を追加] [残り全部→新しい回] [保存] ``` #### 主要な操作 | 操作 | 方法 | 説明 | |---|---|---| | 圃場を回に割り当て | 圃場行の「→N回目」ドロップダウン | 未割り当て→指定回に移動 | | 圃場を回から戻す | 圃場行の「←戻す」ボタン | 回→未割り当てに移動 | | 圃場を別の回に移動 | 圃場行の「移動...」ドロップダウン | 回の間で移動 | | グループを回に一括割り当て | 回内の「グループを追加...」ドロップダウン | グループの全圃場を一括割り当て | | グループを別の回に移動 | グループ★行の「移動...」ドロップダウン | グループの全圃場を回の間で一括移動 | | グループを未割り当てに戻す | グループ★行の「移動...」→「未割り当てに戻す」 | グループの全圃場を一括で未割り当てに戻す | | 残り全部を一括割り当て | 「残り全部→新しい回」ボタン | 未割り当て全圃場を新しい回に追加 | | 回の追加 | 「+回を追加」ボタン | 空の回を追加 | | 回の削除 | 回ヘッダーの「×」ボタン | 回を削除、中の圃場は未割り当てに戻る | | 回の日付設定 | 日付入力フィールド | デフォルトは1回目の日付 | | 対象肥料の絞り込み | チェックボックス | 選択した肥料だけ表示 | #### 積載合計のリアルタイム表示 各回のヘッダーに、その回の肥料ごとの合計袋数と総袋数を表示。 圃場を追加・削除するたびに即時再計算(サーバー通信なし)。 --- ## PDF 出力 `GET /api/fertilizer/delivery/{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 # 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/ └── delivery_pdf.html # 回ごと1ページ PDF テンプレート ``` ### Frontend ``` frontend/src/app/distribution/ ├── page.tsx # 一覧ページ ├── new/page.tsx # 新規作成(ラッパー) ├── [id]/edit/page.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 を削除 --- ## 注意点 ### 施肥計画との関係 - 運搬計画は施肥計画への直接FKを持たない - 年度ベースで、その年度の全 FertilizationEntry を参照して圃場×肥料の袋数を取得する - 施肥計画を変更すると、未割り当ての圃場の袋数は自動で反映される - 既に運搬回に割り当て済みの DeliveryTripItem.bags は変わらない(コピー済み) ### 集計はクライアントサイド計算 画面上の集計(グループ小計・回の積載合計)は API を呼ばずクライアントで計算。 PDF生成時のみサーバーサイドで同じ計算を実施。 ### エラー表示方針 施肥計画機能と同じく alert/confirm 廃止・インラインバナーに統一。 ### 散布実績との連携 - 運搬計画の `DeliveryTripItem` が散布実績画面(`/fertilizer/spreading`)の候補データソースとなる - `DeliveryTrip.date != null` の明細のみを「運搬済み」とみなし、散布候補に含める - 散布実績画面から運搬計画を指定して遷移する場合(`?delivery_plan_id=N`)、日付フィルタは適用されない(その計画の全明細が候補になる) - 散布実績の保存時に在庫 `USE` が作成される(運搬時点では在庫変動なし) ### WorkRecord 自動生成 - `DeliveryTrip` に日付が保存されると、`WorkRecord`(`work_type=fertilizer_delivery`)が自動生成される - 実装: `apps/workrecords/services.py` の `sync_delivery_work_record()` - `DeliveryTrip` の日付が削除されると、対応する `WorkRecord` も削除される - `WorkRecord` は索引として機能し、明細データは `DeliveryTrip` / `DeliveryTripItem` 側が保持する