概要

項目内容
案件IDMAS-022
カテゴリFP&A・レポーティング
PhaseP2
優先度★★
所要時間2-3時間(新規ファイル + メニュー1行追記のみ)
対象ファイル600_report/610_variance_drilldown.js(新規作成)
000_infra/002_constants.jsConstants.MENU_DEFINITION への項目1行追記)
参照元シート65_pl_variance(MAS-001 で生成済の予実差異分析シート)
前提案件MAS-001(予実差異分析)— 本案件は MAS-001 の発展版

目的

MAS-001 で可視化された予実差異(65_pl_variance)において、特定した「差異が大きい科目×年月」の原因を、INV単位で即座に参照できるドリルダウン機能を追加する。

差異分析の現状課題:

  • 65_pl_variance では集計結果(差異額・差異率)は把握できるが、「差異を生んでいる個別 INV」にたどり着くには 32_wrk_invoice を手動フィルタする必要がある
  • 本機能により、ユーザーは差異分析シート上で科目名と年月のセルを含む領域を選択 → メニュー実行の2ステップで、原因 INV 一覧を別シートに表示できる

期待される効果: 差異要因の即時特定。予実レビュー MTG の生産性向上。

現在のコード

新規機能のため既存実装はない。ただし以下の既存構造に依存する:

依存先確認済の構造利用内容
000_infra/002_constants.js L206-324Constants.MENU_DEFINITION 配列。MAS-214 でメニューは onOpen() 直書きから MENU_DEFINITION ループ方式に移行済メニュー項目の追加は本配列の適切なカテゴリに1行追記で行う(101_sys_config.jsonOpen() 本体は編集不要)
100_config/101_sys_config.js L323-350onOpen()Constants.MENU_DEFINITION.forEach で動的生成編集不要(本配列参照元として確認のみ)
200_data/202_repository.js L152-208InvoiceRepository.findAll(){ headers: string[], dtos: InvoiceDTO[] } を返す(dtos 配列のみではない).dtos を参照してフィルタ
000_infra/003_contracts.js L38-67InvoiceDTO のフィールド名: 有効フラグ / 請求ID(INV) / 発生日(P/L計上日) / 請求ステータス / 取引先名 / 科目名 / 摘要 / 税抜金額_計画 / 消費税額_計画 / 税込金額_計画表示列のキー名として使用
000_infra/004_utils.js L92-99Utils.parseDateToYm(val) は Date型 / "YYYY/MM" / "YYYY-MM" / "YYYY年MM月" 等を受け取り "YYYY-MM" 文字列を返すINV の発生日を年月に正規化してフィルタ
600_report/608_datamart_render.js L128-18265_pl_variance のレイアウト: 行1=タイトル / 行2=2段ヘッダー上段(月名を merge 配置。通期=B2:E2、M1=F2:I2、M2=J2:M2 ...)/ 行3=サブヘッダー(実績/予算/差異額/差異率)/ 行4+=データ行(列A=科目名)アクティブセルから科目名・年月を逆引きするロジックの根拠

修正方針

UI 設計(単一セル選択方式)

65_pl_variance のレイアウトから、アクティブセル1個で科目名・年月の両方を逆引きできる:

導出項目取得方法
科目名sheet.getRange(activeRow, 1).getValue()(列A=科目名列)
年月activeCol から所属グループ g を算出: g = Math.floor((activeCol - 2) / 4)。グループ開始列 startCol = 2 + g * 4行2 の値を Utils.parseDateToYm() で正規化
グループ判定g === 0 → 通期(YYYY年通期扱い。後述の人間検討事項 #1 を参照) / g === 1..12 → 月別

ユーザー操作: 差異が気になるセル(例: 2026-01 の売上高の差異額セル)を選択 → メニュー実行。2セル選択や範囲選択は不要。

一時シートの生成

  • シート名: F22_[科目名(先頭15文字・禁止文字置換)]_[年月]
    • 例: F22_外注費_2026-01
  • GAS のシート名は31文字以内 + 禁止文字 / \ ? * [ ] : があるため、科目名を replace(/[\/\\?\*\[\]:]/g, '_') で置換後に .substring(0, 15) でトリミング
  • 同名シートが既存する場合は削除してから再生成(同じ条件で再実行した際の最新性を保証)

データ取得とフィルタ

  • InvoiceRepository.findAll() を使用し戻り値 .dtos から絞り込む。シートの直接参照禁止getSheetByName('32_wrk_invoice').getValues() 等は使わない。Repositoryパターン遵守)
  • フィルタ条件(全て AND):
    1. dto['有効フラグ'] !== false && String(dto['有効フラグ']).toUpperCase() !== 'FALSE'(有効行のみ。CLAUDE.md 規約)
    2. Utils.parseDateToYm(dto['発生日(P/L計上日)']) === 選択年月
    3. dto['科目名'] === 選択科目名

結果シートへの書き込み

  • 表示列ヘッダー(この順序・この表記で固定):

    ['請求ID(INV)', '発生日(P/L計上日)', '取引先名', '科目名', '摘要', '請求ステータス', '税抜金額_計画', '消費税額_計画', '税込金額_計画']
    

    科目名 を含めることで、フィルタ結果の検証を容易にする(同一科目名で全行揃っているはず)

  • 書き込み方式: sheet.getRange(2, 1, rows.length, headers.length).setValues(rows)一括書き込み

    • appendRow のループ書き込み禁止(200件超でもAPIコール1回に抑える。大量データ対策)
  • 1行目にヘッダー行を書き込みBOLD設定 + フィルター設定

削除ボタン

  • 一時シートに図形描画(sheet.insertImage ではなく sheet.newChart でもなく、より単純に Drawing を想定するが実装上は「図形+スクリプト割当」)で「🗑 このシートを削除」ボタンを配置
  • ボタンに deleteResultSheet_() 関数を割り当て
  • deleteResultSheet_(): アクティブシート名が F22_ で始まる場合のみ削除。それ以外は alert で誤削除警告

メニュー追加(Constants.MENU_DEFINITION

既存メニュー構造 📋 サイドバー: 📊 マート更新002_constants.js L230-239)の末尾に以下を追記する:

{ label: '🔎 差異INVドリルダウン (F-22)', funcName: 'drilldownVariance_', description: '選択セルの科目×年月に該当する INV を一時シートで表示' },

カテゴリ名「📋 サイドバー: 📊 マート更新」は実在のもの(Phase 1 Read 済)。新規グループは作らない。

影響範囲

種別ファイル変更内容
新規追加600_report/610_variance_drilldown.jsdrilldownVariance_() / deleteResultSheet_() の2関数
1行追記000_infra/002_constants.jsConstants.MENU_DEFINITION📋 サイドバー: 📊 マート更新 カテゴリに項目1行追加

既存コード変更なし:

  • 差異分析ロジック(600_report/603_datamart_pl.jsdmBuildPlVarianceOutput_)は不変
  • 65_pl_variance の構造・レンダリング(608_datamart_render.js)は不変
  • InvoiceRepository / Contracts / Utils は参照のみ
  • onOpen() 本体(101_sys_config.js L323-350)は不変(MENU_DEFINITION ループ経由で自動反映される)

注意事項

  1. 読み取り専用機能: InvoiceRepository.save() / append() など書き込みメソッド呼び出し禁止。本機能はデータの参照・表示に特化する。
  2. Repositoryパターン遵守: 32_wrk_invoice をシート直接参照しない。必ず InvoiceRepository.findAll().dtos を使う。
  3. 戻り値型の注意: InvoiceRepository.findAll(){ headers, dtos } オブジェクトを返す。.dtos プロパティを取得すること(配列直接渡し不可)。
  4. シート名32文字制限: 科目名が長い場合、科目名.replace(/[\/\\?\*\[\]:]/g, '_').substring(0, 15) でトリミング。末尾に _YYYY-MM(8文字)+ プレフィックス F22_(4文字)が付くため、科目名部分は15文字以下に抑える必要がある。
  5. メニュー配置: メニュー追加は Constants.MENU_DEFINITION002_constants.js)への1行追記のみ。101_sys_config.jsonOpen() は触らない(MAS-214 以降、メニュー定義は Constants 配列経由で動的生成される)。
  6. 却下INVの扱い: 請求ステータス === '却下' の INV を含めるか否かは人間が検討すべき事項(後述)。初版では全ステータスを表示し、結果シートの「請求ステータス」列でフィルタ確認する方針とする。
  7. 通期セルの扱い: アクティブセルが「通期」列(B-E列)の場合は、年月の特定ができないため alert でガイドして処理終了(人間検討事項 #2 参照)。

エッジケース

#条件挙動理由
E1アクティブシートが 65_pl_variance でないalert('差異分析シート (65_pl_variance) を開いてからセルを選択してください。') で終了誤操作防止
E2アクティブセルが行3以下(ヘッダー領域)alert('データ行のセルを選択してください。') で終了科目名の取得不可
E3アクティブセルが列A(科目名列)alert('月別の数値セル(実績/予算/差異額/差異率のいずれか)を選択してください。') で終了年月の特定不可
E4アクティブセルが通期サマリー列(B-E列、g=0)alert('通期サマリー列はドリルダウン対象外です。月別セルを選択してください。') で終了単一年月への絞込ができない(複数月の合算のため)
E5行2から取得した年月が parseDateToYm で空文字alert('年月の特定に失敗しました。') で終了レイアウト破損検知
E6科目名セルが空文字(グループヘッダー行等)alert('科目名のあるデータ行を選択してください。') で終了集計行("【売上高 計】" 等)や空行への対応
E7該当INVが0件alert('対象データが見つかりませんでした。\\n科目名: [xxx]\\n年月: [YYYY-MM]') で終了、一時シート生成なし空シート生成は混乱を招く
E8マイナス金額のINVが存在そのまま表示(除外しない)返金・値引き・取消伝票は差異要因として重要
E9有効フラグ=FALSE のINVフィルタで除外全処理で有効行のみを対象とする CLAUDE.md 規約
E10該当INVが200件超setValues() 一括書き込みのため問題なしGAS APIコール数の最小化(appendRow ループ禁止)
E11科目名に禁止文字(/ \ ? * [ ] : 等)が含まれるシート名生成時に replace(/[\/\\?\*\[\]:]/g, '_') で置換後、15文字にトリミングGASシート名の禁止文字対策
E12同名の一時シート(F22_XXX_YYYY-MM)が既存既存シートを削除してから再生成再実行時の最新性保証
E1365_pl_variance タブが未生成(MAS-001 未実行)E1 で captured される(getName() !== '65_pl_variance' で弾かれる)ため個別対応不要前提条件の不成立
E14選択セルの行が最終データ行より下(空欄領域)E6(科目名セル空文字)で captured される誤操作防止

実データ検証

実装前に確認すべき項目(GASエディタまたはスプレッドシート上で手動確認):

#検証項目確認方法
V165_pl_variance の実レイアウト: 行2(2段ヘッダー上段)に YYYY-MM 形式の月名が入っているか、Date型かを確認Logger.log(sheet.getRange(2, 6).getValue()) 等で値の型を確認。Utils.parseDateToYm で正規化できることを保証
V265_pl_variance の科目名列(列A)に、グループ見出し行("■ 売上高" 等)や空行が混在することを確認E6 のバリデーションが正しく機能するか
V332_wrk_invoice科目名 列の格納値が、差異分析シートの科目名表記と完全一致することを確認マスタ表記との乖離があれば該当INV 0 件になる
V432_wrk_invoice発生日(P/L計上日) 列の格納値が Date 型か文字列型か(環境により異なる可能性)Utils.parseDateToYm は両形式対応済だが、想定外形式(元号表記等)がないか確認
V5差異率セル(各グループの4列目、0.0% フォーマット)が選択された場合も、g の算出((col - 2) % 4 === 3)が正しく動作するかE4 のガード条件に影響する

関連ドキュメント

仕様書関連箇所
dev_mas-001_variance_analysis.md本案件の前提。65_pl_variance のレイアウト定義・差異計算ロジック
dev_mas-025_budget_variance.mdMAS-001 から分離された予算 vs 実績差異(66_pl_budget_variance)。本機能の対象は MAS-001 の 65_pl_variance のみ
dev_mas-020_yoy_comparison.md前年同月比較。差異ではなく YoY 差分のため本機能の対象外
CLAUDE.mdRepositoryパターン・シート書き込みルール・有効フラグ規約

人間が検討すべき事項

  1. 請求ステータス === '却下' の INV を表示するか

    • 却下 INV は承認されていないため、予実差異の「要因」には該当しない見方もできる
    • 一方で「なぜ却下されたのか/却下されなければ差異がどうだったか」の参考情報として有用
    • 初版案: 全ステータス表示。結果シートの「請求ステータス」列でユーザーが必要に応じてフィルタ
  2. 通期サマリー列(g=0)を選択した場合の挙動

    • 現在の仕様では alert でガイドして終了(エッジケース E4)
    • 代替案: 通期を選択した場合は「年月」条件を外して全期間の該当INVを表示する
    • 初版案: 終了方式(年月が特定できないため。必要なら将来機能として拡張)
  3. 一時シートの自動削除タイミング

    • 現在はボタン押下による手動削除のみ
    • 代替案1: セッション終了時のトリガーで自動削除(実装コスト高)
    • 代替案2: 起動時(onOpen)に F22_ で始まるシートを一括削除
    • 初版案: 手動削除のみ。運用で問題があれば N-XX として追加改修
  4. F22_ プレフィックスで始まる他シートとの競合

    • 現時点では他に F22_ で始まるシート生成機能は存在しないが、将来追加される場合を考慮
    • 初版案: 現状維持。将来 MAS-022 系機能が増えた場合は接頭辞を F22DD_ 等に変更
  5. 結果シートの表示列の拡張要望

    • 現在の 9 列(INV / 発生日 / 取引先 / 科目 / 摘要 / ステータス / 税抜 / 税 / 税込)で十分か
    • 追加候補: 申請種別 / 決済手段 / PJ名 / 組織名 / 決済日_計画 / 自動仕訳JNL_ID
    • 初版案: 9列で確定。ユーザーフィードバックを受けて拡張

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

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-022「予実差異の要因ドリルダウン(INV単位)」を実装してください。

## 実行前タスク

以下のファイルを Read し、仕様書記載の固有名詞と実コードが一致することを確認する:

1. `000_infra/002_constants.js` L206-324 — `Constants.MENU_DEFINITION` の既存カテゴリ名(特に `📋 サイドバー: 📊 マート更新` L230)・項目の並びを確認
2. `100_config/101_sys_config.js` L323-350 — `onOpen()` が `MENU_DEFINITION.forEach` で動的生成していることを確認(本ファイルは編集不要)
3. `200_data/202_repository.js` L152-208 — `InvoiceRepository.findAll()` の戻り値型 `{ headers, dtos }` を確認
4. `000_infra/003_contracts.js` L38-67 — `InvoiceDTO` の全フィールド名(`有効フラグ` / `請求ID(INV)` / `発生日(P/L計上日)` / `請求ステータス` / `取引先名` / `科目名` / `摘要` / `税抜金額_計画` / `消費税額_計画` / `税込金額_計画`)を確認
5. `000_infra/004_utils.js` L92-99 — `Utils.parseDateToYm(val)` のシグネチャと対応入力形式を確認
6. `600_report/608_datamart_render.js` L128-182 — `65_pl_variance` のレイアウト(行2=月名 merge、列A=科目名、4列1グループ)を確認
7. `docs/dev/dev_mas-022_variance_drilldown.md` — 本仕様書

## 修正対象ファイル

- `600_report/610_variance_drilldown.js`(新規作成)
- `000_infra/002_constants.js`(`Constants.MENU_DEFINITION` への項目1行追記のみ)

## 実装内容

### 1. `600_report/610_variance_drilldown.js` を新規作成

以下の2関数を実装する。

#### 1-A. `drilldownVariance_()` — メニューから呼び出されるエントリポイント

処理フロー:

1. アクティブシートを取得し、`sheet.getName() !== '65_pl_variance'` なら `alert('差異分析シート (65_pl_variance) を開いてからセルを選択してください。')` で終了
2. アクティブセル `range = sheet.getActiveCell()` から `row = range.getRow()` / `col = range.getColumn()` を取得
3. バリデーション(以下のいずれかで `alert` して終了):
   - `row < 4` → 'データ行のセルを選択してください。'
   - `col === 1` → '月別の数値セル(実績/予算/差異額/差異率のいずれか)を選択してください。'
   - `g = Math.floor((col - 2) / 4)` を算出し `g === 0` → '通期サマリー列はドリルダウン対象外です。月別セルを選択してください。'
4. 科目名を取得: `const accountName = String(sheet.getRange(row, 1).getValue()).trim()`
   - 空文字なら `alert('科目名のあるデータ行を選択してください。')` で終了
5. 年月を取得: `const monthCellRawVal = sheet.getRange(2, 2 + g * 4).getValue()`。`const ymStr = Utils.parseDateToYm(monthCellRawVal)`。空文字なら `alert('年月の特定に失敗しました。')` で終了
6. `InvoiceRepository.findAll().dtos` をフィルタ:

   ```js
   const result = InvoiceRepository.findAll();
   const filtered = result.dtos.filter(function(dto) {
     if (dto['有効フラグ'] === false || String(dto['有効フラグ']).toUpperCase() === 'FALSE') return false;
     if (Utils.parseDateToYm(dto['発生日(P/L計上日)']) !== ymStr) return false;
     if (String(dto['科目名']).trim() !== accountName) return false;
     return true;
   });
   ```

7. `filtered.length === 0` の場合は `alert('対象データが見つかりませんでした。\\n科目名: ' + accountName + '\\n年月: ' + ymStr)` で終了(一時シート生成なし)
8. 一時シート名を生成:

   ```js
   const safeName = accountName.replace(/[\/\\?\*\[\]:]/g, '_').substring(0, 15);
   const resultSheetName = 'F22_' + safeName + '_' + ymStr;
   ```

9. 同名シートが既存なら削除してから新規作成:

   ```js
   const ss = sheet.getParent();
   const existing = ss.getSheetByName(resultSheetName);
   if (existing) ss.deleteSheet(existing);
   const out = ss.insertSheet(resultSheetName);
   ```

10. 表示列ヘッダー(**この順序・表記で固定**):

    ```js
    const displayHeaders = ['請求ID(INV)', '発生日(P/L計上日)', '取引先名', '科目名', '摘要', '請求ステータス', '税抜金額_計画', '消費税額_計画', '税込金額_計画'];
    ```

11. ヘッダー行書き込み + BOLD設定:

    ```js
    out.getRange(1, 1, 1, displayHeaders.length).setValues([displayHeaders]).setFontWeight('bold');
    ```

12. データ行の構築(DTO → 配列):

    ```js
    const rows = filtered.map(function(dto) {
      return displayHeaders.map(function(h) { return dto[h] !== undefined ? dto[h] : ''; });
    });
    ```

13. 一括書き込み(**`appendRow` ループ禁止**):

    ```js
    out.getRange(2, 1, rows.length, displayHeaders.length).setValues(rows);
    ```

14. フィルターを設定:

    ```js
    out.getRange(1, 1, rows.length + 1, displayHeaders.length).createFilter();
    ```

15. 列幅自動調整(任意):

    ```js
    out.autoResizeColumns(1, displayHeaders.length);
    ```

16. 削除ボタン(図形)を追加:

    ```js
    // GAS では DrawingService の直接操作は限定的。代替としてシート名 'F22_' プレフィックスと
    // deleteResultSheet_() 関数を組み合わせ、A1 セル上部に注記を入れる:
    //   "🗑 このシートを削除するには、メニュー [📊 マート更新] → [🗑 F22 結果シート削除] を実行"
    // 図形ボタンのスクリプト割当は GAS API では直接制御不可なため、
    // 削除はメニュー項目から deleteResultSheet_() を呼ぶ方式で実装する。
    ```

    実装シンプル化のため、**仕様書当初の「図形ボタン」ではなくメニューから削除する方式**に変更する(GAS の図形描画 API は限定的で、スクリプト割当は手動操作に依存する)。
17. 結果シートをアクティブ化: `ss.setActiveSheet(out)`

#### 1-B. `deleteResultSheet_()` — 結果シート削除関数

```js
function deleteResultSheet_() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const ui = SpreadsheetApp.getUi();
  const sheet = ss.getActiveSheet();
  const name = sheet.getName();
  if (!name.startsWith('F22_')) {
    ui.alert('⚠️ 削除対象は F22_ で始まる一時シートのみです。\\n現在のシート: ' + name);
    return;
  }
  const res = ui.alert('確認', name + ' を削除しますか?', ui.ButtonSet.YES_NO);
  if (res === ui.Button.YES) {
    ss.deleteSheet(sheet);
  }
}
```

### 2. `000_infra/002_constants.js` の `Constants.MENU_DEFINITION` を修正

L230-239 の `📋 サイドバー: 📊 マート更新` カテゴリの `items` 配列末尾に、以下の2項目を追加:

```js
{ label: '🔎 差異INVドリルダウン (F-22)', funcName: 'drilldownVariance_', description: '選択セルの科目×年月に該当する INV を一時シートで表示' },
{ label: '🗑 F22 結果シート削除', funcName: 'deleteResultSheet_', description: 'アクティブな F22_ で始まる一時シートを削除' },
```

追記位置は `savePlSnapshot` の直後(カテゴリ配列末尾)。既存項目の順序は変更しない。

## 制約

- `InvoiceRepository.save()` / `append()` 等の書き込みメソッド呼び出し禁止(参照専用)
- シート直接参照禁止(`getSheetByName('32_wrk_invoice').getValues()` 等)。必ず `InvoiceRepository.findAll().dtos` を使う
- `appendRow` のループ書き込み禁止。`setValues()` で一括書き込みする
- `101_sys_config.js` の `onOpen()` 本体は編集しない(MENU_DEFINITION 経由で自動反映)
- 新規ファイル・新規メニューカテゴリは作らない(既存構造への追記のみ)

## エッジケース(実装時に必ず対応)

| 条件 | 挙動 |
|------|------|
| アクティブシートが 65_pl_variance でない | alert して終了 |
| アクティブセルが行3以下 | alert('データ行のセルを選択してください。') |
| アクティブセルが列A | alert('月別の数値セル...を選択してください。') |
| アクティブセルが通期列 (g=0) | alert('通期サマリー列はドリルダウン対象外です。') |
| 年月が parseDateToYm で空 | alert('年月の特定に失敗しました。') |
| 科目名が空文字 | alert('科目名のあるデータ行を選択してください。') |
| 該当INVが0件 | alert + 一時シート生成なし |
| マイナス金額INV | そのまま表示(除外しない) |
| 有効フラグ=FALSE | フィルタで除外 |
| シート名に禁止文字 | replace(/[\/\\?\*\[\]:]/g, '_') で置換後15文字にトリミング |
| 200件超の該当INV | setValues() 一括書き込みのためタイムアウト問題なし |
| 同名の既存シート | 削除してから再生成 |

## 動作確認

1. `npm run push:dev` で開発環境にデプロイ
2. 開発用スプレッドシートをリロード → `onOpen()` により `📋 サイドバー: 📊 マート更新` に新項目が出ることを確認
3. MAS-001 の差異分析が未実行なら `buildBudgetTrendDataMart` を実行して `65_pl_variance` を生成
4. `65_pl_variance` を開き、差異額の大きいセル(例: 2026-01 の外注費の差異額セル)を選択
5. メニュー `🔎 差異INVドリルダウン (F-22)` を実行
6. `F22_外注費_2026-01` 等の一時シートが生成され、9列のヘッダーと該当INV一覧が表示されることを確認
7. エッジケーステスト:
   - 列A(科目名セル)を選択してメニュー実行 → 適切なalert
   - 通期列(B-E)を選択してメニュー実行 → 適切なalert
   - 存在しない科目×年月の組み合わせ(グループヘッダー行等)を選択 → 'データが見つかりませんでした' alert
8. 生成された一時シートをアクティブにしてメニュー `🗑 F22 結果シート削除` を実行 → 確認ダイアログ後に削除されることを確認
9. 他のシート(例: `61_pl_act`)をアクティブにして `🗑 F22 結果シート削除` を実行 → '削除対象は F22_ で始まる...' alert が出ることを確認
10. `900_test/901_test_runner.js` の全テストが引き続き通ることを確認(RPA・Action A/B・マート更新系への影響がないこと)
11. 動作問題なしを確認後、`git commit` → `git push` → PR → main マージ

### 拡張思考の使用状況

| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| Phase 1(コード調査) | あり | MENU_DEFINITION 構造・InvoiceDTO フィールド名・65_pl_variance レイアウトの確認に使用 |
| Phase 2(実装) | なし | 仕様書でコード構造が完全定義済み。Phase 1 確定事項の書き出しに徹する |

推奨実行モデル

工程推奨モデル理由
仕様書作成(本ドキュメント)Claude Sonnet 4.6複数ファイル横断の構造理解が必要(65_pl_variance レイアウト・MENU_DEFINITIONInvoiceRepositoryInvoiceDTO)だが、会計ロジック変更や高度な設計判断は不要
Phase 2 実装Claude Haiku 4.5仕様書でコード構造・関数シグネチャ・エッジケースが完全定義済み。判断要素なし。Constants.MENU_DEFINITION への1行追記と新規ファイル作成のみ

変更履歴

日付変更内容
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実行: 2-1(骨格〜20行)/2-2(概要〜注意事項〜300行)/2-3a(エッジケース〜人間検討事項〜200行)/2-3b(実装プロンプト〜変更履歴〜250行)/2-4(<details>プロンプト全文記録)に分割。
4. 各Stepで何を書くかを具体指示: 設計判断をPhase 2実行時に持ち込まないよう、各Stepの内容をPhase 1で確定させてから書き出す。

======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 F-22「予実差異の要因ドリルダウン(INV単位)」の開発仕様書を作成してください。
作成後、`docs/_config.json` の `nav` 配列の適切なセクションにも必ず追記してください。

---

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

以下をすべてツールで読み込み、Phase 2で設計判断を再考しないよう情報を確定させること。

### 1-A: 案件定義の読み込み
- `docs/_internal/TODO_future.md` — F-22の案件名・概要・期待効果・人間が検討すべき事項を取得

### 1-B: プロジェクト規約の読み込み
- `CLAUDE.md` — コーディング規約・ファイル番号体系・メニュー運用ルールを把握

### 1-C: 既存仕様書テンプレートの読み込み
- `docs/dev/dev_mas-001_variance_analysis.md` — 予実差異分析の既存仕様。差異分析シートの構造(シート名・列レイアウト・科目名/年月の格納位置)を正確に把握する

### 1-D: 関連GASコードの調査(Grep→Read順。推測で書かない)

以下を **Read** で開いて確認すること。Grepのヒット行だけで構造を類推しない(失敗パターン #18-#20 直接対策)。

| ファイル | 確認すべき事項 |
|---------|--------------|
| `100_config/101_sys_config.js` | `onOpen()` の実在するメニュー名・グループ構造を一字一句確認。**造語禁止**(失敗パターン #20) |
| `200_data/202_repository.js` | `InvoiceRepository.findAll()` の戻り値型が `{ headers: string[], dtos: Object[] }` であることを確認(dtos配列のみではない) |
| `000_infra/003_contracts.js` | `InvoiceDTO` の全フィールド名を確認。特に `発生日(P/L計上日)`・`科目名`・`請求ステータス`・`有効フラグ`・`税抜金額_計画`・`消費税額_計画`・`税込金額_計画` の実際の表記 |
| `000_infra/004_utils.js` | `parseDateToYm(val)` のシグネチャと対応入力形式(Date型・"YYYY-MM"・"YYYY/MM"等)を確認 |
| `600_report/` 配下の関連ファイル | 差異分析シートへの書き込み関数・シートキー(`Utils.getSheetNameByKey` に渡すキー文字列)を確認 |

**重要**: `onOpen()` 内のメニュー名、既存のメニューグループ名は必ず `101_sys_config.js` を Read してから記述する。記憶・推測で書いた固有名詞は必ずミスを生む。

### 1-E: 実装ファイル配置の確定
- `600_report/` 配下の既存ファイル連番(601〜608)を確認し、新規ファイル名を `600_report/609_variance_drilldown.js` と確定する(conflictがあれば調整)
- 差異分析シートの実際のシート名(`Utils.getSheetNameByKey` 経由で引けるキー、またはシート直接名)をPhase 1で特定し、Phase 2にリテラルで埋め込む

---

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

出力先: `docs/dev/dev_mas-022_variance_drilldown.md`

**【絶対制約】1回のツール呼び出しで全内容を出力しない。以下のStep順に分割実行する。**

### Step 2-1: 骨格の作成(File Write、〜20行)

`docs/_internal/dev_spec_prompt_template.md` のセクション構成に従い、見出しのみの骨格を作成する:
# F-22: 予実差異の要因ドリルダウン(INV単位)
## 概要 / ## 目的 / ## 現在のコード / ## 修正方針 / ## 影響範囲 /
## 注意事項 / ## エッジケース / ## 実データ検証 /
## 関連ドキュメント / ## 人間が検討すべき事項 /
## 実装プロンプト(Claude Code 用)/ ## 推奨実行モデル / ## 変更履歴 /
## 仕様書作成プロンプト

### Step 2-2: 前半セクションの追記(File Edit または Bash、〜300行)

以下を書く(Phase 1で確定した固有名詞・行番号のみ使用。推測補完しない):

**概要テーブル**: 案件ID=F-22、カテゴリ=FP&A・レポーティング、対象ファイル=`600_report/609_variance_drilldown.js`(新規)・`100_config/101_sys_config.js`(onOpenメニュー追記)

**目的**: 予実差異分析シートで特定した科目×年月の差異原因を、INV単位で即座に参照できるドリルダウン機能を追加する。

**現在のコード**: 新規追加のため既存コードなし。ただし `101_sys_config.js` の `onOpen()` へのメニュー追加箇所を特定し、行番号を明記する(Phase 1で確認した実在する行番号を記載)。

**修正方針**:
- ユーザーが予実差異分析シート(Phase 1で特定したシート名を記載)で、科目名セルと年月セルを選択してメニューを実行する方式
- 実行後、`F22_[科目名]_[年月]` 形式の一時シートを生成してドリルダウン結果を表示(シート名は32文字以内に収まることを確認する。超える場合は科目名を先頭15文字に切り詰める)
- 一時シートには図形描画で「🗑 このシートを削除」ボタンを配置し、`deleteResultSheet_()` 関数を割り当てる
- データ取得は `InvoiceRepository.findAll()` を使用し戻り値 `.dtos` から絞り込む。シートの直接参照禁止(Repositoryパターン遵守)
- フィルタ条件: `有効フラグ !== false`(有効行のみ)かつ `Utils.parseDateToYm(dto['発生日(P/L計上日)']) === 選択年月` かつ `dto['科目名'] === 選択科目名`
- 結果シートへの書き込みは `sheet.getRange(startRow, 1, rows.length, headers.length).setValues(rows)` で一括書き込みする(appendRowループ禁止。大量データ対策)
- メニュー追加は `101_sys_config.js` の `onOpen()` 内、Phase 1で確認した既存メニュー構造の適切な位置に追記する(メニューグループ名は実在するもののみ使用)

**表示列ヘッダー**(この順序・この表記で固定):
['請求ID(INV)', '発生日(P/L計上日)', '取引先名', '科目名', '摘要', '請求ステータス', '税抜金額_計画', '消費税額_計画', '税込金額_計画']
※`科目名` を含めることで、フィルタ結果の検証を容易にする

**影響範囲**: 新規ファイル `609_variance_drilldown.js` 追加(既存コード変更なし)、`101_sys_config.js` のonOpen()にメニュー項目1行追加のみ。既存の差異分析・Repository・Subledgerロジックへの変更なし。

**注意事項**:
1. 本機能はデータの参照・表示に特化。`InvoiceRepository.save()` 等の書き込みメソッド呼び出し禁止
2. `InvoiceRepository.findAll()` の戻り値は `{ headers, dtos }` オブジェクト。`.dtos` を参照してフィルタする(配列直接渡し不可)
3. 一時シート名はGASの32文字制限に注意。`科目名.substring(0, 15)` でトリミング処理を実装する
4. メニュー名・グループ名は `101_sys_config.js` を Read して確認した実在の文字列のみ使用(造語禁止)
5. `請求ステータス === '却下'` のINVを含めるか否かは人間が検討すべき事項(後述)

### Step 2-3a: エッジケース〜人間検討事項の追記(File Edit または Bash、〜200行)

(エッジケーステーブル・実データ検証・関連ドキュメント・人間検討事項の詳細は省略)

### Step 2-3b: 実装プロンプト〜変更履歴の追記(File Edit または Bash、〜250行)

実装プロンプトは行頭4スペースインデントで出力(バッククォート3つ以上で囲まない):
(実装プロンプト本文は省略)

**推奨実行モデルテーブル**:

| 工程 | 推奨モデル | 理由 |
|------|----------|------|
| Phase 1調査+仕様書作成 | Claude Sonnet | 複数ファイル横断調査が必要だが会計ロジック変更なし |
| Phase 2実装 | Claude Haiku | 仕様書でコード構造が完全定義済み。判断要素なし |

**変更履歴テーブル**: `| 2026-04-20 | 初版作成 |`

### Step 2-4: 仕様書作成プロンプトの記録(File Edit または Bash、最重量工程・必ず独立Step)

末尾の `## 仕様書作成プロンプト` セクションに、`<details><summary>展開して表示</summary>` ブロックで本 `<instruction>` 全文を記録する。

---

## Phase 3: `_config.json` への追記と構文チェック

仕様書作成後、`docs/_config.json` の `nav` 配列「§E.5 FP&A・レポーティング」セクションに以下を追加する:

{ "file": "dev/dev_mas-022_variance_drilldown.md", "title": "E.5.X F-22 予実差異ドリルダウン(INV単位)" }

追記後、`docs/_config.json` が有効なJSONであることを `JSON.parse` 等で確認する(構文エラーはビルド失敗を引き起こす)。

完了後、`git add`・`git commit` を実行する:
git add docs/dev/dev_mas-022_variance_drilldown.md docs/_config.json
git commit -m "docs: F-22 予実差異ドリルダウン(INV単位)の開発仕様書を作成"
</instruction>

Phase 1 調査結果の主要乖離(仕様書内で反映済)

実装時に仕様書テンプレート記載事項と実コードの間に以下の差分があったため、仕様書では実コードに合わせて記載している:

  1. 新規ファイル名: テンプレートは 609_variance_drilldown.js だったが、600_report/609_datamart_kpi.js が既存のため 610_variance_drilldown.js に変更
  2. メニュー追加先: テンプレートは 101_sys_config.jsonOpen() 直接編集を指定していたが、MAS-214 以降 onOpen()Constants.MENU_DEFINITION ループによる動的生成方式に移行済のため、編集対象を 000_infra/002_constants.jsMENU_DEFINITION に変更
  3. UI方式: テンプレートは「2セル選択」を記載していたが、65_pl_variance のレイアウト(列A=科目名 / 行2=月名 merge / 4列1グループ)から 単一セル選択で科目名・年月の両方を逆引き可能と判断し、単一セル方式を採用
  4. 削除ボタン方式: テンプレートは「図形描画でボタンを配置」を記載していたが、GAS の Drawing API はスクリプト割当の直接制御ができないため、メニューから deleteResultSheet_() を呼ぶ方式に変更