概要

項目内容
案件IDMAS-126
カテゴリ新機能
Phase未定
優先度未定
所要時間M
対象ファイル400_domain/408_rpa_ord_auto_expand.js(新規)、400_domain/407_rpa_orchestrator.js(追記)、000_infra/002_constants.js(MENU_DEFINITION 追記)
前提案件なし

目的

31_wrk_order に登録された継続契約の発注(ORD)から、開始年月終了年月 の範囲で月次 INV を自動生成し、手動起票による漏れ・重複・ミスを排除する。生成された INV は 請求ステータス = '未処理' 固定とし、Human-in-the-Loop 原則に従いユーザーが目視確認してから承認する運用フローを前提とする。

現在のコード

現在は 32_wrk_invoice の INV を手動で 1 件ずつ起票している。継続契約 ORD に対する月次 INV 自動生成機能は未実装。コードスニペットは不要(新規追加のため)。

修正方針

変更 1: 400_domain/408_rpa_ord_auto_expand.js を新規作成

新関数 generateOrdAutoExpand() を実装する(グローバル公開関数、メニュー呼び出し可能)。

変更 2: 400_domain/407_rpa_orchestrator.js に RPAService エントリを追加

generateOrdExpand: function() { return generateOrdAutoExpand(); },

変更 3: 000_infra/002_constants.jsMENU_DEFINITION に新メニュー項目を追加

Constants.MENU_DEFINITION 配列に新カテゴリ「📋 ORD管理」を追加し、以下のアイテムを登録する:

{
  category: '📋 ORD管理',
  items: [
    { label: '継続契約→INV自動展開', funcName: 'generateOrdAutoExpand', description: '契約形態=継続のORDから月次INVを一括生成' },
  ]
},

処理フロー

  1. LockService.getScriptLock() で排他ロックを取得。取得失敗時は処理を中断し Utils.logError() でログを残す。
  2. OrderRepository.findAll() で全 ORD を取得し、dto['契約形態'] === '継続' かつ dto['発注ステータス'] === '発注済' かつ dto['有効フラグ'] === true でフィルタ。
  3. InvoiceRepository.findAll() で全 INV を取得し、冪等性チェック用 Set を構築。キー形式: dto['親発注ID(ORD)'] + '_' + Utils.parseDateToYm(dto['発生日(P/L計上日)'])
  4. 対象 ORD ごとに Utils.parseDateToYm()開始年月終了年月 を正規化し、Utils.addMonths() で月次ループ(上限: Constants.MONTH_ITERATION_LIMIT = 120)。
  5. 各月の InvoiceDTO を以下のフィールドマッピングテーブルに従い生成する。
  6. 冪等性チェック: Set にキーが存在する月はスキップ(二重計上防止)。
  7. 生成した InvoiceDTO 配列を InvoiceRepository.append() で書き込む。
  8. Utils.toastResult(FUNC, '生成件数: ' + count + '件') でトースト通知。Utils.auditLog('RUN', ...) で監査ログを記録。

OrderDTO → InvoiceDTO フィールドマッピングテーブル

InvoiceDTO フィールド導出元 / 計算式デフォルト値
有効フラグtrue
請求ID(INV)RpaCommon.generateInvId(dateStr, offset) で採番
親発注ID(ORD)OrderDTO['発注ID(ORD)']
起票日時new Date()
起票者'RPA_ORD_AUTO'
申請種別'請求書受領(AP)'
発生日(P/L計上日)月次ループの curYm(例: '2026-04')を curYm + '-01' に変換した文字列
決済日_計画''(空文字)
請求ステータス'未処理'
収支区分'支出'
取引先名OrderDTO['取引先名']
PJ名OrderDTO['PJ名'](空の場合は InvoiceRepository.append()'指定なし_共通費など' を自動付与)
組織名OrderDTO['組織名']
諸表区分InvoiceRepository.append() が科目マスタから自動付与
大分類InvoiceRepository.append() が科目マスタから自動付与
科目名''(空文字)※別途ユーザーが入力 or 別案件で ORD に科目列を追加して転記''
税区分'課税'
通貨'JPY'
税抜金額_計画Math.round(OrderDTO['税抜金額_発注'] / 継続月数) ※継続月数 = 開始年月から終了年月の月数差 + 1
消費税額_計画Math.round(OrderDTO['消費税額_発注'] / 継続月数)
税込金額_計画税抜金額_計画 + 消費税額_計画 ※独立して丸め計算(端数は最終月に集計しない)
未決済残高(自動計算)''
決済手段''
摘要OrderDTO['契約・件名'] + ' ' + curYm
証憑URLOrderDTO['証憑URL']
自動仕訳JNL_ID''

InvoiceRepository.append() の自動付与仕様

InvoiceRepository.append()科目名 フィールドを元に AccountRepository.findAsMap() を呼び出し、諸表区分大分類 を自動付与する。科目名 が空文字のまま append() を呼ぶと 諸表区分大分類 は付与されないため、財務諸表の分類が機能しない点に注意(「人間が検討すべき事項」参照)。

INV ID 発番方法

RpaCommon.generateInvId(dateStr, offset) を使用する。dateStr は実行日の yyyyMMdd 形式文字列(Utilities.formatDate(new Date(), tz, 'yyyyMMdd'))。offset はバッチ内の連番(0始まり)。戻り値: 'INV_yyyyMMdd_NNNN'

影響範囲

ファイル / シート変更内容
400_domain/408_rpa_ord_auto_expand.js新規作成。generateOrdAutoExpand() を実装
400_domain/407_rpa_orchestrator.jsRPAServicegenerateOrdExpand を追記
000_infra/002_constants.jsMENU_DEFINITION に「📋 ORD管理」カテゴリを追加
32_wrk_invoice シート新規 INV 行が末尾に追記される
既存機能変更なし(既存 Repository・RpaCommon は読み取りのみ使用)

注意事項

  • 途中解約時に既存生成済み INV を無効化する場合は、ユーザーが手動で 有効フラグ を FALSE に変更すること(本案件スコープ外)。
  • 契約形態 の格納値は実データ検証で確認した表記と完全一致させること('継続''継続(定期)' 等の表記ゆれに注意)。
  • LockService.getScriptLock() のロック取得失敗時は処理を中断し Utils.logError() でログを残すこと。

影響範囲

注意事項

エッジケース

条件処理内容理由
税抜金額_発注 < 0当該 ORD をスキップ、Utils.logError() でログ出力負の月次 INV は会計上の異常値
税抜金額_発注 = 0月額 0 円の INV を生成(スキップしない)0 円仕訳として記録する(将来の変更に備えた継続契約の存在証明)
開始年月 > 終了年月(期間不正)当該 ORD をスキップ、Utils.logInfo() で警告ログ出力月次ループが無限ループになるのを防ぐ
継続月数 ≤ 0(Utils.addMonths の差分が 0 以下)ゼロ除算回避のためスキップ、Utils.logInfo() でログ出力月次按分の除算でゼロ除算が発生する
冪等性キーが既存 INV に存在する月当該月の INV 生成をスキップ二重計上防止
有効フラグ = FALSE の ORDフィルタ段階で除外済みのためスキップロジック不要OrderRepository.findAll() 後のフィルタで除外

実データ検証

実装前に MCP 等で以下を確認すること:

  1. 31_wrk_order の「契約形態」列の実際の格納値('継続''継続(定期)' 等の表記ゆれがないか)
  2. 31_wrk_order の「発注ステータス」列の実際の格納値('発注済' の表記を確認)
  3. 32_wrk_invoice の「親発注ID(ORD)」列の値形式(冪等性キー設計の裏取り。'ORD_YYYYMMDD_NNNN' 形式であることを確認)
  4. 11_mst_account に継続契約向け科目(例: 外注費・支払手数料等)が登録済みかを確認

関連ドキュメント

ドキュメント用途
E.2.17 MAS-122 ORD↔INV 消化状況の整合性チェックORD と INV の紐付けロジック参照
E.2.18 MAS-124 31タブ発注残高エイジング&超過発注アラート継続発注の残高管理
E.2.14 MAS-114 インボイス適格請求書の要件チェック生成 INV のバリデーション
E.6.2 MAS-118 RPA対象月列RPA月次処理の実装パターン参照
MAS-125 発注ステータス管理ワークフロー発注ステータス = '発注済' のフィルタ前提

人間が検討すべき事項

科目名の決定方針(最重要)

InvoiceRepository.append()科目名 を元に 諸表区分大分類 を自動付与するため、科目名 が空のまま生成すると財務諸表分類が機能しない。

推奨方針: 31_wrk_order シートに「科目名」列を追加する DDL 変更(別案件)を行い、その値を INV に転記する。実装時は 科目名 を空で生成しつつ、請求ステータス = '未処理' のレビュー工程でユーザーが手動設定する運用とする。

代替方針: 生成後にユーザーが手動で科目名を設定する(請求ステータス = '未処理' のレビュー工程で対応)。財務諸表への反映は科目名設定後に Action A を再実行。

Human-in-the-Loop 運用フロー

生成される 請求ステータス'未処理' 固定。ユーザーが目視確認後に手動承認する運用フローを前提とする(PRD プロダクトポリシー準拠)。

途中解約時のルール

TODO_future.md に記載の「途中解約時のルール」は本案件スコープ外。既存生成済み INV の無効化は手動運用(有効フラグ を FALSE に変更)とする。

実装プロンプト(Claude Code 用)

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-126「発注→INV自動展開(継続契約)」を実装してください。

## 実行前タスク
以下のファイルを Read して内容を確認してから実装すること:
- `000_infra/003_contracts.js` — OrderDTO / InvoiceDTO の全フィールド名を確認
- `200_data/202_repository.js` — OrderRepository.findAll() / InvoiceRepository.findAll() / InvoiceRepository.append() の仕様を確認
  - append() は科目名を元に諸表区分・大分類を自動付与し、PJ名未指定時は '指定なし_共通費など' をデフォルト設定する
- `400_domain/400_rpa_common.js` — RpaCommon.generateInvId() の引数シグネチャを確認
- `400_domain/407_rpa_orchestrator.js` — RPAService の既存エントリ構造を確認し、追記箇所を特定
- `000_infra/002_constants.js` — MENU_DEFINITION の構造を確認し、新カテゴリ追記箇所を特定。MONTH_ITERATION_LIMIT の値を確認
- `000_infra/004_utils.js` — Utils.parseDateToYm() / Utils.addMonths() / Utils.toastResult() / Utils.logInfo() / Utils.logError() の引数シグネチャを確認

## 修正対象ファイル
- `400_domain/408_rpa_ord_auto_expand.js` — 新規作成
- `400_domain/407_rpa_orchestrator.js` — RPAService への generateOrdExpand エントリ追記のみ
- `000_infra/002_constants.js` — MENU_DEFINITION への「📋 ORD管理」カテゴリ追加のみ

## 実装内容

### `400_domain/408_rpa_ord_auto_expand.js`

```
function generateOrdAutoExpand() {
  const FUNC = 'generateOrdAutoExpand';
  // 1. LockService で排他ロック
  const lock = LockService.getScriptLock();
  if (!lock.tryLock(5000)) {
    Utils.logError(FUNC, new Error('排他ロック取得失敗。他の処理が実行中です。'));
    return;
  }
  try {
    Utils.logInfo(FUNC, '処理開始');
    const tz = Session.getScriptTimeZone();
    const now = new Date();
    const dateStr = Utilities.formatDate(now, tz, 'yyyyMMdd');
    RpaCommon.resetIdCache();

    // 2. 全 ORD を取得し継続・発注済・有効のものをフィルタ
    const ordResult = OrderRepository.findAll();
    const targets = ordResult.dtos.filter(function(dto) {
      if (dto['有効フラグ'] === false || String(dto['有効フラグ']).toUpperCase() === 'FALSE') return false;
      if (dto['契約形態'] !== '継続') return false;
      if (dto['発注ステータス'] !== '発注済') return false;
      return true;
    });

    // 3. 全 INV を取得し冪等性 Set を構築
    const invResult = InvoiceRepository.findAll();
    const existingKeys = new Set();
    invResult.dtos.forEach(function(dto) {
      const key = String(dto['親発注ID(ORD)'] || '') + '_' + Utils.parseDateToYm(dto['発生日(P/L計上日)']);
      if (key !== '_') existingKeys.add(key);
    });

    // 4-7. 対象 ORD ごとに月次 INV を生成
    const newDtos = [];
    let offset = 0;

    targets.forEach(function(ord) {
      const startYm = Utils.parseDateToYm(ord['開始年月']);
      const endYm = Utils.parseDateToYm(ord['終了年月']);

      // エッジケース: 期間不正チェック
      if (!startYm || !endYm || startYm > endYm) {
        Utils.logInfo(FUNC, '期間不正のためスキップ: ' + String(ord['発注ID(ORD)']));
        return;
      }

      // 継続月数を計算
      const [sy, sm] = startYm.split('-').map(Number);
      const [ey, em] = endYm.split('-').map(Number);
      const months = (ey - sy) * 12 + (em - sm) + 1;

      // エッジケース: 月数 <= 0
      if (months <= 0) {
        Utils.logInfo(FUNC, '継続月数不正のためスキップ: ' + String(ord['発注ID(ORD)']));
        return;
      }

      const baseAmt = ord['税抜金額_発注'];

      // エッジケース: 金額マイナス
      if (typeof baseAmt === 'number' && baseAmt < 0) {
        Utils.logError(FUNC, new Error('税抜金額_発注が負のためスキップ: ' + String(ord['発注ID(ORD)'])));
        return;
      }

      const monthlyExcl = Math.round(baseAmt / months);
      const monthlyTax = Math.round((ord['消費税額_発注'] || 0) / months);
      const monthlyIncl = monthlyExcl + monthlyTax;

      let limit = 0;
      let curYm = startYm;
      while (curYm <= endYm && limit < Constants.MONTH_ITERATION_LIMIT) {
        limit++;
        const idempotencyKey = String(ord['発注ID(ORD)']) + '_' + curYm;

        if (!existingKeys.has(idempotencyKey)) {
          const invDto = {
            '有効フラグ':         true,
            '請求ID(INV)':        RpaCommon.generateInvId(dateStr, offset++),
            '親発注ID(ORD)':      ord['発注ID(ORD)'],
            '起票日時':           now,
            '起票者':             'RPA_ORD_AUTO',
            '申請種別':           '請求書受領(AP)',
            '発生日(P/L計上日)':  curYm + '-01',
            '決済日_計画':        '',
            '請求ステータス':     '未処理',
            '収支区分':           '支出',
            '取引先名':           ord['取引先名'],
            'PJ名':               ord['PJ名'] || '',
            '組織名':             ord['組織名'],
            '科目名':             '',
            '税区分':             '課税',
            '通貨':               'JPY',
            '税抜金額_計画':      monthlyExcl,
            '消費税額_計画':      monthlyTax,
            '税込金額_計画':      monthlyIncl,
            '未決済残高(自動計算)': '',
            '決済手段':           '',
            '摘要':               String(ord['契約・件名'] || '') + ' ' + curYm,
            '証憑URL':            ord['証憑URL'] || '',
            '自動仕訳JNL_ID':     '',
          };
          newDtos.push(invDto);
          existingKeys.add(idempotencyKey);
        }
        curYm = Utils.addMonths(curYm, 1);
      }
    });

    // 7. append
    const count = InvoiceRepository.append(newDtos);

    // 8. トースト通知 & 監査ログ
    Utils.toastResult(FUNC, '生成完了: ' + count + '件の INV を生成しました。');
    Utils.auditLog('RUN', '32_wrk_invoice', '', '', FUNC, '', { generated: count }, 'Env=' + Env.name());
    Utils.logInfo(FUNC, '処理完了: ' + count + '件');

  } catch (e) {
    Utils.logError(FUNC, e);
    SpreadsheetApp.getUi().alert('🚨 ' + FUNC + ' でエラーが発生しました', e.message, SpreadsheetApp.getUi().ButtonSet.OK);
  } finally {
    lock.releaseLock();
  }
}
```

## 制約
- シートを直接操作しない。読み取りは OrderRepository.findAll() / InvoiceRepository.findAll()、書き込みは InvoiceRepository.append() のみ使用
- 既存関数・既存シートのスキーマを変更しない(科目名列追加は別案件)
- 列番号のハードコード禁止。ヘッダー名ベースで参照する(Repository 経由で自動対応)

## エッジケース
| 条件 | 処理内容 | 理由 |
|---|---|---|
| 税抜金額_発注 < 0 | 当該 ORD をスキップ、Utils.logError() でログ出力 | 負の月次 INV は会計上の異常値 |
| 税抜金額_発注 = 0 | 月額 0 円の INV を生成(スキップしない) | 0 円仕訳として記録する |
| 開始年月 > 終了年月(期間不正) | 当該 ORD をスキップ、Utils.logInfo() で警告ログ出力 | 月次ループが無限ループになるのを防ぐ |
| 継続月数 ≤ 0 | ゼロ除算回避のためスキップ、Utils.logInfo() でログ出力 | 月次按分の除算でゼロ除算が発生する |
| 冪等性キーが既存 INV に存在する月 | 当該月の INV 生成をスキップ | 二重計上防止 |
| 有効フラグ = FALSE の ORD | フィルタ段階で除外済みのためスキップロジック不要 | OrderRepository.findAll() 後のフィルタで除外 |

## 動作確認
1. npm run push:dev でデプロイ
2. メニュー「📋 ORD管理」→「継続契約→INV自動展開」を実行
3. 32_wrk_invoice に期待する月数分の INV が生成されたことを確認
4. 同じ ORD に対して再実行し、INV が重複しないこと(冪等性)を確認
5. エッジケース対象 ORD(金額マイナス・期間不正)でスキップされることをログで確認

### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| 実行前タスク(Read/調査) | あり | フィールドマッピング・冪等性キー確定 |
| 実装(Write/Edit) | なし | 確定済み内容の書き下し |

推奨実行モデル

工程推奨モデル理由
Phase 1(Read・調査・設計)SonnetRepository / Utils の組み合わせ確認など中程度の判断が必要
Phase 2(仕様書 Write/Edit)Sonnet確定済み内容の書き下し
Phase 3(コード実装)Sonnet既存 Repository・Utils を組み合わせた中程度の判断。コードが完全定義済みなら Haiku でも可

変更履歴

日付内容
2026-04-21初版作成

仕様書作成プロンプト(再現性・監査性のため)

展開して表示
<instruction>
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
1. **拡張思考の使い分け**: Phase 1(調査・設計)ではフル活用。ファイル名・関数名・行番号・エッジケース・Step 分割粒度・固有名詞(フィールド名/定数名/メニューラベル)を**ここで完全確定**させる。Phase 2(清書)では各 Step 内で拡張思考を最小限に抑え、Phase 1 確定済み内容の書き下しに徹する。出力途中で再考しない。
2. **テキスト報告の禁止**: 「〜を作成します」等、text のみで tool_use なしに turn を終了しない。説明は 1 文以内。直ちに tool を呼ぶ。
3. **4-5 分割の Write/Edit 実行**: Step 2-1(骨格 ~20行)/ 2-2(概要〜注意事項 ~300行)/ 2-3a(エッジケース〜人間検討事項 ~200行)/ 2-3b(実装プロンプト〜変更履歴 ~250行)/ 2-4(`<details>` プロンプト記録)に分割。1 回の Write/Edit は 300 行以内。
4. **各 Step で何を書くかを具体指示**: 設計判断を Phase 2 実行時に持ち込まない。

======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 S-54「発注→INV自動展開(継続契約)」の開発仕様書を作成してください。
作成後は `docs/_config.json` の `nav` 配列の適切なセクションに必ず登録すること。

---

## Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)

以下を順番に Read / Grep し、Phase 2 で設計判断を再考しなくて済む状態まで確定させること。
**Grep は「どこにあるか」の発見まで。「どう書くか」の判断は必ず Read で裏取り。推測した瞬間に手を止めて Read する。**

### 1-A: 案件定義の読み込み
- `docs/_internal/TODO_future.md` を Grep し、S-54 の案件名・概要・人間が検討すべき事項を取得する。

### 1-B: プロジェクト規約の読み込み
- `CLAUDE.md` を Read し、コーディング規約・ファイル番号体系・メニュー登録ルール・マイグレーションガイドラインを把握する。

### 1-C: 既存コードの調査(Read で必ず裏取り。以下を順に実行)

1. **`000_infra/003_contracts.js`** — `OrderDTO` と `InvoiceDTO` の全フィールド名を確認する。仕様書に記載するフィールド名はすべてここから引用すること(推測・造語禁止)。
2. **`200_data/202_repository.js`** — 以下を確認する:
   - `OrderRepository.findAll()` の戻り値型(`{ headers, dtos }` の構造)
   - `InvoiceRepository.findAll()` の戻り値型
   - `InvoiceRepository.append()` が自動付与するフィールド(`諸表区分`・`大分類` を AccountRepository 経由で付与すること、`PJ名` 未指定時のデフォルト値 `'指定なし_共通費など'` を把握する)
3. **`400_domain/407_rpa_orchestrator.js`** — 以下を確認する:
   - `RPAService` の公開関数の命名パターン(public か private `_` suffix か)
   - 既存のメニュー呼び出し可能関数の構造(ラッパー関数の有無)
   - 新関数をこのファイルに追記するか、新規ファイル `408_*.js` を作成するかを判断する
4. **`400_domain/400_rpa_common.js`** — INV の ID 発番方法を確認する(`RpaCommon` に `generateNextId_` 等のヘルパーがあるか、`Utils.getIdPrefixConfig()` との組み合わせを把握する)。
5. **`100_config/101_sys_config.js`** — `onOpen()` の `ui.createMenu` を Read し、新機能のメニュー登録先(メニュー名・区切り位置・隣接ラベル)を**実在する文字列のまま**確認する。造語禁止。
6. **`000_infra/004_utils.js`** — `Utils.parseDateToYm()`・`Utils.addMonths()`・`Utils.toastResult()`・`Utils.logInfo()`・`Utils.logError()` の引数シグネチャを確認する。
7. **`000_infra/002_constants.js`** — `Constants.SHEET_DEFAULTS` の `32_wrk_invoice` エントリを Read し、`smartAddRow` 経由でのデフォルト値マップを確認する(`InvoiceRepository.append()` が参照しないことにも注意)。

### 1-D: 参考テンプレートの読み込み
- `docs/dev/` 配下の新機能系仕様書を 1 件 Read し、セクション構成・実装プロンプトのフォーマット(行頭 4 スペースインデント)を把握する。

### Phase 1 で確定させること(Phase 2 では再考しない)
- 新関数の配置ファイル・関数名(menu-callable なら public、内部ヘルパーなら `_` suffix)
- メニュー追加先のラベル文字列(`onOpen()` から引用した実在する文字列)
- OrderDTO → InvoiceDTO の全フィールドマッピング(`発生日(P/L計上日)` の値形式、`税抜金額_計画`・`消費税額_計画`・`税込金額_計画` の月次按分計算式)
- INV の ID 発番方法(`RpaCommon` 等の実在する関数名)
- 冪等性チェックキーの正確な形式(実際の InvoiceDTO フィールド名を使った文字列、例: `dto['親発注ID(ORD)'] + '_' + Utils.parseDateToYm(dto['発生日(P/L計上日)'])`)
- 科目名の決定方針(DDL変更案 or 代替案)
- `InvoiceRepository.append()` が `科目名` 空のまま呼ばれると `諸表区分`・`大分類` が付与されない点の影響確認

---

## Phase 2: 仕様書の分割作成

出力先: `docs/dev/dev_mas-126_auto_inv_from_ord.md`
**1 回の Write/Edit は 300 行以内。必ず以下の Step に分割すること。**

### Step 2-1: 骨格の作成 (Write ~20行)
見出しのみ。本文は空で可。必須セクション順:

MAS-126: 発注→INV自動展開(継続契約)

概要 / ## 目的 / ## 現在のコード / ## 修正方針 / ## 影響範囲 / ## 注意事項

エッジケース / ## 実データ検証 / ## 関連ドキュメント / ## 人間が検討すべき事項

実装プロンプト(Claude Code 用)/ ## 推奨実行モデル / ## 変更履歴

仕様書作成プロンプト(再現性・監査性のため)


### Step 2-2: 前半セクションの追記 (Edit or Bash ~300行)
Phase 1 で確定済みの内容を書き下す。再考しない。以下を書く:

- **概要**: テーブル(案件ID=S-54, カテゴリ=新機能, Phase=未定, 優先度=未定, 所要時間=M, 対象ファイル=Phase 1 で確定したファイルパス, 前提案件=なし)
- **目的**: 継続契約 ORD から月次 INV を自動生成し、手動起票ミス・漏れを排除する旨を 1〜3 文で記述
- **現在のコード**: 「現在は INV を手動で 1 件ずつ起票している。継続契約に対する月次 INV 自動生成機能は未実装」と記述。コードスニペットは不要(新規追加のため)
- **修正方針**: 以下の構成で記述
  - 変更 1: Phase 1 で確定した配置ファイルに新関数(Phase 1 で確定した実在する関数名)を追加
  - 変更 2: `100_config/101_sys_config.js` の `onOpen()` に新メニュー項目を追加(Phase 1 で確定した実在するラベル文字列)
  - 処理フロー(箇条書き):
    1. `LockService.getScriptLock()` で排他ロック取得
    2. `OrderRepository.findAll()` で全 ORD 取得 → `契約形態 === '継続'`・`発注ステータス === '発注済'`・`有効フラグ === true` でフィルタ
    3. `InvoiceRepository.findAll()` で全 INV 取得 → 冪等性 Set を構築(キー形式は Phase 1 で確定した文字列)
    4. 対象 ORD ごとに `Utils.parseDateToYm()` で `開始年月`〜`終了年月` を正規化し、`Utils.addMonths()` で月次ループ
    5. 各月の InvoiceDTO を生成(フィールドマッピングは下記テーブル参照)
    6. 冪等性チェック:Set にキーが存在する月はスキップ
    7. 生成した InvoiceDTO 配列を `InvoiceRepository.append()` で書き込み
    8. `Utils.toastResult()` で生成件数をトースト通知
  - **OrderDTO → InvoiceDTO フィールドマッピングテーブル**(Phase 1 で確定した実在するフィールド名のみ使用。造語禁止): 導出元・計算式・デフォルト値を列ごとに明記。`消費税額_計画`・`税込金額_計画` の月次按分計算式も含む
  - `InvoiceRepository.append()` が `科目名` を元に `諸表区分`・`大分類` を自動付与する仕様を明記(科目名が空のまま append すると自動付与されない)
  - INV ID 発番方法(Phase 1 で確定した実在する関数名)を明記
- **影響範囲**: 変更ファイル(Phase 1 で確定)、`32_wrk_invoice` シートへの行追記、既存機能への影響なし
- **注意事項**:
  - 途中解約時に既存生成済み INV を無効化する場合はユーザーが手動で `有効フラグ` を FALSE に変更すること(本案件スコープ外)
  - `契約形態` の格納値は実データ検証で確認した表記と完全一致させること(表記ゆれに注意)
  - `LockService` のロック取得失敗時は処理を中断し `Utils.logError()` でログを残すこと

### Step 2-3a: エッジケース〜人間検討事項の追記 (Edit or Bash ~200行)
Phase 1 で確定済みの内容を書き下す。以下を書く:

- **エッジケース**: テーブル形式(条件 | 処理内容 | 理由)
  - `税抜金額_発注` < 0: 当該 ORD をスキップ、`Utils.logError()` でログ出力
  - `税抜金額_発注` = 0: 月額 0 円の INV を生成(スキップしない。0 円仕訳として記録する)
  - `開始年月 > 終了年月`(期間不正): 当該 ORD をスキップ、`Utils.logInfo()` で警告ログ出力
  - 継続月数 ≤ 0(`Utils.addMonths` の差分が 0 以下): ゼロ除算回避のためスキップ、`Utils.logInfo()` でログ出力
  - 冪等性キーが既存 INV に存在する月: 当該月の INV 生成をスキップ(二重計上防止)
  - `有効フラグ` = FALSE の ORD: フィルタ段階で除外済みのためスキップロジック不要
- **実データ検証**: MCP 等で実装前に確認すべき項目
  - `31_wrk_order` の「契約形態」列の実際の格納値(`'継続'` と `'継続(定期)'` 等の表記ゆれがないか)
  - `31_wrk_order` の「発注ステータス」列の実際の格納値(`'発注済'` の表記を確認)
  - `32_wrk_invoice` の「親発注ID(ORD)」列の値形式(冪等性キー設計の裏取り)
  - `11_mst_account` に継続契約向け科目(例: 外注費・支払手数料等)が登録済みかを確認
- **関連ドキュメント**: Phase 1 で発見した関連仕様書リンクをテーブル形式で記載
- **人間が検討すべき事項**:
  - **科目名の決定方針(最重要)**: `InvoiceRepository.append()` は `科目名` を元に `諸表区分`・`大分類` を自動付与するため、空のまま生成すると財務諸表分類が機能しない。**推奨方針**: `31_wrk_order` シートに「科目名」列を追加する DDL 変更(別案件)を行い、その値を INV に転記する。**代替方針**: 生成後にユーザーが手動で科目名を設定する(`請求ステータス = '未処理'` のレビュー工程で対応)
  - 生成される `請求ステータス` は `'未処理'` 固定(Human-in-the-Loop)。ユーザーが目視確認後に手動承認する運用フローを前提とする
  - `TODO_future.md` に記載の「途中解約時のルール」は本案件スコープ外。既存生成済み INV の無効化は手動運用とする

### Step 2-3b: 実装プロンプト〜変更履歴の追記 (Edit or Bash ~250行)
行頭 4 スペースインデント。バッククォートで囲まない。以下を書く:

    あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
    案件 S-54「発注→INV自動展開(継続契約)」を実装してください。

    ## 実行前タスク
    (Phase 1 で特定した全ファイルを列挙し、各ファイルで確認すべきポイントを明記)

    ## 修正対象ファイル
    (Phase 1 で確定したファイルパスと「のみ」または「への追記」を明示)

    ## 実装内容
    (処理フロー・OrderDTO→InvoiceDTO フィールドマッピングテーブル・冪等性ロジック・ID発番方法を具体的に記述)

    ## 制約
    - シートを直接操作しない。読み取りは OrderRepository.findAll() / InvoiceRepository.findAll()、書き込みは InvoiceRepository.append() のみ使用
    - 既存関数・既存シートのスキーマを変更しない(科目名列追加は別案件)
    - 列番号のハードコード禁止。ヘッダー名ベースで参照する

    ## エッジケース
    (Step 2-3a のエッジケーステーブルをそのまま転記)

    ## 動作確認
    1. npm run push:dev でデプロイ
    2. メニューから新機能を実行(Phase 1 で確定した実在するメニューラベルを記載)
    3. 32_wrk_invoice に期待する月数分の INV が生成されたことを確認
    4. 同じ ORD に対して再実行し、INV が重複しないこと(冪等性)を確認
    5. エッジケース対象 ORD(金額マイナス・期間不正)でスキップされることをログで確認

    ### 拡張思考の使用状況
    | フェーズ | 拡張思考 | 備考 |
    |---------|---------|------|
    | 実行前タスク(Read/調査) | あり | フィールドマッピング・冪等性キー確定 |
    | 実装(Write/Edit) | なし | 確定済み内容の書き下し |

- **推奨実行モデル**: テーブル(工程 | 推奨モデル | 理由)。既存 Repository・Utils を組み合わせた中程度の判断 → Sonnet
- **変更履歴**: `| 2026-04-20 | 初版作成 |`

### Step 2-4: 仕様書作成プロンプトの記録 (Edit or Bash)
`## 仕様書作成プロンプト(再現性・監査性のため)` セクションに `<details><summary>展開して表示</summary>` ブロックを追加し、この `<instruction>` 全文をそのまま記録する。

---

## Phase 3: 保存・登録・コミット

### 3-A: `docs/_config.json` にナビゲーション登録(必須)
`nav` 配列の §E.6(パイプライン・RPA・外部連携)セクションに追加(既存エントリの連番を確認してから採番する):
```json
{ "file": "dev/dev_mas-126_auto_inv_from_ord.md", "title": "E.6.X MAS-126 発注→INV自動展開(継続契約)" }

追記後、JSON 構文エラーがないことを確認する。

3-B: docs/_internal/changelog.md への追記

ヘッダー直後の先頭行に追記:

| 2026-04-20 | [dev_mas-126_auto_inv_from_ord.md](dev_mas-126_auto_inv_from_ord.md) | 初版作成。継続契約ORDから月次INVを自動生成する機能の仕様書 |

3-C: コミット&プッシュ

git add docs/dev/dev_mas-126_auto_inv_from_ord.md docs/_config.json docs/_internal/changelog.md
git commit -m "docs: MAS-126 発注→INV自動展開(継続契約)の開発仕様書を作成

継続契約ORDから月次INVを自動生成する機能の仕様書。
冪等性チェック・エッジケース・科目名DDL変更案(別案件)を含む。"
git push -u origin <現在のブランチ名>
```