Files
keinasystem/document/14_マスタードキュメント_分配計画編.md
Akira 13c21ed7de ローカル更新済み:
13_マスタードキュメント_施肥計画編.md — 散布実績セクション整備、在庫連携・集計ルール・WorkRecord自動生成・前年度コピーのセクション追加、旧「散布確定モーダル」記述削除、型定義・ファイル構成・将来の拡張を更新
14_マスタードキュメント_分配計画編.md — 散布実績との連携・WorkRecord自動生成のセクション追加
CLAUDE.md — データモデル(SpreadingSession/Item, WorkRecord, actual_bags)追加、プロジェクト構造にfertilizer/workrecordsアプリ追加、実装状況に散布実績・作業記録索引を追記、更新履歴に2026-03-17エントリ追加
2026-03-17 20:31:22 +09:00

15 KiB
Raw Permalink Blame History

マスタードキュメント:運搬計画機能(旧・分配計画)

作成: 2026-03-02 最終更新: 2026-03-16 対象機能: 運搬計画(施肥計画の肥料を軽トラで運ぶ単位で計画・記録する) 実装状況: 本番稼働中


概要

施肥計画で決めた圃場ごとの肥料袋数を、軽トラ1回分の積載単位で運搬計画にまとめる機能。 実際の作業では一度に全部運べないため、「何回目にどのグループのどの肥料を何袋運ぶか」を計画・記録する。

旧設計(分配計画)からの変更理由

旧設計は「1つの施肥計画の圃場をグループ分けする」だけだった。 実運用で以下のギャップが判明2026-03-16

  1. 複数の施肥計画が混在する - 軽トラには品種をまたいで積む
  2. 単一の施肥計画が分割される - 1回で運びきれない
  3. 全肥料を一度に運ぶわけではない - 運ぶ肥料を指定する必要がある
  4. 圃場単位の合計袋数は不要 - グループ×肥料の合計が重要
  5. 同じグループの圃場を回ごとに分割する - 載りきらないときは次の回に
  6. 作業記録でもある - 運搬した日付を記録したい

機能スコープ

IN実装対象 OUT対象外
年度単位の運搬計画作成 購入管理
配送先グループへの圃場割り当て 肥料の在庫管理
運搬回ごとの圃場×肥料割り当て ルート最適化
回ごとの積載合計リアルタイム表示
圃場を回の間で移動する操作
「残り全部」一括割り当て
回ごとの運搬日記録
PDF出力回ごとに1ページ

データモデル

旧モデルからの移行

旧(削除) 新(追加) 備考
DistributionPlan DeliveryPlan FK(FertilizationPlan) 廃止 → year ベース
DistributionGroup DeliveryGroup ほぼ同等
DistributionGroupField DeliveryGroupField ほぼ同等
(なし) DeliveryTrip 新規:運搬回
(なし) DeliveryTripItem 新規:運搬明細(圃場×肥料単位)

DeliveryPlan運搬計画

フィールド 制約 説明
id int PK
year int required 年度
name varchar(200) required 計画名
created_at / updated_at datetime auto
  • ordering = ['-year', 'name']
  • 施肥計画への直接FK なし(年度ベースで全施肥計画を横断)

DeliveryGroup配送先グループ

フィールド 制約 説明
id int PK
delivery_plan FK(DeliveryPlan) CASCADE
name varchar(100) required グループ名(例: キウイ, 足川北)
order PositiveIntegerField default=0 表示順
  • unique_together = [['delivery_plan', 'name']]
  • ordering = ['order', 'id']

DeliveryGroupFieldグループ圃場割り当て

フィールド 制約 説明
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計画

DeliveryTrip運搬回

フィールド 制約 説明
id int PK
delivery_plan FK(DeliveryPlan) CASCADE
order PositiveIntegerField default=0 何回目(表示順)
name varchar(100) blank 任意の名前(例: "たちはるか電気炉さい"
date DateField nullable 運搬日(デフォルト: 1回目の日付を引き継ぎ
  • ordering = ['order', 'id']

DeliveryTripItem運搬明細

フィールド 制約 説明
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']]
  • bags は施肥計画の FertilizationEntry から自動計算で初期値を設定するが、手動上書きも可能

ER図概念

DeliveryPlan (運搬計画)
├── year, name
│
├── groups → DeliveryGroup (配送先グループ)
│   ├── name, order
│   └── fields → DeliveryGroupField → Field
│
└── trips → DeliveryTrip (運搬回)
    ├── order, name, date
    └── items → DeliveryTripItem
        ├── field → Field
        ├── fertilizer → Fertilizer
        └── bags

袋数の算出ルール

  1. 運搬計画作成時、年度の全 FertilizationEntry を参照して「グループ×肥料→圃場×袋数」を自動算出
  2. ユーザーが運搬回に圃場を割り当てると、該当する FertilizationEntry の bags が DeliveryTripItem.bags にコピーされる
  3. 手動で bags を上書きすることも可能(施肥計画との差異は許容)
  4. 「残り全部」操作: 施肥計画の合計 既に割り当て済みの回の合計 = 残り

API エンドポイント

すべて 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: グループに割り当てられていない圃場

書き込みリクエストPOST/PUT

{
  "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 を全削除→再作成する全置換方式。


フロントエンド画面

運搬計画一覧 /distribution

  • 年度セレクタ(localStorage distributionYear で保持)
  • テーブル: 計画名・グループ数・回数
  • アクション: PDF・編集・削除

運搬計画編集 /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回目の日付
対象肥料の絞り込み チェックボックス 選択した肥料だけ表示

積載合計のリアルタイム表示

各回のヘッダーに、その回の肥料ごとの合計袋数と総袋数を表示。 圃場を追加・削除するたびに即時再計算(サーバー通信なし)。


PDF 出力

GET /api/fertilizer/delivery/{id}/pdf/

フォーマット

  • WeasyPrint、A4横向き
  • 回ごとに1ページ1回目=1ページ目、2回目=2ページ目...

各ページの内容

━━━ 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}.pdf

ファイル構成(予定)

Backend

backend/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

frontend/src/app/distribution/
├── page.tsx                            # 一覧ページ
├── new/page.tsx                        # 新規作成(ラッパー)
├── [id]/edit/page.tsx                  # 編集(ラッパー)
└── _components/DeliveryEditPage.tsx    # 編集共通コンポーネント

マイグレーション方針

旧モデルDistribution*)の扱い

  1. 新モデルDelivery*)を追加するマイグレーションを作成
  2. 旧モデルDistribution*)は削除マイグレーションで除去
  3. 旧データは少量のため、データ移行は行わない(手動で再作成)

マイグレーション順序

  1. 000X_add_delivery_models.py - DeliveryPlan, DeliveryGroup, DeliveryGroupField, DeliveryTrip, DeliveryTripItem を追加
  2. 000Y_remove_distribution_models.py - DistributionPlan, DistributionGroup, DistributionGroupField を削除

注意点

施肥計画との関係

  • 運搬計画は施肥計画への直接FKを持たない
  • 年度ベースで、その年度の全 FertilizationEntry を参照して圃場×肥料の袋数を取得する
  • 施肥計画を変更すると、未割り当ての圃場の袋数は自動で反映される
  • 既に運搬回に割り当て済みの DeliveryTripItem.bags は変わらない(コピー済み)

集計はクライアントサイド計算

画面上の集計(グループ小計・回の積載合計)は API を呼ばずクライアントで計算。 PDF生成時のみサーバーサイドで同じ計算を実施。

エラー表示方針

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

散布実績との連携

  • 運搬計画の DeliveryTripItem が散布実績画面(/fertilizer/spreading)の候補データソースとなる
  • DeliveryTrip.date != null の明細のみを「運搬済み」とみなし、散布候補に含める
  • 散布実績画面から運搬計画を指定して遷移する場合(?delivery_plan_id=N)、日付フィルタは適用されない(その計画の全明細が候補になる)
  • 散布実績の保存時に在庫 USE が作成される(運搬時点では在庫変動なし)

WorkRecord 自動生成

  • DeliveryTrip に日付が保存されると、WorkRecordwork_type=fertilizer_delivery)が自動生成される
  • 実装: apps/workrecords/services.pysync_delivery_work_record()
  • DeliveryTrip の日付が削除されると、対応する WorkRecord も削除される
  • WorkRecord は索引として機能し、明細データは DeliveryTrip / DeliveryTripItem 側が保持する