MAS-022: 予実差異の要因ドリルダウン(INV単位)
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-022 |
| カテゴリ | FP&A・レポーティング |
| Phase | P2 |
| 優先度 | ★★ |
| 所要時間 | 2-3時間(新規ファイル + メニュー1行追記のみ) |
| 対象ファイル | 600_report/610_variance_drilldown.js(新規作成)000_infra/002_constants.js(Constants.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-324 | Constants.MENU_DEFINITION 配列。MAS-214 でメニューは onOpen() 直書きから MENU_DEFINITION ループ方式に移行済 | メニュー項目の追加は本配列の適切なカテゴリに1行追記で行う(101_sys_config.js の onOpen() 本体は編集不要) |
100_config/101_sys_config.js L323-350 | onOpen() は Constants.MENU_DEFINITION.forEach で動的生成 | 編集不要(本配列参照元として確認のみ) |
200_data/202_repository.js L152-208 | InvoiceRepository.findAll() は { headers: string[], dtos: InvoiceDTO[] } を返す(dtos 配列のみではない) | .dtos を参照してフィルタ |
000_infra/003_contracts.js L38-67 | InvoiceDTO のフィールド名: 有効フラグ / 請求ID(INV) / 発生日(P/L計上日) / 請求ステータス / 取引先名 / 科目名 / 摘要 / 税抜金額_計画 / 消費税額_計画 / 税込金額_計画 | 表示列のキー名として使用 |
000_infra/004_utils.js L92-99 | Utils.parseDateToYm(val) は Date型 / "YYYY/MM" / "YYYY-MM" / "YYYY年MM月" 等を受け取り "YYYY-MM" 文字列を返す | INV の発生日を年月に正規化してフィルタ |
600_report/608_datamart_render.js L128-182 | 65_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):
dto['有効フラグ'] !== false && String(dto['有効フラグ']).toUpperCase() !== 'FALSE'(有効行のみ。CLAUDE.md 規約)Utils.parseDateToYm(dto['発生日(P/L計上日)']) === 選択年月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.js | drilldownVariance_() / deleteResultSheet_() の2関数 |
| 1行追記 | 000_infra/002_constants.js | Constants.MENU_DEFINITION の 📋 サイドバー: 📊 マート更新 カテゴリに項目1行追加 |
既存コード変更なし:
- 差異分析ロジック(
600_report/603_datamart_pl.jsのdmBuildPlVarianceOutput_)は不変 65_pl_varianceの構造・レンダリング(608_datamart_render.js)は不変InvoiceRepository/Contracts/Utilsは参照のみonOpen()本体(101_sys_config.jsL323-350)は不変(MENU_DEFINITION ループ経由で自動反映される)
注意事項
- 読み取り専用機能:
InvoiceRepository.save()/append()など書き込みメソッド呼び出し禁止。本機能はデータの参照・表示に特化する。 - Repositoryパターン遵守:
32_wrk_invoiceをシート直接参照しない。必ずInvoiceRepository.findAll().dtosを使う。 - 戻り値型の注意:
InvoiceRepository.findAll()は{ headers, dtos }オブジェクトを返す。.dtosプロパティを取得すること(配列直接渡し不可)。 - シート名32文字制限: 科目名が長い場合、
科目名.replace(/[\/\\?\*\[\]:]/g, '_').substring(0, 15)でトリミング。末尾に_YYYY-MM(8文字)+ プレフィックスF22_(4文字)が付くため、科目名部分は15文字以下に抑える必要がある。 - メニュー配置: メニュー追加は
Constants.MENU_DEFINITION(002_constants.js)への1行追記のみ。101_sys_config.jsのonOpen()は触らない(MAS-214 以降、メニュー定義は Constants 配列経由で動的生成される)。 - 却下INVの扱い:
請求ステータス === '却下'の INV を含めるか否かは人間が検討すべき事項(後述)。初版では全ステータスを表示し、結果シートの「請求ステータス」列でフィルタ確認する方針とする。 - 通期セルの扱い: アクティブセルが「通期」列(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)が既存 | 既存シートを削除してから再生成 | 再実行時の最新性保証 |
| E13 | 65_pl_variance タブが未生成(MAS-001 未実行) | E1 で captured される(getName() !== '65_pl_variance' で弾かれる)ため個別対応不要 | 前提条件の不成立 |
| E14 | 選択セルの行が最終データ行より下(空欄領域) | E6(科目名セル空文字)で captured される | 誤操作防止 |
実データ検証
実装前に確認すべき項目(GASエディタまたはスプレッドシート上で手動確認):
| # | 検証項目 | 確認方法 |
|---|---|---|
| V1 | 65_pl_variance の実レイアウト: 行2(2段ヘッダー上段)に YYYY-MM 形式の月名が入っているか、Date型かを確認 | Logger.log(sheet.getRange(2, 6).getValue()) 等で値の型を確認。Utils.parseDateToYm で正規化できることを保証 |
| V2 | 65_pl_variance の科目名列(列A)に、グループ見出し行("■ 売上高" 等)や空行が混在することを確認 | E6 のバリデーションが正しく機能するか |
| V3 | 32_wrk_invoice の 科目名 列の格納値が、差異分析シートの科目名表記と完全一致することを確認 | マスタ表記との乖離があれば該当INV 0 件になる |
| V4 | 32_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.md | MAS-001 から分離された予算 vs 実績差異(66_pl_budget_variance)。本機能の対象は MAS-001 の 65_pl_variance のみ |
| dev_mas-020_yoy_comparison.md | 前年同月比較。差異ではなく YoY 差分のため本機能の対象外 |
| CLAUDE.md | Repositoryパターン・シート書き込みルール・有効フラグ規約 |
人間が検討すべき事項
請求ステータス === '却下'の INV を表示するか- 却下 INV は承認されていないため、予実差異の「要因」には該当しない見方もできる
- 一方で「なぜ却下されたのか/却下されなければ差異がどうだったか」の参考情報として有用
- 初版案: 全ステータス表示。結果シートの「請求ステータス」列でユーザーが必要に応じてフィルタ
通期サマリー列(g=0)を選択した場合の挙動
- 現在の仕様では
alertでガイドして終了(エッジケース E4) - 代替案: 通期を選択した場合は「年月」条件を外して全期間の該当INVを表示する
- 初版案: 終了方式(年月が特定できないため。必要なら将来機能として拡張)
- 現在の仕様では
一時シートの自動削除タイミング
- 現在はボタン押下による手動削除のみ
- 代替案1: セッション終了時のトリガーで自動削除(実装コスト高)
- 代替案2: 起動時(
onOpen)にF22_で始まるシートを一括削除 - 初版案: 手動削除のみ。運用で問題があれば N-XX として追加改修
F22_プレフィックスで始まる他シートとの競合- 現時点では他に
F22_で始まるシート生成機能は存在しないが、将来追加される場合を考慮 - 初版案: 現状維持。将来 MAS-022 系機能が増えた場合は接頭辞を
F22DD_等に変更
- 現時点では他に
結果シートの表示列の拡張要望
- 現在の 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_DEFINITION・InvoiceRepository・InvoiceDTO)だが、会計ロジック変更や高度な設計判断は不要 |
| 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 調査結果の主要乖離(仕様書内で反映済)
実装時に仕様書テンプレート記載事項と実コードの間に以下の差分があったため、仕様書では実コードに合わせて記載している:
- 新規ファイル名: テンプレートは
609_variance_drilldown.jsだったが、600_report/609_datamart_kpi.jsが既存のため610_variance_drilldown.jsに変更 - メニュー追加先: テンプレートは
101_sys_config.jsのonOpen()直接編集を指定していたが、MAS-214 以降onOpen()はConstants.MENU_DEFINITIONループによる動的生成方式に移行済のため、編集対象を000_infra/002_constants.jsのMENU_DEFINITIONに変更 - UI方式: テンプレートは「2セル選択」を記載していたが、
65_pl_varianceのレイアウト(列A=科目名 / 行2=月名 merge / 4列1グループ)から 単一セル選択で科目名・年月の両方を逆引き可能と判断し、単一セル方式を採用 - 削除ボタン方式: テンプレートは「図形描画でボタンを配置」を記載していたが、GAS の Drawing API はスクリプト割当の直接制御ができないため、メニューから
deleteResultSheet_()を呼ぶ方式に変更