# 通知設計書 2-購買発注キャンセル通知

## 概要

本ドキュメントは、InvenTreeシステムにおける「購買発注キャンセル通知」の設計を定義する。この通知は、購買発注（Purchase Order）がキャンセルされた際に担当者および関係者に送信される。

### 本通知の処理概要

本通知は、購買発注がCANCELLEDステータスに遷移した際に、担当者および関係者に対してキャンセルの発生を通知する機能を提供する。

**業務上の目的・背景**：購買発注がキャンセルされた場合、関係者が速やかにその情報を把握し、サプライヤーへの連絡、関連業務の中止、在庫計画の見直しなどの対応を行えるようにすることが目的である。キャンセル情報の伝達漏れによる業務の混乱を防止する。

**通知の送信タイミング**：購買発注の`cancel_order()`メソッドが呼び出され、ステータスがCANCELLED（キャンセル済み）に変更された時点でトリガーされる。具体的には`PurchaseOrder._action_cancel()`メソッド内で`notify_responsible()`が呼び出される。

**通知の受信者**：購買発注の`responsible`フィールドに設定されたユーザーまたはグループ、および`subscribed_users()`で取得される購読ユーザー（関連パーツの購読者）が受信者となる。発注作成者（`created_by`）は通知から除外される。

**通知内容の概要**：購買発注がキャンセルされたことを伝えるメッセージ、発注の詳細ページへのリンクが含まれる。件名は「Purchase Order canceled」となる。

**期待されるアクション**：受信者は通知を確認後、サプライヤーへのキャンセル連絡、関連する製造指示や在庫計画の見直し、必要に応じた代替発注の検討などを行うことが期待される。

## 通知種別

- メール通知
- アプリ内通知（UI通知）
- Slack通知（設定されている場合）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 非同期（プラグインベース） |
| 優先度 | 高 |
| リトライ | プラグイン実装に依存 |

### 送信先決定ロジック

1. `PurchaseOrder.responsible` - 発注の責任者（ユーザーまたはグループ）
2. `PurchaseOrder.subscribed_users()` - 発注に含まれるパーツの購読者
3. 上記から`created_by`（発注作成者）を除外

```python
# 送信先決定ロジック（order/models.py 行876-882）
notify_responsible(
    self,
    PurchaseOrder,
    exclude=self.created_by,
    content=InvenTreeNotificationBodies.OrderCanceled,
    extra_users=self.subscribed_users(),
)
```

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | システム設定に依存 |
| 送信元名称 | InvenTree |
| 件名 | Purchase Order canceled |
| 形式 | HTML |

### 本文テンプレート

```html
<!-- templates/email/canceled_order_assigned.html -->
{% extends "email/email.html" %}

{% load i18n %}
{% load inventree_extras %}

{% block title %}
{{ message }}
{% if link %}
<p>{% trans "Click on the following link to view this order" %}: <a href="{{ link }}">{{ link }}</a></p>
{% endif %}
{% endblock title %}
```

### 添付ファイル

| ファイル名 | 形式 | 条件 | 説明 |
|----------|------|------|------|
| なし | - | - | 本通知に添付ファイルはない |

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| message | 通知メッセージ | InvenTreeNotificationBodies.OrderCanceled.message | Yes |
| link | 発注詳細ページURL | construct_absolute_url(instance.get_absolute_url()) | Yes |
| instance | 発注オブジェクト | PurchaseOrderインスタンス | Yes |
| name | 通知名（件名） | "Purchase Order canceled" | Yes |
| verbose_name | モデル表示名 | "Purchase Order" | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 画面操作/API | PurchaseOrder.cancel_order() | can_cancel=True（OPEN状態） | 発注がキャンセルされた時 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| データインポート中 | `isImportingData()`がTrueの場合 |
| データリビルド中 | `isRebuildingData()`がTrueの場合 |
| マイグレーション実行中 | `isRunningMigrations()`がTrueの場合 |
| 最近の通知 | 同一カテゴリ・オブジェクトへの通知が1日以内にある場合（check_recent） |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[cancel_order呼び出し] --> B{can_cancel?}
    B -->|Yes| C[ステータスをCANCELLEDに更新]
    B -->|No| Z[処理終了]
    C --> D[save実行]
    D --> E[trigger_event発火]
    E --> F[notify_responsible呼び出し]
    F --> G[送信先ユーザー収集]
    G --> H[context構築]
    H --> I[trigger_notification呼び出し]
    I --> J{check_recent?}
    J -->|最近通知済み| K[スキップ]
    J -->|未通知| L[各プラグインで送信]
    L --> M[NotificationEntry記録]
    M --> N[終了]
    K --> N
```

## データベース参照・更新仕様

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| order_purchaseorder | 発注情報取得 | 主テーブル |
| order_purchaseorderlineitem | 発注明細取得 | subscribed_users用 |
| users_owner | 責任者情報取得 | responsible参照 |
| auth_user | ユーザー情報取得 | 通知先ユーザー |
| auth_group | グループ情報取得 | グループ通知時 |
| common_notificationentry | 重複通知チェック | check_recent用 |

### テーブル別参照項目詳細

#### order_purchaseorder

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | 発注識別 | 対象発注 |
| reference | 発注番号 | 通知表示用 |
| status | ステータス | キャンセル可否判定（OPEN状態か） |
| responsible_id | 責任者 | 通知先決定 |
| created_by_id | 作成者 | 通知除外対象 |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| order_purchaseorder | UPDATE | ステータスをCANCELLEDに更新 |
| common_notificationentry | INSERT | 通知履歴記録 |
| common_notificationmessage | INSERT | UI通知メッセージ保存 |

#### 送信ログテーブル

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|-------|------|
| INSERT | category | "order.canceled_purchaseorder" | 通知カテゴリ |
| INSERT | key | PurchaseOrder.pk | 対象オブジェクトID |
| INSERT | updated | 現在日時 | 通知日時 |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 送信失敗 | プラグインエラー | log_errorで記録、他プラグインは継続 |
| テンプレートエラー | テンプレート不正 | 例外キャッチしてログ記録 |
| 宛先不正 | 無効なユーザー/グループ | フィルタで除外（is_active=False等） |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | プラグイン実装依存 |
| リトライ間隔 | プラグイン実装依存 |
| リトライ対象エラー | 一時的な送信エラー |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 制限なし |
| 1日あたり上限 | 同一オブジェクト・カテゴリに対して1回（check_recent=1日） |

### 配信時間帯

制限なし（キャンセル時に即時送信）

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

- 通知受信者は対象オブジェクトへの`view`権限が必要（`check_user_permission`で検証）
- 非アクティブユーザー（is_active=False）は通知対象から除外
- 発注情報へのリンクはシステム設定のベースURLを使用して構築

## 備考

- InvenTreeNotificationBodies.OrderCanceledは購買発注、販売発注、製造指示、返品発注で共通利用
- キャンセル可能な状態は`PurchaseOrderStatusGroups.OPEN`（PENDING、PLACED、ON_HOLD）
- キャンセル後のステータス遷移は不可

---

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

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

### 推奨読解順序

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

通知の基本データ構造である`NotificationBody`と`InvenTreeNotificationBodies.OrderCanceled`を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | notifications.py | `src/backend/InvenTree/common/notifications.py` | OrderCanceledの定義（行64-70） |

**読解のコツ**: `OrderCanceled`は新規発注と同様のデータ構造だが、`name`と`message`が異なる点に注意。

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

購買発注のキャンセル処理がどこから始まるかを特定する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | models.py | `src/backend/InvenTree/order/models.py` | PurchaseOrder.cancel_order()メソッド（行842-846） |

**主要処理フロー**:
1. **行842-846**: `cancel_order()`が`handle_transition()`を呼び出し
2. **行867-882**: `_action_cancel()`でステータス更新と通知送信

#### Step 3: キャンセル条件を理解する

キャンセル可能な条件を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | models.py | `src/backend/InvenTree/order/models.py` | can_cancelプロパティ（行859-865） |

**主要処理フロー**:
- **行865**: `status in PurchaseOrderStatusGroups.OPEN`でOPEN状態かチェック

#### Step 4: 通知送信処理を理解する

実際の通知送信処理を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | helpers_model.py | `src/backend/InvenTree/InvenTree/helpers_model.py` | notify_responsible()関数（行297-326） |
| 4-2 | notifications.py | `src/backend/InvenTree/common/notifications.py` | trigger_notification()関数（行87-218） |

#### Step 5: メールテンプレートを理解する

メール本文のテンプレートを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | canceled_order_assigned.html | `src/backend/InvenTree/templates/email/canceled_order_assigned.html` | メール本文テンプレート |

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

```
PurchaseOrder.cancel_order() [order/models.py:842]
    │
    ├─ handle_transition() [generic/states/__init__.py]
    │      └─ _action_cancel() [order/models.py:867]
    │              │
    │              ├─ trigger_event(PurchaseOrderEvents.CANCELLED)
    │              │
    │              └─ notify_responsible() [InvenTree/helpers_model.py:297]
    │                      │
    │                      └─ notify_users() [InvenTree/helpers_model.py:329]
    │                              │
    │                              └─ trigger_notification() [common/notifications.py:87]
    │                                      │
    │                                      ├─ check_recent() [common/models.py]
    │                                      │
    │                                      └─ plugin.send_notification() [各通知プラグイン]
    │
    └─ subscribed_users() [order/models.py:593]
```

### データフロー図

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

PurchaseOrder          notify_responsible()              メール送信
  ├─ pk           ───▶     │                        ───▶ UI通知保存
  ├─ responsible          ├─ context構築                  Slack送信
  ├─ created_by           ├─ ターゲット収集
  └─ status=OPEN          └─ trigger_notification()
       │
       └─ CANCELLED
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| models.py | `src/backend/InvenTree/order/models.py` | ソース | PurchaseOrderモデル、cancel_order()実装 |
| notifications.py | `src/backend/InvenTree/common/notifications.py` | ソース | 通知基盤、OrderCanceled定義 |
| helpers_model.py | `src/backend/InvenTree/InvenTree/helpers_model.py` | ソース | notify_responsible()ヘルパー |
| events.py | `src/backend/InvenTree/order/events.py` | ソース | PurchaseOrderEvents.CANCELLEDイベント定義 |
| canceled_order_assigned.html | `src/backend/InvenTree/templates/email/canceled_order_assigned.html` | テンプレート | メール本文テンプレート |
| status_codes.py | `src/backend/InvenTree/order/status_codes.py` | ソース | PurchaseOrderStatusGroups.OPEN定義 |
