作成: 2026-03-02
最終更新: 2026-05-15
対象機能: 運搬計画(施肥計画の肥料を軽トラで運ぶ単位で計画・記録する)
実装状況: 本番稼働中
施肥計画で決めた圃場ごとの肥料袋数を、軽トラ1回分の積載単位で運搬計画にまとめる機能。
実際の作業では一度に全部運べないため、「何回目にどのグループのどの肥料を何袋運ぶか」を計画・記録する。
旧設計は「1つの施肥計画の圃場をグループ分けする」だけだった。
実運用で以下のギャップが判明(2026-03-16):
| IN(実装対象) | OUT(対象外) |
|---|---|
| 年度単位の運搬計画作成 | 購入管理 |
| 配送先グループへの圃場割り当て | 肥料の在庫管理 |
| 運搬回ごとの圃場×肥料割り当て | ルート最適化 |
| 回ごとの積載合計リアルタイム表示 | |
| 圃場を回の間で移動する操作 | |
| 「残り全部」一括割り当て | |
| 回ごとの運搬日記録 | |
| PDF出力(回ごとに1ページ) |
| 旧(削除) | 新(追加) | 備考 |
|---|---|---|
| DistributionPlan | DeliveryPlan | FK(FertilizationPlan) 廃止 → year ベース |
| DistributionGroup | DeliveryGroup | ほぼ同等 |
| DistributionGroupField | DeliveryGroupField | ほぼ同等 |
| (なし) | DeliveryTrip | 新規:運搬回 |
| (なし) | DeliveryTripItem | 新規:運搬明細(圃場×肥料単位) |
| フィールド | 型 | 制約 | 説明 |
|---|---|---|---|
| id | int | PK | |
| year | int | required | 年度 |
| name | varchar(200) | required | 計画名 |
| created_at / updated_at | datetime | auto |
ordering = ['-year', 'name']| フィールド | 型 | 制約 | 説明 |
|---|---|---|---|
| id | int | PK | |
| delivery_plan | FK(DeliveryPlan) | CASCADE | |
| name | varchar(100) | required | グループ名(例: キウイ, 足川北) |
| order | PositiveIntegerField | default=0 | 表示順 |
unique_together = [['delivery_plan', 'name']]ordering = ['order', 'id']| フィールド | 型 | 制約 | 説明 |
|---|---|---|---|
| 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計画| フィールド | 型 | 制約 | 説明 |
|---|---|---|---|
| id | int | PK | |
| delivery_plan | FK(DeliveryPlan) | CASCADE | |
| order | PositiveIntegerField | default=0 | 何回目(表示順) |
| name | varchar(100) | blank | 任意の名前(例: "たちはるか電気炉さい") |
| date | DateField | nullable | 運搬日(デフォルト: 1回目の日付を引き継ぎ) |
ordering = ['order', 'id']| フィールド | 型 | 制約 | 説明 |
|---|---|---|---|
| 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']]DeliveryPlan (運搬計画)
├── year, name
│
├── groups → DeliveryGroup (配送先グループ)
│ ├── name, order
│ └── fields → DeliveryGroupField → Field
│
└── trips → DeliveryTrip (運搬回)
├── order, name, date
└── items → DeliveryTripItem
├── field → Field
├── fertilizer → Fertilizer
└── bags
すべて JWT 認証(Authorization: Bearer <token>)。
| メソッド | 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出力 |
{
"id": 1,
"year": 2026,
"name": "2026春 肥料運搬",
"group_count": 5,
"trip_count": 3,
"created_at": "...",
"updated_at": "..."
}
{
"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: グループに割り当てられていない圃場{
"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 を全削除→再作成する全置換方式。
/distributionlocalStorage distributionYear で保持)/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回目の日付 |
| 対象肥料の絞り込み | チェックボックス | 選択した肥料だけ表示 |
各回のヘッダーに、その回の肥料ごとの合計袋数と総袋数を表示。
圃場を追加・削除するたびに即時再計算(サーバー通信なし)。
GET /api/fertilizer/delivery/{id}/pdf/
━━━ 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}.pdfbackend/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/src/app/distribution/
├── page.tsx # 一覧ページ
├── new/page.tsx # 新規作成(ラッパー)
├── [id]/edit/page.tsx # 編集(ラッパー)
└── _components/DeliveryEditPage.tsx # 編集共通コンポーネント
000X_add_delivery_models.py - DeliveryPlan, DeliveryGroup, DeliveryGroupField, DeliveryTrip, DeliveryTripItem を追加000Y_remove_distribution_models.py - DistributionPlan, DistributionGroup, DistributionGroupField を削除画面上の集計(グループ小計・回の積載合計)は API を呼ばずクライアントで計算。
PDF生成時のみサーバーサイドで同じ計算を実施。
施肥計画機能と同じく alert/confirm 廃止・インラインバナーに統一。
DeliveryTripItem が散布実績画面(/fertilizer/spreading)の候補データソースとなるDeliveryTrip.date != null の明細のみを「運搬済み」とみなし、散布候補に含める?delivery_plan_id=N)、日付フィルタは適用されない(その計画の全明細が候補になる)USE が作成される(運搬時点では在庫変動なし)DeliveryTrip に日付が保存されると、WorkRecord(work_type=fertilizer_delivery)が自動生成されるapps/workrecords/services.py の sync_delivery_work_record()DeliveryTrip の日付が削除されると、対応する WorkRecord も削除されるWorkRecord は索引として機能し、明細データは DeliveryTrip / DeliveryTripItem 側が保持するIssue #72 の 計画横断調整フロー は、将来的に運搬計画にも適用する。
運搬計画では次の前提で扱う。
このとき、
DeliveryTrip / WorkRecord 側に正本として残すという分離を維持する。
共通方針は docs/27_マスタードキュメント_計画横断調整編.md を参照。