diff --git a/改善案/在庫管理機能実装案.md b/改善案/在庫管理機能実装案.md new file mode 100644 index 0000000..aaa4c76 --- /dev/null +++ b/改善案/在庫管理機能実装案.md @@ -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 詳細仕様 +- 画面ワイヤー案 + +まで具体化できる。