MAS-105: 工数入力・PJ原価連携
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-105 |
| カテゴリ | PJ管理 |
| Phase | P2 |
| 優先度 | ★★ |
| 所要時間 | 4-5時間 |
| 実装ステータス | 📝 仕様書段階・実装未着手 (2026-04-28 監査時点) |
| 対象ファイル | 000_infra/002_constants.js(SHEET_DEFAULTS 変更)200_data/202_repository.js(ResourceRepository / HeadcountRepository 新設)100_config/101_sys_config.js(BUD_RSCE DDL ヘッダー変更)400_domain/420_project_profitability.js(労務費算出ロジックの置換)templates/operations_sidebar.html(操作パネルに工数入力ガイドを追加・任意) |
| 前提案件 | MAS-006(PJ別管理会計の共通費配賦・実装済み) |
| 後続案件 | MAS-026(TDABC:時間主導型ABC)・MAS-027(M365監査ログ連携) |
目的
現行の PJ 別損益(78_pj_pl / 79_pj_monthly)は 27_bud_resource(70_bud_resource)の 稼働率 (%) に 22_bud_headcount の月額給与+社保会社負担分を掛けた「概算配賦」で労務費を算出している(400_domain/420_project_profitability.js L73-86)。これでは「8月に 80% の稼働」「9月に 50% の稼働」といった比率表現しか扱えず、以下の管理会計上の要請を満たせない。
- 時間単価(キャパシティレート)の可視化 — 時給換算の原価が経営会議で説明できない。
- MAS-026(TDABC)の前提データ — 「時間単価 × PJ別消費時間」による間接費配賦には実工数が必要。
- 「時間を食って赤字の PJ」の検出 — 売上 A ランクでも工数を消費している PJ を PJ 営業利益で負にマッピングできない。
本案件では、70_bud_resource を 「稼働率 (%)」から「工数 (h)」入力 に切り替え、22_bud_headcount の月額給与+社保料率から算出する 時間単価 × PJ 別工数 で PJ 別人件費を算出して 78_pj_pl に反映する。稼働率ベースの「ざっくり按分」から実工数ベースに進化させることで、MAS-026 / MAS-027 / MAS-039(ABC分析)の土台を築く。
現在のコード
1. 000_infra/002_constants.js L78(SHEET_DEFAULTS の 70_bud_resource エントリ)
{ pattern: '70_bud_resource', prefix: 'RSC_', defaults: { '稼働率(%)': 1.0, _dynamic: { '開始年月': 'nextYm' } } },
稼働率(%)を 1.0(100%)で自動補完。smartAddRow/bulkAssignDefaultsが新規行に適用。_dynamic.開始年月:nextYm(= 翌月YYYY-MM)を自動設定。
2. 100_config/101_sys_config.js L648(BUD_RSCE DDL ヘッダー)
'BUD_RSCE': { headers: ["有効フラグ","要員名","PJ・案件名","対象年月","稼働率(%)"], color: "#e69138" },
3. 000_infra/002_constants.js L76(SHEET_DEFAULTS の 22_bud_headcount エントリの社保料率)
{ pattern: '22_bud_headcount', prefix: 'EMP_',
defaults: { ..., '健保料率': 0.05, '介護保険料率': 0, '厚年料率': 0.0915, '雇用保険料率': 0.0065, '子ども・子育て拠出金率': 0.0036, ..., '月額給与・報酬': 0, ... } },
社会保険料率は個別フィールドとして独立している(22_bud_headcount の DDL でも 健保料率 / 介護保険料率 / 厚年料率 / 雇用保険料率 / 子ども・子育て拠出金率 の 5 列が定義されている — L661)。一方、420_project_profitability.js は 社保会社負担率 という合算済みの単一列(社保会社負担率)を読み取るコードになっている(L44)。
4. 400_domain/420_project_profitability.js の現行労務費ロジック
(B) 人員ごとの月額人件費マップ作成 — L40-60
const idxIn = hcData[0].indexOf('開始年月'), idxOut = hcData[0].indexOf('終了年月'),
idxName = hcData[0].indexOf('氏名・ポジション'), idxSal = hcData[0].indexOf('月額給与・報酬'),
idxSoc = hcData[0].indexOf('社保会社負担率'), idxType = hcData[0].indexOf('雇用形態');
// ...
const totalLaborCost = sal + (empType !== '業務委託' ? Math.round(sal * soc) : 0);
// hcMap[name][ym] = totalLaborCost // 月額人件費(=給与+社保)を月別に保持
(C) 労務費(配賦原価)の集計: Resource × Headcount — L73-86
const idxName = rsData[0].indexOf('要員名'), idxPj = rsData[0].indexOf('PJ・案件名'),
idxYm = rsData[0].indexOf('対象年月'), idxRate = rsData[0].indexOf('稼働率(%)');
// ...
const baseSal = (hcMap[name] && hcMap[name][ym]) ? hcMap[name][ym] : 0;
pjObj[ym].laborCost += Math.round(baseSal * rate);
工数合計(共通費の「工数比」配賦用) — L469-482
if (sheetRsce) {
var rsData = sheetRsce.getDataRange().getValues();
var idxRsPj = rsData[0].indexOf('PJ・案件名'), idxRsRate = rsData[0].indexOf('稼働率(%)');
for (var ri = 1; ri < rsData.length; ri++) {
// ...
if (rpj && dstPjs[rpj]) dstWorkload[rpj] = (dstWorkload[rpj] || 0) + rr;
}
}
ここでも「稼働率」を工数相当として扱っているため、配賦ルール.method === '工数比' でも実質稼働率比と区別がない。
5. 200_data/202_repository.js
ResourceRepository / HeadcountRepository は未実装。420_project_profitability.js が直接 sheet.getDataRange().getValues() と indexOf で列を引く旧パターン。新規 Repository は本案件のスコープ内で追加する。
修正方針
3 Step 構成で実装する。
Step 1 — スキーマ変更・Repository 新設
(1-a) 000_infra/002_constants.js L78(SHEET_DEFAULTS の 70_bud_resource エントリ)
// Before
{ pattern: '70_bud_resource', prefix: 'RSC_', defaults: { '稼働率(%)': 1.0, _dynamic: { '開始年月': 'nextYm' } } },
// After
{ pattern: '70_bud_resource', prefix: 'RSC_', defaults: { '工数(h)': 0, _dynamic: { '対象年月': 'nextYm' } } },
稼働率(%)を工数(h)に置き換え(デフォルト 0h)。_dynamic.開始年月→_dynamic.対象年月に変更。70_bud_resourceの実シートヘッダーでは列名は対象年月(DDL:101_sys_config.jsL648)であり、開始年月は実在しない列名だった(現行の defaults は DDL と齟齬があった)。
(1-b) 100_config/101_sys_config.js L648(BUD_RSCE DDL ヘッダー)
// Before
'BUD_RSCE': { headers: ["有効フラグ","要員名","PJ・案件名","対象年月","稼働率(%)"], color: "#e69138" },
// After
'BUD_RSCE': { headers: ["有効フラグ","要員名","PJ・案件名","対象年月","工数(h)"], color: "#e69138" },
既存データに対しては setupAllSchemas() によるヘッダー上書きが走る。既存の稼働率値は 800_ops/809_migration_resource_hours.js(要新設・本案件スコープ外オプション)で「稼働率 × 160h = 初期工数」として推定値を流し込むか、手動で工数入力をやり直す かを PO 判断で決定する(下記「人間が検討すべき事項」参照)。
(1-c) 200_data/202_repository.js への追加(末尾、AccountRepository.resetCache 関数定義の直後、L351 近辺)
OrderRepository(L107-146)と同一パターン。
// =====================================================================
// ResourceRepository — 70_bud_resource
// =====================================================================
var ResourceRepository = {
_getSheet: function() { return Utils.getSheetByKey('BUD_RSCE', '70_bud_resource') || Utils.getSheetByKey('BUD_RSCE', '27_bud_resource'); },
findAll: function() { return readSheetAsDtos_(ResourceRepository._getSheet()); },
save: function(dtos) {
var sheet = ResourceRepository._getSheet();
if (!sheet) return;
var headers = sheet.getRange(1, 1, 1, sheet.getMaxColumns()).getValues()[0].map(function(h){return String(h).trim();});
writeDtosToSheet_(sheet, headers, dtos);
},
append: function(dtos) {
var sheet = ResourceRepository._getSheet();
if (!sheet) return 0;
var headers = sheet.getRange(1, 1, 1, sheet.getMaxColumns()).getValues()[0].map(function(h){return String(h).trim();});
return appendDtosToSheet_(sheet, headers, dtos, 1);
},
};
// =====================================================================
// HeadcountRepository — 22_bud_headcount
// =====================================================================
var HeadcountRepository = {
_getSheet: function() { return Utils.getSheetByKey('BUD_HC', '22_bud_headcount'); },
findAll: function() { return readSheetAsDtos_(HeadcountRepository._getSheet()); },
save: function(dtos) {
var sheet = HeadcountRepository._getSheet();
if (!sheet) return;
var headers = sheet.getRange(1, 1, 1, sheet.getMaxColumns()).getValues()[0].map(function(h){return String(h).trim();});
writeDtosToSheet_(sheet, headers, dtos);
},
append: function(dtos) {
var sheet = HeadcountRepository._getSheet();
if (!sheet) return 0;
var headers = sheet.getRange(1, 1, 1, sheet.getMaxColumns()).getValues()[0].map(function(h){return String(h).trim();});
return appendDtosToSheet_(sheet, headers, dtos, 1);
},
};
_getSheetのシステムキーBUD_RSCE/BUD_HCは01_sys_configに登録済み(101_sys_config.jsL594・L599 で確認済み)。ResourceRepositoryのフォールバック名は70_bud_resourceを優先し、旧タブ名27_bud_resourceも二段フォールバック(現行420_project_profitability.jsL16 と同じ扱い)。
(1-d) 03_sys_params に CFG_MONTHLY_WORKING_HOURS を登録(シート作業)
| パラメータキー | 値 | 備考 |
|---|---|---|
CFG_MONTHLY_WORKING_HOURS | 160 | 月の標準労働時間。時間単価算出の分母。未登録でも Constants.getParam('CFG_MONTHLY_WORKING_HOURS', 160) のデフォルト 160h が効くため、登録は任意 |
Step 2 — 時間単価算出ロジックの実装
400_domain/420_project_profitability.js L40-60 の hcMap 構築ロジックを、HeadcountRepository を使った DTO ベースに置き換え、時間単価(円/h)を月別に算出するマップとして再定義する。
// 新規ヘルパー(ファイル末尾 L869 以降に追加)
function calcHourlyRate_(empDto, workingHours) {
var sal = Utils.parseAmt(empDto['月額給与・報酬']);
if (!sal || sal <= 0) return 0;
if (!workingHours || workingHours <= 0) return 0;
var empType = String(empDto['雇用形態'] || '').trim();
// 業務委託は社保対象外
if (empType === '業務委託') return Math.round(sal / workingHours);
var healthRate = Number(empDto['健保料率']) || 0;
var careRate = Number(empDto['介護保険料率']) || 0;
var pensionRate = Number(empDto['厚年料率']) || 0;
var employRate = Number(empDto['雇用保険料率']) || 0;
var childRate = Number(empDto['子ども・子育て拠出金率']) || 0;
var socialRate = healthRate + careRate + pensionRate + employRate + childRate;
var totalLaborCost = sal + Math.round(sal * socialRate);
return Math.round(totalLaborCost / workingHours);
}
- 分子
月額給与・報酬 + 会社負担社会保険料合計。社保合算は既存の DDL で定義された 5 料率を合計。 - 分母
CFG_MONTHLY_WORKING_HOURS(Constants.getParam('CFG_MONTHLY_WORKING_HOURS', 160))。 - 業務委託(
雇用形態 === '業務委託')は社保ゼロのまま単価計算(旧コード L47 の分岐を保持)。
人員×月の時間単価マップ構築(L40-60 を置換)
var workingHours = Constants.getParam('CFG_MONTHLY_WORKING_HOURS', 160);
var hcRateMap = {}; // [name][ym] = 時間単価 (円/h)
var hcResult = HeadcountRepository.findAll();
for (var i = 0; i < hcResult.dtos.length; i++) {
var e = hcResult.dtos[i];
var flag = e['有効フラグ'];
if (flag === false || String(flag).toUpperCase() === 'FALSE') continue;
var name = String(e['氏名・ポジション'] || '').trim();
if (!name) continue;
var rate = calcHourlyRate_(e, workingHours);
if (rate <= 0) continue;
var curYm = Utils.parseDateToYm(e['開始年月']);
var endYm = Utils.parseDateToYm(e['終了年月']) || '2099-12';
if (!curYm) continue;
if (!hcRateMap[name]) hcRateMap[name] = {};
var limit = 0;
while (curYm <= endYm && curYm <= targetMonths[11] && limit < Constants.MONTH_ITERATION_LIMIT) {
hcRateMap[name][curYm] = rate;
curYm = Utils.addMonths(curYm, 1);
limit++;
}
}
Step 3 — PJ 別人件費計算と 78_pj_pl への書き込み
(3-a) 労務費集計(L73-86 の置換)
// ResourceRepository で DTO 取得
var rsResult = ResourceRepository.findAll();
for (var i = 0; i < rsResult.dtos.length; i++) {
var r = rsResult.dtos[i];
var flag = r['有効フラグ'];
if (flag === false || String(flag).toUpperCase() === 'FALSE') continue;
var name = String(r['要員名'] || '').trim();
var pjObj = getPj(r['PJ・案件名']);
var hours = Math.max(0, Utils.parseAmt(r['工数(h)'])); // マイナス値は 0 扱い
var ym = Utils.parseDateToYm(r['対象年月']);
if (!name || !pjObj || hours === 0 || !ym || !targetMonths.includes(ym)) continue;
var rate = (hcRateMap[name] && hcRateMap[name][ym]) ? hcRateMap[name][ym] : 0;
if (rate === 0) {
Utils.logInfo('buildProjectProfitability', '時間単価=0のためスキップ: ' + name + '/' + ym);
continue;
}
pjObj[ym].laborCost += Math.round(rate * hours);
}
- 同一従業員 × 同一月 × 同一 PJ の複数行は
+=で 工数の合算(Repository ループ内で自然に合算)。 - 存在しない PJ コード/マスタ未登録従業員は
pjObj/rateが 0 でスキップ、Utils.logInfoで警告。
(3-b) 共通費配賦「工数比」の分母を工数合計に置換(L469-482 の置換)
var dstWorkload = {};
var rsResult2 = ResourceRepository.findAll(); // 必要なら上段と共有化
for (var ri = 0; ri < rsResult2.dtos.length; ri++) {
var r2 = rsResult2.dtos[ri];
if (r2['有効フラグ'] === false || String(r2['有効フラグ']).toUpperCase() === 'FALSE') continue;
var rpj = String(r2['PJ・案件名'] || '').trim();
var rhr = Math.max(0, Utils.parseAmt(r2['工数(h)']));
if (rpj && dstPjs[rpj]) dstWorkload[rpj] = (dstWorkload[rpj] || 0) + rhr;
}
'工数比' メソッドの配賦ロジック(L521)は変更不要(分母 totalDstWorkload の意味が稼働率合計から工数合計に変わるだけで、比率の意味は保たれる)。
(3-c) 78_pj_pl への冪等性書き込み
現行コードは sheet78.clear() → setValues() の 全置換(L703)で、対象年月以外も含めて毎回丸ごと再生成する方式。これを維持する(シート丸ごと再描画は既存の Human-in-the-Loop ポリシー — 生成 → 目視確認 → 必要なら手動編集 — と整合)。
(3-d) 77_pj_raw のラベル更新(任意)
L460 の配賦ラベル '労務費(工数)' は既存と同じ表記で良いが、L459 の科目表示名 '🤖労務費(工数配賦)' を '🤖労務費(時間単価×工数)' に変更すると、ロジック変更が画面から読み取れて良い(変更は任意。既存フォーマット尊重ならスキップ)。
Human-in-the-Loop ポリシー(PO 判断ポイント)
計算結果を 78_pj_pl に直接書き込むか、中間シート 78_pj_pl_draft 経由で人間が承認してから本番反映する 2 段階フローにするかは PO 判断。本仕様書では 直接書き込みをデフォルト とし、後から 2 段階フローへ差し替えられるよう buildProjectProfitability() の末尾 sheet78.clear() → setValues() 部分を独立関数 write78PjPl_(plOut) に抽出することだけを推奨する(今回のスコープでは関数抽出まで。実際の 2 段階フローは別案件)。
影響範囲
| ファイル | 変更種別 | 変更量 |
|---|---|---|
000_infra/002_constants.js | SHEET_DEFAULTS L78 のエントリ 1 行差し替え | ±1 行 |
100_config/101_sys_config.js | BUD_RSCE DDL ヘッダー L648 の 1 語差し替え | ±1 行 |
200_data/202_repository.js | ResourceRepository / HeadcountRepository を末尾に 2 ブロック追加 | +50 行 |
400_domain/420_project_profitability.js | L40-86(労務費算出)・L469-482(工数集計)を置換、calcHourlyRate_ をファイル末尾に追加 | ±80 行 |
templates/operations_sidebar.html | L62-66 「📊 マート更新」に「工数単価算出(プレビュー)」ボタン追加は任意 | 0-3 行 |
03_sys_params | CFG_MONTHLY_WORKING_HOURS = 160 を 1 行追加(任意・シート直接編集) | 0-1 行 |
注意事項
Constants.SHEET_DEFAULTSの70_bud_resourceエントリ変更はsmartAddRow/bulkAssignDefaultsの既存挙動に影響する。稼働率(%)のデフォルトが消えるので、旧クライアントから新規行を追加すると工数(h)が 0 で入る(意図通り)。一方、旧シートにはまだ稼働率(%)列が物理的に残るため、setupAllSchemas()でヘッダーを上書きする手順を忘れないこと。DDL 再実行前に、既存データのバックアップ(MAS-201スプレッドシート定期バックアップ)を取得してから進める。78_pj_plは DDL (setupAllSchemas) で管理されない動的生成タブ(CLAUDE.md 「DDL で管理されないタブ」セクション)。ヘッダーや列数はbuildProjectProfitability()が毎回動的に決めているため、本案件で列幅や列順を変えても DDL 側の変更は不要。ただし77_pj_rawのカラムを追加する場合は L753 のヘッダー配列と行データ両方を変更する必要がある。列参照はヘッダー名ベース (
indexOf/Contracts.toDto)。列番号ハードコード禁止(CLAUDE.md コーディング規約)。本案件で DTO 方式に移行するため、indexOf('工数(h)')が-1を返さないよう、ヘッダー定義の変更順序を 「DDL → 実シートのsetupAllSchemas()再実行 → コード差し替え →npm run push:dev」 の順で行う。有効フラグ = FALSE の行は全処理でスキップ(CLAUDE.md 規約)。Repository 経由で DTO を取得した後、必ず
if (dto['有効フラグ'] === false || String(dto['有効フラグ']).toUpperCase() === 'FALSE') continue;を入れること。22_bud_headcountの社保会社負担率列(合算列)は420_project_profitability.jsL44 の既存ロジックが参照しているが、DDL(L661)とSHEET_DEFAULTSには存在しない。つまり旧コードは実シートに手動で追加された派生列を参照していた可能性が高い。本案件では 5 料率合算方式に切り替えるため、派生列社保会社負担率への依存を完全に除去し、calcHourlyRate_が 5 料率を個別に読むよう変更する。これにより DDL と実データの齟齬が解消される。MONTH_ITERATION_LIMIT(Constants.MONTH_ITERATION_LIMIT = 120)を使うこと。旧コード L55 のlimit < 120ハードコードは新規コードでは定数参照に統一する。Repository 追加順序に注意。
ResourceRepository/HeadcountRepositoryは202_repository.jsに追加するため、ファイル読込順で420_project_profitability.jsより前に評価される(200 < 420)。この順序を変えない限り、420_側からResourceRepository.findAll()呼び出しは問題なく動作する。
エッジケース
| # | 条件 | 動作 | 理由 |
|---|---|---|---|
| E01 | CFG_MONTHLY_WORKING_HOURS が 0 または未設定(Constants.getParam のデフォルト 160 でも満たされない異常値) | calcHourlyRate_ は 0 を返す。全 PJ の労務費 0 で計算継続。Utils.logInfo('buildProjectProfitability', 'CFG_MONTHLY_WORKING_HOURS が 0 または未設定のため時間単価 = 0') | ゼロ除算防止。運用ミスが全社停止を招かない防御策 |
| E02 | 従業員の 月額給与・報酬 が 0(無報酬役員等) | calcHourlyRate_ は 0 を返す。その従業員の工数は 0 円で計算される | 無報酬役員や未支給月の正常ケース |
| E03 | 70_bud_resource の 要員名 が 22_bud_headcount に存在しない | hcRateMap[name] が undefined → 労務費 0。Utils.logInfo で警告 | マスタ不整合の検知と処理継続の両立 |
| E04 | 存在しない PJ・案件名 が入力された | getPj() が null / projMaster に存在しない PJ をスキップ。Utils.logInfo で警告 | PJ マスタ不整合の検知 |
| E05 | 工数(h) にマイナス値が入力された | Math.max(0, Utils.parseAmt(...)) で 0 クランプ。計算継続 | 不正値の防御的処理(集計を負に歪めない) |
| E06 | 特定月の従業員合計工数が CFG_MONTHLY_WORKING_HOURS × 2(= 320h)を超過 | 計算は実行するが Utils.logInfo で超過警告を出力 | 入力ミスの早期検知(過剰工数の兆候) |
| E07 | 同一従業員 × 同一月 × 同一 PJ のデータが複数行存在(日次/週次で細分入力した場合) | 全行の工数を合算して計算(pjObj[ym].laborCost += Math.round(rate * hours) を行ごとに累積) | 分割入力(日次/週次/月次のハイブリッド)を許容 |
| E08 | 対象年月 が targetMonths(当期 12 ヶ月)の範囲外 | targetMonths.includes(ym) でスキップ | 期外入力を集計から除外し、前期/来期データ混入を防ぐ |
| E09 | 70_bud_resource に 稼働率(%) 列が物理的に残留している(DDL 再実行前) | 新コードは 工数(h) 列しか読まない。稼働率列は無視される(=すべての工数が 0 扱い) | DDL 再実行の必要性を検知させる。setupAllSchemas() を必ず再実行する運用手順 |
| E10 | 雇用形態 === '業務委託' の要員 | 社保ゼロで 時間単価 = 月額 / 工数。社保 5 料率は読まない | 業務委託は社保対象外という旧コード L47 の挙動を保持 |
| E11 | 従業員の 開始年月 が空白 | Utils.parseDateToYm が "" → continue でスキップ | DTO 必須項目の欠損防御 |
| E12 | 終了年月 が空白 | ` | |
| E13 | 配賦区分 === '配賦元' の PJ に工数入力 | getPj() 内で null を返しスキップ(L67) | 配賦元 PJ は 79_pj_monthly の対象外(既存ロジック保持) |
| E14 | buildProjectProfitability() の 2 回連続実行 | 2 回目も同じ結果を全置換で上書き(sheet78.clear() → setValues() は冪等) | 冪等性保証 |
| E15 | 一部の従業員のみ 5 料率 のうち一部が未入力(例: 健保 0.05 厚年 0 雇用 0.0065) | 未入力の料率は 0 として加算(欠損を 0 で扱う)→ 社保合算が過小評価される | 22_bud_headcount の入力不足を検知する責務は 22_bud_headcount 側のバリデーションに委ねる |
| E16 | 同一人物名で 22_bud_headcount に複数行(適用年度が異なる等) | 同一 [name][ym] キーで後勝ちで上書き。注意が必要 | 新旧給与の切り替え月は 開始年月 / 終了年月 で切られるべき。重複月を許容しない運用ルールを推奨 |
| E17 | MONTH_ITERATION_LIMIT (= 120) を超えるレンジ(10 年以上) | ループ上限で打ち切り | 無限ループ防止 |
実データ検証
MCP ツール等で本番/開発スプレッドシートを確認すべき項目:
70_bud_resourceの実列構造 — DDL 定義["有効フラグ","要員名","PJ・案件名","対象年月","稼働率(%)"](101_sys_config.jsL648)と実シートヘッダーの一致を確認。もし実シートに追加列(例:作業内容/備考等)が手動で足されていたら、DDL 再実行時にその列が 保持されるか破棄されるか を確認する(setupAllSchemas()の DDL 適用ポリシーに依存)。22_bud_headcountの社会保険料フィールド名 — DDL(L661)に健保料率 / 介護保険料率 / 厚年料率 / 雇用保険料率 / 子ども・子育て拠出金率が定義されていることと、実シートで各列が全社員分入力されていることを確認。社保会社負担率列の存在有無 — 現行420_project_profitability.jsL44 はidxSoc = hcData[0].indexOf('社保会社負担率')を読んでいる。実シートで派生列として存在する場合、本案件後は参照されなくなるため列の論理削除(= 有効フラグ運用外)を検討。03_sys_paramsにCFG_MONTHLY_WORKING_HOURSキーが存在するか — 存在しない場合は 160 がデフォルト。経営会議等で「標準月次工数」を変更したい場合は、本キーを明示的に登録する必要あり。78_pj_plの列構造 — 人件費セクション'labor'(plSections配列 L546)がそのまま使われることを確認。ラベル'労務費 (工数配賦)'はそのまま。28_bud_allocationの按分方法='工数比'を使っている配賦ルール — 本案件で工数合計の意味が「稼働率の合計」から「工数(h)の合計」に変わるため、工数比を使っている行が既存の配賦ルールに存在する場合、配賦結果の大きさが変わる可能性あり。変更前後の78_pj_plを並べて検証する。
関連ドキュメント
| 仕様書 / ドキュメント | 関連箇所 |
|---|---|
| PRD プロダクトポリシー | Human-in-the-Loop: 自動計算 → 目視確認 → 承認の 3 段階フロー |
| MAS-006 PJ別管理会計の共通費配賦 | 本案件の前提。78_pj_pl の共通費配賦ロジックの全体像 |
| MAS-026 TDABC(時間主導型 ABC) | 本案件の後続案件。時間単価の算出ロジックはそのまま TDABC の入力になる |
| MAS-027 M365 監査ログ連携 | 本案件の代替候補。工数入力を M365 ログから自動推定 |
| ADR A.3 78 vs 79 タブの役割分担 | 78_pj_pl(PJ 横並び P/L)と 79_pj_monthly(PJ 別採算一覧)の責務境界 |
| B.3 統合テスト手順 | 「マート更新テスト(P/L・B/S・CF)」に本案件のテストケースを追加する |
| CLAUDE.md | 変更時の動作確認テスト: 400_domain/420_project_profitability.js 変更 → PJ 別採算テスト |
人間が検討すべき事項
- 工数の入力粒度(日次 / 週次 / 月次) — 本仕様書では月次集計を前提とし、同一 PJ × 同一月の複数行は合算する(E07)。日次入力を許容するなら
日付列を追加する必要があり、スキーマ変更が発生する。 - 入力 UI の設計(シート直接入力 / フォーム / サイドバー) — 現状は
70_bud_resourceシート直接編集。MAS-027(M365 ログから自動推定)との将来統合を見据え、今は「手入力のまま」か「専用フォーム」かを決める。 - 勤怠管理 SaaS との連携(MAS-099 は対象外扱いに再分類済み) — 給与計算 SaaS(freee 人事労務等)経由で勤怠実績を取り込む場合の I/F をどうするか。
- 計算結果の直接書き込み vs 2 段階確認フロー — 本仕様書ではデフォルトで直接書き込み(
78_pj_plを全置換)。Human-in-the-Loop ポリシー(CLAUDE.md・PRD)に照らすと、重要性の高い計算(PJ 営業利益の確定等)は中間シート78_pj_pl_draft経由で人間承認後に本番反映するのが望ましい。PO 判断待ち。 70_bud_resource既存データ(稼働率ベース)のマイグレーション方針 — 稼働率 0.8(80%)の行を「0.8 × 160h = 128h」として推定値を流し込むか、履歴をクリアして手動で工数入力をやり直すか。前者なら800_ops/809_migration_resource_hours.jsを新設(冪等・メニュー登録)する。PO 判断待ち。社保会社負担率派生列の扱い — 実シートで手動運用されている可能性のある社保会社負担率列を、本案件後に削除するか残すか。残す場合は「表示用」と割り切って参照ロジックから完全に切り離す。CFG_MONTHLY_WORKING_HOURSの会社別設定 — 160h は 8h × 20 営業日のラフ値。実際には月によって営業日が 19-23 日で揺れる。月次でCFG_MONTHLY_WORKING_HOURS_2026_04のように動的キーを増やすか、160h 固定で割り切るか。本仕様書は 160h 固定 を推奨(運用簡素化)。
実装プロンプト(Claude Code 用)
Step 1: スキーマ変更・Repository 新設
あなたは GAS 会計システム (bizlp-gas-accounting) のシニア開発者です。
CLI エージェントである「Claude Code」として、以下の指示に従い、案件 MAS-105「工数入力・PJ原価連携」の **Step 1**(スキーマ変更・Repository 新設)を実装してください。
## 実行前タスク
1. `CLAUDE.md` のコーディング規約(列参照ルール・有効フラグ処理・Repository パターン)を確認。
2. `000_infra/002_constants.js` L73-87 の `SHEET_DEFAULTS` の構造を確認。
3. `200_data/202_repository.js` L107-146 の `OrderRepository` の実装パターンを確認。
4. `100_config/101_sys_config.js` L594-L661 の `BUD_RSCE` / `BUD_HC` DDL を確認。
5. 仕様書: `docs/dev/dev_mas-105_workforce_costing.md`
## 修正対象ファイル
- `000_infra/002_constants.js`(`SHEET_DEFAULTS` L78 のエントリ 1 行差し替え)
- `100_config/101_sys_config.js`(`BUD_RSCE` DDL ヘッダー L648 の 1 語差し替え)
- `200_data/202_repository.js`(末尾に `ResourceRepository` / `HeadcountRepository` 2 ブロック追加)
- 他ファイルの変更はしないこと
## 実装内容
1. `002_constants.js` L78 を以下に差し替え:
`{ pattern: '70_bud_resource', prefix: 'RSC_', defaults: { '工数(h)': 0, _dynamic: { '対象年月': 'nextYm' } } },`
2. `101_sys_config.js` L648 の BUD_RSCE ヘッダーを `["有効フラグ","要員名","PJ・案件名","対象年月","工数(h)"]` に変更。
3. `202_repository.js` 末尾の `AccountRepository.resetCache` 関数の直後に、仕様書「Step 1 — (1-c)」の `ResourceRepository` と `HeadcountRepository` を追加する。
- `_getSheet` のキーは `BUD_RSCE` / `BUD_HC`(登録済み)。フォールバック名は `70_bud_resource` / `22_bud_headcount`。
- `findAll` / `save` / `append` の 3 メソッドを既存の `OrderRepository` と同一パターンで実装。
## 制約
- 列番号ハードコード禁止(ヘッダー名ベース)
- 有効フラグ = FALSE のスキップは Repository 利用側で実装(Repository 自体は生データを返す)
- 既存 Repository 関数(`OrderRepository` 等)の改変禁止
- ファイル読込順(200 < 420)に依存した宣言順序を崩さない
## エッジケース
仕様書のエッジケース E01-E17 のうち、Step 1 のスコープ内で対応するのは以下:
- E09: `70_bud_resource` に旧 `稼働率(%)` 列が残っている場合 → `setupAllSchemas()` 再実行で上書き
## 動作確認
1. `npm run push:dev` で開発環境にデプロイ
2. GAS エディタで `setupAllSchemas()` を実行 → `70_bud_resource` のヘッダーが `工数(h)` に変わっていることを確認
3. GAS エディタで `ResourceRepository.findAll()` を試行 → `{ headers, dtos }` が返ることを確認
4. 同様に `HeadcountRepository.findAll()` を試行
## 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---|:---:|---|
| `SHEET_DEFAULTS` の差し替え | なし | 仕様書で完全定義済み |
| DDL ヘッダーの変更 | なし | 仕様書で完全定義済み |
| `Repository` の新設 | あり(軽微) | 既存パターンへの準拠判断のみ |
Step 2: 時間単価算出ロジックの実装
あなたは GAS 会計システム (bizlp-gas-accounting) のシニア開発者です。
案件 MAS-105「工数入力・PJ原価連携」の **Step 2**(時間単価算出ロジック)を実装してください。Step 1 は完了している前提です。
## 実行前タスク
1. `CLAUDE.md`
2. `000_infra/004_utils.js` の `Utils.parseDateToYm` / `Utils.parseAmt` / `Utils.addMonths` / `Utils.logInfo` の仕様を確認
3. `000_infra/002_constants.js` の `Constants.getParam(key, defaultVal)` / `Constants.MONTH_ITERATION_LIMIT` を確認
4. `400_domain/420_project_profitability.js` L40-60 の現行 `hcMap` 構築ロジック(`社保会社負担率` 参照含む)を確認
5. 仕様書: `docs/dev/dev_mas-105_workforce_costing.md`
## 修正対象ファイル
- `400_domain/420_project_profitability.js` のみ
## 実装内容
1. **ファイル末尾 L869 以降に `calcHourlyRate_(empDto, workingHours)` を追加**:
- `月額給与・報酬 <= 0` または `workingHours <= 0` → 0 を返す
- `雇用形態 === '業務委託'` → `Math.round(月額 / workingHours)` を返す(社保ゼロ)
- それ以外 → `社保率 = 健保料率 + 介護保険料率 + 厚年料率 + 雇用保険料率 + 子ども・子育て拠出金率`、`社保額 = Math.round(月額 × 社保率)`、`時間単価 = Math.round((月額 + 社保額) / workingHours)`
2. **L40-60 の `hcMap` 構築ロジックを置換**:
- `HeadcountRepository.findAll()` で DTO 配列を取得
- `有効フラグ` FALSE をスキップ
- `workingHours = Constants.getParam('CFG_MONTHLY_WORKING_HOURS', 160)`
- `calcHourlyRate_(e, workingHours)` で時間単価を算出
- `hcRateMap[name][ym] = rate` を `開始年月` から `終了年月`(または `targetMonths[11]`)まで月単位で展開
- ループ上限は `Constants.MONTH_ITERATION_LIMIT`
3. **既存の `idxSoc` / `社保会社負担率` への参照を完全削除**。DDL に存在しない派生列への依存を断つ。
4. **変数名**: 旧 `hcMap[name][ym] = totalLaborCost`(月額人件費)→ 新 `hcRateMap[name][ym] = hourlyRate`(時間単価)へ変更。Step 3 で `hcRateMap` を参照する。
## 制約
- 列番号ハードコード禁止(DTO のキー名で参照)
- 有効フラグ FALSE の行スキップ必須
- 業務委託分岐(`雇用形態 === '業務委託'` の社保ゼロ)を必ず保持
- 旧 `hcMap` 変数名は **全て `hcRateMap` に改名** (Step 3 で参照を揃えるため)
## エッジケース
仕様書の E01-E02, E10-E12, E15, E17 を満たすこと。特に `CFG_MONTHLY_WORKING_HOURS=0` での `calcHourlyRate_ → 0` の分岐を忘れない。
## 動作確認
1. `npm run push:dev`
2. GAS エディタで `buildProjectProfitability()` を実行(Step 3 未実装のため一時的に工数集計は 0 となるが、エラーなく完走することを確認)
3. `Utils.logInfo` ログで `hcRateMap` の値が月別に展開されていることを確認(`console.log(JSON.stringify(hcRateMap))` をデバッグ目的で一時追加しても良い)
## 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---|:---:|---|
| `calcHourlyRate_` の実装 | あり(軽微) | 5 料率合算の式と業務委託分岐 |
| `hcRateMap` 構築ループの書き換え | あり | 旧 `hcMap` の月別展開ロジックとの対応関係 |
| 派生列 `社保会社負担率` の完全除去 | あり | 互換コードを残さない判断 |
Step 3: PJ 別人件費計算と 78_pj_pl への書き込み
あなたは GAS 会計システム (bizlp-gas-accounting) のシニア開発者です。
案件 MAS-105「工数入力・PJ原価連携」の **Step 3**(PJ 別人件費計算と 78_pj_pl 書き込み)を実装してください。Step 1・2 は完了している前提です。
## 実行前タスク
1. `CLAUDE.md`
2. `000_infra/003_contracts.js` の `Contracts.toDto` / `Contracts.toDtoList` の仕様
3. `400_domain/420_project_profitability.js` の全体構造(特に L73-86 労務費集計、L463-496 工数合計、L536-746 78_pj_pl 書き込み)
4. 仕様書: `docs/dev/dev_mas-105_workforce_costing.md`
## 修正対象ファイル
- `400_domain/420_project_profitability.js` のみ
- `77_pj_raw` のラベル変更は任意(必要なら L459 `'🤖労務費(工数配賦)'` → `'🤖労務費(時間単価×工数)'`)
## 実装内容
1. **L73-86 の労務費集計を DTO ベースに置換**:
- `ResourceRepository.findAll()` で DTO 取得
- `有効フラグ` FALSE をスキップ
- `hours = Math.max(0, Utils.parseAmt(r['工数(h)']))`(マイナス値は 0 クランプ)
- `ym = Utils.parseDateToYm(r['対象年月'])` が `targetMonths` に含まれない場合スキップ
- `rate = hcRateMap[name][ym]` が 0 の場合 `Utils.logInfo` で警告後スキップ
- `pjObj[ym].laborCost += Math.round(rate * hours)` で合算
- 合計工数が `CFG_MONTHLY_WORKING_HOURS × 2` を超える場合 `Utils.logInfo` で警告(E06)
2. **L469-482 の `dstWorkload` 集計を DTO ベースに置換**:
- 同じく `ResourceRepository.findAll()` を再利用(Step 3-(a) で既に取得した結果を変数に保持して使い回しても良い)
- `rhr = Math.max(0, Utils.parseAmt(r2['工数(h)']))`
- `dstPjs[rpj]` 所属のみ `dstWorkload[rpj] += rhr`
3. **`78_pj_pl` は全置換維持**(`sheet78.clear()` → `setValues()`)。冪等性 OK。
4. **(任意・推奨)`78_pj_pl` への書き込み部分を独立関数に抽出**:
- `write78PjPl_(plOut)` を追加(L702-746 を切り出し)
- 将来 2 段階フロー(`78_pj_pl_draft` 経由)への差し替えを容易にする
## 制約
- 列番号ハードコード禁止
- 有効フラグ FALSE の行スキップ必須
- 旧 `rate = Number(row[idxRate])` の稼働率参照は完全削除
- `classifyAcct_` の `'🤖労務費(工数配賦)'` 分岐は、ラベルを変えるなら同時に修正(現状は変えない想定)
## エッジケース
仕様書の E01-E17 を全て満たすこと。特に:
- E03: `hcRateMap[name]` undefined で 0 処理(スキップ時にログ出力)
- E04: `projMaster` に無い PJ は `getPj` で null 返却(既存ロジック)
- E05: `Math.max(0, ...)` での 0 クランプ
- E07: 複数行の累積加算
- E14: 2 回連続実行で同結果(`sheet78.clear()` の冪等性)
## 動作確認
1. `npm run push:dev`
2. `70_bud_resource` に以下を投入:
- 要員 A / PJ_X / 2026-04 / 40h
- 要員 A / PJ_X / 2026-04 / 20h(分割入力テスト)
- 要員 B / PJ_Y / 2026-04 / 160h
3. GAS エディタで `buildProjectProfitability()` を実行
4. `78_pj_pl` を開き、「労務費 (工数配賦)」セクションに以下を確認:
- PJ_X の 2026-04 行: `要員Aの時間単価 × 60h` の値
- PJ_Y の 2026-04 行: `要員Bの時間単価 × 160h` の値
5. 2 回連続実行して同じ値になることを確認(冪等性)
6. 工数 -5h を投入して E05 が発動することを確認
7. 存在しない PJ 名を投入して E04 が発動(`Utils.logInfo` 警告)することを確認
## 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---|:---:|---|
| L73-86 の労務費集計置換 | あり | 旧 `baseSal * rate` → 新 `rate * hours` の意味変換 |
| L469-482 の工数合計置換 | あり | 「工数比」配賦の分母の意味が変わる影響範囲分析 |
| `write78PjPl_` の関数抽出(任意) | あり | 将来の 2 段階フロー拡張を見越した設計判断 |
| エッジケース E01-E17 の実装 | あり | 17 ケース × 具体的な発動条件の特定 |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
| Step 1: スキーマ変更・Repository 新設 | Claude Sonnet | 既存 OrderRepository パターンへの準拠判断が中心。ロジック判断なし |
| Step 2: 時間単価算出ロジック実装 | Claude Sonnet | 会計ロジック(社保合算・業務委託分岐)の理解と既存関数の適用判断 |
| Step 3: PJ 損益シート連携・冪等性実装 | Claude Opus | 複数ファイル横断(420_ / 77_pj_raw / 78_pj_pl)の設計判断、冪等性設計、エッジケース 17 件の網羅実装 |
| 仕様書作成(本ドキュメント) | Claude Opus 4.7 (1M context) | スキーマ変更の全社影響・DTO 変換・HITL ポリシー・MAS-026 / MAS-027 との後続連携を横断的に判断 |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-19 | 初版作成 |
仕様書作成プロンプト
展開して表示
【タイムアウト回避・実行原則(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 1で全固有名詞・行番号・設計判断を確定させ、Phase 2では書き下しのみ行う。
======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 S-33「工数入力・PJ原価連携」の開発仕様書を作成してください。
作成後は `docs/_config.json` の `nav` 配列の適切なセクションにも必ず追記してください。
---
## Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)
### 1-A: 案件定義の読み込み
`docs/_internal/TODO_future.md` を検索し、S-33 の案件名・カテゴリ・Phase・優先度・概要・人間が検討すべき事項を取得する。
### 1-B: プロジェクト規約の読み込み
`CLAUDE.md` を読み込み、コーディング規約(列参照ルール・有効フラグ処理・Repositoryパターン)・ファイル番号体系・テスト手順を把握する。
### 1-C: 既存仕様書テンプレートの読み込み
`docs/dev/dev_mas-093_cf_actual_only.md`(既存パターン横展開)または `docs/dev/dev_mas-001_variance_analysis.md`(新機能)を1件読み込み、フォーマットを把握する。
### 1-D: 関連コードの調査
**読み方のルール(Grep vs Read)**: 仕様書に固有名詞(関数名・シート名・列名・定数名)を書く前は必ず `Read` で裏取りすること。「〜という名前だろう」と推測した瞬間に手を止めてReadする。Grepは「どこにあるか」の発見まで、「どう書くか」の判断は必ずReadで行う。
以下のファイルをこの順番でReadし、確認ポイントを全て把握してから Phase 2 に進むこと:
1. **`000_infra/002_constants.js`**
- `Constants.SHEET_DEFAULTS` 配列内の `70_bud_resource` エントリ: `{ pattern, prefix, defaults }` の型と全フィールド名(現行の `稼働率(%)` や `開始年月` 等)を確認
- `Constants.SHEET_DEFAULTS` 配列内の `22_bud_headcount` エントリ: 社会保険料率フィールド名(`健保料率`・`厚年料率`・`雇用保険料率`・`子ども・子育て拠出金率`・`介護保険料率`等)と `月額給与・報酬` フィールド名を確認
- `Constants.ID_PREFIX_MAP` 内の `70_bud_resource` エントリ(プレフィックス `RSC_` と `isDate` 設定を確認)
- `Constants.getParam(key, defaultVal)` の呼び出しシグネチャを確認(`03_sys_params` からパラメータ取得に使用)
2. **`000_infra/004_utils.js`**
- `Utils.parseDateToYm`・`Utils.parseAmt`・`Utils.logInfo`・`Utils.logError` の引数・戻り値を確認
3. **`000_infra/003_contracts.js`**
- `Contracts.toDto`・`Contracts.toDtoList`・`Contracts.toRow` の使い方を確認
4. **`200_data/202_repository.js`**
- 既存Repository(`OrderRepository` 等)の `findAll` / `save` / `append` のパターン全体を確認し、新設 `ResourceRepository`・`HeadcountRepository` の実装テンプレートとして把握する
- 内部ヘルパー `readSheetAsDtos_`・`writeDtosToSheet_`・`appendDtosToSheet_`・`findLastRow_` の仕様を確認
5. **`400_domain/420_project_profitability.js`**(※`402_`ではなく`420_`が正しいファイル番号)
- 現行の稼働率ベース配賦ロジックの主要関数名・行番号・処理フローを特定する
- `78_pj_pl` シートへの書き込みパターン(列構造・クリア方法)を特定する
6. **`100_config/101_sys_config.js`**
- `onOpen()` のメニュー構造を確認し、工数計算実行メニューを追加する位置を特定する(メニュー名の固有名詞を実在する文字列で引用すること)
- `70_bud_resource` と `22_bud_headcount` のシステムキー(`01_sys_config` シートに登録されているキー)を確認する(`Utils.getSheetByKey` の第1引数として使用)
---
## Phase 2: 仕様書の分割作成
**出力先**: `docs/dev/dev_mas-105_workforce_costing.md`(**S は大文字**)
### Step 2-1: 骨格の作成(File Write、~20行)
以下の見出しのみ。本文は空で可:
# S-33: 工数入力・PJ原価連携
## 概要
## 目的
## 現在のコード
## 修正方針
## 影響範囲
## 注意事項
## エッジケース
## 実データ検証
## 関連ドキュメント
## 人間が検討すべき事項
## 実装プロンプト(Claude Code 用)
## 推奨実行モデル
## 変更履歴
## 仕様書作成プロンプト
### Step 2-2: 概要〜注意事項の追記(File Edit または Bash heredoc、~300行)
Phase 1の調査結果に基づき以下を記述する(固有名詞はReadで確認済みの実在する文字列のみ使用):
- **概要テーブル**: 案件ID=S-33、対象ファイル(Phase 1で特定した実ファイル名を記載)、前提案件等
- **目的**: 稼働率ベース概算配賦から工数ベース実費計算への移行目的を1〜3文で記述
- **現在のコード**: `Constants.SHEET_DEFAULTS` の `70_bud_resource` エントリで確認した実際のフィールド名と `420_project_profitability.js` の現行配賦ロジック関数名・行番号を記載
- **修正方針**: 以下3Stepで記述
- **Step 1: スキーマ変更・Repository新設**
- `002_constants.js` の `Constants.SHEET_DEFAULTS` 内 `70_bud_resource` エントリ変更: `稼働率(%)` を `工数(h)` に変更し `対象年月` フィールドを追加(`{ pattern, prefix, defaults }` 型はReadで確認した構造に従うこと)
- `202_repository.js` に `ResourceRepository`(`70_bud_resource` 用)と `HeadcountRepository`(`22_bud_headcount` 用)を**新設**。既存の `OrderRepository` のパターンに準拠し `findAll` / `save` / `append` の3メソッドを実装。`_getSheet()` のシステムキーはPhase 1で `101_sys_config.js` から確認した実在するキーを使用(未確認の場合はフォールバック名で実装し「要確認」と注記)
- **Step 2: 時間単価算出ロジックの実装**
- `420_project_profitability.js` に `calcHourlyRate_(empDto)` ヘルパー関数を新設
- 算出式: `時間単価 = (月額給与・報酬 + 会社負担社会保険料合計) / CFG_MONTHLY_WORKING_HOURS`
- 会社負担社会保険料合計 = Phase 1で `22_bud_headcount` エントリから確認した各率フィールド(`健保料率`・`厚年料率`・`雇用保険料率`・`子ども・子育て拠出金率`・`介護保険料率`)× `月額給与・報酬` の合算
- `CFG_MONTHLY_WORKING_HOURS` は `Constants.getParam('CFG_MONTHLY_WORKING_HOURS', 160)` で取得(`03_sys_params` から読み込み。未設定時デフォルト 160)
- **Step 3: PJ別人件費計算と `78_pj_pl` への書き込み**
- 計算式: `PJ別人件費 = 時間単価 × PJ別工数(h)`(同一従業員・同一月・同一PJの複数行は工数を合算)
- 冪等性保証: 対象年月の既存データを `78_pj_pl` からクリアしてから新規書き込み(クリア方法はPhase 1で `420_project_profitability.js` を確認した書き込みパターンに従う)
- Human-in-the-Loopポリシー: 計算結果の直接書き込みか中間シート経由の2段階確認フローかはPO判断待ち(現状は直接書き込みをデフォルトとして実装し、後から2段階フローに差し替え可能な設計とする)
- **影響範囲**: `000_infra/002_constants.js`・`200_data/202_repository.js`・`400_domain/420_project_profitability.js`・`100_config/101_sys_config.js`(メニュー追加)の変更量概算
- **注意事項**:
- `Constants.SHEET_DEFAULTS` の `70_bud_resource` エントリ変更は既存の `smartAddRow` / `bulkAssignDefaults` 挙動に影響する。スキーマ変更前に既存データのバックアップとマイグレーション方針(800番台スクリプトの新設要否)を確認すること
- `78_pj_pl` は DDL (setupAllSchemas) で管理されない動的生成タブのため、スキーマ変更の影響範囲を事前確認すること(CLAUDE.md の「DDLで管理されないタブ」参照)
- 列参照はヘッダー名ベース(`indexOf` / `buildHeaderIndex_`)。列番号ハードコード禁止(CLAUDE.md コーディング規約)
- 有効フラグ = FALSE の行は全処理でスキップ(CLAUDE.md 規約)
### Step 2-3a: エッジケース〜人間が検討すべき事項の追記(File Edit または Bash、~200行)
- **エッジケーステーブル**(テーブル形式: 条件 / 動作 / 理由):
| 条件 | 動作 | 理由 |
|------|------|------|
| `CFG_MONTHLY_WORKING_HOURS` が 0 または未設定 | 時間単価 = 0 として計算継続。`Utils.logInfo` で警告出力 | ゼロ除算防止 |
| 従業員の月額給与・報酬が 0 | 時間単価 = 0 として計算継続 | 無報酬役員等の正常ケース |
| `70_bud_resource` の従業員名が `22_bud_headcount` に存在しない | 該当工数を計算対象から除外。`Utils.logInfo` で警告出力 | マスタ不整合の検知 |
| 存在しないPJコードが入力された場合 | 該当工数を計算対象から除外。`Utils.logInfo` で警告出力 | PJマスタ不整合の検知 |
| 工数にマイナス値が入力された場合 | 0 として扱い計算継続 | 不正値の防御的処理 |
| 特定月の従業員合計工数が `CFG_MONTHLY_WORKING_HOURS` × 2 を超過 | 計算は実行するが `Utils.logInfo` で超過警告を出力 | 入力ミスの可能性を通知 |
| 同一従業員・同一月・同一PJのデータが複数行存在 | 全行の工数を合算して計算 | 分割入力を許容 |
- **実データ検証**(MCP等で確認すべき項目):
- `70_bud_resource` の実際の列構造(DDL定義の `稼働率(%)` フィールドと実シートの一致確認)
- `22_bud_headcount` の社会保険料フィールド名(`Constants.SHEET_DEFAULTS` の定義と実シートヘッダーの一致確認)
- `03_sys_params` に `CFG_MONTHLY_WORKING_HOURS` キーが存在するか(存在しない場合は手動追加が必要)
- `78_pj_pl` の列構造(人件費科目行の位置とクリア対象範囲の確認)
- **関連ドキュメント**: Phase 1で発見した関連仕様書・アーキテクチャドキュメントへのリンク
- **人間が検討すべき事項**:
- `TODO_future.md` のS-33から転記した既存の検討事項(工数入力粒度・入力UI・勤怠管理システム連携等)
- 【追加】計算結果の直接書き込みか2段階確認フロー(中間シート出力→人間確認・承認→本番反映)かの選択。Human-in-the-Loopポリシー(CLAUDE.md・PRD)に照らしPO判断が必要
- 【追加】`70_bud_resource` 既存データ(稼働率ベース)のマイグレーション方針。スキーマ変更後の既存データ取り扱いをPOと合意し、必要に応じて `800_ops/` に冪等マイグレーションスクリプトを新設する
### Step 2-3b: 実装プロンプト〜変更履歴の追記(File Edit または Bash、~250行)
**【注意】実装プロンプトはバッククォートのコードブロックで囲まず、行頭4スペースインデントで出力すること。**
以下の内容を記述する:
- **実装プロンプト(Claude Code 用)**: Step分割(Step 1 / Step 2 / Step 3)それぞれを別プロンプトとして記述。各プロンプトには以下を含める:
- 役割定義(シニア開発者として案件S-33 Step Nを実装)
- 実行前タスク(読み込むべきファイルと確認ポイント)
- 修正対象ファイルと変更種別(「のみ」「への追記」を明示)
- 実装内容(具体的手順・コード例)
- 制約(列番号ハードコード禁止・有効フラグFALSEスキップ必須・既存Repository関数の改変禁止等)
- エッジケース(Step 2-3aのテーブルを転記)
- 動作確認(`npm run push:dev` 後の検証手順)
- 拡張思考の使用状況テーブル
- **推奨実行モデルテーブル**:
| 工程 | 推奨モデル | 理由 |
|------|-----------|------|
| Step 1: スキーマ変更・Repository新設 | Claude Sonnet | 既存パターンへの準拠判断が必要 |
| Step 2: 時間単価算出ロジック実装 | Claude Sonnet | 会計ロジックの理解と既存関数の適用判断が必要 |
| Step 3: PJ損益シート連携・冪等性実装 | Claude Opus | 複数ファイル横断の設計判断・冪等性設計が必要 |
- **変更履歴テーブル**: `| 2026-04-19 | 初版作成 |`
### Step 2-4: 仕様書作成プロンプトの記録(File Edit または Bash)
末尾に `<details><summary>展開して表示</summary>` ブロックを設け、この `<instruction>` タグで囲まれたプロンプト全文を記録する。
---
## Phase 3: 保存と記録
### 3-A: `docs/_config.json` への追記(必須)
`docs/_config.json` の `nav` 配列の §E.5(FP&A・レポーティング)に以下を追加する:
{ "file": "dev/dev_mas-105_workforce_costing.md", "title": "E.5.X S-33 工数入力・PJ原価連携" }
追記後、JSON構文エラーがないことを `cat docs/_config.json | python3 -m json.tool` 等で確認する。
### 3-B: changelog への追記
`docs/_internal/changelog.md` の先頭行(ヘッダーの直後)に追記:
| 2026-04-19 | [dev_mas-105_workforce_costing.md](dev_mas-105_workforce_costing.md) | 初版作成。稼働率ベースから工数ベース実費計算への移行仕様。ResourceRepository・HeadcountRepository新設、420_project_profitability.jsのロジック置換。 |
### 3-C: コミット&プッシュ
git add docs/dev/dev_mas-105_workforce_costing.md docs/_internal/changelog.md docs/_config.json
git commit -m "docs: S-33 工数入力・PJ原価連携の開発仕様書を作成
稼働率ベースから工数ベース実費計算への移行仕様。
ResourceRepository・HeadcountRepository新設、420_project_profitability.jsのロジック置換。
https://claude.ai/code/session_XXXXX"
git push -u origin {現在のブランチ}