# 機能設計書 6-部品価格管理

## 概要

本ドキュメントは、InvenTreeシステムにおける部品価格管理機能の設計を記述する。部品価格管理は、購入価格、販売価格、内部価格、BOMコストなど多面的な価格情報をキャッシュ・計算し、原価計算や見積作成の基盤を提供する。

### 本機能の処理概要

**業務上の目的・背景**：
製造業・販売業において、正確なコスト把握と価格設定は利益確保の要である。部品価格管理機能は、仕入れ価格、サプライヤー価格、販売価格、内部価格など複数の価格情報を一元管理し、BOM積み上げによる組立品原価の自動計算を実現する。価格計算はバックグラウンドでキャッシュされ、リアルタイムの価格照会を高速化する。また、数量別価格帯（Price Break）をサポートし、大量購入時の単価割引にも対応する。

**機能の利用シーン**：
- 部品の価格履歴を確認し、コスト変動を分析する場面
- サプライヤーからの見積価格を登録し、最適調達先を選定する場面
- 組立品のBOMコストを自動計算し、原価を把握する場面
- 販売価格を設定し、利益率を確認する場面
- 数量別価格を設定し、ボリュームディスカウントを管理する場面

**主要な処理内容**：
1. 購入価格（履歴ベース）の追跡
2. サプライヤー価格（価格帯）の管理
3. 内部価格（価格帯）の管理
4. 販売価格（価格帯）の管理
5. BOMコストの自動計算
6. バリアント価格の集約
7. 総合価格（最小/最大）の算出

**関連システム・外部連携**：
- Part（部品）との連携（価格情報の保持）
- SupplierPart（サプライヤー部品）との連携
- PurchaseOrder（発注）との連携（購入履歴）
- SalesOrder（受注）との連携（販売履歴）
- BomItem（BOM）との連携（コスト積み上げ）
- 通貨換算システムとの連携

**権限による制御**：
- 価格情報の閲覧は設定で制御可能
- 価格帯の作成・編集には各モデルの編集権限が必要

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 14 | 部品詳細画面 | 主機能 | 価格タブでの価格情報表示・編集 |
| 17 | サプライヤー部品詳細画面 | 参照画面 | サプライヤー価格帯の設定 |

## 機能種別

計算処理 / キャッシュ管理 / CRUD操作

## 入力仕様

### 販売価格帯（PartSellPriceBreak）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| part | integer | Yes | 部品ID | salable=Trueの部品のみ |
| quantity | decimal | Yes | 数量閾値 | 1以上 |
| price | money | Yes | 単価 | |
| price_currency | string | No | 通貨コード | デフォルト通貨 |

### 内部価格帯（PartInternalPriceBreak）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| part | integer | Yes | 部品ID | |
| quantity | decimal | Yes | 数量閾値 | 1以上 |
| price | money | Yes | 単価 | |
| price_currency | string | No | 通貨コード | デフォルト通貨 |

### 価格オーバーライド（PartPricing）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| override_min | money | No | 最小価格オーバーライド | |
| override_max | money | No | 最大価格オーバーライド | |

### 入力データソース

- 画面入力（価格帯編集フォーム）
- REST API（api-part-sale-price-list, api-part-internal-price-list）
- 発注・受注履歴からの自動取得

## 出力仕様

### 出力データ（PartPricing）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| pk | integer | 価格情報の主キー |
| part | integer | 部品ID |
| currency | string | 計算に使用した通貨 |
| bom_cost_min | money | BOMコスト最小 |
| bom_cost_max | money | BOMコスト最大 |
| purchase_cost_min | money | 購入コスト最小 |
| purchase_cost_max | money | 購入コスト最大 |
| internal_cost_min | money | 内部コスト最小 |
| internal_cost_max | money | 内部コスト最大 |
| supplier_price_min | money | サプライヤー価格最小 |
| supplier_price_max | money | サプライヤー価格最大 |
| variant_cost_min | money | バリアントコスト最小 |
| variant_cost_max | money | バリアントコスト最大 |
| overall_min | money | 総合最小価格 |
| overall_max | money | 総合最大価格 |
| sale_price_min | money | 販売価格最小 |
| sale_price_max | money | 販売価格最大 |
| sale_history_min | money | 販売履歴最小 |
| sale_history_max | money | 販売履歴最大 |
| updated | datetime | 最終更新日時 |

### 出力先

- 画面表示（価格パネル）
- REST API レスポンス
- レポート出力

## 処理フロー

### 処理シーケンス

```
1. 価格更新スケジュール
   └─ schedule_for_update呼び出し → バックグラウンドタスク登録

2. 価格計算（update_pricing）
   └─ BOMコスト計算 → 購入コスト計算 → 内部コスト計算 → サプライヤーコスト計算 → バリアントコスト計算 → 販売コスト計算 → 総合価格算出 → DB保存

3. BOMコスト計算
   └─ BOMアイテム取得 → 各子部品の価格取得 → 数量×単価の積み上げ → 最小/最大算出

4. カスケード更新
   └─ 価格変更検出 → 親組立品の価格更新スケジュール → テンプレート部品の価格更新スケジュール
```

### フローチャート

```mermaid
flowchart TD
    A[価格更新トリガー] --> B[schedule_for_update]
    B --> C{既にスケジュール済み?}
    C -->|Yes| D[スキップ]
    C -->|No| E[scheduled_for_update = True]
    E --> F[バックグラウンドタスク登録]

    F --> G[update_pricing実行]
    G --> H[update_bom_cost]
    G --> I[update_purchase_cost]
    G --> J[update_internal_cost]
    G --> K[update_supplier_cost]
    G --> L[update_variant_cost]
    G --> M[update_sale_cost]

    H --> N[update_overall_cost]
    I --> N
    J --> N
    K --> N
    L --> N
    M --> N

    N --> O[DB保存]
    O --> P{価格変更あり?}
    P -->|Yes| Q[親組立品更新スケジュール]
    Q --> R[テンプレート部品更新スケジュール]
    P -->|No| S[終了]
    R --> S
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | 通貨統一 | 価格計算はデフォルト通貨に統一して実行 | 常時 |
| BR-002 | BOM対象 | BOMコストはassembly=Trueの部品のみ計算 | part.assembly |
| BR-003 | サプライヤー対象 | サプライヤー価格はpurchaseable=Trueの部品のみ計算 | part.purchaseable |
| BR-004 | バリアント対象 | バリアントコストはis_template=Trueの部品のみ計算 | part.is_template |
| BR-005 | 購入履歴対象 | 購入履歴は完了した発注のみ対象 | PurchaseOrderStatus.COMPLETE |
| BR-006 | オーバーライド優先 | override_min/maxが設定されている場合は計算値を上書き | override設定時 |
| BR-007 | 内部価格優先 | PART_BOM_USE_INTERNAL_PRICE設定時は内部価格が優先 | 設定有効時 |
| BR-008 | 再帰防止 | 価格計算の再帰深度はMAX_PRICING_DEPTH=50で制限 | カスケード更新時 |

### 計算ロジック

```python
# BOMコスト計算
def update_bom_cost(self, save=True):
    if not self.part.assembly:
        self.bom_cost_min = None
        self.bom_cost_max = None
        return

    cumulative_min = Money(0, currency_code)
    cumulative_max = Money(0, currency_code)

    for bom_item in self.part.get_bom_items():
        bom_item_min = None
        bom_item_max = None

        for sub_part in bom_item.get_valid_parts_for_allocation():
            sub_part_pricing = sub_part.pricing
            sub_part_min = self.convert(sub_part_pricing.overall_min)
            sub_part_max = self.convert(sub_part_pricing.overall_max)

            if sub_part_min is not None:
                if bom_item_min is None or sub_part_min < bom_item_min:
                    bom_item_min = sub_part_min

            if sub_part_max is not None:
                if bom_item_max is None or sub_part_max > bom_item_max:
                    bom_item_max = sub_part_max

        if bom_item_min is not None:
            cumulative_min += bom_item_min * bom_item.quantity

        if bom_item_max is not None:
            cumulative_max += bom_item_max * bom_item.quantity

    self.bom_cost_min = cumulative_min if any_min_elements else None
    self.bom_cost_max = cumulative_max if any_max_elements else None

# 総合価格計算
def update_overall_cost(self):
    min_costs = [self.bom_cost_min, self.purchase_cost_min, self.internal_cost_min]
    max_costs = [self.bom_cost_max, self.purchase_cost_max, self.internal_cost_max]

    # サプライヤー価格の追加（設定による）
    if get_global_setting('PRICING_USE_SUPPLIER_PRICING', True):
        min_costs.append(self.supplier_price_min)
        max_costs.append(self.supplier_price_max)

    # バリアント価格の追加（設定による）
    if get_global_setting('PRICING_USE_VARIANT_PRICING', True):
        min_costs.append(self.variant_cost_min)
        max_costs.append(self.variant_cost_max)

    # 最小/最大の算出
    overall_min = min(c for c in min_costs if c is not None) if any(c is not None for c in min_costs) else None
    overall_max = max(c for c in max_costs if c is not None) if any(c is not None for c in max_costs) else None

    # オーバーライドの適用
    if self.override_min is not None:
        overall_min = self.convert(self.override_min)
    if self.override_max is not None:
        overall_max = self.convert(self.override_max)

    self.overall_min = overall_min
    self.overall_max = overall_max
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 販売価格帯作成 | part_partsellpricebreak | INSERT | 新規価格帯レコード挿入 |
| 販売価格帯削除 | part_partsellpricebreak | DELETE | 価格帯レコード削除 |
| 内部価格帯作成 | part_partinternalpricebreak | INSERT | 新規価格帯レコード挿入 |
| 内部価格帯削除 | part_partinternalpricebreak | DELETE | 価格帯レコード削除 |
| 価格キャッシュ更新 | part_partpricing | INSERT/UPDATE | 計算済み価格の保存 |

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

#### part_partpricing

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT/UPDATE | bom_cost_min, bom_cost_max, purchase_cost_min, purchase_cost_max, internal_cost_min, internal_cost_max, supplier_price_min, supplier_price_max, variant_cost_min, variant_cost_max, overall_min, overall_max, sale_price_min, sale_price_max, sale_history_min, sale_history_max, currency, updated | 計算値 | バックグラウンドで実行 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| MissingRate | 通貨換算エラー | 為替レートが未設定 | 為替レートを設定 |
| IntegrityError | 整合性エラー | 重複する価格帯（同一部品・数量） | 既存価格帯を編集 |

### リトライ仕様

- 価格計算はバックグラウンドで実行されるため、失敗時は再スケジュール可能

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

- 価格帯の作成・削除は単一トランザクション内で実行
- 価格キャッシュ更新はバックグラウンドタスクで非同期実行
- IntegrityErrorは握り潰し（並行更新への対応）

## パフォーマンス要件

- 価格計算: バックグラウンドで実行（UIをブロックしない）
- 価格取得: キャッシュから直接取得（100ms以内）
- 再帰深度制限: MAX_PRICING_DEPTH=50

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

- 価格情報の閲覧権限制御
- APIエンドポイントは認証必須

## 備考

- djmoney.contrib.exchange.models.convert_moneyで通貨換算
- PRICING_*設定で計算対象を制御可能
- scheduled_for_updateフラグで重複スケジュールを防止

---

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

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

### 推奨読解順序

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

PartPricingモデルと価格帯モデルの構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | models.py | `src/backend/InvenTree/part/models.py` | PartPricingクラス（2729-3468行目） |
| 1-2 | models.py | `src/backend/InvenTree/part/models.py` | PartSellPriceBreakクラス（3523-3543行目） |
| 1-3 | models.py | `src/backend/InvenTree/part/models.py` | PartInternalPriceBreakクラス（3546-3564行目） |

**読解のコツ**:
- PartPricingはPartと1:1関連（OneToOneField）
- 各コスト（bom_cost, purchase_cost等）は最小/最大の2値で管理
- schedule_for_updateで遅延実行、update_pricingで実際の計算

**主要処理フロー**:
- **2758-2761行目**: is_validプロパティ（キャッシュ有効性判定）
- **2785-2869行目**: schedule_for_updateメソッド（バックグラウンドタスク登録）
- **2871-2927行目**: update_pricingメソッド（メイン計算処理）
- **2962-3040行目**: update_bom_costメソッド（BOMコスト計算）
- **3042-3106行目**: update_purchase_costメソッド（購入コスト計算）
- **3108-3132行目**: update_internal_costメソッド（内部コスト計算）
- **3134-3167行目**: update_supplier_costメソッド（サプライヤーコスト計算）
- **3169-3202行目**: update_variant_costメソッド（バリアントコスト計算）
- **3204-3271行目**: update_overall_costメソッド（総合価格計算）
- **3273-3325行目**: update_sale_costメソッド（販売コスト計算）

#### Step 2: シグナルハンドラを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | models.py | `src/backend/InvenTree/part/models.py` | post_save/post_deleteシグナルハンドラ（4302-4350行目） |

**主要処理フロー**:
- **4313-4330行目**: update_pricing_after_editシグナル
- **4333-4350行目**: update_pricing_after_deleteシグナル

#### Step 3: フロントエンドを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | PartPricingPanel.tsx | `src/frontend/src/pages/part/PartPricingPanel.tsx` | 価格表示UIコンポーネント |

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

```
トリガーイベント（BomItem保存、価格帯保存等）
    │
    ├─ post_save signal
    │      └─ part.schedule_pricing_update()
    │             └─ PartPricing.schedule_for_update()
    │                    └─ バックグラウンドタスク登録
    │
    └─ part.tasks.update_part_pricing()
           └─ PartPricing.update_pricing()
                  ├─ update_bom_cost()
                  │      └─ BomItem.get_valid_parts_for_allocation()
                  │             └─ sub_part.pricing.overall_min/max
                  ├─ update_purchase_cost()
                  │      └─ PurchaseOrderLineItem照会
                  ├─ update_internal_cost()
                  │      └─ PartInternalPriceBreak照会
                  ├─ update_supplier_cost()
                  │      └─ SupplierPriceBreak照会
                  ├─ update_variant_cost()
                  │      └─ バリアント部品のpricing照会
                  ├─ update_sale_cost()
                  │      └─ PartSellPriceBreak, SalesOrderLineItem照会
                  └─ update_overall_cost()
                         └─ 最小/最大算出
```

### データフロー図

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

BomItem変更 ─────┐
                │
価格帯変更 ─────┼───▶ schedule_for_update() ───▶ タスクキュー
                │              │
発注完了 ───────┘              ▼
                       update_part_pricing()
                              │
                              ▼
                       update_pricing()
                              │
                    ┌─────────┼─────────┐
                    │         │         │
                    ▼         ▼         ▼
               BOMコスト  購入コスト  販売コスト
                 計算       計算       計算
                    │         │         │
                    └─────────┼─────────┘
                              │
                              ▼
                       update_overall_cost()
                              │
                              ▼
                       part_partpricing テーブル
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| models.py | `src/backend/InvenTree/part/models.py` | ソース | PartPricing, PriceBreakモデル定義 |
| tasks.py | `src/backend/InvenTree/part/tasks.py` | ソース | update_part_pricingタスク |
| api.py | `src/backend/InvenTree/part/api.py` | ソース | 価格帯APIビュー定義 |
| serializers.py | `src/backend/InvenTree/part/serializers.py` | ソース | 価格シリアライザ定義 |
| PartPricingPanel.tsx | `src/frontend/src/pages/part/PartPricingPanel.tsx` | ソース | フロントエンド価格パネル |
| settings.py | `src/backend/InvenTree/common/settings.py` | ソース | PRICING_*設定 |
