# 画面設計書 25-在庫アイテム詳細画面

## 概要

本ドキュメントは、InvenTree Flutterアプリケーションにおける「在庫アイテム詳細画面」の設計仕様を定義する。在庫アイテムの詳細情報を表示し、数量調整、移動、テスト結果管理など多岐にわたる在庫操作を提供する画面である。

### 本画面の処理概要

在庫アイテム詳細画面は、個別の在庫アイテム（StockItem）に関するすべての詳細情報を表示し、在庫管理に必要な操作を提供する中核的な画面である。ロケーション、数量、ステータス、履歴、テスト結果など、在庫管理に必要な情報と機能を一元的に提供する。

**業務上の目的・背景**：製造業や倉庫管理において、個々の在庫アイテムの状態を正確に把握し操作することは極めて重要である。この画面は、在庫の数量調整（追加/削除/カウント）、ロケーション移動、テスト結果記録など、日常的な在庫管理業務を支援する。シリアル番号管理対応により、個別追跡が必要な部品も適切に管理できる。

**画面へのアクセス方法**：
- 在庫アイテム一覧画面から在庫をタップして遷移
- 在庫ロケーション画面から在庫をタップして遷移
- 部品詳細画面の在庫タブから在庫をタップ
- バーコードスキャンで在庫を識別した際に遷移
- 購入注文詳細画面の受領済みタブから遷移

**主要な操作・処理内容**：
1. 在庫基本情報の表示（部品名、ロケーション、数量、ステータス）
2. 数量調整（追加、削除、カウント）- 非シリアル化アイテムのみ
3. ロケーション移動（転送）
4. 在庫アイテム編集
5. 在庫アイテム削除
6. テスト結果の表示・記録
7. 履歴（トラッキング）の表示
8. ノートの表示・編集
9. 添付ファイルの表示
10. ラベル印刷
11. バーコードスキャンによるロケーション移動
12. バーコード割当/解除
13. 位置特定（locate機能）

**画面遷移**：
- 遷移元：在庫アイテム一覧、在庫ロケーション、部品詳細、バーコードスキャン、購入注文詳細
- 遷移先：部品詳細画面、ロケーション詳細画面、テスト結果画面、履歴画面、ノート画面、添付ファイル画面、販売注文詳細画面、会社詳細画面、サプライヤー部品詳細画面

**権限による表示制御**：
- 編集ボタンは `widget.item.canEdit` がtrueの場合のみ表示
- 数量調整・移動ボタンは `widget.item.canEdit` がtrueの場合のみ表示
- 削除ボタンは `widget.item.canDelete` がtrueの場合のみ表示
- ラベル印刷は設定で有効化されている場合のみ表示

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 15 | 在庫アイテム詳細表示 | 主機能 | 在庫アイテムの詳細情報表示 |
| 17 | 在庫数量調整 | 補助機能 | 追加/削除/カウントボタン押下時に数量調整フォーム表示 |
| 18 | 在庫移動 | 補助機能 | 転送ボタン押下時にロケーション移動フォーム表示 |
| 19 | 在庫アイテム編集 | 補助機能 | 編集ボタン押下時に在庫情報編集フォーム表示 |
| 20 | 在庫アイテム削除 | 補助機能 | 削除ボタン押下時に削除確認ダイアログ表示 |
| 21 | 在庫履歴表示 | 遷移先機能 | 履歴項目押下時に履歴画面へ遷移 |
| 22 | テスト結果表示 | 遷移先機能 | テスト結果項目押下時にテスト結果画面へ遷移 |
| 44 | バーコード割当 | 補助機能 | バーコード割当ボタン押下時にバーコードスキャン開始 |
| 46 | 在庫スキャン操作 | 補助機能 | バーコードスキャンでロケーションへ移動 |
| 53 | ラベル印刷 | 補助機能 | ラベル印刷ボタン押下時に印刷処理実行 |
| 54 | 添付ファイル表示 | 補助機能 | 添付ファイル項目押下時に添付ファイル一覧表示 |
| 56 | ノート表示・編集 | 遷移先機能 | ノート項目押下時にノート画面へ遷移 |
| 69 | アイテム位置特定 | 補助機能 | 位置特定ボタン押下時にアイテムの物理的位置を特定 |
| 5 | パーツ詳細表示 | 遷移先機能 | パーツ項目押下時にパーツ詳細画面へ遷移 |

## 画面種別

詳細

## URL/ルーティング

Flutter Navigator によるプッシュ遷移。`StockDetailWidget` に `InvenTreeStockItem` インスタンスを渡して表示。

## 入出力項目

| 項目名 | 入出力 | 型 | 必須 | 説明 |
|--------|--------|------|------|------|
| item | 入力 | InvenTreeStockItem | Yes | 表示対象の在庫アイテムインスタンス |

## 表示項目

### ヘッダータイル

| 項目名 | 表示位置 | データ型 | 説明 |
|--------|----------|----------|------|
| 部品サムネイル | Card.leading | Image | タップで部品詳細へ |
| 部品名 | Card.title | String | 紐づく部品のフルネーム |
| 部品説明 | Card.subtitle | String | 部品の説明 |
| 数量/利用不可 | Card.trailing | String | 数量（ステータス色）または"Unavailable" |

### 詳細タイル

| 項目名 | 表示位置 | データ型 | 表示条件 |
|--------|----------|----------|---------|
| ロケーション | ListTile | リンク | locationId > 0 |
| ロケーション未設定 | ListTile | テキスト | locationId <= 0 |
| デフォルトロケーション | ListTile | リンク | defaultLocation != null |
| シリアル番号 | ListTile | テキスト | isSerialized() |
| 数量 | ListTile | テキスト | !isSerialized() かつ isInStock |
| 利用不可警告 | ListTile | テキスト | !isInStock |
| ステータス | ListTile | テキスト | 常時（色付き） |
| サプライヤー部品 | ListTile | リンク | supplierPartId > 0 |
| 製造中 | ListTile | テキスト | isBuilding |
| 販売注文 | ListTile | リンク | hasSalesOrder |
| 顧客 | ListTile | リンク | hasCustomer |
| バッチコード | ListTile | テキスト | batch.isNotEmpty |
| パッケージング | ListTile | テキスト | packaging.isNotEmpty |
| 有効期限 | ListTile | テキスト | expiryEnabled かつ expiryDate != null |
| 最終更新日 | ListTile | テキスト | updatedDateString.isNotEmpty |
| 棚卸日 | ListTile | テキスト | stocktakeDateString.isNotEmpty |
| 外部リンク | ListTile | リンク | hasLink |
| テスト結果 | ListTile | リンク | stockShowTests または testResultCount > 0 |
| 購入価格 | ListTile | テキスト | hasPurchasePrice |
| 履歴 | ListTile | リンク | stockShowHistory かつ trackingItemCount > 0 |
| ノート | ListTile | リンク | 常時 |
| 添付ファイル | ListTile | リンク | attachmentCount > 0 |

## イベント仕様

### 1-在庫編集ボタン押下

アプリバーの編集アイコンをタップすると、在庫編集フォームが表示される。

**処理フロー**：
1. `IconButton.onPressed` で `_editStockItem(context)` を呼び出し
2. `widget.item.editForm()` でAPIフォームを表示（part, quantity, location, serial_numbersは除外）
3. 成功時に `refresh(context)` と "Stock item updated" スナックバー表示

### 2-位置特定ボタン押下

アプリバーの位置特定アイコンをタップすると、物理的なアイテム位置を特定する。

**処理フロー**：
1. `IconButton.onPressed` で `api.locateItemOrLocation(context, item: widget.item.pk)` を呼び出し
2. サーバーがlocate mixinをサポートしている場合のみ表示

### 3-数量追加

SpeedDialの「追加」ボタンをタップすると、数量追加フォームが表示される。

**処理フロー**：
1. `SpeedDialChild.onTap` で `_addStockDialog()` を呼び出し
2. `launchApiForm()` で追加フォームを表示（URL: stock/add/）
3. 成功時に `refresh(context)` と更新メッセージ表示

### 4-数量削除

SpeedDialの「削除」ボタンをタップすると、数量削除フォームが表示される。

**処理フロー**：
1. `SpeedDialChild.onTap` で `_removeStockDialog()` を呼び出し
2. `launchApiForm()` で削除フォームを表示（URL: stock/remove/）
3. 成功時に `refresh(context)` と更新メッセージ表示

### 5-数量カウント

SpeedDialの「カウント」ボタンをタップすると、棚卸フォームが表示される。

**処理フロー**：
1. `SpeedDialChild.onTap` で `_countStockDialog()` を呼び出し
2. `launchApiForm()` でカウントフォームを表示（URL: stock/count/）
3. 成功時に `refresh(context)` と更新メッセージ表示

### 6-在庫移動

SpeedDialの「転送」ボタンをタップすると、ロケーション移動フォームが表示される。

**処理フロー**：
1. `SpeedDialChild.onTap` で `_transferStockDialog(context)` を呼び出し
2. `launchApiForm()` で転送フォームを表示（URL: stock/transfer/）
3. 成功時に `refresh(context)` と更新メッセージ表示

### 7-在庫削除

SpeedDialの「削除」ボタンをタップすると、削除確認ダイアログが表示される。

**処理フロー**：
1. `SpeedDialChild.onTap` で `_deleteItem(context)` を呼び出し
2. `confirmationDialog()` で確認ダイアログ表示
3. 承認時に `widget.item.delete()` を呼び出し
4. 成功時に `Navigator.pop()` で画面を閉じ、成功メッセージ表示

### 8-バーコードスキャン（ロケーション移動）

SpeedDialのバーコードスキャンボタンをタップすると、ロケーションスキャンモードが起動する。

**処理フロー**：
1. `SpeedDialChild.onTap` で `scanBarcode()` を呼び出し
2. `StockItemScanIntoLocationHandler(widget.item)` をハンドラとして設定
3. スキャン完了後に `refresh(context)` で再読み込み

### 9-部品タップ

ヘッダーの部品情報をタップすると、部品詳細画面へ遷移する。

**処理フロー**：
1. `ListTile.onTap` が発火
2. `InvenTreePart().get(widget.item.partId)` で部品を取得
3. `part.goToDetailPage(context)` で遷移

### 10-ロケーションタップ

ロケーションのListTileをタップすると、ロケーション詳細画面へ遷移する。

**処理フロー**：
1. `ListTile.onTap` が発火
2. `InvenTreeStockLocation().get(widget.item.locationId)` でロケーション取得
3. `loc.goToDetailPage(context)` で遷移

## データベース更新仕様

### 操作別データベース影響一覧

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| 画面表示 | StockItem | SELECT | 在庫アイテム情報の取得 |
| 画面表示 | Part | SELECT | 紐づく部品情報の取得 |
| 画面表示 | Attachment | SELECT | 添付ファイル数の取得 |
| 画面表示 | SalesOrder | SELECT | 紐づく販売注文の取得 |
| 在庫編集 | StockItem | UPDATE | 在庫アイテム情報の更新 |
| 数量追加 | StockItem | UPDATE | 数量の増加、履歴記録 |
| 数量削除 | StockItem | UPDATE | 数量の減少、履歴記録 |
| 数量カウント | StockItem | UPDATE | 数量の確定、履歴記録 |
| 在庫移動 | StockItem | UPDATE | ロケーションの変更、履歴記録 |
| 在庫削除 | StockItem | DELETE | 在庫アイテムの削除 |
| バーコード割当 | StockItem | UPDATE | customBarcodeフィールドの更新 |

### テーブル別更新項目詳細

#### StockItem テーブル

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | 全フィールド | pk = 指定ID | reload()で再取得 |
| UPDATE | batch, status, packaging, link, serial | フォーム入力値 | editForm()経由 |
| UPDATE | quantity | 追加/削除/カウント値 | stock/add/, stock/remove/, stock/count/ |
| UPDATE | location | 転送先ロケーションID | stock/transfer/ |
| UPDATE | custom_barcode | スキャンしたバーコード値 | バーコード割当時 |
| DELETE | - | pk = 指定ID | delete()メソッド |

## メッセージ仕様

| メッセージID | メッセージ種別 | 表示条件 | メッセージ内容 |
|-------------|---------------|----------|---------------|
| MSG_ITEM_UPDATED | 成功 | 編集・数量調整成功時 | "Stock item updated"（L10().stockItemUpdated） |
| MSG_ITEM_DELETED | 成功 | 削除成功時 | "Stock item deleted"（L10().stockItemDeleteSuccess） |
| MSG_DELETE_FAILED | エラー | 削除失敗時 | "Failed to delete stock item"（L10().stockItemDeleteFailure） |
| MSG_UNAVAILABLE | 警告 | 在庫なし状態 | "Unavailable"（L10().unavailable） |
| MSG_LOCATION_NOT_SET | 情報 | ロケーション未設定 | "Location not set"（L10().locationNotSet） |

## 例外処理

| 例外条件 | 処理内容 | ユーザーへの表示 |
|---------|---------|-----------------|
| 在庫リロード失敗 | Navigator.pop()で画面を閉じる | 前の画面に戻る |
| 削除失敗 | エラーメッセージ表示 | 削除失敗メッセージ |
| APIエラー | Sentryに報告 | 該当項目が非表示 |
| 権限不足 | 操作ボタン非表示 | 閲覧のみ可能 |

## 備考

- シリアル化アイテム（serialNumber.isNotEmpty && quantity == 1）は数量調整ボタンが非表示
- 有効期限機能は `api.getGlobalBooleanSetting("STOCK_ENABLE_EXPIRY")` で有効化を確認
- テスト結果表示は `stockShowTests` 設定とアイテムのtrackable属性に依存
- 履歴表示は `stockShowHistory` 設定に依存
- ラベル印刷は `api.supportsModernLabelPrinting` がtrueの場合のみ利用可能

---

## コードリーディングガイド

本画面を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | stock.dart | `lib/inventree/stock.dart` | InvenTreeStockItemクラス（134-574行）。quantity, serialNumber, isSerialized(), 各種ステータスゲッター |
| 1-2 | stock.dart | `lib/inventree/stock.dart` | transferFields()（156-178行）、adjustStock()（501-534行）などの操作メソッド |

**読解のコツ**: `isSerialized()` は `serialNumber.isNotEmpty && quantity.toInt() == 1` で判定している。

#### Step 2: エントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | stock_detail.dart | `lib/widget/stock/stock_detail.dart` | StockDetailWidget（32-39行）。InvenTreeStockItemを受け取る |
| 2-2 | stock_detail.dart | `lib/widget/stock/stock_detail.dart` | _StockItemDisplayState（41-856行）。主要なStateクラス |

**主要処理フロー**:
1. **45行**: `getAppBarTitle()` で "Stock Item" を返す
2. **206-317行**: `request()` でデータ取得。部品、デフォルトロケーション、テスト結果、添付ファイル数等を非同期取得
3. **517-855行**: `getTiles()` で詳細タイルリストを構築

#### Step 3: 数量調整操作を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | stock_detail.dart | `lib/widget/stock/stock_detail.dart` | _addStockDialog()（367-391行）、_removeStockDialog()（402-426行）、_countStockDialog()（428-456行） |
| 3-2 | stock_detail.dart | `lib/widget/stock/stock_detail.dart` | _transferStockDialog()（461-476行）。移動フォーム |

**主要処理フロー**:
- **379-390行**: launchApiForm()でstock/add/へPOST
- **414-425行**: launchApiForm()でstock/remove/へPOST
- **444-455行**: launchApiForm()でstock/count/へPOST
- **464-475行**: launchApiForm()でstock/transfer/へPOST

#### Step 4: 詳細タイル構築を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | stock_detail.dart | `lib/widget/stock/stock_detail.dart` | headerTile()（478-509行）。ヘッダーCardの構築 |
| 4-2 | stock_detail.dart | `lib/widget/stock/stock_detail.dart` | getTiles()（517-855行）。各種情報タイルの条件分岐と構築 |

#### Step 5: アクションボタンを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | stock_detail.dart | `lib/widget/stock/stock_detail.dart` | appBarActions()（57-86行）。編集・位置特定ボタン |
| 5-2 | stock_detail.dart | `lib/widget/stock/stock_detail.dart` | actionButtons()（88-157行）。SpeedDial操作ボタン（数量調整、移動、印刷、削除） |
| 5-3 | stock_detail.dart | `lib/widget/stock/stock_detail.dart` | barcodeButtons()（159-192行）。バーコードスキャン関連 |

### プログラム呼び出し階層図

```
StockDetailWidget (StatefulWidget)
    │
    └─ _StockItemDisplayState (RefreshableState)
           │
           ├─ request()
           │      │
           │      ├─ widget.item.reload() → API GET /api/stock/{pk}/
           │      ├─ InvenTreePart().get() → 部品情報取得
           │      ├─ widget.item.getTestResults() → テスト結果取得
           │      └─ InvenTreeAttachment().countAttachments()
           │
           ├─ getTiles()
           │      │
           │      ├─ headerTile() → Card (部品情報)
           │      ├─ ロケーションタイル → LocationDisplayWidget
           │      ├─ ステータスタイル
           │      ├─ テスト結果タイル → StockItemTestResultsWidget
           │      └─ 履歴タイル → StockItemHistoryWidget
           │
           ├─ actionButtons()
           │      │
           │      ├─ _countStockDialog() → API POST /api/stock/count/
           │      ├─ _removeStockDialog() → API POST /api/stock/remove/
           │      ├─ _addStockDialog() → API POST /api/stock/add/
           │      ├─ _transferStockDialog() → API POST /api/stock/transfer/
           │      ├─ selectAndPrintLabel()
           │      └─ _deleteItem() → widget.item.delete()
           │
           └─ barcodeButtons()
                  │
                  ├─ StockItemScanIntoLocationHandler
                  └─ customBarcodeAction
```

### データフロー図

```
[入力]                    [処理]                         [出力]

InvenTreeStockItem ───▶ _StockItemDisplayState       ───▶ ListView (getTiles)
                             │                                │
                             ├─ request()                     ├─ headerTile
                             │      │                         ├─ ロケーション
                             │      ├─ reload()               ├─ 数量/シリアル
                             │      ├─ getPart()              ├─ ステータス
                             │      └─ getTestResults()       ├─ テスト結果
                             │                                └─ 履歴
                             └─ actionButtons
                                    │
                                    ├─ 数量調整 → API POST
                                    └─ 移動 → API POST
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| stock_detail.dart | `lib/widget/stock/stock_detail.dart` | ソース | 在庫アイテム詳細画面のメインウィジェット |
| stock.dart | `lib/inventree/stock.dart` | ソース | 在庫アイテムデータモデル |
| part.dart | `lib/inventree/part.dart` | ソース | 部品データモデル（関連表示用） |
| stock_item_test_results.dart | `lib/widget/stock/stock_item_test_results.dart` | ソース | テスト結果ウィジェット |
| stock_item_history.dart | `lib/widget/stock/stock_item_history.dart` | ソース | 履歴ウィジェット |
| notes_widget.dart | `lib/widget/notes_widget.dart` | ソース | ノート表示・編集 |
| attachment_widget.dart | `lib/widget/attachment_widget.dart` | ソース | 添付ファイル一覧 |
| dialogs.dart | `lib/widget/dialogs.dart` | ソース | 確認ダイアログ（削除時） |
| api_form.dart | `lib/api_form.dart` | ソース | APIフォーム（数量調整等） |
| barcode.dart | `lib/barcode/barcode.dart` | ソース | バーコードスキャン基盤 |
| stock.dart | `lib/barcode/stock.dart` | ソース | 在庫関連バーコードハンドラ |
| labels.dart | `lib/labels.dart` | ソース | ラベル印刷機能 |
| preferences.dart | `lib/preferences.dart` | ソース | 設定管理（表示オプション） |
