# マスタードキュメント:分配計画機能 > **作成**: 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 `)。 | メソッド | 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) ```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, "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"}] }, "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) ```json { "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構成 ```typescript // 基本情報 const [name, setName] = useState('') const [fertilizationPlanId, setFertilizationPlanId] = useState('') // 施肥計画詳細(施肥計画選択後に取得) const [fertPlanDetail, setFertPlanDetail] = useState(null) // ローカルグループ(tempId で管理、保存時にサーバーへ送信) const [groups, setGroups] = useState([]) // LocalGroup = { tempId: string, name: string, order: number, fieldIds: number[], isRenamingName?: string } ``` #### UI構成 1. **計画基本情報**: 計画名テキスト + 施肥計画セレクタ 2. **グループ割り当て**: - 新規グループ追加(名前入力 + 追加ボタン) - グループカード(↑↓順序変更・鉛筆名前変更・×削除) - グループ内圃場(×解除)+ 肥料別袋数をインライン表示 - 未割り当て圃場セクション(グループ選択ドロップダウンで割り当て) 3. **集計プレビュー**: グループ×肥料マトリクス(リアルタイム・サーバー通信なし) --- ## 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 廃止・インラインバナーに統一。