分配計画機能を実装
施肥計画の圃場を配置場所単位でグループ化し、グループ×肥料の集計表を 表示・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>
This commit is contained in:
241
document/14_マスタードキュメント_分配計画編.md
Normal file
241
document/14_マスタードキュメント_分配計画編.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# マスタードキュメント:分配計画機能
|
||||
|
||||
> **作成**: 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)
|
||||
|
||||
```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<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構成
|
||||
|
||||
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 廃止・インラインバナーに統一。
|
||||
Reference in New Issue
Block a user