MAS-020: 前年同月比較(YoY)の自動表示
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-020 |
| カテゴリ | FP&A |
| Phase | P2 |
| 優先度 | ★★ |
| 所要時間 | 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列追加)。列幅自動調整で対応
注意事項
66_pl_prev_yearが存在しない場合: YoY列を出力しない(初年度は前年データがないため正常動作)- 科目名のマッチング: 前年と当年で科目名が変更された場合、マッチしない科目は
—表示。科目マスタ変更時はスナップショットの再保存が必要 - スナップショット保存のタイミング: 年度末の月次締め完了後に1回実行。複数回実行しても上書きされるため問題なし
- 62タブ(YTD): 前年スナップショットは単月値のみ保持。
dmToYtdArray_()を前年データにも適用してYTD累計を算出 - 計画タブ(63/64)はスコープ外: 計画のYoYは意味が異なるため別案件
- 列幅の増加: 差異列が12列追加(合計25列→ヘッダー含む)。列幅自動調整で対応
- MAS-177実装後の移行: スナップショット方式を動的集計に置き換え。
66_pl_prev_yearは廃止 - スナップショットのデータ乖離: 前年度の仕訳修正後はスナップショットを再保存すること。保存日時をスクリプトプロパティに記録して追跡可能
エッジケース
| 条件 | 表示値 | 理由 |
|---|---|---|
| 前年スナップショットなし(初年度) | 差異列自体を出力しない | フォールバック。従来レイアウト維持 |
| 前年に存在しない科目(新規追加) | — | 比較対象なし |
| 当年に存在しない科目(廃止) | 行自体が出力されない | 当年の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.md | filterValues / 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は予実差異で使用済み) |