# 機能設計書 16-販売注文明細管理

## 概要

本ドキュメントは、InvenTreeシステムにおける販売注文明細管理機能の設計内容を記述した機能設計書である。販売注文に紐づく個々の販売品目（明細行）の管理について詳細を定義する。

### 本機能の処理概要

**業務上の目的・背景**：
販売注文は複数の販売可能部品から構成される。各部品の数量、単価、引当状況、出荷状況を個別に管理することで、販売進捗の詳細な追跡と正確な出荷処理を実現する。本機能は、販売注文の明細レベルでの管理を提供し、部分出荷や明細ごとの納期管理を可能にする。

**機能の利用シーン**：
- 販売注文に販売品目を追加する場面
- 各明細の引当状況・出荷状況を確認する場面
- 明細ごとの納期を設定・管理する場面
- 販売単価を設定・更新する場面
- 在庫引当を明細レベルで管理する場面

**主要な処理内容**：
1. 販売明細の追加・編集・削除処理
2. 販売可能部品との紐づけ管理
3. 引当数量の追跡
4. 出荷数量の追跡
5. 販売単価の設定と更新
6. 明細ごとの納期管理

**関連システム・外部連携**：
- 販売注文管理機能（親レコード）
- 部品管理機能（販売可能部品）
- 在庫引当機能
- 出荷管理機能

**権限による制御**：
- 販売明細の作成・編集は販売担当者権限が必要
- 販売注文がロック状態の場合は編集不可

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 22 | 販売注文詳細画面 | 主機能 | 販売明細の一覧表示・追加・編集・削除 |
| 14 | 部品詳細画面 | 参照画面 | 販売可能部品選択時の参照 |
| 16 | 在庫アイテム詳細画面 | 関連画面 | 引当された在庫アイテムの詳細 |

## 機能種別

CRUD操作 / 計算処理 / 関連データ管理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| order | ForeignKey | Yes | 親販売注文 | SalesOrder |
| part | ForeignKey | Yes | 部品 | Part、salable=True必須 |
| quantity | DecimalField | Yes | 販売数量 | 正の数値 |
| sale_price | MoneyField | No | 販売単価 | 金額 |
| reference | CharField | No | 参照番号（最大100文字） | - |
| notes | CharField | No | メモ（最大500文字） | - |
| target_date | DateField | No | 納期 | 販売注文のtarget_date継承可 |

### 入力データソース

- 画面入力（販売明細追加フォーム）
- APIリクエスト

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| quantity | Decimal | 販売数量 |
| shipped | Decimal | 出荷済み数量 |
| allocated_quantity | Decimal | 引当済み数量 |
| fulfilled_quantity | Decimal | 履行済み数量 |
| is_fully_allocated | bool | 完全引当フラグ |
| is_overallocated | bool | 過剰引当フラグ |
| is_completed | bool | 完了フラグ（shipped >= quantity） |
| total_line_price | Money | 明細合計金額（quantity * price） |

### 出力先

- order_salesorderlineitemテーブル
- 販売注文の合計金額（親レコード更新）

## 処理フロー

### 処理シーケンス

```
1. 販売明細の追加
   └─ 販売可能部品選択
   └─ 数量・単価入力
   └─ 販売可能検証
   └─ 保存

2. 販売明細の編集
   └─ ロック状態チェック
   └─ バリデーション実行
   └─ 親販売注文の合計金額更新

3. 在庫引当
   └─ 在庫アイテムの選択
   └─ 引当数量の設定
   └─ 引当の保存

4. 出荷処理（出荷管理機能で実行）
   └─ shipped数量の更新
   └─ 完了状態の判定

5. 販売明細の削除
   └─ ロック状態チェック
   └─ 引当の削除
   └─ 親販売注文の合計金額更新
```

### フローチャート

```mermaid
flowchart TD
    A[明細追加リクエスト] --> B{販売注文ロック?}
    B -->|Yes| C[ValidationError]
    B -->|No| D[部品選択]
    D --> E{salable=True?}
    E -->|No| F[ValidationError]
    E -->|Yes| G[数量・単価入力]
    G --> H[明細保存]
    H --> I[親販売注文保存]
    I --> J[合計金額更新]
    J --> K[終了]
    C --> K
    F --> K
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-16-01 | 販売可能制約 | salable=Trueの部品のみ販売明細に追加可能 | 明細追加・編集時 |
| BR-16-02 | 数量制約 | 販売数量は正の数値 | 明細追加・編集時 |
| BR-16-03 | バーチャル部品 | virtual=Trueの部品は引当不要、出荷時に自動完了 | 引当・出荷時 |
| BR-16-04 | ロック制約 | 親販売注文がロック状態の場合は編集不可 | 明細追加・編集・削除時 |
| BR-16-05 | 完了判定 | shipped >= quantityで完了 | 出荷処理後 |

### 計算ロジック

**引当済み数量（allocated_quantity）**:
```python
def allocated_quantity(self):
    if not self.pk:
        return 0
    query = self.allocations.aggregate(
        allocated=Coalesce(Sum('quantity'), Decimal(0))
    )
    return query['allocated']
```

**完全引当判定（is_fully_allocated）**:
```python
def is_fully_allocated(self) -> bool:
    if self.part and self.part.virtual:
        return True
    if self.order.status == SalesOrderStatus.SHIPPED:
        return self.fulfilled_quantity() >= self.quantity
    return self.allocated_quantity() >= self.quantity
```

**明細合計金額（total_line_price）**:
```python
@property
def total_line_price(self):
    if self.price:
        return self.quantity * self.price
```

## データベース操作仕様

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 明細追加 | order_salesorderlineitem | INSERT | 新規明細レコードの作成 |
| 明細編集 | order_salesorderlineitem | UPDATE | 明細情報の更新 |
| 明細削除 | order_salesorderlineitem | DELETE | 明細レコードの削除 |
| 出荷更新 | order_salesorderlineitem | UPDATE | shippedフィールドの更新 |
| 親販売注文更新 | order_salesorder | UPDATE | total_priceの再計算 |

### テーブル別操作詳細

#### order_salesorderlineitem

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT/UPDATE | order_id | 親販売注文ID | FK: order_salesorder |
| INSERT/UPDATE | part_id | 部品ID | FK: part_part、salable=True |
| INSERT/UPDATE | quantity | 販売数量 | DecimalField(15,5) |
| INSERT/UPDATE | shipped | 出荷済み数量 | DecimalField(15,5)、default=0 |
| INSERT/UPDATE | sale_price | 販売単価 | MoneyField |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| part_not_salable | ValidationError | salable=Falseの部品を指定 | 販売可能な部品を選択 |
| quantity_invalid | ValidationError | 数量が0以下 | 正の数量を入力 |
| order_locked | ValidationError | 親販売注文がロック状態 | 新規販売注文を作成 |

### リトライ仕様

- データベース一時エラー：トランザクション単位でリトライ
- 親販売注文の合計金額更新エラー：明細保存後に再試行

## トランザクション仕様

- 明細の作成・編集・削除は親販売注文の保存と同一トランザクション内で実行
- save()メソッドで親販売注文のsave()も呼び出し
- delete()メソッドで親販売注文のsave()も呼び出し

## パフォーマンス要件

- 明細一覧表示：500ms以内（50件まで）
- 明細追加：200ms以内
- 合計金額再計算：明細100件で500ms以内

## セキュリティ考慮事項

- 親販売注文のロック状態で編集を制御
- 明細の変更は販売注文レベルで監査可能

## 備考

- バーチャル部品は在庫引当が不要で、出荷時に自動的に出荷完了となる
- 明細の引当は別の引当管理機能（SalesOrderAllocation）で管理

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | models.py | `src/backend/InvenTree/order/models.py` | SalesOrderLineItemクラスの定義 |
| 1-2 | models.py | `src/backend/InvenTree/order/models.py` | OrderLineItem抽象クラスの定義 |

**読解のコツ**:
- **2055-2183行目**: SalesOrderLineItemクラスの定義
- **1712-1810行目**: OrderLineItem抽象クラス（共通フィールド・メソッド）
- **2096-2102行目**: orderフィールド（SalesOrderへのFK）
- **2104-2112行目**: partフィールド（PartへのFK、salable制約）

#### Step 2: バリデーション処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | models.py | `src/backend/InvenTree/order/models.py` | SalesOrderLineItem.clean()メソッド |

**主要処理フロー**:
- **2082-2094行目**: clean()メソッドでバリデーション
- **2090-2093行目**: salable=True検証

#### Step 3: 引当・出荷状態を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | models.py | `src/backend/InvenTree/order/models.py` | allocated_quantity(), is_fully_allocated()メソッド |
| 3-2 | models.py | `src/backend/InvenTree/order/models.py` | is_completed()メソッド |

**主要処理フロー**:
- **2148-2160行目**: allocated_quantity()メソッド
- **2162-2171行目**: is_fully_allocated()メソッド
- **2177-2183行目**: is_completed()メソッド

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

```
SalesOrderLineItem.save()
    │
    ├─ ロック状態チェック
    │      └─ order.check_locked()
    │
    ├─ バリデーション
    │      └─ clean()
    │             └─ salable=True 検証
    │
    └─ 親販売注文更新
           └─ order.save()（合計金額再計算）

SalesOrderLineItem.allocated_quantity()
    │
    └─ allocations.aggregate(Sum('quantity'))
           └─ SalesOrderAllocation集計

SalesOrderLineItem.is_fully_allocated()
    │
    ├─ virtual部品チェック → True
    ├─ SHIPPED状態 → fulfilled_quantity() >= quantity
    └─ その他 → allocated_quantity() >= quantity
```

### データフロー図

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

部品             ───▶ バリデーション          ───▶ ValidationError
  (選択)                │                           (エラー時)
                        │
数量・単価       ───▶ 明細作成
  (入力)                │
                        ▼
                  親販売注文保存          ───▶ order_salesorderlineitem
                        │                       テーブル
                        │
                        ▼
                  合計金額更新            ───▶ total_price更新
                        │
                        │
在庫アイテム     ───▶ 引当処理               ───▶ order_salesorderallocation
  (選択)                │                           テーブル
                        │
                        ▼
                  出荷処理               ───▶ shippedフィールド更新
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| models.py | `src/backend/InvenTree/order/models.py` | ソース | SalesOrderLineItemモデル定義 |
| models.py | `src/backend/InvenTree/order/models.py` | ソース | SalesOrderAllocationモデル定義 |
| api.py | `src/backend/InvenTree/order/api.py` | ソース | 販売明細APIエンドポイント |
| serializers.py | `src/backend/InvenTree/order/serializers.py` | ソース | 販売明細シリアライザー |
