MAS-012: 目標逆算型人員計画シミュレーション
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-012 |
| カテゴリ | FP&A・シミュレーション |
| Phase | P3 |
| 優先度 | ★ |
| 対象ファイル | 000_infra/002_constants.js(MENU_DEFINITION にメニュー追加), 200_data/202_repository.js(HeadcountRepository 新設), SimHeadcount.html(新規HTMLダイアログ), 600_report/612_sim_headcount.js(シミュレーションロジック本体、PR #337 で実装済) |
| 前提案件 | なし(22_bud_headcount と 42_trn_journal の実データがあれば独立着手可能) |
目的
目標月次売上と一人当たり月次売上高から、必要な採用人数・採用活動開始月を逆算し、94_sim_headcount シートに月別の採用計画として出力する。経営者・採用責任者が「売上目標 X 円を達成するには何人採用すればよいか/いつ採用活動を始めればよいか」を即時に把握できるようにする。
Human-in-the-Loop の原則に従い、シミュレーション結果はあくまで人間のレビュー対象とし、22_bud_headcount(予算シート)への自動反映はスコープ外とする。
関連既存コード
新機能のため「現在のコード」は存在しない。以下は実装時に参照する既存実装。
JournalRepository.findAll() — 売上実績取得に使用
200_data/202_repository.js L259-272。戻り値は配列ではなく { headers: string[], dtos: JournalEntryDTO[] } オブジェクトであり、フィルタは必ず result.dtos.filter(...) で行う。
// 200_data/202_repository.js L259-272
var JournalRepository = {
_getSheet: function() {
return Utils.getSheetByKey('TRN_JOUR', '42_trn_journal');
},
findAll: function() {
return readSheetAsDtos_(JournalRepository._getSheet());
},
// ...
};
readSheetAsDtos_(sheet) — HeadcountRepository 実装パターン
200_data/202_repository.js L19-29。シート全体をヘッダー付き DTO 配列に変換する共通ヘルパー。HeadcountRepository.findAll() もこの関数に委譲するだけでよい。
// 200_data/202_repository.js L19-29
function readSheetAsDtos_(sheet) {
if (!sheet) return { headers: [], dtos: [] };
var data = sheet.getDataRange().getValues();
if (data.length === 0) return { headers: [], dtos: [] };
var headers = data[0].map(function(h) { return String(h).trim(); });
var dtos = [];
for (var i = 1; i < data.length; i++) {
dtos.push(Contracts.toDto(headers, data[i]));
}
return { headers: headers, dtos: dtos };
}
Utils.getSheetByKey(key, fallbackName) — シート取得
000_infra/004_utils.js L40-50。01_sys_config に登録されたシステムキーで解決し、未登録時は第2引数のシート名で取得する。22_bud_headcount のシステムキーは BUD_HC(100_config/101_sys_config.js L776 の confSheet.appendRow(['BUD_HC', '', '22_bud_headcount', '予算_人員計画・採用シミュレータ']) で登録されている)。
Utils.parseDateToYm, Utils.addMonths, Utils.parseAmt — 日付・金額処理
000_infra/004_utils.js L92-99 / L127-132 / L191-198。それぞれ以下のシグネチャ。
| 関数 | 引数 | 戻り値 |
|---|---|---|
parseDateToYm(val) | Date / 'YYYY-MM-DD' / 'YYYY/MM' 等 | 'YYYY-MM' 文字列、パース不可なら '' |
addMonths(ymStr, months) | 'YYYY-MM' 文字列 + 整数(正負可) | 'YYYY-MM' 文字列 |
parseAmt(val) | カンマ区切り・全角数字等を含む文字列 or 数値 | number、パース不可なら 0 |
Constants.MENU_DEFINITION と onOpen() — メニュー構造
000_infra/002_constants.jsL206-324 のMENU_DEFINITION配列にカテゴリ単位でメニュー項目を定義している。100_config/101_sys_config.jsL323-350 のonOpen()はMENU_DEFINITIONをループして動的にメニューを生成する。メニュー項目の追加は101_sys_config.jsのonOpen本体ではなく、002_constants.jsのMENU_DEFINITIONに対して行う。- 本機能は FP&A 系のため「📋 サイドバー: 📊 マート更新」カテゴリ(L229-239)に追加する。既存のラベル「財務3表の更新」「📅 基準年月指定で更新」「プロジェクト別 採算」「📊 KPIダッシュボード再描画」「📸 前年度P/Lスナップショット」の並びに、「🧮 人員計画シミュレーション」として末尾に追加する。
22_bud_headcount 列構造
100_config/101_sys_config.js L1142 のコメントおよび 000_infra/002_constants.js L76 の SHEET_DEFAULTS から判明している列:
| 列 | ヘッダー名 | 用途 |
|---|---|---|
| A(1) | 有効フラグ | false / 'FALSE' の行は全処理でスキップ |
| B(2) | 管理ID | EMP_ プレフィックス(ID_PREFIX_MAP, 002_constants.js L103) |
| C(3) | 氏名 | — |
| D(4) | 雇用形態 | 正社員・業務委託等 |
| E(5) | 科目名 | — |
| G(7) | 適用年度 | fiscalYear 自動設定 |
| H(8) | 入社年月 | — |
| I(9) | 退職年月 | 参考情報 (docs/spec/spec_rpa_hc.md L47) |
| J(10) | 開始年月 | RPA 対象期間の開始 |
| K(11) | 終了年月 | RPA 対象期間の終了。空 = 無期限(docs/spec/spec_rpa_hc.md L49) |
| L(12) | 月額給与・報酬 | — |
「現在有効人員数」を判定する際は、在籍期間の正本である 開始年月 と 終了年月 を使用する(退職年月 は参考扱いなので使わない)。
修正方針
アーキテクチャ
[SimHeadcount.html] ── google.script.run ──▶ [シミュレーションロジック.js]
(入力フォーム) │
├─▶ [HeadcountRepository.findAll()]
│ → 22_bud_headcount から有効人員を月別集計
│
├─▶ [JournalRepository.findAll().dtos.filter(...)]
│ → 過去12ヶ月の売上実績を月別集計
│
└─▶ [94_sim_headcount シート]
→ 前提パラメータ + 月別採用計画を出力
入力パラメータ(SimHeadcount.html ダイアログで受け取る)
| # | 項目 | 型 | デフォルト値 | 説明 |
|---|---|---|---|---|
| ① | 目標年月 | 'YYYY-MM' 文字列 | 今月 + 12ヶ月 | 目標売上を達成したい年月 |
| ② | 目標月次売上 | 数値 | 空欄 | 目標年月に達成したい月次売上高(円) |
| ③ | 一人当たり月次売上高 | 数値 | 自動計算値 | 後述の自動計算ロジックで算出、ユーザー上書き可 |
| ④ | 採用リードタイム | 月数 | 2 | 採用活動開始から入社までの月数 |
| ⑤ | 既存人員の月次退職率 | 小数 | 0.01(=1%) | 月あたりの退職率 |
出力シート 94_sim_headcount
- 取得:
var simSheet = ss.getSheetByName('94_sim_headcount') || ss.insertSheet('94_sim_headcount');で行う。存在しない場合は動的に作成する(DDLsetupAllSchemasの管理外)。 - 新規作成時: ヘッダー行を書き込む(列構成は「出力レイアウト」参照)。
- 冪等性:
実行のたびに 2 行目以降をクリアしてから書き込む。重複行を発生させない。if (simSheet.getLastRow() > 1) { simSheet.getRange(2, 1, simSheet.getLastRow() - 1, simSheet.getLastColumn()).clearContent(); }
計算ロジック
(a) 一人当たり月次売上高(自動計算)
- 売上実績の月別集計:
JournalRepository.findAll()の戻り値は{ headers, dtos }オブジェクトなので、.dtos.filter(dto => ...)で次の条件を満たす仕訳を抽出する。dto['収支区分'] === '収入'dto['科目名']が売上科目('売上高'/'商品売上高'/'コンサルティング売上高'/'講師・研修売上高'/'受託開発売上高'/'ライセンス・SaaS売上高'/'保守・サポート売上高'/'その他売上高')のいずれかUtils.parseDateToYm(dto['発生日(P/L計上日)'])が「今月から過去12ヶ月」の YYYY-MM 範囲内(例: 現在2026-04なら2025-04〜2026-03)
- 月ごとの売上合計を
{ 'YYYY-MM': 金額 }マップに集計する。金額はUtils.parseAmt(dto['金額'])でパースする(ヘッダー名は42_trn_journalの DDL を実装時に Read で確認すること)。 - 有効人員数の月別集計:
HeadcountRepository.findAll().dtosをフィルタし、各対象月ymにおいて以下を満たす行を 1 人としてカウントする。dto['有効フラグ'] !== false && String(dto['有効フラグ']).toUpperCase() !== 'FALSE'Utils.parseDateToYm(dto['開始年月']) <= ymdto['終了年月']が空欄 ORUtils.parseDateToYm(dto['終了年月']) >= ym
- 対象 12 ヶ月の各月について
月次売上 ÷ 月次有効人員数を計算し、その平均値を「一人当たり月次売上高」とする。有効人員数 = 0 の月はスキップ(ゼロ除算防止)。
(b) 目標人員数
var targetHeadcount = Math.ceil(目標月次売上 / 一人当たり月次売上高);
切り上げ前の計算値(小数)は出力行に参考値として併記する。
(c) 現在有効人員数
現在年月(今月)時点で上記 (a)-3 の条件を満たす行数。
(d) 必要採用人数
var N = 目標年月 と 今月の月数差; // Utils.addMonths で差分算出、または (y1*12+m1) - (y0*12+m0) で計算
var 期待残存人員 = Math.floor(現在有効人員数 * Math.pow(1 - 月次退職率, N));
var 必要採用人数 = Math.max(0, targetHeadcount - 期待残存人員);
(e) 採用活動開始月
var recruitStartYm = Utils.addMonths(目標年月, -採用リードタイム);
(f) 採用人数の月別配分
- 採用活動開始月から目標年月までの各月に均等配分する。
Math.floor(必要採用人数 / 配分月数)を各月に割り当て、端数(余り)は最終月に加算する(合計人数の整合性を保つ)。
出力レイアウト(94_sim_headcount)
| 行 | 内容 |
|---|---|
| 1 | ヘッダー行(列名) |
| 2〜 | 前提パラメータブロック(①〜⑤の入力値、自動計算の中間値を含む) |
| 空行 | セクション区切り |
| 後続 | 月別採用計画テーブル(年月・採用人数・累計採用人数・期待人員数・備考) |
setValues() で一括書き込みする。ヘッダー行は新規作成時のみ書き込む(既存シートには 2 行目以降のクリア後に上書きされる)。
HeadcountRepository の新設
200_data/202_repository.js の末尾に以下を追記する。
// =====================================================================
// HeadcountRepository — 22_bud_headcount (読み取り専用)
// =====================================================================
var HeadcountRepository = {
/** @private */
_getSheet: function() {
return Utils.getSheetByKey('BUD_HC', '22_bud_headcount');
},
/**
* 全人員計画レコードを DTO 配列で取得する。
* @returns {{ headers: string[], dtos: Object[] }}
*/
findAll: function() {
return readSheetAsDtos_(HeadcountRepository._getSheet());
},
};
※ システムキー BUD_HC は 100_config/101_sys_config.js L776 で 01_sys_config に登録済みであることを Phase 1-3d で確認済み。未登録環境でも第2引数のフォールバック名 22_bud_headcount で取得できる。
MENU_DEFINITION へのメニュー追加
000_infra/002_constants.js L229-239 の「📋 サイドバー: 📊 マート更新」カテゴリ(source: 'sidebar')の items 配列末尾に以下を追加する。
{ label: '🧮 人員計画シミュレーション', funcName: 'openHeadcountSimulation', description: '目標月次売上から必要採用人数・採用開始月を逆算するダイアログを表示' },
openHeadcountSimulation はシミュレーションロジック GAS ファイルで定義し、HtmlService.createHtmlOutputFromFile('SimHeadcount').setWidth(480).setHeight(640) を SpreadsheetApp.getUi().showModalDialog(...) で表示する。
影響範囲
| ファイル | 変更内容 | 変更量 |
|---|---|---|
000_infra/002_constants.js | MENU_DEFINITION 「📋 サイドバー: 📊 マート更新」に 1 項目追加 | +1行 |
200_data/202_repository.js | HeadcountRepository を末尾に新設 | +約15行 |
SimHeadcount.html | HTMLサービスダイアログUI 新規作成 | 新規 |
| シミュレーションロジックGASファイル | openHeadcountSimulation + calcDefaultRevPerHead + runHeadcountSimulation 等の関数群 | 新規(配置先は「人間が検討すべき事項」で決定) |
94_sim_headcount シート | 実行時に動的生成(DDL setupAllSchemas の管理外) | — |
既存動作への影響: 新機能のため既存処理への副作用なし。JournalRepository / HeadcountRepository は読み取りのみで副作用なし。22_bud_headcount 本体は変更しない。
注意事項
JournalRepository.findAll()の戻り値は{ headers, dtos }オブジェクト。必ず.dtosプロパティを介してフィルタすること(result.filter(...)はTypeError: result.filter is not a functionになる)。202_repository.jsL259-272 を参照。HeadcountRepository._getSheet()のシステムキーは'BUD_HC'を使用する(100_config/101_sys_config.jsL776 で01_sys_configに登録済み)。Utils.getSheetByKeyはキー未登録時に第2引数'22_bud_headcount'をフォールバック名として使用する(004_utils.jsL40-50)。22_bud_headcountへの自動反映はスコープ外。Human-in-the-Loop の原則に従い、シミュレーション結果のレビューは人間が行い、必要に応じて手動で22_bud_headcountに転記する運用とする。94_sim_headcountは DDL(setupAllSchemas)の管理外。実行時に動的作成し、CLAUDE.mdの「DDL (setupAllSchemas) で管理されないタブ」一覧に将来追記することを「人間が検討すべき事項」に記載する。- 数値パースはすべて
Utils.parseAmt()を通す(カンマ区切り・全角数字・空欄対応)。 - 列参照はヘッダー名ベース(
Contracts.toDto経由で DTO プロパティとしてアクセス、またはheaders.indexOf(列名))。列番号ハードコード禁止(CLAUDE.md「データアクセス」コーディング規約)。 - 有効フラグ=FALSE の行は全処理でスキップ(CLAUDE.md コーディング規約)。判定は
flag === false || String(flag).toUpperCase() === 'FALSE'のパターンに統一する(AccountRepository.findAsMap,PartnerRepository.findPaymentTermsMap等と同一)。 - メニュー追加は
002_constants.jsのMENU_DEFINITION。101_sys_config.jsのonOpen()はMENU_DEFINITIONを動的にループして生成するだけなので、onOpen()本体は変更しない。 42_trn_journalの金額列名は実装時に確認。本仕様では便宜的にdto['金額']と記載しているが、実際の列名(例:金額_税抜/金額_税込/借方金額/貸方金額)は実装開始前に202_repository.jsのContracts定義または42_trn_journalのヘッダー行を Read で確認すること。売上(収入)の月次合計として正しい列を選ぶ。
エッジケース
| 条件 | 表示値・動作 | 理由 |
|---|---|---|
| 一人当たり月次売上高 = 0 または入力が空 | ダイアログに「一人当たり月次売上高が 0 です。入力値を確認してください」を表示し処理中断 | ゼロ除算防止 |
| 過去12ヶ月の売上実績または人員データが不足(売上件数=0 または 人員データ=0) | ダイアログの自動計算フィールドを空欄表示し「データ不足のため手動入力してください」メッセージを添える。処理は中断せず手動入力で続行可能 | データなし時のフォールバック |
| 必要採用人数の計算結果 < 0 | 出力値 = 0。備考欄に「現有人員で目標達成可能(人員余剰の可能性あり)」と記載 | マイナス採用人数は存在しない |
| 目標人員数が小数(例: 5.5 人) | Math.ceil で切り上げ(6 人)。切り上げ前の計算値(小数)も出力行の参考値列に併記 | 安全側(多め)に倒す |
| 採用人数の月別均等配分に端数が出る(例: 7 人を 3 ヶ月に分ける) | Math.floor(7/3) = 2 を各月に、余り 1 を最終月に加算 → 2, 2, 3 | 配分合計数の整合性を確保 |
| 月次退職率 = 0(退職なし想定) | 正常計算(Math.pow(1-0, N) = 1 → 期待残存人員 = 現在有効人員数) | 境界値として正常動作することを明示 |
| 月次退職率 = 1(全員退職想定) | Math.pow(0, N) = 0 → 期待残存人員 = 0 → 必要採用人数 = 目標人員数 | 境界値の正常動作 |
| 採用リードタイム > 目標年月までの残月数 | 採用活動開始月が現在月より過去になる。結果は出力するが、出力シートの備考欄に「採用活動開始月が過去です。リードタイムの見直しを推奨」と警告メッセージを記載 | 非現実的な計画を人間が認識できるようにする |
22_bud_headcount に有効レコードが存在しない(現在有効人員数 = 0) | 必要採用人数 = 目標人員数(全員採用)として計算。備考欄に「既存人員なし(全員新規採用として計算)」と記載 | ゼロ乗算は 0 × ... = 0 で正常動作するが、意図を明確化 |
目標年月が過去月(例: 2026-01 を 2026-04 に入力) | 警告ダイアログ「目標年月が過去です。未来の年月を指定してください」を表示し処理中断 | シミュレーションの前提を崩す入力を防止 |
42_trn_journal が未生成 or 行数 0(新規プロジェクト環境) | 売上実績 = 0 件として扱い、一人当たり月次売上高の自動計算結果は空欄。「データ不足」メッセージを表示 | データなし時のフォールバック(上記の「データ不足」ケースに統合) |
実データ検証
Phase 1 で確定済みの事項
| 確認項目 | 結果 | 出典 |
|---|---|---|
22_bud_headcount の在籍期間を示す列名 | 開始年月 (J列=10) と 終了年月 (K列=11) を使用。退職年月 (I列=9) は参考情報のため使用しない | docs/spec/spec_rpa_hc.md L45-49, 100_config/101_sys_config.js L1142, 400_domain/401_rpa_hc.js L29 |
22_bud_headcount の 終了年月 が空欄のレコードの扱い | 空 = 無期限(在籍中)として扱う | docs/spec/spec_rpa_hc.md L49 |
22_bud_headcount のシステムキー | 'BUD_HC' | 100_config/101_sys_config.js L776 |
42_trn_journal の 収支区分 列の値 | '収入' / '支出'(売上 = '収入' を使用) | docs/arch/frd_financial_logic.md, 既存実装 |
| 売上系科目名 | '売上高' (411) を基本とし、細分類として '商品売上高' / 'コンサルティング売上高' / '講師・研修売上高' / '受託開発売上高' / 'ライセンス・SaaS売上高' / '保守・サポート売上高' / 'その他売上高' (810-819) が存在 | docs/master/mst_account.md L193-204 |
実装前に確認すること
| 確認項目 | 確認方法 | 理由 |
|---|---|---|
42_trn_journal の「金額」を表す列名 | 202_repository.js の Contracts.JournalEntryDTO 定義を Read、または MCP で 42_trn_journal のヘッダー行を取得 | 本仕様書では便宜的に dto['金額'] としているが、実際の列名(金額_税抜 / 借方金額 / 貸方金額 等)を正確に合わせる必要がある |
22_bud_headcount のヘッダー行(全列) | MCP で実データのヘッダー行を取得 | Contracts.toDto で正しくプロパティ名がマップされることを確認 |
| 売上科目フィルタの妥当範囲 | 「人間が検討すべき事項」で方針決定後に実装 | '売上高' のみにするか、AccountRepository.findAsMap() で stmt='P/L' && cat='売上' の全科目を動的取得するか |
関連ドキュメント
| ドキュメント | 関連箇所 |
|---|---|
| MAS-094 実績集計の基準年月プルダウン選択 | HTMLサービスダイアログUI 実装パターン、メニュー追加パターン |
| MAS-011 ボトムアップ型What-ifシミュレーション | シミュレーション系機能の姉妹案件 |
| spec_rpa_hc.md | 22_bud_headcount の列定義(開始年月 / 終了年月 / 退職年月 の用途) |
| mst_account.md | 売上系科目(411, 810-819)の一覧 |
| arch_data_model.md | 42_trn_journal / 22_bud_headcount のスタースキーマ上の位置 |
| CLAUDE.md | DDL 管理外タブ一覧、コーディング規約(有効フラグ、ヘッダー名ベース参照) |
人間が検討すべき事項
TODO_future.md から転記: 一人当たり売上高や稼働率の前提値(
docs/_internal/TODO_future.mdL246 の MAS-012 行「人間が検討すべき事項」)シミュレーションロジック GAS ファイルの配置先: 候補は以下の 3 つ。
300_ui/302_ui_simulation.js(UI レイヤーに配置 — ダイアログ起点の機能として自然)600_report/609_sim_headcount.js(レポーティング系の末尾に配置 —94_sim_headcountが 9x 系のシートであることと整合)700_sim/701_sim_headcount.js(新規ディレクトリ「シミュレーション」を600_report/と800_ops/の間に新設 — MAS-005/MAS-010/MAS-011/MAS-013 等の将来シミュレーション案件をここに集約)
本仕様では 方針未確定 とし、実装者が CLAUDE.md の「GAS ファイル番号体系」ルールと将来の MAS-005/MAS-010/MAS-011/MAS-013 拡張計画を踏まえて決定する。現時点では候補 3(新規
700_sim/)が拡張性の観点で有力。94_sim_headcountシートを DDL(setupAllSchemas)に追加するか: 初版では動的生成のみ。本機能が定着して列構成が固まった段階で、100_config/101_sys_config.jsのsetupAllSchemasに追加し、CLAUDE.md の「DDL 管理外タブ一覧」から削除することを検討する。売上科目フィルタの範囲:
- 案 A: ハードコード — Phase 1-4 で確認した
'売上高'/'商品売上高'/'コンサルティング売上高'/'講師・研修売上高'/'受託開発売上高'/'ライセンス・SaaS売上高'/'保守・サポート売上高'/'その他売上高'の 8 科目の配列をConstantsに定義してフィルタに使う - 案 B: 動的取得 —
AccountRepository.findAsMap()でstmt === 'P/L'かつcat === '売上高'/cat.includes('売上高')の科目を動的取得(科目マスタが変わっても自動追従) - 案 B を推奨(CLAUDE.md「会計ロジック」コーディング規約の「キーワード推測による自動分類禁止」に反しない。科目マスタの
大分類を権威とする)。
- 案 A: ハードコード — Phase 1-4 で確認した
採用計画を
22_bud_headcountへ反映する「実行」機能: 本案件スコープ外。将来的に「この計画を予算に反映」ボタンを追加する場合、22_bud_headcountへのappend処理と承認フロー(Human-in-the-Loop)の追加設計が必要。一人当たり月次売上高の自動計算の集計対象期間: 本仕様では直近 12 ヶ月固定だが、直近 3 ヶ月(短期トレンド)/ 直近 6 ヶ月 / 当期全体 等の選択肢を UI で用意するか検討。
月次退職率のデフォルト値
0.01(1%): 業界平均(年率 10-15%)から逆算した値。自社の過去実績から動的に算出する拡張は将来課題(22_bud_headcountの退職年月列を集計すれば可能)。
実装プロンプト(Claude Code 用)
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-012「目標逆算型人員計画シミュレーション」を実装してください。
## 実行前タスク
以下のファイルを Read してから実装を開始すること(Grep で場所を特定してから Read する):
- `100_config/101_sys_config.js` L776: `BUD_HC` システムキーが `01_sys_config` に登録済みであることを確認
- `100_config/101_sys_config.js` L1142: `22_bud_headcount` の列マップ(A〜AN)のコメントで正式な列名を確認
- `000_infra/002_constants.js` L229-239: `MENU_DEFINITION` の「📋 サイドバー: 📊 マート更新」カテゴリ(`source: 'sidebar'`)を確認
- `200_data/202_repository.js` L19-29: `readSheetAsDtos_` の戻り値型
- `200_data/202_repository.js` L259-272: `JournalRepository.findAll()` の戻り値が `{ headers, dtos }` オブジェクトであること
- `200_data/202_repository.js` L304-346: `AccountRepository.findAsMap()` のパターン(有効フラグ判定のデファクトロジック)
- `000_infra/004_utils.js` L40-50: `Utils.getSheetByKey(key, fallbackName)` の挙動
- `000_infra/004_utils.js` L92-99 / L127-132 / L191-198: `parseDateToYm` / `addMonths` / `parseAmt` の引数と戻り値
- `docs/dev/dev_mas-012_headcount_simulation.md` の全文(特にエッジケーステーブル・実データ検証・人間が検討すべき事項)
- MCP または GAS で `42_trn_journal` のヘッダー行を取得し、「金額」を表す正確な列名(`金額_税抜` / `借方金額` / `貸方金額` 等)を確定させる
## 修正対象ファイル
- `200_data/202_repository.js`: `HeadcountRepository` を末尾に追記(既存 Repository は変更しない)
- `000_infra/002_constants.js`: `MENU_DEFINITION` の「📋 サイドバー: 📊 マート更新」カテゴリに 1 項目追加
- `SimHeadcount.html`: 新規作成(HTMLサービスダイアログUI)
- シミュレーションロジックGASファイル: 新規作成(配置先は仕様書「人間が検討すべき事項」で決定した場所。推奨は `700_sim/701_sim_headcount.js`)
## 実装内容
### 1. `HeadcountRepository` の新設(`202_repository.js` 末尾に追記)
以下のコードをそのまま末尾に追記する(`PartnerRepository` / `AccountRepository` の既存パターンを踏襲):
var HeadcountRepository = {
_getSheet: function() {
return Utils.getSheetByKey('BUD_HC', '22_bud_headcount');
},
findAll: function() {
return readSheetAsDtos_(HeadcountRepository._getSheet());
},
};
`save` / `append` メソッドは不要(本案件では読み取り専用)。
### 2. `MENU_DEFINITION` へのメニュー追加(`002_constants.js`)
`MENU_DEFINITION` 配列内の `category: '📋 サイドバー: 📊 マート更新'` エントリ(L229-239 付近)の `items` 配列末尾(`'📸 前年度P/Lスナップショット'` の直後)に以下の 1 行を追加する:
{ label: '🧮 人員計画シミュレーション', funcName: 'openHeadcountSimulation', description: '目標月次売上から必要採用人数・採用開始月を逆算するダイアログを表示' },
既存の項目ラベル・関数名・description は変更しない。
### 3. シミュレーション実行関数の実装(新規GASファイル)
以下の関数を定義する:
- `openHeadcountSimulation()`: `HtmlService.createHtmlOutputFromFile('SimHeadcount').setWidth(480).setHeight(640)` を `SpreadsheetApp.getUi().showModalDialog(...)` で表示
- `calcDefaultRevPerHead()`: 過去 12 ヶ月の売上実績と有効人員数から一人当たり月次売上高のデフォルト値を返す。売上実績または人員データが 0 件の場合は `null` を返す
- `runHeadcountSimulation(params)`: メインのシミュレーション関数。引数は `{ 目標年月, 目標月次売上, 一人当たり月次売上高, 採用リードタイム, 月次退職率 }` オブジェクト
実装の重要ポイント:
- 出力シート取得・冪等化:
var ss = getWebSpreadsheet_();
var simSheet = ss.getSheetByName('94_sim_headcount') || ss.insertSheet('94_sim_headcount');
if (simSheet.getLastRow() > 1) {
simSheet.getRange(2, 1, simSheet.getLastRow() - 1, simSheet.getLastColumn()).clearContent();
}
- `JournalRepository.findAll()` の戻り値は `{ headers, dtos }` オブジェクト。`result.dtos.filter(...)` でフィルタすること。`result.filter(...)` は `TypeError` になる
- `HeadcountRepository.findAll().dtos` も同様に `.dtos` 経由でアクセス
- 日付の月変換は `Utils.parseDateToYm(dto['発生日(P/L計上日)'])` で統一
- 月数差分・採用活動開始月の計算は `Utils.addMonths(ymStr, months)` を使用
- 入力値の数値パースは `Utils.parseAmt(val)` を使用
- 有効フラグ判定は `flag === false || String(flag).toUpperCase() === 'FALSE'` のパターン(`AccountRepository.findAsMap` と同一)
- エッジケース(仕様書のエッジケーステーブル参照)をすべて実装すること。特に:
1. 一人当たり月次売上高 = 0 → 処理中断 + エラーメッセージ(ゼロ除算防止)
2. 必要採用人数 < 0 → 0 に強制 + 「現有人員で目標達成可能(人員余剰の可能性あり)」備考
3. 目標人員数が小数 → `Math.ceil` で切り上げ(切り上げ前の値も参考値として出力)
4. 採用リードタイム > 残月数 → 採用活動開始月が過去になる場合は警告メッセージを備考欄に出力
5. 目標年月が過去 → エラーダイアログで中断
- 出力レイアウト: 1 行目ヘッダー、2 行目〜前提パラメータブロック、空行を挟んで月別採用計画テーブル
### 4. `SimHeadcount.html` の実装
- 入力フォーム: 目標年月(`<input type="month">` or `<input type="text" pattern="\d{4}-\d{2}">`)、目標月次売上(数値)、一人当たり月次売上高(数値)、採用リードタイム(数値、デフォルト 2)、月次退職率(小数、デフォルト 0.01)
- ダイアログ表示時に `google.script.run.withSuccessHandler(cb).calcDefaultRevPerHead()` でデフォルト値を取得し、一人当たり月次売上高フィールドに自動入力する。`null` が返った場合は「データ不足のため手動入力してください」メッセージを表示
- 「実行」ボタン押下時に `google.script.run.withSuccessHandler(onSuccess).withFailureHandler(onError).runHeadcountSimulation(params)` で呼び出し
- 実行完了時は `alert('シミュレーションが完了しました。94_sim_headcount シートを確認してください。')` 等のメッセージ表示後、`google.script.host.close()` でダイアログを閉じる
- エラー時は `onError` でダイアログ内にエラーメッセージを表示(ダイアログは閉じない)
## 制約
- `JournalRepository.findAll()` / `HeadcountRepository.findAll()` の戻り値に直接 `.filter()` を呼ばない。必ず `.dtos` を介すこと
- シミュレーション結果を `22_bud_headcount` に自動書き込みしない(スコープ外)
- 列参照はヘッダー名ベース(`dto['列名']` や `headers.indexOf(列名)`)で行い、列番号ハードコード禁止
- MCP `add_rows` は使わない。GAS の `setValues()` / `appendRow()` で書き込む
- 既存の Repository・`MENU_DEFINITION` の他項目・DDL関連コード(`setupAllSchemas`)を誤って変更しない
- `101_sys_config.js` の `onOpen()` 本体は変更不要(`MENU_DEFINITION` を動的にループするだけなので、`002_constants.js` の変更だけで自動反映される)
- 科目フィルタは仕様書「人間が検討すべき事項」で決定した方針(案 A ハードコード or 案 B `AccountRepository` 経由)に従うこと
## エッジケース
仕様書「エッジケース」テーブルに従って実装すること。
特に以下 3 点は必ず実装する:
1. 一人当たり月次売上高 = 0 → 処理中断 + エラーメッセージ(ゼロ除算防止)
2. 必要採用人数 < 0 → 0 に強制 + 「現有人員で目標達成可能」備考
3. 目標人員数が小数 → `Math.ceil` で切り上げ
## 動作確認
1. `npm run push:dev` で開発用 GAS にデプロイ
2. 開発用スプレッドシートをリロードし、右側の「📋 サイドバー: 📊 マート更新」メニュー(操作パネル経由)に「🧮 人員計画シミュレーション」が表示されることを確認
3. メニュークリック → `SimHeadcount.html` ダイアログが表示されること
4. ダイアログの一人当たり月次売上高フィールドに自動計算値が表示される(または「データ不足」メッセージ)こと
5. 目標年月(例: `2027-04`)・目標月次売上(例: 10,000,000)を入力して「実行」をクリック
6. `94_sim_headcount` シートが作成(または上書き)され、前提パラメータと月別採用計画が出力されること
7. 目標月次売上を低い値(現有人員で達成可能な水準)に設定し、採用人数 = 0 かつ「現有人員で目標達成可能」備考が出力されること
8. 一人当たり月次売上高を 0 に設定してエラーメッセージが表示されること
9. 目標年月に過去月(例: `2026-01`)を入力してエラーダイアログで中断されること
10. 2 回連続実行し、2 回目の実行で前回の結果が上書きされ重複行が発生しないことを確認(冪等性チェック)
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|--------|------|
| 調査・設計(Phase 1相当) | あり | 既存コードの理解・列名・システムキー `BUD_HC`・`MENU_DEFINITION` 挿入位置・売上科目の確定 |
| 実装(Phase 2相当) | なし | 仕様書の書き下しに徹する |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
HeadcountRepository 新設 + MENU_DEFINITION メニュー追加 | Claude Sonnet 4.6 | 既存パターン踏襲・挿入位置の特定のみ |
| HTMLサービスダイアログ + シミュレーションロジック実装 | Claude Opus 4.6 | 複数ファイル横断(HTML/GAS)・UI とバックエンドの設計判断・エッジケース網羅が必要 |
| 動作確認 | ユーザー手動 | GAS エディタでのメニュー操作・ダイアログ入力・シート確認が必要 |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-21 | 初版作成 |
仕様書作成プロンプト
展開して表示
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
1. **拡張思考の使い分け**: Phase 1(設計)では拡張思考をフル活用し、ファイル名形式・エッジケース一覧・Step 分割粒度・固有名詞(関数名/シート名/列名/行番号)を完全に確定させる。Phase 2(清書)の各 Step 内では拡張思考を最小限に抑え、Phase 1 で確定済みの内容の書き下しに徹する。出力途中で再考しない。
2. **テキスト報告の禁止**: 「〜を作成します」等の text のみで tool_use なしに turn を終了しない。説明は 1 文以内。直ちに tool を呼ぶ。
3. **4-5 分割の Write/Edit 実行**: 仕様書作成は以下の Step に分けて実行する:
- 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)のシニア開発者兼仕様書ライターです。
案件 MAS-012「目標逆算型人員計画シミュレーション」の開発仕様書を作成してください。
開発仕様書を新規作成した場合は、`docs/_config.json` の `nav` 配列の適切なセクションにも必ず追記してください。
---
## Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)
以下の順序で Read/MCP を実行し、Phase 2 で設計判断を再考しなくて済む状態を作ること。
**Grep は「どこにあるか」の発見まで。「どう書くか」の判断は必ず Read で裏取りすること。**
### 1-1. 案件定義の取得
`docs/_internal/TODO_future.md` を Grep し MAS-012 行を特定 → Read で「概要」「人間が検討すべき事項」を把握する。
### 1-2. フォーマット参照テンプレートの確認
`docs/dev/dev_mas-094_boundary_month_selector.md`(UI・メニュー系新機能)を Read し、セクション構成・実装プロンプトの行頭4スペース形式を確認する。
### 1-3. 既存コードの調査(各ファイルを Read すること)
**a. `000_infra/002_constants.js`**
- `SHEET_DEFAULTS` 配列内の `22_bud_headcount` エントリ全体を Read し、フィールド名・型・`_dynamic` 設定を把握する。
- `ID_PREFIX_MAP` 内の `22_bud_headcount` エントリ(`prefix: 'EMP_'` と `digit` を確認)。
**b. `000_infra/004_utils.js`**
- `parseDateToYm(val)` の引数型(Date/string 両対応)と戻り値(`"YYYY-MM"` 文字列)を確認。
- `addMonths(ymStr, months)` の引数と戻り値を確認。
- `parseAmt(val)` の引数と戻り値を確認。
**c. `200_data/202_repository.js`**
- `readSheetAsDtos_(sheet)` の戻り値の型(`{ headers: string[], dtos: Object[] }`)を確認。
- `JournalRepository.findAll()` の戻り値の型を確認する。**戻り値は配列ではなく `{ headers: string[], dtos: JournalEntryDTO[] }` オブジェクトであり、フィルタは `result.dtos.filter(...)` で行う**ことを行番号まで確認して把握する。
- `writeDtosToSheet_(sheet, headers, dtos)` と `appendDtosToSheet_(sheet, headers, dtos, lastRowCol)` の関数シグネチャと「ヘッダー行を保持したままデータ行のみ操作する」動作を確認する。
- 既存 `AccountRepository._getSheet()` での `Utils.getSheetByKey(key, fallbackName)` 呼び出しパターン(第1引数のシステムキー命名規則)を確認する。
**d. `100_config/101_sys_config.js`**
- `onOpen()` 関数全体を Read し、**実在するメニューラベルの文字列(絵文字を含む正確な文字列)と行番号**を確認する。シミュレーション機能を追加するサブメニューの挿入位置を特定する。
- `Utils.getSheetByKey` に渡している既存システムキー(例: `'WRK_INVC'`、`'TRN_JOUR'`)の命名規則を確認し、`22_bud_headcount` に対応するシステムキーが `01_sys_config` シートに登録されているかを特定する。**未登録の場合は「システムキー未登録 → fallback名 `'22_bud_headcount'` を使用する」と判定する**。
### 1-4. MCP による実データ検証
- **`22_bud_headcount` のヘッダー行を取得**: 有効フラグ列名・`開始年月`・`適用年度`・退職または終了を示す列(`退職年月` / `終了年月` 等)が存在するかを確認する。これらの列名は「現在有効人員数」の算出ロジックに直接影響するため、仕様書に実名を明記する。列名が取得できない場合は「要確認(実装前にMCPで確認のこと)」と仕様書に記載する。
- **`42_trn_journal` の `科目名` 列と `収支区分` 列を確認**: 売上系科目名の実際の値(`'売上高'` のみか複数種類あるか)と、`収支区分` 列の実際の値(`'収入'` / `'支出'`)を確認する。
---
## Phase 2: 仕様書の分割作成
出力先: `docs/dev/dev_mas-012_headcount_simulation.md`
**Phase 1 の調査結果で固有名詞(関数名・シート名・列名・行番号・メニューラベル)をすべて確定させてから各 Step を書き下すこと。**
### Step 2-1: 骨格の作成(File Write, ~20行)
全セクション見出しのみのスケルトンファイルを作成する。以下の見出しをすべて含めること:
`# MAS-012: 目標逆算型人員計画シミュレーション`
セクション: `## 概要`, `## 目的`, `## 関連既存コード`, `## 修正方針`, `## 影響範囲`, `## 注意事項`, `## エッジケース`, `## 実データ検証`, `## 関連ドキュメント`, `## 人間が検討すべき事項`, `## 実装プロンプト(Claude Code 用)`, `## 推奨実行モデル`, `## 変更履歴`, `## 仕様書作成プロンプト`
### Step 2-2: 概要〜注意事項の記述(Edit/Bash, ~300行)
Phase 1 で確認した固有名詞のみを使用して以下を記述する:
**「概要」テーブル**: 案件ID=MAS-012、カテゴリ=FP&A・シミュレーション、対象ファイル=`100_config/101_sys_config.js`(メニュー追加)・`200_data/202_repository.js`(`HeadcountRepository` 新設)・`SimHeadcount.html`(新規HTMLダイアログ)・シミュレーションロジックGASファイル(新規)
**「目的」**: 目標月次売上と一人当たり売上高から必要採用人数・採用開始月を逆算し、`94_sim_headcount` シートに出力する。Human-in-the-Loop により、結果のレビューは人間が行う。
**「関連既存コード」**(新機能のため「現在のコード」はなし。関連既存実装を列挙):
- `JournalRepository.findAll()` — 売上実績取得に使用。戻り値は `{ headers: string[], dtos: JournalEntryDTO[] }` であり、`result.dtos.filter(...)` でフィルタする(`202_repository.js` 行番号を明記)
- `readSheetAsDtos_(sheet)` — `HeadcountRepository` 新設時の実装パターン
- `Utils.parseDateToYm(val)`, `Utils.addMonths(ymStr, months)`, `Utils.parseAmt(val)` — 日付・金額処理(`004_utils.js` 行番号を明記)
- `onOpen()` — Phase 1-3d で確認したメニュー挿入箇所(行番号を明記)
**「修正方針」**:
アーキテクチャ:
- **UI**: `101_sys_config.js` の `onOpen()` に Phase 1-3d で確認した実在メニューの直後にシミュレーション用サブメニュー項目を追加。`HtmlService.createHtmlOutputFromFile('SimHeadcount')` でダイアログを表示する。HTML ファイルは `SimHeadcount.html` としてプロジェクトルートに配置する。
- **入力パラメータ(ダイアログで受け取る)**:
- ① 目標年月(YYYY-MM 形式の文字列)
- ② 目標月次売上(数値)
- ③ 一人当たり月次売上高(数値、後述の自動計算値をデフォルト表示。ユーザーによる上書き可)
- ④ 採用リードタイム(月数、デフォルト 2)
- ⑤ 既存人員の月次退職率(小数、デフォルト 0.01 = 1%)
- **出力シート**: `94_sim_headcount`。取得は `var simSheet = ss.getSheetByName('94_sim_headcount') || ss.insertSheet('94_sim_headcount');` で行い、存在しない場合は動的作成する。新規作成時はヘッダー行を書き込む。
- **冪等性**: 実行のたびに `if (simSheet.getLastRow() > 1) { simSheet.getRange(2, 1, simSheet.getLastRow() - 1, simSheet.getLastColumn()).clearContent(); }` でデータ行をクリアしてから書き込む。
計算ロジック:
- **一人当たり月次売上高(自動計算)**:
1. `JournalRepository.findAll().dtos` をフィルタ: `収支区分 === '収入'` かつ `科目名` が Phase 1-4 で確認した売上科目名 かつ `Utils.parseDateToYm(dto['発生日(P/L計上日)'])` が過去12ヶ月の YYYY-MM 範囲内
2. 月ごとの売上合計を集計(`Utils.parseDateToYm` でキー化)
3. `HeadcountRepository.findAll().dtos` から月ごとの有効人員数を算出: `有効フラグ !== false && String(有効フラグ).toUpperCase() !== 'FALSE'` かつ `開始年月 <= targetYm` かつ(Phase 1-4 で確認した退職/終了列が空 OR 退職/終了年月 > targetYm)
4. 対象12ヶ月の各月 (月次売上 ÷ 月次有効人員数) の平均値。人員ゼロ月はスキップ
- **目標人員数の算出**: `Math.ceil(目標月次売上 / 一人当たり月次売上高)`
- **必要採用人数**: `Math.max(0, 目標人員数 - Math.floor(現在有効人員数 × Math.pow(1 - 月次退職率, N)))` ※ N = 現在年月から目標年月までの月数。月数の計算には `Utils.addMonths` を使用
- **採用活動開始月**: `Utils.addMonths(目標年月, -採用リードタイム)`
- **採用人数の月別配分**: 均等配分し端数は最終月に加算(`Math.floor` + 最終月加算)
- **出力行レイアウト**: 前提パラメータ(①〜⑤)を先頭行群に記録し、月別シミュレーション結果テーブルを後続行に出力
`HeadcountRepository` の新設(`202_repository.js` の最末尾に追記):
```
var HeadcountRepository = {
_getSheet: function() {
// Phase 1-3d で確認したシステムキーを使用。未登録なら第2引数のフォールバック名で取得
return Utils.getSheetByKey('【Phase1-3dで確認したキー】', '22_bud_headcount');
},
findAll: function() {
return readSheetAsDtos_(HeadcountRepository._getSheet());
},
};
```
※ システムキーは Phase 1-3d の調査結果を使用。未登録の場合は第1引数を `''`(空文字)とし、常にフォールバック名で取得する形にする。
**「影響範囲」**:
| ファイル | 変更内容 | 変更量 |
|---------|---------|:------:|
| `100_config/101_sys_config.js` | `onOpen()` にシミュレーションメニュー追加 | +約5行 |
| `200_data/202_repository.js` | `HeadcountRepository` を末尾に新設 | +約15行 |
| `SimHeadcount.html` | HTMLサービスダイアログUI 新規作成 | 新規 |
| シミュレーションロジックGASファイル | シミュレーション関数の新規作成(配置先は「人間が検討すべき事項」参照) | 新規 |
| `94_sim_headcount` シート | 動的生成(DDL管理外) | — |
**「注意事項」**:
1. `JournalRepository.findAll()` の戻り値は配列ではなく `{ headers, dtos }` オブジェクト。必ず `.dtos` プロパティを介してフィルタすること(`result.filter(...)` は TypeError になる)
2. `HeadcountRepository._getSheet()` のシステムキーは Phase 1-3d で確認済みの値を使用する。`Utils.getSheetByKey` はキー未登録の場合に第2引数のシート名をフォールバックとして使用する(`004_utils.js` の `getSheetByKey` 実装を参照)
3. シミュレーション結果を `22_bud_headcount`(予算シート)へ自動反映する機能は本案件のスコープ外。Human-in-the-Loop の観点から人間がレビューしてから手動転記する運用とする
4. `94_sim_headcount` は DDL(`setupAllSchemas`)の管理外とする。実行時に動的作成し、CLAUDE.md の DDL管理外タブ一覧に将来追記することを「人間が検討すべき事項」に記載する
5. 数値パースはすべて `Utils.parseAmt()` を通すこと(カンマ区切り・全角数字対応)
6. 列参照はヘッダー名ベース(`indexOf` / `Contracts.toDto` 経由)とし、列番号ハードコード禁止
### Step 2-3a: エッジケース〜人間が検討すべき事項の記述(Edit/Bash, ~200行)
(省略 — 仕様書本体の「エッジケース」「実データ検証」「関連ドキュメント」「人間が検討すべき事項」テーブルと同じ内容を指示)
### Step 2-3b: 実装プロンプト〜変更履歴の記述(Edit/Bash, ~250行)
**【注意】実装プロンプトはバッククォートのコードブロック(` ``` `)で囲まず、すべての行を行頭スペース4つのインデントで出力すること。**
(省略 — 仕様書本体の「実装プロンプト」「推奨実行モデル」「変更履歴」と同じ内容を指示)
### Step 2-4: 仕様書作成プロンプトの記録(Edit/Bash)
ファイル末尾に以下の形式で追記する:
```
## 仕様書作成プロンプト
<details><summary>展開して表示</summary>
[この <instruction> タグ内のプロンプト全文をそのまま貼り付け]
</details>
```
---
## Phase 3: `_config.json` への追記・changelog 追記・コミット
### 3-1. `docs/_config.json` への追記(必須)
`nav` 配列の §E.5(FP&A・レポーティング)セクションに以下を追加する。追記後に JSON 構文エラーがないことを確認すること:
```json
{ "file": "dev/dev_mas-012_headcount_simulation.md", "title": "E.5.X MAS-012 目標逆算型人員計画シミュレーション" }
```
(`E.5.X` の連番 X は既存エントリの最大番号 + 1 に修正すること)
### 3-2. `docs/_internal/changelog.md` への追記
ヘッダー直後の先頭行に追記:
```
| 2026-04-20 | [dev_mas-012_headcount_simulation.md](dev_mas-012_headcount_simulation.md) | 初版作成。目標月次売上から採用必要人数を逆算するシミュレーション機能の仕様書 |
```
### 3-3. コミット&プッシュ
```
git add docs/dev/dev_mas-012_headcount_simulation.md docs/_internal/changelog.md docs/_config.json
git commit -m "docs: MAS-012 目標逆算型人員計画シミュレーションの開発仕様書を作成
目標月次売上と一人当たり売上高から採用必要人数を逆算するシミュレーション機能の仕様。
HeadcountRepository 新設・HTMLサービスダイアログ・94_sim_headcount シート出力を含む。"
git push -u origin HEAD
```