Files
keinasystem/document/14_マスタードキュメント_分配計画編.md
Akira 466eef128c 分配計画機能を実装
施肥計画の圃場を配置場所単位でグループ化し、グループ×肥料の集計表を
表示・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>
2026-03-02 09:43:20 +09:00

8.0 KiB
Raw Blame History

マスタードキュメント:分配計画機能

作成: 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構成

  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.entriesgroups.fieldIds からクライアントで計算する。 PDF生成時のみサーバーサイドで同じ計算を実施。

PUT の全置換方式

PUT 時は groups.all().delete() → 再作成。部分更新は非対応。

未割り当て圃場の扱い

  • 施肥計画に含まれる圃場のうちグループに割り当てられていないものは「未割り当て」として表示
  • PDF にも「未割り当て」グループとして出力される(ゼロの場合は出力なし)

エラー表示方針

施肥計画機能と同じく alert/confirm 廃止・インラインバナーに統一。