概要

項目内容
案件IDMAS-020
カテゴリFP&A
PhaseP2
優先度★★
所要時間2時間
対象ファイル600_report/603_datamart_pl.js, 600_report/602_datamart_main.js, 600_report/608_datamart_render.js, 100_config/101_sys_config.js
出力先タブ61(P/L実績 単月), 62(P/L実績 YTD)
前提案件MAS-177(多年度データ基盤)— ただし暫定スナップショット方式で回避可能

目的

61 P/L 実績タブに前年同月との差異列を自動追加し、経営トレンド(YoY)を可視化する。MAS-177(多年度基盤)の前段階として、前年度スナップショットシート方式で暫定実装する。

MAS-177(多年度基盤)との関係

方式概要評価理由
A: 前年度スナップショット年度末にP/L集計結果を 65_pl_prev_year に保存MAS-177不要。シンプル。MAS-177後に動的集計へ移行可能
B: MAS-177 待ち多年度基盤構築後に前年データを動的に集計MAS-177の実装待ち
C: 42_trn_journal から前年再集計仕訳データから前年P/Lを毎回再計算×GAS 6分制限リスク

案A(前年度スナップショット)を採用

現在のコード

P/L 出力の構造(603_datamart_pl.js)

dmBuildPlOutput_(ctx) が P/L の出力配列を構築(L196-403)。

// L232-242: ヘッダー構築
// ['P/L科目 (表示区分 > 勘定科目)', '通期(Total)', '2025-08', ..., '2026-07']
// 各科目行: [科目名, Total, M1値, M2値, ..., M12値] の13要素配列
  • isActualOnly フラグで実績専用モード判定(L204)
  • filterValues() / filterWithRecalcTotal() で境界月以降を空白化・Total再計算(L207-230)

YTD 累計変換(603_datamart_pl.js L107-109)

function dmToYtdArray_(arr) {
  // 13要素配列 [Total, M1-M12] を累計配列に変換
}

マート更新の流れ(602_datamart_main.js)

L157:     function buildBudgetTrendDataMart(overrideBoundary)
L211:     dmIngestData_(ctx, sheetInv, sheetBank, sheetAcct)
L218-219: dmCalcPl_(ctx) 等
L238:     plOut = dmBuildPlOutput_(ctx)
L302-306: dmApplyDwhFormat_(sheetPlM, plOut.outM, ...) でシート書き込み

現在は当年度のみ処理。前年度データの読み込み・参照は一切ない。

DDL: 65タブの現状(101_sys_config.js)

65_pl_variance が予実差異分析用として既に登録済み。前年スナップショットは別シートキー PL_PREV_YEAR で新設する。

修正方針

Step 1: 前年度スナップショットの保存機能

602_datamart_main.js の末尾に、現在の P/L 実績を専用シートに保存する関数を追加。

/**
 * F-20: 現在のP/L実績を前年度スナップショットとして保存する
 * メニュー「📊 マート更新」→「📸 前年度P/Lスナップショット保存」から実行
 */
function savePlSnapshot() {
  var FUNC = 'savePlSnapshot';
  var ss = getWebSpreadsheet_();
  var ui = SpreadsheetApp.getUi();

  var srcSheet = ss.getSheetByName('61_pl_monthly');
  if (!srcSheet) { ui.alert('🚨 61_pl_monthly が見つかりません'); return; }

  var data = srcSheet.getDataRange().getValues();
  var snapSheetName = '66_pl_prev_year';
  var snapSheet = ss.getSheetByName(snapSheetName);
  if (!snapSheet) { snapSheet = ss.insertSheet(snapSheetName); }
  snapSheet.clear();
  snapSheet.getRange(1, 1, data.length, data[0].length).setValues(data);

  // 保存日時をスクリプトプロパティに記録
  PropertiesService.getScriptProperties().setProperty(
    'PL_SNAPSHOT_DATE',
    Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm')
  );

  Utils.logInfo(FUNC, snapSheetName + ' に ' + data.length + '行を保存');
  ui.alert('📸 前年度P/Lスナップショットを保存しました(' + data.length + '行)');
}

シート名: 66_pl_prev_year(65は予実差異分析で使用済み、66を使用)

Step 2: 前年データの読み込み(602_datamart_main.js)

buildBudgetTrendDataMart() 内、L238(dmBuildPlOutput_)の前に前年データを読み込み ctx に設定。

// F-20: 前年度スナップショットの読み込み
var prevYearSheet = ss.getSheetByName('66_pl_prev_year');
ctx.prevYearPlData = prevYearSheet ? prevYearSheet.getDataRange().getValues() : null;

Step 3: P/L出力にYoY差異列を追加(603_datamart_pl.js)

dmBuildPlOutput_(ctx) を拡張。ctx.prevYearPlData がある場合のみ差異列を挿入。

前年データの照合:

/** F-20: 前年スナップショットから科目名→月別データのマップを構築 */
function buildPrevYearMap_(prevYearData) {
  var map = {};
  if (!prevYearData) return map;
  for (var i = 2; i < prevYearData.length; i++) { // ヘッダー2行をスキップ
    var name = String(prevYearData[i][0]).trim();
    if (name) map[name] = prevYearData[i].slice(1); // [Total, M1, M2, ..., M12]
  }
  return map;
}

出力レイアウト(差異列あり):

| P/L科目 | 通期(Total) | 2025-08 | △YoY | 2025-09 | △YoY | ... | 2026-07 | △YoY |

差異計算: 当年値 - 前年値(単純差額)。色で好意的/不利を表現。

ヘッダー拡張:

// 差異列ありの場合: 各月の後に '△YoY' 列を挿入
var headerRow = [labelCell, totalLabel];
for (var mi = 0; mi < months.length; mi++) {
  headerRow.push(months[mi]);
  if (hasPrevYear) headerRow.push('△YoY');
}

各科目行の拡張:

// 前年マップから該当科目の月別データを取得
var prevRow = prevYearMap[accountName] || null;
var dataRow = [accountName, totalValue];
for (var mi = 0; mi < 12; mi++) {
  dataRow.push(currentValues[mi]);
  if (hasPrevYear) {
    var prevVal = prevRow ? (Number(prevRow[mi + 1]) || 0) : 0;
    var diff = (Number(currentValues[mi]) || 0) - prevVal;
    dataRow.push(prevRow ? diff : '—');
  }
}

62 タブ(YTD)への対応:

前年スナップショットは単月値のみ保持。YTD差異は dmToYtdArray_() を前年データにも適用して算出。

var prevMonthly = prevYearMap[accountName]; // [Total, M1, ..., M12]
var prevYtd = dmToYtdArray_(prevMonthly);   // YTD累計に変換
// prevYtd[mi] と当年YTD値を比較して差異を計算

Step 4: 差異列の条件付き書式(608_datamart_render.js)

dmApplyDwhFormat_ で差異列(偶数列、3列目以降の2列おき)に条件付き書式を適用。

表示条件
+1,234収益科目で当年 > 前年、または費用科目で当年 < 前年緑 (#00aa00)
-567上記の逆赤 (#cc0000)
前年データなし灰色
0差異なし

符号の方式: 単純差額 + 色で好意/不利を表現(方式B)。 差額は常に「当年 - 前年」。色だけで評価を表す。数値フォーマット: +#,##0;△ #,##0;"-"

Step 5: メニュー登録(101_sys_config.js)

「📊 マート更新」メニューに追加。

ui.createMenu('📊 マート更新')
  .addItem('財務3表(P/L・B/S・C/F)の更新', 'buildBudgetTrendDataMart')
  .addItem('📅 基準年月を指定して更新', 'buildDataMartWithCustomBoundary')  // S-22
  .addItem('📸 前年度P/Lスナップショット保存', 'savePlSnapshot')             // F-20
  .addItem('プロジェクト別 採算(限界利益)の生成', 'buildProjectProfitability')
  .addToUi();

DDL の configDefaults にも追加:

if (!existKeys.includes('PL_PREV_YEAR'))
  confSheet.appendRow(['PL_PREV_YEAR', '', '66_pl_prev_year', '前年度P/Lスナップショット']);

影響範囲

変更ファイル変更量内容
600_report/603_datamart_pl.js~50行追加buildPrevYearMap_() + YoY差異列の挿入ロジック
600_report/602_datamart_main.js~25行追加savePlSnapshot() + 前年データ読み込み
600_report/608_datamart_render.js~15行追加差異列の条件付き書式
100_config/101_sys_config.js~3行追加メニュー登録 + DDL追加
  • 既存動作への影響: 66_pl_prev_year が存在しない場合は従来通りのレイアウト(差異列なし)で出力。完全にフォールバック
  • 下流への波及: 61/62タブの列数が増加(差異列12列追加)。列幅自動調整で対応

注意事項

  1. 66_pl_prev_year が存在しない場合: YoY列を出力しない(初年度は前年データがないため正常動作)
  2. 科目名のマッチング: 前年と当年で科目名が変更された場合、マッチしない科目は 表示。科目マスタ変更時はスナップショットの再保存が必要
  3. スナップショット保存のタイミング: 年度末の月次締め完了後に1回実行。複数回実行しても上書きされるため問題なし
  4. 62タブ(YTD): 前年スナップショットは単月値のみ保持。dmToYtdArray_() を前年データにも適用してYTD累計を算出
  5. 計画タブ(63/64)はスコープ外: 計画のYoYは意味が異なるため別案件
  6. 列幅の増加: 差異列が12列追加(合計25列→ヘッダー含む)。列幅自動調整で対応
  7. MAS-177実装後の移行: スナップショット方式を動的集計に置き換え。66_pl_prev_year は廃止
  8. スナップショットのデータ乖離: 前年度の仕訳修正後はスナップショットを再保存すること。保存日時をスクリプトプロパティに記録して追跡可能

エッジケース

条件表示値理由
前年スナップショットなし(初年度)差異列自体を出力しないフォールバック。従来レイアウト維持
前年に存在しない科目(新規追加)比較対象なし
当年に存在しない科目(廃止)行自体が出力されない当年のP/L出力に行がないため
前年・当年ともにゼロ0(黒文字)差異なし
当年売上0、前年売上100-100(赤文字)収益科目の減少 = 不利
当年費用0、前年費用100-100(緑文字)費用科目の減少 = 好意的
isActualOnly で境界月以降が空白化差異列も空白化filterValues / filterWithRecalcTotal の適用後にYoY差異を計算するため連動
差異列のTotal(通期合計)当年Total - 前年Total加算可能指標のため、月別差異の合計と一致する

フィルタ関数の選択基準:

  • 差異列の値は加算可能指標(金額差額)→ filterWithRecalcTotal で境界月以降を空白化しTotalを再計算

実データ検証(MCP でのデータ確認が必要な場合)

確認項目確認方法理由
61タブの現在の出力行数・列数MCP で 61_pl_monthly の getDataRange() サイズを確認差異列追加後のレイアウト計算に必要
61タブのヘッダー行構造(行1-2)MCP で先頭2行を取得buildPrevYearMap_ のヘッダースキップ行数を確定
66番台のシート名が未使用かMCP でシート一覧を取得66_pl_prev_year の番号が衝突しないことを確認

関連ドキュメント

仕様書関連箇所
CLAUDE.md財務諸表の列幅自動調整はラベル列を除く数値・日付列のみ
dev_mas-001_variance_analysis.md予実差異の計算パターン・表示ルール。YoYも同じ差異表示ルールを準拠
dev_mas-024_bep_analysis.mdfilterValues / filterWithRecalcTotal の使い分け

人間が検討すべき事項

#項目詳細
1前年データの保持方法本仕様書では案A(スナップショット)を採用。MAS-177構築時に動的集計へ移行する方針でよいか。TODO_future.md から転記
2スナップショット保存の自動化年度切替時に自動保存するか、手動実行のみとするか。初版は手動
3差異列の表示位置本仕様書では「各月の右隣」を採用。代替案: 末尾にまとめて配置(12ヶ月トレンドの俯瞰性は高いが特定月の比較は不便)
4差異の符号解釈本仕様書では「単純差額 + 色で好意/不利を表現」(方式B)を採用。代替案: 符号反転方式(常に+=好意的)。MAS-001との整合性要確認

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

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-020「前年同月比較(YoY)の自動表示」を実装してください。

## 実行前タスク
以下のファイルを読み込んでください:
1. `600_report/603_datamart_pl.js` — `dmBuildPlOutput_(ctx)` の出力配列構造(L196-403)。ヘッダー構築(L232-242)と科目行構築(L244-269)を重点確認
2. `600_report/602_datamart_main.js` — `buildBudgetTrendDataMart()` の全体フロー(L157-306)。前年データ読み込みの挿入ポイント(L238の前)を確認
3. `600_report/608_datamart_render.js` — `dmApplyDwhFormat_()` の条件付き書式パターン
4. `600_report/601_datamart_ingest.js` — `dmToYtdArray_()` のシグネチャ(L107-109)
5. `100_config/101_sys_config.js` — メニュー構築(L328-331)とDDL configDefaults
6. `docs/dev/dev_mas-001_variance_analysis.md` — 差異計算の設計パターン(参考)
7. `CLAUDE.md`
8. `docs/dev/dev_mas-020_yoy_comparison.md` — 本仕様書

## 修正対象ファイル
- `600_report/603_datamart_pl.js` — `buildPrevYearMap_()` 追加 + `dmBuildPlOutput_()` 拡張
- `600_report/602_datamart_main.js` — `savePlSnapshot()` 追加 + 前年データ読み込み
- `600_report/608_datamart_render.js` — 差異列の条件付き書式追加
- `100_config/101_sys_config.js` — メニュー登録 + DDL追加

## 実装内容

### A: savePlSnapshot() の追加(602_datamart_main.js 末尾)
61_pl_monthly の全データを 66_pl_prev_year にコピーする関数。
保存日時をスクリプトプロパティ `PL_SNAPSHOT_DATE` に記録。

### B: 前年データの読み込み(602_datamart_main.js L238の前)
66_pl_prev_year シートが存在すればデータを読み込み ctx.prevYearPlData に設定。
存在しなければ null(YoY列を出力しない)。

### C: buildPrevYearMap_() の追加(603_datamart_pl.js)
前年スナップショットから科目名→月別データのマップを構築。
ヘッダー2行をスキップ。科目名をキーに [Total, M1, ..., M12] を値として格納。

### D: dmBuildPlOutput_() の拡張(603_datamart_pl.js L232-269)
ctx.prevYearPlData がある場合:
1. ヘッダーに各月の後に '△YoY' 列を挿入
2. 各科目行で前年同月の値を取得し、差異(当年 - 前年)を計算して挿入
3. 前年データなしの科目は '—' を出力
4. Total列も差異を計算(当年Total - 前年Total)
5. 62タブ(YTD): dmToYtdArray_() を前年データにも適用してから差異計算
ctx.prevYearPlData がない場合: 従来通りのレイアウト(フォールバック)

### E: 差異列の条件付き書式(608_datamart_render.js)
差異列の数値フォーマット: `+#,##0;△ #,##0;"-"`
正の差異(収益増/費用減): 緑文字 (#00aa00)
負の差異: 赤文字 (#cc0000)

### F: メニュー登録 + DDL追加(101_sys_config.js)
「📊 マート更新」メニューに「📸 前年度P/Lスナップショット保存」を追加。
configDefaults に PL_PREV_YEAR → 66_pl_prev_year を追加。

## 制約
- 66_pl_prev_year が存在しない場合はYoY列を出力しない(初年度対応)
- 計画タブ(63/64)は対象外
- 差異は「当年 - 前年」の単純差額。符号反転はしない
- 列参照はヘッダー名ベース。列番号ハードコード禁止
- 既存のP/L出力(差異列なし)と完全互換性を維持

## エッジケース
| 条件 | 表示値 | 理由 |
|------|--------|------|
| 前年スナップショットなし | 差異列なし(フォールバック) | 初年度対応 |
| 前年に存在しない科目 | '—' | 比較対象なし |
| 前年・当年ともにゼロ | 0(黒文字) | 差異なし |
| isActualOnly で境界月以降空白 | 差異列も空白 | フィルタ連動 |

## 実データ検証
- 61タブの先頭2行(ヘッダー構造)を確認し、buildPrevYearMap_ のスキップ行数を確定
- 66番台のシート名が未使用であることを確認

## 動作確認
`npm run push:dev` 後:
1. メニュー「📊 マート更新」→「📸 前年度P/Lスナップショット保存」を実行
   → 66_pl_prev_year シートが作成され、61の内容がコピーされること
2. 「財務3表の更新」を実行
   → 61タブに各月の後に「△YoY」列が表示されること
3. 収益科目の差異が正の場合は緑、負の場合は赤で表示されること
4. 費用科目の差異が正の場合は赤、負の場合は緑で表示されること
5. 66_pl_prev_year を削除 → 再度「財務3表の更新」を実行
   → 従来通りのレイアウト(差異列なし)で出力されること
6. 62タブ(YTD)でも同様に差異列が表示されること

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

| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| savePlSnapshot() | なし | シート全コピーのみ |
| 前年データ読み込み | なし | シート存在チェック + getValues() |
| buildPrevYearMap_() | なし | 科目名マッチングの単純マップ構築 |
| dmBuildPlOutput_() 拡張 | あり | ヘッダー/行構造の拡張、filterValues連動、YTD前年累計計算 |
| 条件付き書式 | あり | 差異列の特定、収益/費用による色分けルール |

推奨実行モデル

工程推奨モデル理由
仕様書作成(本ドキュメント)Claude Opus 4.6多年度対応の設計判断、暫定方式の選定、P/L出力構造の拡張設計
実装Claude Opus 4.6複数ファイル横断、P/L出力配列の構造変更、filterValues連動、条件付き書式の符号ルール適用
動作確認ユーザー手動スナップショット保存 → マート更新 → 差異列確認の一連操作

変更履歴

日付変更内容
2026-04-15初版作成。暫定方式(前年度スナップショット)で MAS-177 なしの実装を設計
2026-04-15レビュー反映: スナップショット乖離リスク、差異列レイアウト代替案、符号解釈代替案、YTD前年累計ロジック追記
2026-04-16テンプレート準拠で全面改訂。エッジケース・実データ検証セクション追加、行番号を最新コードに更新、実装プロンプトを4字インデントに変更、シート名を66_pl_prev_yearに変更(65は予実差異で使用済み)