在庫管理機能実装案

This commit is contained in:
Akira
2026-03-13 13:13:40 +09:00
parent 7825f0eb30
commit f74dc4c4b7

View File

@@ -0,0 +1,616 @@
# 在庫管理機能実装案
> 作成日: 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層で整理する。
1. 共通資材層
2. 種別別の詳細層
3. 在庫履歴層
### 4.2 推奨モデル構成
#### Material
資材共通マスタ。在庫管理の主キーになる。
想定フィールド:
- `id`
- `name` 資材名
- `material_type` 資材種別
- `maker` メーカー
- `stock_unit` 在庫単位
- `is_active` 使用可否
- `notes` 備考
- `created_at`
- `updated_at`
`material_type` の候補:
- `fertilizer`
- `pesticide`
- `seedling`
- `other`
`stock_unit` の候補:
- `bag`
- `bottle`
- `kg`
- `liter`
- `piece`
#### FertilizerProfile
肥料専用の属性を持つ。
想定フィールド:
- `id`
- `material` `OneToOneField(Material)`
- `capacity_kg` 1袋重量
- `nitrogen_pct`
- `phosphorus_pct`
- `potassium_pct`
- `dilution_note` 任意、必要なら
#### PesticideProfile
農薬専用の属性を持つ。
想定フィールド:
- `id`
- `material` `OneToOneField(Material)`
- `registration_no` 農薬登録番号
- `formulation` 剤型
- `usage_unit` 使用単位
- `dilution_ratio` 希釈倍率
- `active_ingredient` 有効成分
- `category` 殺菌剤、除草剤、殺虫剤など
- `remark_for_use` 使用上メモ
#### StockTransaction
在庫の増減を管理する履歴テーブル。現在庫はこの集計で算出する。
想定フィールド:
- `id`
- `material` `ForeignKey(Material)`
- `transaction_type`
- `quantity`
- `unit`
- `occurred_on`
- `reference_type`
- `reference_id`
- `note`
- `created_by`
- `created_at`
`transaction_type` の候補:
- `purchase`
- `use`
- `adjustment_plus`
- `adjustment_minus`
- `inventory_count`
- `discard`
`quantity` は正の数で持ち、増減方向は `transaction_type` で判定する方式を推奨する。
#### MaterialStockSnapshot
初期フェーズでは不要。パフォーマンス課題が出た場合のみ追加する。
- 日次や月次の在庫スナップショット
- 一覧高速化用の集計テーブル
---
## 5. Django モデル案
以下はイメージであり、実装時には既存 app 構成に合わせて調整する。
```python
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/materials`
- `backend/apps/inventory`
推奨:
- 共通マスタも在庫履歴も含めて `backend/apps/materials`
理由:
- 肥料専用 app に農薬や種苗を混ぜないため
- 将来の資材計画、散布履歴、購入管理の拡張がしやすいため
推奨ファイル構成:
```text
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}/`
レスポンス例:
```json
{
"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` だけ返す
- 必要に応じて現在庫も返す
レスポンス例:
```json
[
{
"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` を作成
- 既存 `Fertilizer` 1件ごとに `Material(material_type='fertilizer')` を作成
- `Fertilizer.material` を紐づける migration を追加
### フェーズ2
- 在庫一覧画面と入出庫登録画面を実装
- 施肥計画画面で肥料在庫参照を追加
### フェーズ3
- `PesticideProfile` を追加
- 農薬マスタ管理画面を実装
- 将来の農薬散布計画または散布履歴と連携
### データ移行時の注意
- `Fertilizer.name``Material.name` の不整合を防ぐ
- 肥料削除制約と `Material` の削除制約を揃える
- 既存 API を壊さない
- 既存フロント型定義を急に置き換えない
---
## 12. 画面・業務フロー案
### 12.1 資材登録
1. 資材種別を選ぶ
2. 共通項目を入力する
3. 肥料なら肥料専用項目を入力する
4. 農薬なら農薬専用項目を入力する
### 12.2 在庫登録
1. 資材を選ぶ
2. 入庫 or 出庫 or 棚卸調整を選ぶ
3. 数量と日付を入力する
4. メモを残す
### 12.3 施肥計画での利用
1. 従来どおり肥料を選ぶ
2. 肥料ごとの在庫を横に表示する
3. 計画全体の必要量と現在庫の差分を見る
4. 必要なら購入判断につなげる
---
## 13. 採用しない案
### 13.1 単一テーブル案
`Fertilizer``Material` に改名し、肥料も農薬も1テーブルで管理する案。
採用しない理由:
- nullable 項目が増えすぎる
- `material_type` 条件分岐が画面や API に広がる
- 施肥機能に農薬が混ざりやすい
- モデル名と実務概念がずれて保守性が落ちる
### 13.2 完全分離案
肥料在庫テーブルと農薬在庫テーブルを別々に作る案。
採用しない理由:
- 入出庫、棚卸、在庫集計ロジックを二重実装することになる
- 将来の資材種追加に弱い
- 一覧画面や集計の統一感がなくなる
---
## 14. 推奨実装順
最も安全な進め方は以下である。
1. `apps/materials` を追加する
2. `Material``StockTransaction` を実装する
3. 既存 `Fertilizer``material` を紐づける
4. 肥料だけを対象に在庫一覧を表示する
5. 施肥計画画面に在庫参照を追加する
6. その後、農薬マスタと農薬在庫を追加する
---
## 15. 最終提案
本案件では、次の方針を正式案とすることを推奨する。
- 在庫管理の主軸は `Material``StockTransaction`
- 肥料と農薬は詳細テーブルで分ける
- 施肥計画は既存 `Fertilizer` 中心のまま維持する
- まずは「在庫参照」まで実装し、自動出庫連携は後続フェーズに回す
この方針であれば、既存の施肥計画機能を壊さず、農薬対応と将来の資材拡張に耐える構成になる。
---
## 16. 次アクション案
次に着手するなら、以下の順で進めるとよい。
1. `apps/materials` の Django モデルを確定する
2. migration 方針を決める
3. API 設計を確定する
4. フロントの画面導線を決める
5. 施肥計画画面に出す在庫情報の粒度を決める
必要であれば次の段階として、上記案をベースに
- Django モデル実装
- migration 作成方針
- API 詳細仕様
- 画面ワイヤー案
まで具体化できる。