# 通知設計書 3-購買発注期限超過通知

## 概要

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

### 本通知の処理概要

本通知は、日次スケジュールタスクによって購買発注の納期超過をチェックし、前日に納期を迎えた発注に対して通知を送信する機能を提供する。

**業務上の目的・背景**：購買発注の納期管理において、期限超過を早期に検知し、サプライヤーへのフォローアップ、代替調達の検討、関連業務（製造指示等）への影響把握などの対応を促すことが目的である。納期遅延による生産計画への悪影響を最小化する。

**通知の送信タイミング**：日次スケジュールタスク`check_overdue_purchase_orders()`が実行され、target_dateが前日（昨日）であり、かつOPEN状態の購買発注が検出された時点でトリガーされる。発注単位だけでなく、明細行（LineItem）単位の期限超過もチェックされる。

**通知の受信者**：購買発注の`created_by`（作成者）、`responsible`（責任者）、および`subscribed_users()`で取得される購読ユーザーが受信者となる。

**通知内容の概要**：購買発注が期限超過になったことを伝えるメッセージ、発注の詳細ページへのリンク、発注番号とサプライヤー名が含まれる。件名は「Overdue Purchase Order」となる。

**期待されるアクション**：受信者は通知を確認後、サプライヤーへの納期確認連絡、納期遅延の原因調査、代替調達先の検討、関連する製造計画の見直しなどを行うことが期待される。

## 通知種別

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

## 送信仕様

### 基本情報

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

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

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

```python
# 送信先決定ロジック（order/tasks.py 行37-45）
targets: list[User | Group | Owner] = []

if po.created_by:
    targets.append(po.created_by)

if po.responsible:
    targets.append(po.responsible)

targets.extend(po.subscribed_users())
```

## 通知テンプレート

### メール通知の場合

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

### 本文テンプレート

```html
<!-- templates/email/overdue_purchase_order.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 %}

{% block body %}
<tr style="height: 3rem; border-bottom: 1px solid">
    <th>{% trans "Purchase Order" %}</th>
    <th>{% trans "Supplier" %}</th>
</tr>

<tr style="height: 3rem">
    <td style="text-align: center;">{{ order }}</td>
    <td style="text-align: center;">{{ order.supplier }}</td>
</tr>
{% endblock body %}
```

### 添付ファイル

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

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| message | 通知メッセージ | "Purchase order {po} is now overdue" | Yes |
| link | 発注詳細ページURL | construct_absolute_url(po.get_absolute_url()) | Yes |
| order | 発注オブジェクト | PurchaseOrderインスタンス | Yes |
| name | 通知名（件名） | "Overdue Purchase Order" | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| スケジュール | check_overdue_purchase_orders() | 日次実行 | 毎日実行される定期タスク |

### 期限超過判定条件

1. **発注単位**: `target_date`が昨日かつステータスがOPEN
2. **明細行単位**: `target_date`が昨日かつ`received < quantity`かつ発注がOPEN

```python
# 判定ロジック（order/tasks.py 行78-101）
yesterday = datetime.now().date() - timedelta(days=1)

# 発注単位
overdue_orders = order.models.PurchaseOrder.objects.filter(
    target_date=yesterday, status__in=PurchaseOrderStatusGroups.OPEN
)

# 明細行単位
overdue_lines = order.models.PurchaseOrderLineItem.objects.filter(
    target_date=yesterday,
    order__status__in=PurchaseOrderStatusGroups.OPEN,
    received__lt=F('quantity'),
)
```

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| データインポート中 | `isImportingData()`がTrueの場合 |
| データリビルド中 | `isRebuildingData()`がTrueの場合 |
| 最近の通知 | 同一カテゴリ・オブジェクトへの通知が1日以内にある場合（check_recent） |
| 既に通知済みの発注 | 同一バッチ内で既に通知した発注はスキップ |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[日次スケジュール起動] --> B[check_overdue_purchase_orders]
    B --> C[yesterdayを計算]
    C --> D[期限超過発注を検索]
    D --> E[期限超過明細行を検索]
    E --> F{発注あり?}
    F -->|Yes| G[notify_overdue_purchase_order呼び出し]
    F -->|No| N[終了]
    G --> H[送信先ユーザー収集]
    H --> I[context構築]
    I --> J[trigger_notification呼び出し]
    J --> K[trigger_event発火]
    K --> L[notified_ordersに追加]
    L --> M{次の発注?}
    M -->|Yes| G
    M -->|No| N
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| order_purchaseorder | 発注情報取得 | 期限超過チェック対象 |
| order_purchaseorderlineitem | 明細行情報取得 | 明細行単位の期限超過チェック |
| users_owner | 責任者情報取得 | responsible参照 |
| auth_user | ユーザー情報取得 | 通知先ユーザー |
| common_notificationentry | 重複通知チェック | check_recent用 |

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

#### order_purchaseorder

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | 発注識別 | 対象発注 |
| reference | 発注番号 | 通知表示用 |
| status | ステータス | OPEN状態フィルタ |
| target_date | 納期 | 昨日の日付と比較 |
| responsible_id | 責任者 | 通知先決定 |
| created_by_id | 作成者 | 通知先決定 |
| supplier_id | サプライヤー | メール表示用 |

#### order_purchaseorderlineitem

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| target_date | 明細納期 | 昨日の日付と比較 |
| received | 受領数量 | 未完了判定 |
| quantity | 発注数量 | 未完了判定 |
| order_id | 発注ID | 発注との紐付け |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| common_notificationentry | INSERT | 通知履歴記録 |
| common_notificationmessage | INSERT | UI通知メッセージ保存 |

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

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

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 送信失敗 | プラグインエラー | log_errorで記録、他プラグインは継続 |
| テンプレートエラー | テンプレート不正 | 例外キャッチしてログ記録 |
| DB接続エラー | データベース不通 | タスク失敗、次回実行時に再試行 |

### リトライ仕様

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

## 配信設定

### レート制限

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

### 配信時間帯

日次スケジュールタスクの実行タイミングに依存（システム設定による）

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

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

## 備考

- スケジュールタスクは`@scheduled_task(ScheduledTask.DAILY)`デコレータで日次実行
- 同一発注に対して発注単位と明細行単位の両方で期限超過が検出されても、通知は1回のみ送信（`notified_orders`で管理）
- プラグインイベント`order.overdue_purchase_order`も発火される

---

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

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

### 推奨読解順序

#### Step 1: スケジュールタスクを理解する

日次で実行されるスケジュールタスクの仕組みを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | tasks.py | `src/backend/InvenTree/order/tasks.py` | check_overdue_purchase_orders()関数（行68-101） |

**読解のコツ**: `@scheduled_task(ScheduledTask.DAILY)`デコレータで日次実行が設定されている。

#### Step 2: 通知送信関数を理解する

個別の発注に対する通知送信処理を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | tasks.py | `src/backend/InvenTree/order/tasks.py` | notify_overdue_purchase_order()関数（行30-65） |

**主要処理フロー**:
1. **行37-45**: 送信先ユーザーの収集（created_by, responsible, subscribed_users）
2. **行47-54**: コンテキスト構築（order, name, message, link, template）
3. **行60-62**: trigger_notification()呼び出し
4. **行65**: trigger_event()でプラグインイベント発火

#### Step 3: イベント定義を理解する

期限超過イベントの定義を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | events.py | `src/backend/InvenTree/order/events.py` | PurchaseOrderEvents.OVERDUE（行14） |

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

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

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

**読解のコツ**: このテンプレートは発注番号とサプライヤー名を表示するテーブルを含む。

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

```
[日次スケジュール]
    │
    └─ check_overdue_purchase_orders() [order/tasks.py:70]
           │
           ├─ PurchaseOrder.objects.filter(target_date=yesterday, status__in=OPEN)
           │
           ├─ PurchaseOrderLineItem.objects.filter(...)
           │
           └─ notify_overdue_purchase_order(po) [order/tasks.py:31]
                  │
                  ├─ targets収集
                  │     ├─ po.created_by
                  │     ├─ po.responsible
                  │     └─ po.subscribed_users()
                  │
                  ├─ context構築
                  │
                  ├─ trigger_notification() [common/notifications.py:87]
                  │     │
                  │     └─ plugin.send_notification() [各通知プラグイン]
                  │
                  └─ trigger_event(PurchaseOrderEvents.OVERDUE)
```

### データフロー図

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

日次スケジュール        check_overdue_purchase_orders()    メール送信
       │           ───▶     │                        ───▶ UI通知保存
       │                    ├─ 期限超過発注検索             Slack送信
       │                    ├─ 期限超過明細検索
       │                    └─ notify_overdue_purchase_order()
       │
       └─ datetime.now().date() - timedelta(days=1)
              = yesterday
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| tasks.py | `src/backend/InvenTree/order/tasks.py` | ソース | スケジュールタスク、通知送信関数 |
| models.py | `src/backend/InvenTree/order/models.py` | ソース | PurchaseOrder, PurchaseOrderLineItemモデル |
| notifications.py | `src/backend/InvenTree/common/notifications.py` | ソース | trigger_notification()関数 |
| events.py | `src/backend/InvenTree/order/events.py` | ソース | PurchaseOrderEvents.OVERDUEイベント定義 |
| overdue_purchase_order.html | `src/backend/InvenTree/templates/email/overdue_purchase_order.html` | テンプレート | メール本文テンプレート |
| status_codes.py | `src/backend/InvenTree/order/status_codes.py` | ソース | PurchaseOrderStatusGroups.OPEN定義 |
