MAS-014: プロジェクト別予実ダッシュボード
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-014 |
| カテゴリ | UX / FP&A |
| Phase | P2 |
| 優先度 | ★★ |
| 所要時間 | 3〜4時間 |
| 対象ファイル | 200_data/202_repository.js(BudgetRepository / ProjectRepository 追加)400_domain/420_project_profitability.js(updatePjVarianceDashboard_() 追加)100_config/101_sys_config.js(シートキー登録)000_infra/002_constants.js(MENU_DEFINITION にエントリ追加) |
| 前提案件 | なし(420_project_profitability.js の既存 loadPjMaster_ / loadAcctMaster_ を再利用するのみ。既存動作の改変は伴わない) |
目的
各 PM(プロジェクトマネージャー)が自身の担当プロジェクトの「予算消化状況」「売上達成率」「利益率」をリアルタイムで一覧確認できるダッシュボードシート(79_pj_variance_dashboard)を新設する。
既存の 78_pj_pl(PJ 横並び P/L)と 79_pj_monthly(PJ 別月次採算表)は 実績中心 で、共通費配賦後の限界利益や科目別明細の表示を主眼としている。一方、本 F-14 は 予実差異(Budget vs Actual)そのものを PJ 別に可視化 することを目的とする。41_trn_budget(予算)と 42_trn_journal(実績)を突き合わせ、売上・費用・利益の各カテゴリで差異額・差異率・予算消化率を 1 行 / 1 PJ の横断ビューで表示する。
現場 PM が自分で数字を叩きに来ることを想定し、シート上部にプルダウン(対象 PJ 選択・単月/累計(YTD) 切替)を設けて絞り込めるようにする。
現在のコード
400_domain/420_project_profitability.js(既存 PJ 集計の中核)
| 関数 / シンボル | 行番号 | 役割 |
|---|---|---|
buildProjectProfitability() | L7 | メイン関数。79_pj_monthly・78_pj_pl・77_pj_raw を生成 |
loadPjMaster_(ss) | L803 | 14_mst_project を { pjName: { code, allocType, pm } } 形式で返す |
loadAcctMaster_(ss) | L822 | 11_mst_account を { acctName: { stmt, cat, disp, dispAcct } } 形式で返す |
loadAllocRules_(ss) | L842 | 28_bud_allocation の配賦ルール読込 |
| 既存シート出力 | L18 / L701 / L750 | 既存は 79_pj_monthly・78_pj_pl・77_pj_raw の 3 マートのみ |
本 F-14 で追加する updatePjVarianceDashboard_() は、loadPjMaster_ / loadAcctMaster_ をそのまま再利用する(車輪の再発明を避ける)。
200_data/202_repository.js(Repository 層)
| 既存 Repository | 行番号 | 本件での位置づけ |
|---|---|---|
readSheetAsDtos_(sheet) | L19-L29 | 全 Repository 共通の DTO 化プライベートヘルパー |
JournalRepository | L259-L298 | findAll() / save() / append() の 3 メソッド構成。本件で findAll() を利用 |
AccountRepository | L304-L350 | 読み取り専用マスタ Repo の最小構成テンプレート(_getSheet + findAll + findAsMap)。BudgetRepository / ProjectRepository はこのパターンに準拠する |
BudgetRepository | — | 未実装。本 F-14 で新設する |
ProjectRepository | — | 未実装。本 F-14 で新設する |
000_infra/003_contracts.js(DTO 型定義)
| DTO | 行番号 | 本件で参照する主要フィールド |
|---|---|---|
BudgetDTO | L132-L149 | 有効フラグ・対象年月("YYYY-MM")・PJ名・収支区分("収入" / "支出")・科目名・予算金額 |
JournalEntryDTO | L95-L129 | 発生日(P/L計上日)・PJ名・収支区分・科目名・税抜金額_実績 |
100_config/101_sys_config.js(システム設定)
| 該当箇所 | 行番号 | 役割 |
|---|---|---|
onOpen() ディスパッチャ | L323-L350 | Constants.MENU_DEFINITION を走査してメニューを構築(直接 addItem しない宣言的方式) |
| シートキー登録ブロック | L770-L823 | if (!existKeys.includes('KEY')) confSheet.appendRow(...) パターンで 01_sys_config にエントリ追加 |
MST_PROJ DDL スキーマ | L836 | 14_mst_project のヘッダーは 有効フラグ, PJコード, プロジェクト名, …, 配賦区分 |
TRN_BUDG DDL スキーマ | L861 | 41_trn_budget のヘッダーは 有効フラグ, 予算ID, 対象年月, 決済予定年月, 予算バージョン, 収支区分, 組織名, PJ名, 科目名, 予算金額, … |
000_infra/002_constants.js(メニュー定義)
| 該当箇所 | 行番号 | 役割 |
|---|---|---|
MENU_DEFINITION | L206-L324 | 宣言的メニュー定義配列 |
📋 サイドバー: 📊 マート更新 カテゴリ | L229-L239 | buildProjectProfitability(PJ 別採算)が所属。本 F-14 のメニュー項目もこのカテゴリに追加する |
プルダウン設置の既存実装
600_report/608_datamart_render.js L114-L115 に明確な参考例がある:
const snapChoices = ctx.targetMonthsWithActBgt.map(v => String(v).replace(/\n/g, ' '));
const snapRule = SpreadsheetApp.newDataValidation().requireValueInList(snapChoices).setAllowInvalid(true).build();
sheetSnap.getRange('B1').clearDataValidations().setNumberFormat('@').setDataValidation(snapRule);
選択値の読み取りは sheet.getRange('A1').getValue() で行う。
修正方針
Step 1: 202_repository.js に読み取り専用 Repository を 2 つ追加
AccountRepository(L304-L350)のパターンに完全準拠。_getSheet + findAll の最小構成とし、内部で既存プライベートヘルパー readSheetAsDtos_ を直接呼び出す。
BudgetRepository- シートキー:
TRN_BUDG(L786 で既に登録済み)、フォールバック名:'41_trn_budget' findAll(): { headers: string[], dtos: BudgetDTO[] }
- シートキー:
ProjectRepository- シートキー:
MST_PROJ(L836 の DDL で登録済み。Utils.getSheetByKeyで解決可能)、フォールバック名:'14_mst_project' findAll(): { headers: string[], dtos: Object[] }
- シートキー:
いずれも save / append は 実装しない(本ダッシュボードは読み取り専用のため不要)。
Step 2: ダッシュボード更新関数の実装
配置場所: 400_domain/420_project_profitability.js の末尾(loadAllocRules_ の下)に updatePjVarianceDashboard_() を追加する。既存の loadPjMaster_ / loadAcctMaster_ を再利用できるため、新規ファイルには分離しない。
処理ステップ:
シート準備(冪等性 = 複数回実行しても結果が同じになる性質、を担保)
ss.getSheetByName('79_pj_variance_dashboard')を取得。無ければinsertSheet()。- 先頭で
sheet.clear()とsheet.getRange(1, 1, sheet.getMaxRows(), sheet.getMaxColumns()).clearDataValidations()を実行。
データ取得
ProjectRepository.findAll()で14_mst_projectの全 DTO を取得。有効フラグ === FALSE(または'FALSE')をスキップ。プロジェクト名列(= PJ 名として扱う実体)・PJコード・PM・責任者名を保持。BudgetRepository.findAll()で41_trn_budgetを取得。有効フラグ === FALSEスキップ。JournalRepository.findAll()で42_trn_journalを取得。
日付正規化(Date 型直接比較・文字列ソート崩れの防止)
- 予算:
対象年月をUtils.parseDateToYm(dto['対象年月'])で"YYYY-MM"に統一。 - 実績:
発生日(P/L計上日)をUtils.parseDateToYm(dto['発生日(P/L計上日)'])で"YYYY-MM"に統一。 - 既存
420_project_profitability.jsL27-L28 と同じUtils.parseDateToYm/Utils.addMonthsを使う。
- 予算:
対象期間の決定
- 基準年月(今期 8 月〜翌 7 月)は
buildProjectProfitabilityL22 と同じロジックで導出。 - シート上部プルダウンの選択値に従い、単月(対象年月 1 ヶ月分)か 累計(YTD)(期首 8 月〜対象年月までの累計)かを切替。
- 基準年月(今期 8 月〜翌 7 月)は
科目フィルタ(
収支区分による分類)- 予算側:
収支区分 === '収入'→ 売上予算、=== '支出'→ 費用予算。 - 実績側: 同上。ただし B/S 科目は除外(
loadAcctMaster_のstmt === 'BS'を弾く)し、実績の金額は税抜金額_実績を使用。 - 既存
buildProjectProfitabilityL130-L140 と整合する分類を採る。科目マスタ未登録の科目名は集計にカウントせず、監査ログUtils.logInfoで警告のみ記録する(キーワード推測による自動分類は禁止。CLAUDE.md 規約 / 失敗パターン #1)。
- 予算側:
PJ 別に予算・実績を集計して
{ pjName: { salesBud, salesAct, costBud, costAct } }を構築- 予算データに存在するが
14_mst_projectに登録がない PJ 名、および実績データに存在するが未登録の PJ 名は、集約して(PJマスタ未登録)という 1 行にまとめる(失敗パターン #1: データ品質問題を隠さず可視化)。 有効フラグ === FALSEの PJ マスタ行は集計対象外。
- 予算データに存在するが
シート上部のパラメータセル
A1: 「🔍 対象PJを選択 ➡️」ラベルを A1 に置き、B1 セルに PJ 選択プルダウン(ProjectRepository.findAll()から有効な PJ 名を動的生成。先頭に(すべて)項目を含める)。C1: 「期間粒度 ➡️」ラベル。D1 セルに期間粒度プルダウン(固定リスト:単月/累計(YTD))。E1: 「基準年月 ➡️」ラベル。F1 セルに対象年月プルダウン(対象 12 ヶ月の YYYY-MM)。setDataValidationパターンは 608_datamart_render.js L114-115 に準拠。
出力列(ヘッダーは以下の順序で固定)
| PJ名 | PM | 売上_予算 | 売上_実績 | 売上_差異額 | 売上_差異率% | 費用_予算 | 費用_実績 | 費用_差異額 | 費用_差異率% | 費用_予算消化率% | 利益_予算 | 利益_実績 | 利益_差異額 | 利益率%_予算 | 利益率%_実績 |
利益 = 売上 − 費用差異額_売上 = 売上_実績 − 売上_予算(実績が予算を上回る=プラス)差異率%_売上 = 差異額_売上 / 売上_予算差異額_費用 = 費用_実績 − 費用_予算(実績が予算を上回る=プラスだが、費用なので赤字方向)差異率%_費用 = 差異額_費用 / 費用_予算費用_予算消化率% = 費用_実績 / 費用_予算利益率%_予算 = 利益_予算 / 売上_予算利益率%_実績 = 利益_実績 / 売上_実績
全社合計行を最終行に追加
- 利益率% は 各 PJ の率を合算しない。全社合計の売上・費用から再計算する(非加算指標を単純合算してはならない。失敗パターン #1)。
書式整備
- ヘッダー行: ダーク背景 + 白文字 + 太字(既存 608_datamart_render.js 流儀に合わせる)。
- 金額列:
'#,##0;[Red]△ #,##0;"-"'。 - 率(%) 列:
'0.0%;[Red]△ 0.0%;"-"'。 - 全社合計行・
(PJマスタ未登録)行は薄灰色ハイライト + 太字。 - 赤字(利益_実績 < 0)の行はアラート色(
#f4cccc+#cc0000)で強調。 setFrozenRows(2)(1 行目パラメータ、2 行目ヘッダー)、setFrozenColumns(2)(PJ 名 + PM 列)。- 既存マート同様、
setFontFamily('BIZ UDGothic')を全域に適用し、余剰列・行をdeleteColumns/deleteRowsで物理削除。
公開関数
updatePjVarianceDashboard()(末尾アンダースコアなし)を定義し、内部でupdatePjVarianceDashboard_()を try/catch で呼ぶ(既存buildProjectProfitabilityと同じエラー処理パターン)。
Step 3: シートキー登録とメニュー登録
シートキー登録:
100_config/101_sys_config.jsの L815-L817 付近(PJ_RAW/PJ_PL/PJ_MONTHLYの直後)に 1 行追加:if (!existKeys.includes('PJ_VAR_DASH')) confSheet.appendRow(['PJ_VAR_DASH', '', '79_pj_variance_dashboard', 'PJ別予実ダッシュボード']);Utils.getSheetByKey('PJ_VAR_DASH', '79_pj_variance_dashboard')で解決できるようになる。メニュー登録:
000_infra/002_constants.jsのMENU_DEFINITIONL230-L239 の📋 サイドバー: 📊 マート更新カテゴリ内、buildProjectProfitabilityの直後に 1 行追加:{ label: '📋 プロジェクト別予実ダッシュボードを更新', funcName: 'updatePjVarianceDashboard', description: 'PJ 別の予算 vs 実績マート (79_pj_variance_dashboard) を再構築' },DDL 追加の要否:
79_pj_variance_dashboardは動的生成のマートシートで、既存の77_pj_raw/78_pj_pl/79_pj_monthlyと同様に DDL(setupAllSchemasのschemasオブジェクト)には登録しない(既存 3 マートも登録されていない)。
影響範囲
| 区分 | 対象 | 影響内容 |
|---|---|---|
| 新規追加のみ | 200_data/202_repository.js | BudgetRepository / ProjectRepository 追加。既存 Repository 未変更 |
| 新規追加のみ | 400_domain/420_project_profitability.js | updatePjVarianceDashboard() / updatePjVarianceDashboard_() 追加。既存 buildProjectProfitability 未変更 |
| 設定追加のみ | 100_config/101_sys_config.js | PJ_VAR_DASH シートキー登録 1 行追加 |
| 設定追加のみ | 000_infra/002_constants.js | MENU_DEFINITION に 1 項目追加 |
| 新規シート | 79_pj_variance_dashboard | 起動時は未作成。メニュー初回実行時に自動生成 |
| 影響なし | 既存マート 77_pj_raw / 78_pj_pl / 79_pj_monthly | 本件は別シート(79_pj_variance_dashboard)に独立出力するため影響なし |
| 影響なし | 財務諸表(61/62/71/81 等) | 読み取りのみで一切改変しない |
注意事項
- 既存ロジック再利用:
420_project_profitability.jsのloadPjMaster_/loadAcctMaster_を再利用すること。PJ マスタ読込・科目分類を別関数で再実装しない(失敗パターン #1: 重複実装)。 - 造語禁止: メニュー名 / シートキー文字列 / 定数名は Phase 1 で確認した実在文字列のみ使用する。例として、メニューカテゴリは
📋 サイドバー: 📊 マート更新(002_constants.js L230)であり、「📊 FP&Aレポート」等の存在しないカテゴリを作ってはならない(失敗パターン #18-#20)。 - 列参照はヘッダー名ベース:
indexOf('PJ名')等でヘッダー位置を取得し、列番号ハードコードを避ける(CLAUDE.md 規約)。 - 科目マスタ (
11_mst_account) 未登録の科目名: キーワード推測で売上/費用を自動分類しない。未登録はUtils.logInfoで警告のみ記録し、集計から除外する(CLAUDE.md 規約)。 - 有効フラグ = FALSE の行: PJ マスタ・予算・実績のすべてで、
有効フラグがfalse(bool)または'FALSE'(文字列)なら集計対象から除外する。 - PJ マスタ未登録 PJ: 予算・実績データに存在するが
14_mst_projectに未登録の PJ 名は、削除せず(PJマスタ未登録)行に集約表示する(データ品質問題の可視化)。 - 非加算指標の扱い: 利益率% を単純合算しない。全社合計行は合算後の売上・費用から率を再計算する(失敗パターン #1)。
- 冪等性: 処理冒頭で
sheet.clear()+clearDataValidations()を実行し、何度実行しても同じ結果に収束すること。
エッジケース
計算式を含むため、ゼロ除算やデータ欠落のケースを明示する。
| # | 条件 | 表示値 | 理由 |
|---|---|---|---|
| 1 | 売上実績 = 0 のときの 利益率%_実績 | "-" | ゼロ除算防止 |
| 2 | 売上予算 = 0 のときの 利益率%_予算 | "-" | ゼロ除算防止 |
| 3 | 差異率% の分母(予算)= 0 | "-" | ゼロ除算防止 |
| 4 | 費用予算 = 0 かつ 費用実績 > 0 のときの 予算消化率% | "予算外支出" | 予算未計上の支出を明示(管理会計上の異常値) |
| 5 | 費用予算 = 0 かつ 費用実績 = 0 のときの 予算消化率% | "0%" | 正常ケース |
| 6 | 実績・予算データに存在するが 14_mst_project に未登録の PJ 名 | (PJマスタ未登録) 行に集約(最終行の 1 行上、全社合計の直前) | データ品質問題を隠さず可視化 |
| 7 | 予算のみ存在する PJ(実績なし) | 実績 = 0、差異 = 0 − 予算金額(マイナス表示) | 未消化予算の可視化 |
| 8 | 実績のみ存在する PJ(予算なし) | 予算 = 0、予算消化率% = "予算外支出" | 予算外支出の検出 |
| 9 | 全社合計行の 利益率%_予算 / 利益率%_実績 | 全社合計の売上・費用から再計算 | 非加算指標を単純合算してはならない(失敗パターン #1) |
| 10 | 有効フラグ = FALSE の PJ マスタ行 | 集計対象から除外(プルダウンにも非表示) | CLAUDE.md: 有効フラグ=FALSE の行は全処理でスキップ |
| 11 | 科目マスタ未登録の科目名 | 集計からスキップ。Utils.logInfo に [F-14] 未登録科目: ${acct} を記録 | キーワード推測による自動分類を避ける(CLAUDE.md 規約) |
| 12 | 累計(YTD) モードで対象年月が期首(8月)の場合 | 当月 1 ヶ月分のみ集計(8 月〜8 月) | YTD の境界条件を明示 |
| 13 | プルダウンで (すべて) 選択 | 全 PJ を表示(個別 PJ 絞り込みは無効化) | 初期値。フィルタ未適用状態 |
| 14 | 利益_実績 < 0 の行 | 行全体を薄赤(#f4cccc)+赤字文字(#cc0000)で強調 | PM への視覚的警告 |
実データ検証
MCP ツールによる live データ検証は本ワークスペース(Codespaces sub 環境)では未実施。Phase 1 では DDL スキーマ(101_sys_config.js L826-L861)および既存コード参照(420_project_profitability.js L809-L819)から確定した。実装後に dev 環境で下記を再検証すること。
14_mst_project 列構造(DDL 定義より)
有効フラグ, PJコード, プロジェクト名, PJ小区分, PJ大区分, 社内外, 資産化,
顧客・取引先名, PJ区分, 契約形態, ステータス, 資産化対象, PM・責任者名, 配賦区分
- PJ 名として参照する列:
プロジェクト名(既存loadPjMaster_L811 で確定) - 有効フラグ列:
有効フラグ(bool /'FALSE'文字列の両方を判定) - PM 表示用列:
PM・責任者名
41_trn_budget 列構造と 収支区分 実データ値(DDL + DTO 定義より)
有効フラグ, 予算ID, 対象年月, 決済予定年月, 予算バージョン, 収支区分,
組織名, PJ名, 科目名, 予算金額, 摘要, 収支区分コード, 組織コード, PJコード, 主科目コード
収支区分格納値:"収入"/"支出"(BudgetDTO定義 L139 / 既存 RPA コード'収支区分': '支出'L208 等より確認)対象年月フォーマット:"YYYY-MM"(文字列)
42_trn_journal の 収支区分 と PJ名(DTO 定義より)
収支区分:"収入"/"支出"(JournalEntryDTO定義 L102)PJ名:14_mst_projectのプロジェクト名と一致する自由文字列。未指定時は RPA 起票側で'指定なし_共通費など'が入る(202_repository.jsL202)税抜金額_実績を実績金額として採用(BudgetDTO.予算金額と同じ税抜ベースで比較するため)
実装後の dev 検証ステップ(npm run push:dev 後)
- メニュー「📋 プロジェクト別予実ダッシュボードを更新」を実行し、
79_pj_variance_dashboardシートが生成されることを確認。 - シート上部のプルダウン 3 つ(PJ 選択 / 期間粒度 / 基準年月)が操作可能であること。
- 既存
14_mst_projectの有効 PJ がすべて表示されること。 (PJマスタ未登録)行が必要に応じて出現すること(テストデータで意図的に未登録 PJ 名を含める)。- 全社合計行の利益率 % が、
売上_実績 合計 / 費用_実績 合計から正しく再計算されていること。
関連ドキュメント
docs/dev/dev_F-01_variance_analysis.md— F-01 全社 P/L の予実差異分析。本 F-14 は PJ 別版の位置づけdocs/dev/dev_F-06_project_overhead_allocation.md— PJ 別共通費配賦。本 F-14 は 配賦前 の直接予算 vs 直接実績を対象(共通費配賦ビューは既存78_pj_plで継続提供)docs/dev/dev_S-22_boundary_month_selector.md— 基準年月 UI のリファレンス(ui.prompt方式)。本 F-14 はセル内プルダウン方式を採用するため UI 手法は異なるdocs/adr/003_pj_accounting_scope.md— 78 vs 79 タブの役割分担 ADR。本 F-14 は79_pj_variance_dashboardを「PJ 別予実差異専用」に位置づけるdocs/_internal/failure_patterns.md— 失敗パターン #1(非加算指標の単純合算)、#18-#20(造語による固有名詞の誤り)
人間が検討すべき事項
1. PM 向け公開情報の範囲(TODO_future.md F-14 より)
PM 向けに公開する情報の範囲(原価のみ or 利益率含む)
初期実装では売上・費用・利益・利益率の全列を表示する。ただし、PM ロールに応じて利益率や売上予算を非表示にしたいケースが想定される(例: 外注 PM に対する内部原価非開示)。
推奨対応: 将来の N-11(ロールベースアクセス制御)導入を見据え、表示項目 ON/OFF のフィーチャーフラグを 03_sys_params に設計段階で用意しておく。
| 推奨パラメータキー | 型 | デフォルト | 用途 |
|---|---|---|---|
PJ_DASH_SHOW_REVENUE | boolean | true | 売上_予算 / 売上_実績 / 売上_差異列を表示するか |
PJ_DASH_SHOW_MARGIN | boolean | true | 利益率%_予算 / 利益率%_実績 列を表示するか |
PJ_DASH_SHOW_COST_BUDGET | boolean | true | 費用_予算 / 費用_差異列を表示するか(FALSE なら予算消化率% のみ残す) |
読み取りは既存の Constants.getParam('PJ_DASH_SHOW_REVENUE', 'true') を流用可能。最終的な要否と各デフォルト値はプロダクトオーナー判断を仰ぐこと。
2. プルダウンの初期値と永続化
セルプルダウンの選択値はシート再描画(sheet.clear())でリセットされる。608_datamart_render.js L116 のように、直前の選択値を保持する仕組みを入れるかどうかは UX 検討事項。初期実装は毎回リセットで可。
3. 79_pj_variance_dashboard のタブ並び順
既存タブ 79_pj_monthly との並び順はアルファベット順で 79_pj_monthly → 79_pj_variance_dashboard となる(sortSheetsByName 実行後も同様)。混乱を避けるため、タブ背景色を既存 79 系(PJ 管理会計のオレンジ系)と揃えるかは UI レビュー時に検討。
4. 実績金額の採用基準
本仕様は 税抜金額_実績(42_trn_journal)を採用しているが、予算が税抜で運用されているか税込で運用されているか、現場ルールを確認すること。41_trn_budget.予算金額 の運用が税込であれば、実績も 税込金額_実績 に揃える必要がある(F-14 では税抜採用を前提としたが、要確認)。
実装プロンプト(Claude Code 用)
あなたは GAS 会計システム (bizlp-gas-accounting) のシニア開発者です。
F-14「プロジェクト別予実ダッシュボード」を実装してください。
## 実行前タスク(必須・順序厳守)
下記ファイルをこの順序で Read して、固有名詞・行番号・既存パターンを確定させること。造語・推測禁止。
1. `400_domain/420_project_profitability.js` — 既存 `buildProjectProfitability` L7 / `loadPjMaster_` L803 / `loadAcctMaster_` L822 の構造と呼び出し方。再利用必須。
2. `200_data/202_repository.js` — `AccountRepository` L304-L350(最小 Repository テンプレート)と `readSheetAsDtos_` L19-L29 を確認。
3. `000_infra/003_contracts.js` — `BudgetDTO` L132-L149 と `JournalEntryDTO` L95-L129 のフィールド名を確定。
4. `100_config/101_sys_config.js` L770-L823 — シートキー登録パターン(`if (!existKeys.includes('KEY')) confSheet.appendRow(...)`)。
5. `000_infra/002_constants.js` L206-L324 — `MENU_DEFINITION` と `📋 サイドバー: 📊 マート更新` カテゴリ L229-L239 の位置。
6. `600_report/608_datamart_render.js` L114-L115 — `setDataValidation` の実装パターン。
## 修正対象ファイルと変更量
| ファイル | 変更内容 | 概算行数 |
|---------|---------|---------|
| `200_data/202_repository.js` | `BudgetRepository` + `ProjectRepository` 追加 | +40 行 |
| `400_domain/420_project_profitability.js` | `updatePjVarianceDashboard()` + `updatePjVarianceDashboard_()` 追加 | +180 行 |
| `100_config/101_sys_config.js` | シートキー登録 1 行追加(L815-L817 付近) | +1 行 |
| `000_infra/002_constants.js` | `MENU_DEFINITION` の「📋 サイドバー: 📊 マート更新」カテゴリに 1 項目追加 | +1 行 |
## Step 1: Repository 追加(`200_data/202_repository.js`)
`AccountRepository` L304-L350 の直下に 2 つの Repository を同じパターンで追加:
```js
// =====================================================================
// BudgetRepository — 41_trn_budget (読み取り専用)
// =====================================================================
var BudgetRepository = {
/** @private */
_getSheet: function() {
return Utils.getSheetByKey('TRN_BUDG', '41_trn_budget');
},
/**
* 全予算レコードを DTO 配列で取得する。
* @returns {{ headers: string[], dtos: BudgetDTO[] }}
*/
findAll: function() {
return readSheetAsDtos_(BudgetRepository._getSheet());
},
};
// =====================================================================
// ProjectRepository — 14_mst_project (読み取り専用マスタ)
// =====================================================================
var ProjectRepository = {
/** @private */
_getSheet: function() {
return Utils.getSheetByKey('MST_PROJ', '14_mst_project');
},
/**
* 全 PJ マスタレコードを DTO 配列で取得する。
* @returns {{ headers: string[], dtos: Object[] }}
*/
findAll: function() {
return readSheetAsDtos_(ProjectRepository._getSheet());
},
};
```
## Step 2: ダッシュボード更新関数(`400_domain/420_project_profitability.js` 末尾に追加)
`loadAllocRules_` L842-L869 の直下に以下を追加する。
```js
/**
* F-14: PJ 別予実ダッシュボード (79_pj_variance_dashboard) を再構築する公開関数。
* メニューから呼ばれる。
*/
function updatePjVarianceDashboard() {
var FUNC = 'updatePjVarianceDashboard';
try {
Utils.logInfo(FUNC, '処理開始');
updatePjVarianceDashboard_();
Utils.logInfo(FUNC, '処理完了');
} catch (e) {
Utils.logError(FUNC, e);
SpreadsheetApp.getUi().alert('🚨 ' + FUNC + ' でエラーが発生しました', e.message, SpreadsheetApp.getUi().ButtonSet.OK);
}
}
/** @private */
function updatePjVarianceDashboard_() {
var ss = getWebSpreadsheet_();
var sheetName = Utils.getSheetNameByKey('PJ_VAR_DASH') || '79_pj_variance_dashboard';
var sheet = ss.getSheetByName(sheetName) || ss.insertSheet(sheetName);
// 冪等性: 先頭で全クリア
sheet.clear();
sheet.getRange(1, 1, sheet.getMaxRows(), sheet.getMaxColumns()).clearDataValidations();
var projMaster = loadPjMaster_(ss);
var acctMaster = loadAcctMaster_(ss);
// 対象 12 ヶ月 (8月〜翌7月、既存 buildProjectProfitability と同じロジック)
var today = new Date();
var startYear = (today.getMonth() + 1 >= 8) ? today.getFullYear() : today.getFullYear() - 1;
var targetMonths = [];
for (var i = 0; i < 12; i++) {
var d = new Date(startYear, 7 + i, 1);
targetMonths.push(d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0'));
}
// パラメータセル (B1: PJ選択, D1: 期間粒度, F1: 基準年月)
// 初期値は (すべて) / 単月 / 今月
var currentYm = (function() {
var t = new Date();
return t.getFullYear() + '-' + String(t.getMonth() + 1).padStart(2, '0');
})();
var defaultYm = targetMonths.indexOf(currentYm) >= 0 ? currentYm : targetMonths[0];
var selectedPj = '(すべて)';
var selectedGran = '単月';
var selectedYm = defaultYm;
// 予算・実績集計
var aggMap = {}; // { pjName: { salesBud, salesAct, costBud, costAct } }
function ensureAgg_(pn) {
if (!aggMap[pn]) aggMap[pn] = { salesBud: 0, salesAct: 0, costBud: 0, costAct: 0 };
return aggMap[pn];
}
function inRange_(ym) {
if (!ym) return false;
if (selectedGran === '単月') return ym === selectedYm;
// 累計(YTD): 期首(targetMonths[0]) 〜 selectedYm
return ym >= targetMonths[0] && ym <= selectedYm;
}
// 予算 (41_trn_budget)
var bud = BudgetRepository.findAll();
for (var bi = 0; bi < bud.dtos.length; bi++) {
var bd = bud.dtos[bi];
var bFlag = bd['有効フラグ'];
if (bFlag === false || String(bFlag).toUpperCase() === 'FALSE') continue;
var bYm = Utils.parseDateToYm(bd['対象年月']);
if (!inRange_(bYm)) continue;
var bPj = String(bd['PJ名'] || '').trim() || '指定なし_共通費など';
if (projMaster[bPj] && projMaster[bPj] /* always registered */) {
// 登録済み
} else if (!projMaster[bPj]) {
bPj = '(PJマスタ未登録)';
}
var bAcct = String(bd['科目名'] || '').trim();
var bAcctInfo = acctMaster[bAcct];
if (!bAcctInfo) { Utils.logInfo('F-14', '未登録科目(予算): ' + bAcct); continue; }
if (bAcctInfo.stmt === 'BS') continue;
var bShushi = String(bd['収支区分'] || '').trim();
var bAmt = Number(bd['予算金額']) || 0;
var aggB = ensureAgg_(bPj);
if (bShushi === '収入') aggB.salesBud += bAmt;
else if (bShushi === '支出') aggB.costBud += Math.abs(bAmt);
}
// 実績 (42_trn_journal)
var jrn = JournalRepository.findAll();
for (var ji = 0; ji < jrn.dtos.length; ji++) {
var jd = jrn.dtos[ji];
var jYm = Utils.parseDateToYm(jd['発生日(P/L計上日)']);
if (!inRange_(jYm)) continue;
var jPj = String(jd['PJ名'] || '').trim() || '指定なし_共通費など';
if (!projMaster[jPj]) jPj = '(PJマスタ未登録)';
var jAcct = String(jd['科目名'] || '').trim();
var jAcctInfo = acctMaster[jAcct];
if (!jAcctInfo) { Utils.logInfo('F-14', '未登録科目(実績): ' + jAcct); continue; }
if (jAcctInfo.stmt === 'BS') continue;
var jShushi = String(jd['収支区分'] || '').trim();
var jAmt = Number(jd['税抜金額_実績']) || 0;
var aggA = ensureAgg_(jPj);
if (jShushi === '収入') aggA.salesAct += jAmt;
else if (jShushi === '支出') aggA.costAct += Math.abs(jAmt);
}
// 出力行構築: 有効 PJ (プロジェクトマスタ登録順) → (PJマスタ未登録) → 全社合計
var out = [];
// 1 行目: パラメータセル
out.push(['🔍 対象PJを選択 ➡️', selectedPj, '期間粒度 ➡️', selectedGran, '基準年月 ➡️', selectedYm, '', '', '', '', '', '', '', '', '', '']);
// 2 行目: ヘッダー
out.push(['PJ名', 'PM', '売上_予算', '売上_実績', '売上_差異額', '売上_差異率%', '費用_予算', '費用_実績', '費用_差異額', '費用_差異率%', '費用_予算消化率%', '利益_予算', '利益_実績', '利益_差異額', '利益率%_予算', '利益率%_実績']);
function zeroSafe_(num, den) { return den === 0 ? '-' : (num / den); }
function consumeRate_(act, bud) {
if (bud === 0 && act > 0) return '予算外支出';
if (bud === 0 && act === 0) return 0;
return act / bud;
}
var pjList = Object.keys(projMaster).filter(function(p) {
return selectedPj === '(すべて)' || p === selectedPj;
});
// 実績・予算にあるが未登録 PJ を集約した (PJマスタ未登録) も対象に含める
if (aggMap['(PJマスタ未登録)'] && (selectedPj === '(すべて)' || selectedPj === '(PJマスタ未登録)')) {
pjList.push('(PJマスタ未登録)');
}
var grandSalesBud = 0, grandSalesAct = 0, grandCostBud = 0, grandCostAct = 0;
for (var pi = 0; pi < pjList.length; pi++) {
var pn = pjList[pi];
var a = aggMap[pn] || { salesBud: 0, salesAct: 0, costBud: 0, costAct: 0 };
var pm = (projMaster[pn] && projMaster[pn].pm) || '';
var sDiff = a.salesAct - a.salesBud;
var cDiff = a.costAct - a.costBud;
var profBud = a.salesBud - a.costBud;
var profAct = a.salesAct - a.costAct;
var pDiff = profAct - profBud;
out.push([
pn, pm,
a.salesBud, a.salesAct, sDiff, zeroSafe_(sDiff, a.salesBud),
a.costBud, a.costAct, cDiff, zeroSafe_(cDiff, a.costBud), consumeRate_(a.costAct, a.costBud),
profBud, profAct, pDiff, zeroSafe_(profBud, a.salesBud), zeroSafe_(profAct, a.salesAct)
]);
grandSalesBud += a.salesBud; grandSalesAct += a.salesAct;
grandCostBud += a.costBud; grandCostAct += a.costAct;
}
// 全社合計行 (利益率は合計から再計算)
var gSDiff = grandSalesAct - grandSalesBud;
var gCDiff = grandCostAct - grandCostBud;
var gProfBud = grandSalesBud - grandCostBud;
var gProfAct = grandSalesAct - grandCostAct;
var gPDiff = gProfAct - gProfBud;
out.push([
'🏢 全社合計', '',
grandSalesBud, grandSalesAct, gSDiff, zeroSafe_(gSDiff, grandSalesBud),
grandCostBud, grandCostAct, gCDiff, zeroSafe_(gCDiff, grandCostBud), consumeRate_(grandCostAct, grandCostBud),
gProfBud, gProfAct, gPDiff, zeroSafe_(gProfBud, grandSalesBud), zeroSafe_(gProfAct, grandSalesAct)
]);
// シート書き込み
var rLen = out.length, cLen = out[0].length;
if (sheet.getMaxRows() < rLen) sheet.insertRowsAfter(sheet.getMaxRows(), rLen - sheet.getMaxRows());
if (sheet.getMaxColumns() < cLen) sheet.insertColumnsAfter(sheet.getMaxColumns(), cLen - sheet.getMaxColumns());
sheet.getRange(1, 1, rLen, cLen).setValues(out);
sheet.setFrozenRows(2);
sheet.setFrozenColumns(2);
// プルダウン設置
var pjChoices = ['(すべて)'].concat(Object.keys(projMaster).sort());
if (aggMap['(PJマスタ未登録)']) pjChoices.push('(PJマスタ未登録)');
var ruleB1 = SpreadsheetApp.newDataValidation().requireValueInList(pjChoices).setAllowInvalid(true).build();
sheet.getRange('B1').clearDataValidations().setDataValidation(ruleB1);
var ruleD1 = SpreadsheetApp.newDataValidation().requireValueInList(['単月', '累計(YTD)']).setAllowInvalid(true).build();
sheet.getRange('D1').clearDataValidations().setDataValidation(ruleD1);
var ruleF1 = SpreadsheetApp.newDataValidation().requireValueInList(targetMonths).setAllowInvalid(true).build();
sheet.getRange('F1').clearDataValidations().setDataValidation(ruleF1);
// 書式: ヘッダー行
sheet.getRange(2, 1, 1, cLen).setBackground('#434343').setFontColor('#FFFFFF').setFontWeight('bold');
sheet.getRange(1, 1, 1, cLen).setBackground('#EAEAEA').setFontWeight('bold');
sheet.getRange(1, 2).setBackground('#FFF2CC').setFontColor('#B45F06');
sheet.getRange(1, 4).setBackground('#FFF2CC').setFontColor('#B45F06');
sheet.getRange(1, 6).setBackground('#FFF2CC').setFontColor('#B45F06');
// 全社合計行
sheet.getRange(rLen, 1, 1, cLen).setBackground('#D9EAD3').setFontWeight('bold').setFontColor('#274E13');
// 金額列フォーマット (C〜E, G〜I, L〜N)
sheet.getRange(3, 3, rLen - 2, 3).setNumberFormat('#,##0;[Red]△ #,##0;"-"');
sheet.getRange(3, 7, rLen - 2, 3).setNumberFormat('#,##0;[Red]△ #,##0;"-"');
sheet.getRange(3, 12, rLen - 2, 3).setNumberFormat('#,##0;[Red]△ #,##0;"-"');
// 率 (F, J, K, O, P)
sheet.getRange(3, 6, rLen - 2, 1).setNumberFormat('0.0%;[Red]△ 0.0%;"-"');
sheet.getRange(3, 10, rLen - 2, 2).setNumberFormat('0.0%;[Red]△ 0.0%;"-"');
sheet.getRange(3, 15, rLen - 2, 2).setNumberFormat('0.0%;[Red]△ 0.0%;"-"');
// 赤字強調 (利益_実績 < 0)
var colProfAct = 13; // M 列
for (var r = 3; r < rLen; r++) {
var pv = Number(out[r - 1][colProfAct - 1]);
if (!isNaN(pv) && pv < 0) {
sheet.getRange(r, 1, 1, cLen).setBackground('#F4CCCC').setFontColor('#CC0000');
}
}
// 列幅
sheet.setColumnWidth(1, 200);
sheet.setColumnWidth(2, 120);
for (var c = 3; c <= cLen; c++) sheet.setColumnWidth(c, 105);
sheet.getDataRange().setFontFamily('BIZ UDGothic');
// 余剰列・行削除
if (sheet.getMaxColumns() > cLen) sheet.deleteColumns(cLen + 1, sheet.getMaxColumns() - cLen);
if (sheet.getMaxRows() > rLen) sheet.deleteRows(rLen + 1, sheet.getMaxRows() - rLen);
ss.toast('📋 PJ別予実ダッシュボードを更新しました', '完了', 5);
}
```
## Step 3: シートキー登録とメニュー登録
**`100_config/101_sys_config.js` L815-L817 付近** (`PJ_RAW` / `PJ_PL` / `PJ_MONTHLY` 直後) に 1 行追加:
```js
if (!existKeys.includes('PJ_VAR_DASH')) confSheet.appendRow(['PJ_VAR_DASH', '', '79_pj_variance_dashboard', 'PJ別予実ダッシュボード']);
```
**`000_infra/002_constants.js` L230-L239**(`📋 サイドバー: 📊 マート更新` カテゴリ)の `'プロジェクト別 採算'` 項目の直後に 1 行追加:
```js
{ label: '📋 プロジェクト別予実ダッシュボードを更新', funcName: 'updatePjVarianceDashboard', description: 'PJ 別の予算 vs 実績マート (79_pj_variance_dashboard) を再構築' },
```
## 制約(必ず遵守)
1. **列番号ハードコード禁止**: ヘッダー名ベースの `indexOf` / プロパティアクセスのみ使用(CLAUDE.md 規約)。
2. **科目マスタ未登録の自動推測禁止**: キーワード判定で `売上` / `費用` を推測しない。未登録は `Utils.logInfo` で警告のみ記録し集計から除外(CLAUDE.md 規約)。
3. **メニュー名造語禁止**: 実在するカテゴリ `📋 サイドバー: 📊 マート更新` 内に追加する(002_constants.js L230 を必ず実参照)。新規カテゴリを作らない(失敗パターン #18-#20)。
4. **既存ロジック重複実装禁止**: `loadPjMaster_` / `loadAcctMaster_`(420_project_profitability.js)を再利用。コピーして再実装しない(失敗パターン #1)。
5. **非加算指標の単純合算禁止**: 全社合計の利益率% は合算後の売上・費用から再計算(失敗パターン #1)。
6. **冪等性**: 先頭で `sheet.clear()` + `clearDataValidations()` を必ず実行。
## エッジケース(実装時に必ず網羅)
| # | 条件 | 表示値 |
|---|------|--------|
| 1 | 売上実績 = 0 のときの利益率%(実績) | `"-"` |
| 2 | 売上予算 = 0 のときの利益率%(予算) | `"-"` |
| 3 | 差異率% の分母(予算)= 0 | `"-"` |
| 4 | 費用予算 = 0 かつ費用実績 > 0 の予算消化率% | `"予算外支出"` |
| 5 | 費用予算 = 0 かつ費用実績 = 0 の予算消化率% | `"0%"` |
| 6 | PJ マスタ未登録の PJ 名 | `(PJマスタ未登録)` 行に集約 |
| 7 | 予算のみ存在する PJ | 実績 = 0、差異 = マイナス表示 |
| 8 | 実績のみ存在する PJ | 予算 = 0、予算消化率 = `"予算外支出"` |
| 9 | 全社合計行の利益率% | 全社売上・全社費用から再計算(単純合算不可) |
| 10 | 有効フラグ = FALSE の行 | 全処理でスキップ |
| 11 | 科目マスタ未登録の科目名 | `Utils.logInfo` に記録のみ、集計除外 |
| 14 | 利益_実績 < 0 の行 | 薄赤(`#f4cccc`) + 赤字(`#cc0000`)強調 |
## 動作確認手順
`npm run push:dev` でデプロイ後、以下を dev 環境で確認:
1. メニュー「📋 サイドバー: 📊 マート更新」配下に「📋 プロジェクト別予実ダッシュボードを更新」が表示されること。
2. 実行後に `79_pj_variance_dashboard` シートが新規生成(または再生成)されること。
3. シート B1・D1・F1 のプルダウンで PJ・期間粒度・基準年月が切り替わること(ただし現時点では選択値は次回実行時に参照されるだけで、選択直後には再計算しない。UX 的に再実行が必要なことを 1 行目ラベルで示唆する)。
4. `14_mst_project` に登録された有効 PJ すべてが表示されること。
5. `(PJマスタ未登録)` 行・`🏢 全社合計` 行が最終行に正しく表示されること。
6. 売上実績 = 0 の月を選んだ PJ の利益率%_実績 が `"-"` になること。
7. 費用予算 = 0 だが実績がある PJ の予算消化率% が `"予算外支出"` になること。
8. 利益_実績 < 0 の行が薄赤+赤字強調されること。
9. 既存 `buildProjectProfitability` メニュー実行が壊れていないこと(回帰確認)。
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
Step 1: Repository 追加(BudgetRepository / ProjectRepository) | Haiku | 既存 AccountRepository L304-L350 のパターン完全踏襲。判断要素なし |
Step 2: ダッシュボード更新関数(updatePjVarianceDashboard_()) | Sonnet | 既存 loadPjMaster_ / loadAcctMaster_ との統合、収支区分 と stmt='BS' を組み合わせた集計ロジック、書式整備など中程度の判断が必要 |
| Step 3: シートキー・メニュー登録 | Haiku | 既存パターンへの 1 行追加のみ |
変更履歴
| 日付 | 変更者 | 変更内容 |
|---|---|---|
| 2026-04-20 | Claude Opus 4.7 | 初版作成 |
仕様書作成プロンプト(再現性・監査性のため必ず記録)
展開して表示
【タイムアウト回避・実行原則(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 骨格Write(〜20行)
- 2-2 概要〜注意事項 Edit/Bash(〜300行)
- 2-3a エッジケース〜人間検討事項 Edit/Bash(〜200行)
- 2-3b 実装プロンプト〜変更履歴 Edit/Bash(〜250行)
- 2-4 `<details>` にプロンプト全文記録 Edit/Bash(最重量・必ず独立Step)
4. **各Stepで何を書くかを具体指示**: 設計判断をPhase 2実行時に持ち込まない。各Stepは「Phase 1で確定した情報の清書」に徹する。
======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 F-14「プロジェクト別予実ダッシュボード」の開発仕様書を作成してください。
作成後は `docs/_config.json` の `nav` 配列の §E.5(FP&A・レポーティング)セクションにも必ず追記してください。
---
## Phase 1: 実行前調査(テキスト報告禁止・即座にツール実行)
以下のファイルを**この順序で**Readし、Phase 2の設計判断に必要な情報をすべて確定させること。
**Grepは「どこにあるか」の発見まで。「どう書くか」の判断は必ずReadで行う。** 関数名・シート名・メニュー名・定数名は実在する文字列のみ引用し、推測・造語は禁止(失敗パターン #18-#20)。
### 1-A: 案件定義の読み込み
- `docs/_internal/TODO_future.md` — F-14の案件名・概要・人間が検討すべき事項を取得する
### 1-B: 既存PJ関連実装の確認(最重要・車輪の再発明防止)
- `400_domain/420_project_profitability.js` — PJ別収益性計算の既存ロジックを確認する
- 確認ポイント: 予算・実績の集計方法、科目フィルタ(収支区分の判定値)、PJ集計キーの形式、公開関数名と行番号
- **このファイルの実装を最大限再利用すること。重複するロジックを別関数として書かない**
- 確認結果により「420に関数を追加する」か「新規ファイルに分離する」かを Phase 1 で決定する
- `600_report/601_datamart_ingest.js`(またはファイル名をGrepで特定した上でRead)
- 確認ポイント: マートシート全クリア→再描画のパターン(`sheet.clear()` の呼び出し箇所と行番号)、ヘッダー書き込みと数値フォーマット適用のパターン
### 1-C: データ取得層の確認
- `200_data/202_repository.js`
- 確認ポイント:
- `JournalRepository` の `_getSheet`, `findAll`, `save`, `append` の4メソッド構成と行番号(新設Repositoryの雛形として使う)
- `readSheetAsDtos_` は同ファイル冒頭の `@private` ヘルパーであることを確認。新設Repositoryの `findAll` はこれを内部で呼び出す形で実装する
- `BudgetRepository` および `ProjectRepository` がすでに存在するか確認する(存在すれば新設不要)
- `000_infra/003_contracts.js`
- 確認ポイント:
- `BudgetDTO` の全フィールド(特に `対象年月`, `PJ名`, `予算金額`, `科目名`, `収支区分` の正確なフィールド名)
- `JournalEntryDTO` の全フィールド(特に `発生日(P/L計上日)`, `PJ名`, `税抜金額_実績`, `科目名`, `収支区分` の正確なフィールド名)
### 1-D: システム設定・メニュー構造の確認(固有名詞の裏取り)
- `100_config/101_sys_config.js`
- 確認ポイント:
- `onOpen()` 内のメニュー定義を全文Read。「📊 FP&Aレポート」メニューの正確な文字列と `.addItem` の一覧を確認する(文字列が異なれば実在する正確な名称に差し替える)
- `77_pj_raw` と `78_pj_pl` のシートキー登録パターン(`appendRow` の行形式)を確認し、`79_pj_variance_dashboard` の登録手順を確定する
- DDL(`setupAllSchemas` 等)で `79_pj_variance_dashboard` のスキーマを追加する箇所があるか確認する
- `000_infra/002_constants.js`
- 確認ポイント: `SHEET_DEFAULTS` に `79_pj_variance_dashboard` 相当のエントリが必要か確認する(マートシートは通常不要だが念のため)
### 1-E: プルダウン実装パターンの確認
- `docs/dev/dev_S-22_boundary_month_selector.md`
- 確認ポイント: セルへのプルダウン設置方法(`setDataValidation` の引数形式)、選択値の読み取り方、実際の実装コード例
### 1-F: 実データ検証(MCPツールで確認)
以下をMCPツールで確認し、DDL定義と実データの乖離がないことを検証する:
- `14_mst_project` の列構造(PJ名列・有効フラグ列の正確なヘッダー名)
- `41_trn_budget` の `収支区分` 実データ値(「収入」「支出」等、実際に格納されている文字列)
- `42_trn_journal` の `収支区分` 実データ値と `PJ名` の代表的なフォーマット
---
## Phase 2: 仕様書の分割作成
**【絶対厳守】1回のツール呼び出しで全内容を出力しない。以下5つのStepに分割して実行する。**
出力先: `docs/dev/dev_F-14_pj_variance_dashboard.md`
セクション構成(必須・この順序で全セクションを含めること):
(省略・本仕様書の見出しと一致)
### Step 2-1: 骨格の作成(File Write, 〜20行)
上記セクション見出しのみ。本文は空で可。
### Step 2-2: 概要〜注意事項の追記(File Edit または Bash, 〜300行)
(省略・本仕様書の該当セクションを参照)
### Step 2-3a: エッジケース〜人間検討事項の追記(File Edit または Bash, 〜200行)
(省略・本仕様書の該当セクションを参照)
### Step 2-3b: 実装プロンプト〜変更履歴の追記(File Edit または Bash, 〜250行)
(省略・本仕様書の該当セクションを参照)
### Step 2-4: 仕様書作成プロンプトの記録(File Edit または Bash)
末尾に `<details>` で本プロンプトを記録。
---
## Phase 3: `_config.json` への追記・changelog更新・コミット
### 3-A: `docs/_config.json` への追記(必須)
`nav` 配列の **§E.5(FP&A・レポーティング)** セクションに追加:
{ "file": "dev/dev_F-14_pj_variance_dashboard.md", "title": "E.5.X F-14 プロジェクト別予実ダッシュボード" }
追記後、JSON構文が壊れていないことを確認する(`python3 -m json.tool docs/_config.json` 等)。
### 3-B: `docs/_internal/changelog.md` への追記
先頭行(ヘッダー直後)に追記:
| 2026-04-20 | dev_F-14_pj_variance_dashboard.md | 初版作成。PJ別予実ダッシュボード仕様書 |
### 3-C: コミット&プッシュ
git add → git commit → git push -u origin docs/dev-F-14
📌 取り込み時の注記 (2026-06-02 sub 復元)
本仕様書は旧 F-番号体系で作成され PR 未マージのまま孤立していたドラフトを、
origin/docs/dev-*ブランチから内容無改変で復元し、案件ID のみ MAS 体系へ正規化したもの。status: Open(未実装)。⚠️ ファイル番号ドリフト: 本文「対象ファイル」が指す
600_report/610〜612_*.jsは現行 main で 別機能に使用済み(610=投資分析/MAS-013・611=財務モデリング/MAS-010・612=採用sim/MAS-012)。 実装時にファイル番号の再割当が必要。⚠️ 部分実装あり:
400_domain/420_project_profitability.jsが79_pj_monthlyを出力済み。 本仕様のupdatePjVarianceDashboard_()と機能重複の可能性 — 実装フェーズで突合・統合判断すること。