16 KiB
在庫管理機能実装案
作成日: 2026-03-13 対象プロジェクト:
keinasystem_t02目的: 肥料と農薬を含む資材在庫管理機能を追加し、施肥計画から参照できるようにする
1. 結論
本プロジェクトの在庫管理機能は、以下の構成で実装することを推奨する。
- 共通の資材マスタ
Materialを新設する - 肥料専用情報は
FertilizerProfileとして分離する - 農薬専用情報は
PesticideProfileとして分離する - 在庫数は共通の入出庫履歴
StockTransactionで管理する - 施肥計画は従来どおり「肥料のみ」を対象とし、農薬は混在させない
つまり、設計方針は「共通化するのは在庫管理の土台だけ、業務利用は肥料と農薬で分ける」である。
2. 背景
既存実装では、backend/apps/fertilizer/models.py の Fertilizer モデルが以下の役割を持っている。
- 肥料マスタ
- 施肥計画エントリの参照先
- 窒素量計算の対象
- 分配計画の集計対象
特に、以下の点から Fertilizer はすでに肥料専用モデルになっている。
capacity_kg,nitrogen_pct,phosphorus_pct,potassium_pctを持つ- 施肥計画
FertilizationEntryがFertilizerを直接参照している - 自動計算 API が
Fertilizerの窒素成分を使っている
このため、既存の Fertilizer を単純に「資材」へ拡張すると、以下の問題が起きやすい。
- 農薬に不要な肥料属性が大量にぶら下がる
- 施肥画面で農薬が見えてしまう
- 今後の農薬散布機能で肥料向けロジックが混ざる
- フロントエンドと API の条件分岐が増える
3. 要件整理
今回の要件は大きく分けて次の2つである。
3.1 共通でほしいこと
- 肥料と農薬を「在庫」という観点で一元管理したい
- 入庫、出庫、棚卸、調整の履歴を残したい
- 現在庫を一覧で見たい
- 将来的に種苗など他資材にも拡張できるようにしたい
3.2 分けておきたいこと
- 施肥計画では肥料だけを使いたい
- 農薬散布計画や散布履歴では農薬だけを使いたい
- 肥料特有の計算項目と農薬特有の項目は混在させたくない
4. 推奨アーキテクチャ
4.1 モデル分離の考え方
以下の3層で整理する。
- 共通資材層
- 種別別の詳細層
- 在庫履歴層
4.2 推奨モデル構成
Material
資材共通マスタ。在庫管理の主キーになる。
想定フィールド:
idname資材名material_type資材種別makerメーカーstock_unit在庫単位is_active使用可否notes備考created_atupdated_at
material_type の候補:
fertilizerpesticideseedlingother
stock_unit の候補:
bagbottlekgliterpiece
FertilizerProfile
肥料専用の属性を持つ。
想定フィールド:
idmaterialOneToOneField(Material)capacity_kg1袋重量nitrogen_pctphosphorus_pctpotassium_pctdilution_note任意、必要なら
PesticideProfile
農薬専用の属性を持つ。
想定フィールド:
idmaterialOneToOneField(Material)registration_no農薬登録番号formulation剤型usage_unit使用単位dilution_ratio希釈倍率active_ingredient有効成分category殺菌剤、除草剤、殺虫剤などremark_for_use使用上メモ
StockTransaction
在庫の増減を管理する履歴テーブル。現在庫はこの集計で算出する。
想定フィールド:
idmaterialForeignKey(Material)transaction_typequantityunitoccurred_onreference_typereference_idnotecreated_bycreated_at
transaction_type の候補:
purchaseuseadjustment_plusadjustment_minusinventory_countdiscard
quantity は正の数で持ち、増減方向は transaction_type で判定する方式を推奨する。
MaterialStockSnapshot
初期フェーズでは不要。パフォーマンス課題が出た場合のみ追加する。
- 日次や月次の在庫スナップショット
- 一覧高速化用の集計テーブル
5. Django モデル案
以下はイメージであり、実装時には既存 app 構成に合わせて調整する。
from django.conf import settings
from django.db import models
class Material(models.Model):
class MaterialType(models.TextChoices):
FERTILIZER = 'fertilizer', '肥料'
PESTICIDE = 'pesticide', '農薬'
SEEDLING = 'seedling', '種苗'
OTHER = 'other', 'その他'
class StockUnit(models.TextChoices):
BAG = 'bag', '袋'
BOTTLE = 'bottle', '本'
KG = 'kg', 'kg'
LITER = 'liter', 'L'
PIECE = 'piece', '個'
name = models.CharField(max_length=100, verbose_name='資材名')
material_type = models.CharField(max_length=20, choices=MaterialType.choices, verbose_name='資材種別')
maker = models.CharField(max_length=100, blank=True, null=True, verbose_name='メーカー')
stock_unit = models.CharField(max_length=20, choices=StockUnit.choices, verbose_name='在庫単位')
is_active = models.BooleanField(default=True, verbose_name='使用中')
notes = models.TextField(blank=True, null=True, verbose_name='備考')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['material_type', 'name']
constraints = [
models.UniqueConstraint(fields=['material_type', 'name'], name='uniq_material_type_name'),
]
class FertilizerProfile(models.Model):
material = models.OneToOneField(Material, on_delete=models.CASCADE, related_name='fertilizer_profile')
capacity_kg = models.DecimalField(max_digits=8, decimal_places=3, blank=True, null=True)
nitrogen_pct = models.DecimalField(max_digits=5, decimal_places=2, blank=True, null=True)
phosphorus_pct = models.DecimalField(max_digits=5, decimal_places=2, blank=True, null=True)
potassium_pct = models.DecimalField(max_digits=5, decimal_places=2, blank=True, null=True)
class PesticideProfile(models.Model):
material = models.OneToOneField(Material, on_delete=models.CASCADE, related_name='pesticide_profile')
registration_no = models.CharField(max_length=100, blank=True, null=True)
formulation = models.CharField(max_length=100, blank=True, null=True)
usage_unit = models.CharField(max_length=50, blank=True, null=True)
dilution_ratio = models.CharField(max_length=100, blank=True, null=True)
active_ingredient = models.CharField(max_length=200, blank=True, null=True)
category = models.CharField(max_length=100, blank=True, null=True)
class StockTransaction(models.Model):
class TransactionType(models.TextChoices):
PURCHASE = 'purchase', '入庫'
USE = 'use', '使用'
ADJUSTMENT_PLUS = 'adjustment_plus', '棚卸増'
ADJUSTMENT_MINUS = 'adjustment_minus', '棚卸減'
INVENTORY_COUNT = 'inventory_count', '棚卸記録'
DISCARD = 'discard', '廃棄'
material = models.ForeignKey(Material, on_delete=models.PROTECT, related_name='stock_transactions')
transaction_type = models.CharField(max_length=30, choices=TransactionType.choices)
quantity = models.DecimalField(max_digits=10, decimal_places=3)
occurred_on = models.DateField()
note = models.TextField(blank=True, null=True)
created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
created_at = models.DateTimeField(auto_now_add=True)
6. 既存肥料機能とのつなぎ方
6.1 重要方針
既存の Fertilizer モデルをすぐに削除したり全面改名したりしない。
理由:
- 施肥計画機能がすでに本番稼働中
FertilizationEntryがFertilizerを直接参照している- 分配計画、PDF、計算 API まで連鎖的に影響する
6.2 移行方針
段階的には次のどちらかが現実的である。
案A: 既存 Fertilizer を残しつつ Material を追加する
推奨度: 高
構成:
Materialを新設- 既存
Fertilizerにmaterial = OneToOneField(Material)を追加 - 施肥計画は当面
Fertilizerを使い続ける - 在庫管理は
Materialを使う
メリット:
- 既存施肥機能への影響が最小
- 農薬機能をあとから追加しやすい
- データ移行が安全
デメリット:
- 一時的に
MaterialとFertilizerの二重管理に見える
案B: Fertilizer を廃止して Material + FertilizerProfile に完全移行する
推奨度: 中
構成:
Fertilizerの役割をMaterial + FertilizerProfileに移すFertilizationEntryはFertilizerProfileまたはMaterialを参照するよう変更
メリット:
- モデル構造がきれいになる
デメリット:
- 既存コード改修範囲が大きい
- 移行難易度が高い
- 本番稼働機能への影響が大きい
現時点では案Aを採用し、運用が安定してから必要なら案Bへ寄せるのがよい。
7. 推奨する app 構成
在庫管理を apps/fertilizer に混ぜ込まず、新規 app として切り出すことを推奨する。
候補:
backend/apps/materialsbackend/apps/inventory
推奨:
- 共通マスタも在庫履歴も含めて
backend/apps/materials
理由:
- 肥料専用 app に農薬や種苗を混ぜないため
- 将来の資材計画、散布履歴、購入管理の拡張がしやすいため
推奨ファイル構成:
backend/apps/materials/
├── models.py
├── serializers.py
├── views.py
├── urls.py
├── admin.py
└── migrations/
8. API 実装案
8.1 資材マスタ API
GET /api/materials/materials/POST /api/materials/materials/GET /api/materials/materials/{id}/PUT /api/materials/materials/{id}/DELETE /api/materials/materials/{id}/
主なクエリ:
?material_type=fertilizer?material_type=pesticide?active=true
8.2 在庫履歴 API
GET /api/materials/stock-transactions/POST /api/materials/stock-transactions/GET /api/materials/stock-transactions/{id}/
主なクエリ:
?material_id=?material_type=?date_from=?date_to=
8.3 在庫集計 API
GET /api/materials/stock-summary/GET /api/materials/stock-summary/{material_id}/
レスポンス例:
{
"material_id": 12,
"name": "コシヒカリ専用一発肥料",
"material_type": "fertilizer",
"stock_unit": "bag",
"current_stock": "18.00",
"last_transaction_date": "2026-03-10"
}
8.4 施肥計画からの参照 API
施肥計画画面では、以下のような肥料在庫参照 API を追加する。
GET /api/materials/fertilizer-stock-options/
用途:
- 施肥計画で使う肥料一覧を返す
material_type=fertilizerだけ返す- 必要に応じて現在庫も返す
レスポンス例:
[
{
"material_id": 12,
"fertilizer_id": 3,
"name": "コシヒカリ専用一発肥料",
"current_stock": "18.00",
"stock_unit": "bag"
}
]
ここで重要なのは、施肥計画画面に農薬を返さない専用 API を用意することである。
9. フロントエンド実装案
9.1 新規画面
/materials- 資材一覧
/materials/fertilizers- 肥料マスタ管理
/materials/pesticides- 農薬マスタ管理
/materials/stock- 在庫一覧
/materials/stock/new- 入出庫登録
9.2 一覧画面の基本列
- 資材名
- 種別
- メーカー
- 在庫単位
- 現在庫
- 最終入出庫日
- 使用中フラグ
9.3 施肥計画画面での使い方
既存の肥料選択 UI は維持しつつ、以下の補助情報だけ追加する。
- 肥料名の横に現在庫を表示
- 在庫不足が見込まれる場合は注意表示
- 必要であれば、計画合計と在庫差分を表示
例:
コシヒカリ専用一発肥料 在庫18袋計画必要数 24袋 / 不足 6袋
これにより、施肥計画の UX を壊さずに在庫参照を実現できる。
10. 在庫計算ロジック
10.1 基本ルール
現在庫は StockTransaction の累積で求める。
計算式:
現在庫 = 入庫合計 - 出庫合計 + 調整増合計 - 調整減合計
10.2 推奨ルール
- 在庫は更新値を直接持たず、履歴を正とする
- 誤登録時は履歴修正または取消履歴で対応する
- 初期在庫は
adjustment_plusで登録する - 棚卸差異は
adjustment_plus/adjustment_minusで表現する
10.3 将来連携
将来的には以下と自動連携できる。
- 施肥計画確定時に肥料必要量を引当
- 施肥実績登録時に肥料を出庫
- 農薬散布実績登録時に農薬を出庫
ただし初期フェーズでは「計画から参照のみ」に留め、在庫自動減算はまだ行わない方が安全である。
11. データ移行案
フェーズ1
apps/materialsを追加MaterialとStockTransactionを作成- 既存
Fertilizer1件ごとにMaterial(material_type='fertilizer')を作成 Fertilizer.materialを紐づける migration を追加
フェーズ2
- 在庫一覧画面と入出庫登録画面を実装
- 施肥計画画面で肥料在庫参照を追加
フェーズ3
PesticideProfileを追加- 農薬マスタ管理画面を実装
- 将来の農薬散布計画または散布履歴と連携
データ移行時の注意
Fertilizer.nameとMaterial.nameの不整合を防ぐ- 肥料削除制約と
Materialの削除制約を揃える - 既存 API を壊さない
- 既存フロント型定義を急に置き換えない
12. 画面・業務フロー案
12.1 資材登録
- 資材種別を選ぶ
- 共通項目を入力する
- 肥料なら肥料専用項目を入力する
- 農薬なら農薬専用項目を入力する
12.2 在庫登録
- 資材を選ぶ
- 入庫 or 出庫 or 棚卸調整を選ぶ
- 数量と日付を入力する
- メモを残す
12.3 施肥計画での利用
- 従来どおり肥料を選ぶ
- 肥料ごとの在庫を横に表示する
- 計画全体の必要量と現在庫の差分を見る
- 必要なら購入判断につなげる
13. 採用しない案
13.1 単一テーブル案
Fertilizer を Material に改名し、肥料も農薬も1テーブルで管理する案。
採用しない理由:
- nullable 項目が増えすぎる
material_type条件分岐が画面や API に広がる- 施肥機能に農薬が混ざりやすい
- モデル名と実務概念がずれて保守性が落ちる
13.2 完全分離案
肥料在庫テーブルと農薬在庫テーブルを別々に作る案。
採用しない理由:
- 入出庫、棚卸、在庫集計ロジックを二重実装することになる
- 将来の資材種追加に弱い
- 一覧画面や集計の統一感がなくなる
14. 推奨実装順
最も安全な進め方は以下である。
apps/materialsを追加するMaterialとStockTransactionを実装する- 既存
Fertilizerにmaterialを紐づける - 肥料だけを対象に在庫一覧を表示する
- 施肥計画画面に在庫参照を追加する
- その後、農薬マスタと農薬在庫を追加する
15. 最終提案
本案件では、次の方針を正式案とすることを推奨する。
- 在庫管理の主軸は
MaterialとStockTransaction - 肥料と農薬は詳細テーブルで分ける
- 施肥計画は既存
Fertilizer中心のまま維持する - まずは「在庫参照」まで実装し、自動出庫連携は後続フェーズに回す
この方針であれば、既存の施肥計画機能を壊さず、農薬対応と将来の資材拡張に耐える構成になる。
16. 次アクション案
次に着手するなら、以下の順で進めるとよい。
apps/materialsの Django モデルを確定する- migration 方針を決める
- API 設計を確定する
- フロントの画面導線を決める
- 施肥計画画面に出す在庫情報の粒度を決める
必要であれば次の段階として、上記案をベースに
- Django モデル実装
- migration 作成方針
- API 詳細仕様
- 画面ワイヤー案
まで具体化できる。