施肥計画の圃場を配置場所単位でグループ化し、グループ×肥料の集計表を 表示・PDF出力できる機能を追加。 - Backend: DistributionPlan/Group/GroupField モデル (migration 0003) - API: GET/POST/PUT/DELETE/PDF (/api/fertilizer/distribution/) - Frontend: 一覧・新規作成・編集画面 (/distribution) - Navbar に分配計画メニューを追加 - 集計プレビューはクライアントサイド計算(API不要) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8.0 KiB
8.0 KiB
マスタードキュメント:分配計画機能
作成: 2026-03-02 最終更新: 2026-03-02 対象機能: 分配計画(施肥計画の圃場をグループ化し配置場所単位で集計) 実装状況: 実装完了
概要
施肥計画(FertilizationPlan)で決めた圃場ごとの袋数を、実際に肥料を配置する場所の単位でまとめる機能。 例:「田中エリアにはA肥料12袋・B肥料6袋を持っていく」という単位で計画・PDF出力できる。
機能スコープ
| IN(実装済み) | OUT(対象外) |
|---|---|
| 施肥計画を元に圃場をカスタムグループに割り当て | 購入管理 |
| グループ×肥料の集計表(画面表示) | 実施記録 |
| PDF出力(グループ合計行+圃場サブ行) | |
| グループの順序変更・名前変更 |
データモデル
DistributionPlan(分配計画)
| フィールド | 型 | 制約 | 説明 |
|---|---|---|---|
| id | int | PK | |
| fertilization_plan | FK(FertilizationPlan) | CASCADE | |
| name | varchar(200) | required | 計画名 |
| created_at / updated_at | datetime | auto |
ordering = ['-fertilization_plan__year', 'name']- 1つの施肥計画に対して複数の分配計画を作れる(OneToOneではなくFK)
DistributionGroup(分配グループ)
| フィールド | 型 | 制約 | 説明 |
|---|---|---|---|
| id | int | PK | |
| distribution_plan | FK(DistributionPlan) | CASCADE | |
| name | varchar(100) | required | グループ名 |
| order | PositiveIntegerField | default=0 | 表示順 |
unique_together = [['distribution_plan', 'name']]→ 同一計画内でグループ名重複不可ordering = ['order', 'id']
DistributionGroupField(グループ圃場割り当て)
| フィールド | 型 | 制約 | 説明 |
|---|---|---|---|
| id | int | PK | |
| distribution_plan | FK(DistributionPlan) | CASCADE | 一意制約のために冗長保持 |
| group | FK(DistributionGroup) | CASCADE | |
| field | FK(fields.Field) | PROTECT | 圃場 |
unique_together = [['distribution_plan', 'field']]→ 1圃場=1グループ/1計画ordering = ['field__display_order', 'field__id']
API エンドポイント
すべて JWT 認証(Authorization: Bearer <token>)。
| メソッド | 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) |
一覧レスポンス(DistributionPlanListSerializer)
{
"id": 1,
"name": "2025年コシヒカリ 分配計画",
"fertilization_plan_id": 3,
"fertilization_plan_name": "2025年コシヒカリ施肥計画",
"year": 2025,
"variety_name": "コシヒカリ",
"crop_name": "米",
"group_count": 3,
"field_count": 12,
"created_at": "...",
"updated_at": "..."
}
詳細レスポンス(DistributionPlanReadSerializer)
{
"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"}]
},
"groups": [
{
"id": 10,
"name": "田中エリア",
"order": 0,
"fields": [{"id": 5, "name": "田中上", "area_tan": "1.2000"}]
}
],
"unassigned_fields": [{"id": 7, "name": "未割り当て圃場", "area_tan": "0.5000"}]
}
書き込みリクエスト(POST/PUT)
{
"name": "2025年コシヒカリ 分配計画",
"fertilization_plan_id": 3,
"groups": [
{"name": "田中エリア", "order": 0, "field_ids": [5, 6]},
{"name": "奥地エリア", "order": 1, "field_ids": [7]}
]
}
PUT は groups を全削除→再作成する全置換方式。
フロントエンド画面
分配計画一覧 /distribution
- 年度セレクタ(
localStorage distributionYearで保持) - テーブル: 計画名・施肥計画・作物/品種・グループ数・圃場数
- アクション: PDF・編集・削除
- 削除エラー: インラインバナー(確認なし・失敗したらバナー表示)
分配計画編集 /distribution/new / /distribution/[id]/edit
共通コンポーネント: frontend/src/app/distribution/_components/DistributionEditPage.tsx
State構成
// 基本情報
const [name, setName] = useState('')
const [fertilizationPlanId, setFertilizationPlanId] = useState<number|''>('')
// 施肥計画詳細(施肥計画選択後に取得)
const [fertPlanDetail, setFertPlanDetail] = useState<DistributionPlan['fertilization_plan'] | null>(null)
// ローカルグループ(tempId で管理、保存時にサーバーへ送信)
const [groups, setGroups] = useState<LocalGroup[]>([])
// LocalGroup = { tempId: string, name: string, order: number, fieldIds: number[], isRenamingName?: string }
UI構成
- 計画基本情報: 計画名テキスト + 施肥計画セレクタ
- グループ割り当て:
- 新規グループ追加(名前入力 + 追加ボタン)
- グループカード(↑↓順序変更・鉛筆名前変更・×削除)
- グループ内圃場(×解除)+ 肥料別袋数をインライン表示
- 未割り当て圃場セクション(グループ選択ドロップダウンで割り当て)
- 集計プレビュー: グループ×肥料マトリクス(リアルタイム・サーバー通信なし)
PDF 出力
GET /api/fertilizer/distribution/{id}/pdf/
- WeasyPrint(既存施肥計画PDFと同じ仕組み)
- テンプレート:
backend/apps/fertilizer/templates/fertilizer/distribution_pdf.html - フォーマット: A4横向き
- 内容:
- ★グループ合計行(太字・緑背景)
- 圃場サブ行(小フォント・灰色背景)
- 肥料列合計・総合計
- ファイル名:
distribution_{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 登録
└── templates/fertilizer/
└── distribution_pdf.html # A4横 PDF テンプレート
Frontend
frontend/src/app/distribution/
├── page.tsx # 一覧ページ
├── new/page.tsx # 新規作成(ラッパー)
├── [id]/edit/page.tsx # 編集(ラッパー)
└── _components/DistributionEditPage.tsx # 編集共通コンポーネント
注意点
集計は全クライアントサイド計算
集計プレビューは API を呼ばず、fertPlanDetail.entries と groups.fieldIds からクライアントで計算する。
PDF生成時のみサーバーサイドで同じ計算を実施。
PUT の全置換方式
PUT 時は groups.all().delete() → 再作成。部分更新は非対応。
未割り当て圃場の扱い
- 施肥計画に含まれる圃場のうちグループに割り当てられていないものは「未割り当て」として表示
- PDF にも「未割り当て」グループとして出力される(ゼロの場合は出力なし)
エラー表示方針
施肥計画機能と同じく alert/confirm 廃止・インラインバナーに統一。