MAS-136: 51タブ手動調整値の売上計画への加算(MAS-079 派生)
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-136 |
| カテゴリ | パイプライン / FP&A |
| Phase | P2 |
| 優先度 | ★ |
| 所要時間 | 3〜4 時間(Repository 追加 + DTO 定義 + データマート合流) |
| 対象ファイル | 200_data/202_repository.js, 000_infra/003_contracts.js, 600_report/601_datamart_ingest.js(または 602_datamart_main.js の計画合流ブロック) |
| 前提案件 | MAS-078(パイプライン→CF合流。計画合流パイプラインの骨格を流用)、MAS-079(51タブをデータソース化) |
目的
パイプライン(21_bud_pipeline)および予算・INV から自動計算される売上計画に対して、トップダウンでの手動調整を可能にする。営業所感・経営会議での合意値・シミュレーション用のアップサイド/ダウンサイドを 51 タブ(シートキー PL_PLAN_MANUAL)に入力すると、データマート更新時に計画値へ自動で加算/減算され、P/L 計画・B/S 計画・CF 計画に波及する。これにより「パイプラインの確度加重では拾いきれない将来案件」や「経営判断による意図的な上振れ/下振れ」を計画に反映でき、計画策定の柔軟性と Human-in-the-Loop 性を高める。
現在のコード
1. 計画パイプラインの合流ロジック(600_report/602_datamart_main.js:256-284)
// 5b. 計画パイプライン: 32の全INV(未処理含む)→ P/L計画 + B/S計画 + CF計画
if (sheetPlMPlan || sheetPlYPlan || sheetBsPlan || sheetCfPlan || sheetCfPlanYtd) {
const planCtx = { /* …ctx の計画版… */ };
var planData = dmIngestPlanData_(ctx, sheetInv);
// パイプライン(確度加重)を計画に合流
var sheetPipe = ss.getSheetByName('21_bud_pipeline');
var pipeResult = { planData: [], viewData: [] };
if (sheetPipe) {
pipeResult = dmIngestPipelinePlanData_(ctx, sheetPipe);
planData = planData.concat(pipeResult.planData); // ← ここに手動調整値も合流させる
}
planCtx.finalUnionData = planData;
dmProcessAllEvents_(planCtx);
dmCalcPl_(planCtx);
/* … */
}
現状は「INV(32タブ)+パイプライン加重値(21タブ)」の 2 ソースしか合流していない。ここに第 3 ソースとして**手動調整値(51タブ)**を加算する。
2. Repository パターンの既存実装(200_data/202_repository.js:19-29, 107-146, 300-350)
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 };
}
var AccountRepository = {
_getSheet: function() {
return Utils.getSheetByKey('MST_ACCT', '11_mst_account');
},
findAll: function() {
return readSheetAsDtos_(AccountRepository._getSheet());
},
findAsMap: function() { /* 有効フラグ FALSE の行をスキップしつつ Map 化 */ },
};
新設する ManualPlanRepository もこのパターンに準拠し、readSheetAsDtos_ を再利用する。
3. パイプライン加重計算の既存パターン(600_report/601_datamart_ingest.js:363-440)
dmIngestPipelinePlanData_ が ctx.targetMonths の範囲内だけ仮想 INV イベントを生成し、planData.concat() で合流される構造が既にある。手動調整値も同一構造の仮想 INV イベントを返すヘルパー関数を追加し、同じ方式で合流させる。
修正方針
本案件は「51タブの読み込み対応」であり、シート側の書き込み(DDL 生成・自動クリア等)はスコープ外。既存の計画パイプラインに第 3 のデータソースを合流させるだけに留める。以下 4 ステップで実装する。
Step 1: ManualPlanDTO を 003_contracts.js に定義
003_contracts.js の「2. 仕訳帳 / 予算 DTO」セクション末尾(BudgetDTO 直後)に、51 タブの行データを表す DTO の @typedef を追記する。プロパティは 51 タブのシートヘッダー名と完全一致させる(Repository 共通規約)。
/**
* 51_pl_plan_manual — 売上計画 手動調整レコード
* パイプライン加重値 + INV 計画値に対するトップダウンでの調整を表現する。
* 加算方式(プラス値=上振れ、マイナス値=減額)。
* @typedef {Object} ManualPlanDTO
* @property {boolean} 有効フラグ
* @property {string} 科目名 - 11_mst_account に登録された科目名
* @property {Date|string} 対象年月 - "YYYY-MM" または Date 型
* @property {number} 調整金額 - 税抜・加算値(マイナス可)
* @property {string} 摘要 - 調整理由(営業所感、経営判断、シナリオ名等)
* @property {string} [PJ名] - 任意。未指定時は「全社共通」扱い
* @property {string} [組織名] - 任意
*/
注意: DTO のプロパティ名を「調整金額」とし、パイプライン既存列「加重金額(税抜)」「元金額(税抜)」とは別名にする。混同防止および集計ロジックで区別しやすくするため。
Step 2: ManualPlanRepository を 202_repository.js に追加
AccountRepository の直後(ファイル末尾)に以下を追加する。既存ヘルパー readSheetAsDtos_ をそのまま再利用し、車輪の再発明を避ける。Utils.getSheetByKey でシートキー PL_PLAN_MANUAL を解決し、フォールバック名は 51_pl_plan_manual。
// =====================================================================
// ManualPlanRepository — 51_pl_plan_manual(売上計画 手動調整レコード・読み取り専用)
// =====================================================================
var ManualPlanRepository = {
/** @private */
_getSheet: function() {
return Utils.getSheetByKey('PL_PLAN_MANUAL', '51_pl_plan_manual');
},
/**
* 手動調整レコードを正規化済み DTO 配列で取得する。
* - 有効フラグ FALSE 行はスキップ
* - 科目名 or 対象年月 が空の行はスキップ
* - 対象年月は Utils.parseDateToYm で "YYYY-MM" に正規化
* - 調整金額は Utils.parseAmt で数値化
* @returns {{ headers: string[], dtos: ManualPlanDTO[] }}
*/
findAll: function() {
var raw = readSheetAsDtos_(ManualPlanRepository._getSheet());
var normalized = [];
for (var i = 0; i < raw.dtos.length; i++) {
var dto = raw.dtos[i];
var flag = dto['有効フラグ'];
if (flag === false || String(flag).toUpperCase() === 'FALSE') continue;
var acc = String(dto['科目名'] || '').trim();
var ym = Utils.parseDateToYm(dto['対象年月']);
if (!acc || !ym) continue;
dto['科目名'] = acc;
dto['対象年月'] = ym;
dto['調整金額'] = Utils.parseAmt(dto['調整金額']);
normalized.push(dto);
}
return { headers: raw.headers, dtos: normalized };
},
};
Step 3: dmIngestManualPlanData_ を 601_datamart_ingest.js に追加
パイプライン取込の既存関数 dmIngestPipelinePlanData_ と同じ出力形式(仮想 INV イベント配列)を返すヘルパーを追加する。ctx.targetMonths の範囲内のレコードだけを対象とし、科目マスタ未登録の科目名は警告ログを出してスキップする。同一の 科目名 × 対象年月 を持つ行が複数あれば合算する。
/**
* 51_pl_plan_manual の手動調整レコードを仮想 INV イベントに変換する。
* 同一 (科目名, 対象年月) のレコードは合算する。
* 科目マスタ未登録の科目名はスキップ(warn ログ)。
* @param {Object} ctx - データマートコンテキスト
* @returns {Array<Object>} 仮想 INV イベント配列(planData に concat する)
*/
function dmIngestManualPlanData_(ctx) {
var result = ManualPlanRepository.findAll();
var acctMap = AccountRepository.findAsMap();
var firstMonth = ctx.targetMonths[0];
var lastMonth = ctx.targetMonths[ctx.targetMonths.length - 1];
// 合算マップ: key = 科目名 + "|" + 対象年月 → { 科目名, pYm, amount, desc, pj, org }
var bucket = {};
for (var i = 0; i < result.dtos.length; i++) {
var dto = result.dtos[i];
var acc = dto['科目名'];
var ym = dto['対象年月'];
var amt = dto['調整金額'];
if (!acctMap[acc]) {
Utils.logWarn && Utils.logWarn('S-07: 科目マスタ未登録のためスキップ: ' + acc + ' / ' + ym);
continue;
}
if (ym < firstMonth || ym > lastMonth) continue; // 対象期間外
var key = acc + '|' + ym;
if (!bucket[key]) {
bucket[key] = {
科目名: acc,
pYm: ym,
amount: 0,
descs: [],
pj: String(dto['PJ名'] || '').trim() || '全社共通',
org: String(dto['組織名'] || '').trim(),
};
}
bucket[key].amount += amt;
var d = String(dto['摘要'] || '').trim();
if (d) bucket[key].descs.push(d);
}
var events = [];
for (var key in bucket) {
if (!bucket.hasOwnProperty(key)) continue;
var b = bucket[key];
if (b.amount === 0) continue; // 合算結果がゼロならノイズ除去
var mapped = acctMap[b.科目名];
events.push({
pYm: b.pYm,
sYm: b.pYm, // 手動調整は計画 P/L 発生のみ。CF タイミングは INV と同一月
科目名: b.科目名,
諸表区分: mapped.stmt,
大分類: mapped.cat,
税抜金額: b.amount,
税込金額: b.amount, // 簡略化:税区分は摘要で管理者が指定。本案件では税込=税抜
消費税額: 0,
PJ名: b.pj,
組織名: b.org,
摘要: '【手動調整】' + b.descs.join(' / '),
決済手段: '',
source: 'MANUAL_PLAN', // トレーサビリティ用(既存フィールドとの衝突なし)
});
}
return events;
}
Step 4: 602_datamart_main.js の計画合流ブロックに呼び出しを追加
L274 付近(pipeResult = dmIngestPipelinePlanData_(...) の直後)に、手動調整値の合流を追加する。加算方式(上書きではなく concat)で既存値に積み上げる。
if (sheetPipe) {
pipeResult = dmIngestPipelinePlanData_(ctx, sheetPipe);
planData = planData.concat(pipeResult.planData);
}
// S-07: 51タブ(手動調整)を計画に合流(加算方式)
var manualEvents = dmIngestManualPlanData_(ctx);
if (manualEvents.length > 0) {
planData = planData.concat(manualEvents);
Utils.logInfo && Utils.logInfo('S-07: 手動調整 ' + manualEvents.length + ' 件を計画に合流');
}
アーキテクチャ決定のまとめ
| 決定事項 | 採用方式 | 理由 |
|---|---|---|
| データ取り込み方式 | Repository + readSheetAsDtos_ 再利用 | 既存パターン踏襲で車輪の再発明を回避 |
| DTO 名 | ManualPlanDTO | 予算 BudgetDTO と並列の新規 DTO |
| 計画値への反映方式 | 加算/減算(planData.concat) | 上書きだと「パイプライン+INV」の成果が失われる。加算なら既存値を壊さない |
| リセットタイミング | ユーザーが手動でクリア | 51 タブのデータはクリアされるまで毎回適用 |
| 適用単位 | (科目名, 対象年月) で合算 | 同一月に複数行入力(別シナリオ・別部門等)を想定 |
| マスタ未登録時 | スキップ+警告ログ | 「キーワード推測による自動分類禁止」(CLAUDE.md)に準拠 |
影響範囲
| ファイル | 変更内容 | 規模 |
|---|---|---|
000_infra/003_contracts.js | ManualPlanDTO の @typedef を追加 | +15 行 |
200_data/202_repository.js | ManualPlanRepository を末尾に追加 | +35 行 |
600_report/601_datamart_ingest.js | dmIngestManualPlanData_ を追加 | +55 行 |
600_report/602_datamart_main.js | 計画合流ブロックに呼び出し追加 | +5 行 |
100_config/101_sys_config.js | SYS_CONFIG に PL_PLAN_MANUAL キー登録(事前条件。51タブが未存在なら DDL 対応が必要だが、本仕様書のスコープ外) | 既存前提 |
既存動作への影響:
- 51 タブが未存在の場合、
_getSheetはnullを返し、findAllは空配列を返す → 既存の計画パイプラインは一切変わらない(後方互換) - 51 タブにデータがあっても、マスタ未登録・空行・期間外のものは全てスキップされるため、予期しない副作用は起きない
- CF 計画にも加算される(INV と同一の月・税抜金額で計上されるため、自動的に P/L・B/S・CF すべての計画が一貫して変動)
注意事項
- 51 タブのシート実体は本仕様書のスコープ外。既存
51_list_pipeline_plan(パイプライン展開明細ビュー)とは別の、新規タブ51_pl_plan_manualを想定している。実装前にシート名・シートキー・ヘッダー構成を SYS_CONFIG と DDL(101_sys_config.js)で整備するサブタスクが別途必要(本仕様書では前提条件として扱う)。 - 「加算方式」を厳守。上書き方式(計画値をゼロリセットしてから再計算)は既存パイプライン・INV の計画値を破壊するため採用しない。Repository 内でも
saveメソッドは実装しない(読み取り専用)。 - 毎回マート更新で適用される。ユーザーが 51 タブの値をクリアしない限り、次月以降のマート更新でも同じ調整が積み上がる。「一時的シナリオ」を試したい場合はユーザーが手動で有効フラグ=FALSE か行削除を行う運用とする。
- 税区分は税抜=税込で扱う。手動調整値は簡略化のため税込=税抜として計画に合流する(本来の仕訳では消費税額 = 税抜 × 10% だが、計画段階でのトップダウン調整としては丸めを優先)。税抜ベースで記入する運用ルールを仕様書に明記。
source: 'MANUAL_PLAN'フィールドは既存イベントには存在しない新フィールド。dmProcessAllEvents_以降の処理で未参照でも無害(既存コードは科目名・pYm・税抜金額・諸表区分等のフィールドしか見ない)。将来の監査ログで「この計画値は手動調整由来」を識別するために残す。- AccountRepository のキャッシュ。
dmIngestManualPlanData_はAccountRepository.findAsMap()を呼ぶが、既存マート処理でも同じ関数が使われておりキャッシュヒット前提。追加の負荷は無視できる。
エッジケース
入力値のバリエーションと、合流処理で期待される振る舞いを網羅する。
| # | 条件 | 表示値/振る舞い | 理由 |
|---|---|---|---|
| 1 | 調整金額がプラス値 | 該当 (科目名, 対象年月) の計画値に加算 | 売上計画の上振れ/追加案件を表現。標準ユースケース |
| 2 | 調整金額がマイナス値 | 該当 (科目名, 対象年月) の計画値から減算 | 売上計画の減額調整(受注失注リスク、慎重シナリオ)。マイナスを許容する |
| 3 | 調整金額がゼロ | 合算後もゼロならイベント生成をスキップ | ノイズ行の排除(プラス/マイナスが合算でキャンセルした場合含む) |
| 4 | 科目名 が空 | 当該行スキップ | 必須項目欠損。データマート側でマッピング先が無く、計画に反映不能 |
| 5 | 対象年月 が空 | 当該行スキップ | 必須項目欠損。Utils.parseDateToYm が "" を返すため Repository 側で除外 |
| 6 | 対象年月 がパース不能("次期" 等) | 当該行スキップ(警告ログなし) | Utils.parseDateToYm が "" を返す。既存 Repository 共通の振る舞いに準拠 |
| 7 | 対象年月 が ctx.targetMonths の範囲外 | 当該行スキップ(ログなし) | パイプラインの dmIngestPipelinePlanData_ と同じ仕様。範囲外は自動的に無視 |
| 8 | 有効フラグ = FALSE | 当該行スキップ | プロジェクト共通規約(CLAUDE.md コーディング規約) |
| 9 | 同一 (科目名, 対象年月) の重複行 | 調整金額を合算して 1 イベントとして処理 | 別シナリオ・別担当者の入力を一括反映。合算が最もシンプルで直感的 |
| 10 | 科目名が 11_mst_account に未登録 | 当該行スキップ+Utils.logWarn で警告 | 「キーワード推測による自動分類禁止」(CLAUDE.md)。サイレント適用は誤計画の原因になるため警告ログ必須 |
| 11 | 科目名が 11_mst_account に存在するが 有効フラグ = FALSE | AccountRepository.findAsMap() がスキップするため未登録と同じ扱い | 既存仕様を継承 |
| 12 | PJ名 が空 | 「全社共通」を自動設定 | PJ 別損益(78 タブ)に引きずられない汎用的な調整を表現 |
| 13 | 摘要 が空 | 【手動調整】 のプレフィックスのみ付与(内容なし) | トレーサビリティ確保。イベントには必ず由来を示すタグを付ける |
| 14 | 51 タブ自体が存在しない | _getSheet が null を返し、findAll は空配列。合流ゼロ件で既存パイプライン動作と一致 | 後方互換。MAS-079 未導入環境でも破綻しない |
| 15 | 51 タブにデータ行がゼロ | 同上、合流ゼロ件 | readSheetAsDtos_ が空配列を返す既存挙動に従う |
二重計上の防止
- 本案件は読み込み対応のため、書き込み時のロックはスコープ外。
ManualPlanRepositoryはsave/appendを持たない(意図的)。 - ただし、データマート更新処理 (
dmUpdateAll_) 全体が複数同時に実行されるシナリオ(複数ユーザーの同時メニュー操作、トリガーの競合)を想定し、呼び出し元のデータマートビルダー側でLockService.getScriptLock()による排他制御を行うことを推奨事項として記載する(別案件で対応済みの場合は確認のみ)。 - 51 タブの値自体が変わらない限り、データマートの冪等性は保証される(毎回同じ調整が同じ月に加算されるだけ)。
実データ検証
Human-in-the-Loop の観点から、実装後の動作確認フローを以下に定める。必ず dev 環境で一巡させてから prod に展開すること。
検証フロー
- 前提準備
setupAllSchemasを実行し、51 タブ(シートキーPL_PLAN_MANUAL/ 実体51_pl_plan_manual)がヘッダー有効フラグ / 科目名 / 対象年月 / 調整金額 / 摘要 / PJ名 / 組織名で作成されていることを確認11_mst_accountで検証に使用する科目(例:売上高)が有効フラグ=TRUE で登録されていることを確認
- テストデータ投入
- 行1:
TRUE / 売上高 / 2026-05 / 500,000 / シナリオA上振れ / 全社共通 / "" - 行2:
TRUE / 売上高 / 2026-05 / 300,000 / シナリオB追加案件 / 全社共通 / ""(同月重複 → 合算テスト) - 行3:
TRUE / 売上高 / 2026-06 / -200,000 / 慎重シナリオ減額 / 全社共通 / ""(マイナス値テスト) - 行4:
TRUE / 架空科目 / 2026-05 / 100,000 / マスタ未登録テスト / "" / ""(警告ログ出力テスト) - 行5:
TRUE / "" / 2026-05 / 999,999 / 必須欠損テスト / "" / ""(スキップテスト) - 行6:
FALSE / 売上高 / 2026-05 / 999,999 / 無効化テスト / "" / ""(有効フラグ FALSE スキップテスト)
- 行1:
- マート更新実行
🔄 データマート更新 → 全マート再構築メニューを押下
- 期待値検証
63_pl_monthly_plan(または相当する P/L 計画タブ)で、2026-05の売上高が**+800,000 増加**していることを確認2026-06の売上高が**-200,000 減少**していることを確認- GAS のログ(
Apps Script → 実行ログ)で以下を確認:S-07: 手動調整 2 件を計画に合流等の INFO ログS-07: 科目マスタ未登録のためスキップ: 架空科目 / 2026-05の WARN ログ
- CF・B/S への波及確認
83_cf_monthly_plan(CF 計画タブ)で、2026-05の営業活動キャッシュインが対応額だけ増えていること(入金ラグは INV 既定と同じ扱い)73_bs_monthly_plan(B/S 計画タブ)で、2026-05の売掛金 or 現預金が対応額だけ増えていること
検証ポイント(MCP での事前確認)
- シートキー
PL_PLAN_MANUALが01_sys_config(SYS_CONFIGシート)に登録済みか - 51 タブの実ヘッダー が DTO プロパティ名(
有効フラグ / 科目名 / 対象年月 / 調整金額 / 摘要 / PJ名 / 組織名)と完全一致しているか(列順は Repository 層で吸収されるが、文字列完全一致は必須) 11_mst_accountに存在する科目の日本語表記(例:売上高と売上など揺れがないか)
関連ドキュメント
| 仕様書 | 関連箇所 |
|---|---|
| docs/spec/spec_pipeline_plan.md | dmIngestPipelinePlanData_ の設計(本仕様書の合流方式の元パターン) |
| docs/spec/spec_plan_bs_fix.md | 計画 B/S のフィルター仕様(手動調整値が B/S 計画にも波及する前提) |
| docs/dev/dev_mas-078_pipeline_cf_integration.md | MAS-078 CF 合流。本案件と同じく「計画パイプラインに第 N ソースを合流」する実装パターン |
| CLAUDE.md | コーディング規約・Repository パターン・会計ロジック(科目マスタ未登録=エラー等) |
| docs/spec/spec_dashboard.md | 51 タブの位置付け(ダッシュボード系シートの全体像) |
人間が検討すべき事項
TODO_future.md からの転記
- 手動調整値の上書きルール(リセットタイミング等) → 本仕様書で**「加算/減算方式」として定義済み**。51 タブの値はユーザーが手動でクリアしない限り、データマート更新のたびに毎回適用される。即実装可能(追加の仕様合意不要)。
調査で判明した追加検討事項
- シート実体
51_pl_plan_manualの新設: 既存の51_list_pipeline_plan(パイプライン展開明細ビュー)とタブ番号が重複する可能性あり。実装前にタブ番号割り当て(例:51→51b等の renumber、もしくは既存 51 を退避)をdocs/spec/spec_tab_renumber.mdでレビューする必要がある。 - DDL 対応: 本仕様書は読み込み専用だが、新規タブとして
51_pl_plan_manualをsetupAllSchemasで自動生成させる DDL 追記が別サブタスクとして必要。ヘッダー構成は本仕様書の DTO 定義と完全一致させる。 - 税区分の扱い: 本仕様書では簡略化のため「税抜=税込」として処理している。将来、課税事業者に移行し税抜方式(MAS-089)が採用された場合、
ManualPlanDTOに税区分フィールドを追加しTAX_RATESと突き合わせる拡張が必要。本案件では扱わず、移行時に再検討。 - PJ 別配賦: 「PJ名」未指定の場合「全社共通」となるため、
78_pj_pl(PJ 別損益)には合流しない(共通費配賦ロジックで按分されない)。特定 PJ への調整を反映したい場合はユーザーが明示的に PJ 名を入力する運用とする。
実装プロンプト(Claude Code 用)
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-079「51タブをデータソース化(読み込み対応)— 手動調整値の売上計画への加算」を実装してください。
## 実行前タスク
以下のファイルを読み込み、既存設計パターンを完全に把握してから実装を開始すること。
推測で書かず、実在する関数名・シート名・ヘッダー名のみを引用する。
- `200_data/202_repository.js` — `readSheetAsDtos_` / `AccountRepository.findAll` / `AccountRepository.findAsMap` の実装形。`ManualPlanRepository` は AccountRepository の直後(ファイル末尾)に追加する
- `000_infra/003_contracts.js` — `BudgetDTO` の @typedef フォーマット。`ManualPlanDTO` はその直後に追記する
- `000_infra/004_utils.js` — `Utils.getSheetByKey` / `Utils.parseDateToYm` / `Utils.parseAmt` / `Utils.logWarn` / `Utils.logInfo` の引数仕様
- `600_report/601_datamart_ingest.js` — `dmIngestPipelinePlanData_` (L363-) の仮想 INV イベント出力フォーマット。`dmIngestManualPlanData_` はこのパターンと同形で実装する
- `600_report/602_datamart_main.js` — 計画パイプラインの合流ブロック (L256-284)。`dmIngestManualPlanData_` の呼び出しは `pipeResult = dmIngestPipelinePlanData_(...)` の**直後**に挿入する
- `100_config/101_sys_config.js` — シートキー登録形式(本案件では `PL_PLAN_MANUAL` キーが SYS_CONFIG に登録済みであることを前提とする)
## 修正対象ファイル
- `000_infra/003_contracts.js` — `ManualPlanDTO` @typedef の**追記のみ**
- `200_data/202_repository.js` — `ManualPlanRepository` の**末尾追記のみ**(既存 Repository は一切変更しない)
- `600_report/601_datamart_ingest.js` — `dmIngestManualPlanData_` 関数の**末尾追記のみ**
- `600_report/602_datamart_main.js` — 計画合流ブロックに 1 行の呼び出し追加 + info ログ 1 行のみ
## 実装内容
### 1. `003_contracts.js` に `ManualPlanDTO` を追記
`BudgetDTO` の @typedef 直後に以下を追加:
/**
* 51_pl_plan_manual — 売上計画 手動調整レコード
* パイプライン加重値 + INV 計画値に対するトップダウンでの調整を表現する。
* 加算方式(プラス値=上振れ、マイナス値=減額)。
* @typedef {Object} ManualPlanDTO
* @property {boolean} 有効フラグ
* @property {string} 科目名 - 11_mst_account に登録された科目名
* @property {Date|string} 対象年月 - "YYYY-MM" または Date 型
* @property {number} 調整金額 - 税抜・加算値(マイナス可)
* @property {string} 摘要 - 調整理由
* @property {string} [PJ名] - 任意。未指定時は「全社共通」扱い
* @property {string} [組織名] - 任意
*/
### 2. `202_repository.js` に `ManualPlanRepository` を末尾追記
`AccountRepository` 定義の直後(ファイル末尾)に以下を追加:
// =====================================================================
// ManualPlanRepository — 51_pl_plan_manual(読み取り専用)
// =====================================================================
var ManualPlanRepository = {
_getSheet: function() {
return Utils.getSheetByKey('PL_PLAN_MANUAL', '51_pl_plan_manual');
},
findAll: function() {
var raw = readSheetAsDtos_(ManualPlanRepository._getSheet());
var normalized = [];
for (var i = 0; i < raw.dtos.length; i++) {
var dto = raw.dtos[i];
var flag = dto['有効フラグ'];
if (flag === false || String(flag).toUpperCase() === 'FALSE') continue;
var acc = String(dto['科目名'] || '').trim();
var ym = Utils.parseDateToYm(dto['対象年月']);
if (!acc || !ym) continue;
dto['科目名'] = acc;
dto['対象年月'] = ym;
dto['調整金額'] = Utils.parseAmt(dto['調整金額']);
normalized.push(dto);
}
return { headers: raw.headers, dtos: normalized };
},
};
### 3. `601_datamart_ingest.js` に `dmIngestManualPlanData_` を追記
ファイル末尾に以下を追加。`ctx.targetMonths` 範囲・マスタ未登録・重複合算を全て処理する:
function dmIngestManualPlanData_(ctx) {
var result = ManualPlanRepository.findAll();
var acctMap = AccountRepository.findAsMap();
var firstMonth = ctx.targetMonths[0];
var lastMonth = ctx.targetMonths[ctx.targetMonths.length - 1];
var bucket = {};
for (var i = 0; i < result.dtos.length; i++) {
var dto = result.dtos[i];
var acc = dto['科目名'];
var ym = dto['対象年月'];
var amt = dto['調整金額'];
if (!acctMap[acc]) {
if (Utils.logWarn) Utils.logWarn('MAS-079: 科目マスタ未登録のためスキップ: ' + acc + ' / ' + ym);
continue;
}
if (ym < firstMonth || ym > lastMonth) continue;
var key = acc + '|' + ym;
if (!bucket[key]) {
bucket[key] = {
科目名: acc, pYm: ym, amount: 0, descs: [],
pj: String(dto['PJ名'] || '').trim() || '全社共通',
org: String(dto['組織名'] || '').trim(),
};
}
bucket[key].amount += amt;
var d = String(dto['摘要'] || '').trim();
if (d) bucket[key].descs.push(d);
}
var events = [];
for (var key in bucket) {
if (!bucket.hasOwnProperty(key)) continue;
var b = bucket[key];
if (b.amount === 0) continue;
var mapped = acctMap[b.科目名];
events.push({
pYm: b.pYm,
sYm: b.pYm,
科目名: b.科目名,
諸表区分: mapped.stmt,
大分類: mapped.cat,
税抜金額: b.amount,
税込金額: b.amount,
消費税額: 0,
PJ名: b.pj,
組織名: b.org,
摘要: '【手動調整】' + b.descs.join(' / '),
決済手段: '',
source: 'MANUAL_PLAN',
});
}
return events;
}
### 4. `602_datamart_main.js` の計画合流ブロックに呼び出しを追加
L274 付近(`pipeResult = dmIngestPipelinePlanData_(...)` の直後)に以下を挿入:
// MAS-079: 51タブ(手動調整)を計画に合流(加算方式)
var manualEvents = dmIngestManualPlanData_(ctx);
if (manualEvents.length > 0) {
planData = planData.concat(manualEvents);
if (Utils.logInfo) Utils.logInfo('MAS-079: 手動調整 ' + manualEvents.length + ' 件を計画に合流');
}
## 制約
- **既存 Repository の `findAll` / `findAsMap` は一切変更しない**(既存動作の回帰を防ぐ)
- **`ManualPlanRepository` に `save` / `append` メソッドを追加しない**(読み取り専用。書き込みはユーザー手動)
- **加算方式を厳守**。`planData = manualEvents`(上書き)や `planData.splice(...)` 等で既存イベントを削除しない
- **科目名の曖昧マッチ禁止**(CLAUDE.md 規約)。マスタに存在しない科目名は必ずスキップ + 警告ログ
- **既存の `dmIngestPipelinePlanData_` / `dmIngestPlanData_` のロジックを変更しない**(本案件は第 3 ソースの追加のみ)
- **601_datamart_ingest.js の `dmIngestManualPlanData_` 以外の関数を変更しない**
## エッジケース
| 条件 | 振る舞い | 理由 |
|------|---------|------|
| 調整金額 プラス | 該当月に加算 | 上振れシナリオ(標準) |
| 調整金額 マイナス | 該当月から減算 | 減額調整を許容 |
| 調整金額 合算ゼロ | イベント生成スキップ | ノイズ除去 |
| 科目名 or 対象年月 が空 | 当該行スキップ | 必須項目欠損 |
| `有効フラグ` = FALSE | 当該行スキップ | 共通規約 |
| 同一 `(科目名, 対象年月)` 重複 | 調整金額を合算 | 複数担当者の入力を一括反映 |
| 科目マスタ未登録 | スキップ+Utils.logWarn | キーワード推測禁止(CLAUDE.md) |
| `ctx.targetMonths` 範囲外 | スキップ(ログなし) | パイプライン取込と同仕様 |
| 51 タブ未存在 | `findAll` が空配列 → 合流ゼロ件 | 後方互換 |
| PJ 名 空欄 | 「全社共通」を自動設定 | PJ 別損益に引きずらない |
## 実データ検証
実装前に MCP 等で以下を確認:
- `01_sys_config` に `PL_PLAN_MANUAL` キーが登録されているか(未登録なら先に 101_sys_config.js の `setupAllSchemas` に追加する DDL サブタスクが必要)
- `11_mst_account` の「売上高」が有効フラグ=TRUE で登録されているか(検証に使用)
- 51 タブ(`51_pl_plan_manual`)が存在する場合、ヘッダーが DTO プロパティ名と完全一致しているか
## 動作確認
1. `npm run push:dev` で dev 環境にデプロイ
2. 51 タブに検証データ 6 行を投入(プラス値 × 2、マイナス値、マスタ未登録、必須欠損、有効フラグ FALSE)
3. `🔄 データマート更新 → 全マート再構築` メニューを実行
4. GAS 実行ログで `S-07: 手動調整 2 件を計画に合流` の INFO ログを確認
5. GAS 実行ログで `S-07: 科目マスタ未登録のためスキップ` の WARN ログを確認
6. `63_pl_monthly_plan` で 2026-05 の売上高が +800,000 変動、2026-06 の売上高が -200,000 変動していることを確認
7. `73_bs_monthly_plan` / `83_cf_monthly_plan` の該当月にも金額が波及していることを確認
8. dev で問題なければ `npm run push:prod` で本番に展開し、prod 側でも同一検証を実施
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|:-------:|------|
| 要件把握・既存コード調査 | あり | Repository パターン・合流ブロックの挿入位置を完全把握 |
| 実装(上記コードブロック) | なし | 仕様書で完全定義済み。判断要素なし |
| 動作確認 | なし | 検証手順は仕様書記載の通り |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
| 事前調査(Read) | Claude Sonnet 4.6 | 既存 Repository パターンの理解。中程度の判断 |
| Step 1(DTO 定義) | Claude Haiku 4.5 | 仕様書でコード完全定義済み。@typedef の追記のみ |
| Step 2(Repository 追加) | Claude Haiku 4.5 | 仕様書でコード完全定義済み。既存末尾に追記のみ |
| Step 3(ingest 関数追加) | Claude Sonnet 4.6 | 既存パターンの模倣 + ctx 構造参照が必要。中程度の判断 |
| Step 4(main 合流呼び出し) | Claude Haiku 4.5 | 仕様書で挿入位置・コード完全定義済み |
| 動作確認 | Claude Sonnet 4.6 | ログ・財務諸表の目視確認と差異判断 |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-19 | 初版作成。MAS-079「51タブをデータソース化(読み込み対応)」を手動調整値の加算方式として仕様化。ManualPlanRepository + ManualPlanDTO + dmIngestManualPlanData_ の 3 点セットで計画パイプラインに合流させる方針を確定 |
仕様書作成プロンプト
再現性・監査性・プロンプト改善トレースのため、本仕様書の生成に用いた <instruction> 全文を以下に記録する。
展開して表示
<instruction>
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
1. 拡張思考の使い分け: Phase 1で設計を確定させ、Phase 2では出力に徹する。
2. テキスト報告の禁止: 説明は1文以内。直ちに tool を呼ぶ。
3. 4-5 分割の Write/Edit 実行: 2-1(骨格)/2-2(前半)/2-3a(後半)/2-3b(プロンプト)/2-4(<details>記録)に分割。
4. 各 Step で何を書くかを具体指示: 出力時に設計判断を再考しない。
======================================================================
あなたはGAS会計システムのシニア開発者兼仕様書ライターです。
CLIエージェント「Claude Code」として、案件 S-07「51タブをデータソース化(読み込み対応)」の開発仕様書を作成してください。
## Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)
1. `docs/_internal/TODO_future.md` でS-07の要件(概要、人間が検討すべき事項)を把握。
2. 以下の関連コードとマスタをツールで深く調査し、既存の設計パターンを完全に理解すること。
- `200_data/202_repository.js`: `readSheetAsDtos_` ヘルパー関数の実装と、`AccountRepository` 等の既存Repositoryの構造を把握。
- `000_infra/003_contracts.js`: `toDtoList` の実装と既存DTOの型定義を把握。
- `000_infra/004_utils.js`: `getSheetByKey`, `parseDateToYm`, `parseAmt` の機能を把握。
- `600_report/` 配下のデータマートビルダー(特に売上計画に関連するファイル)を調査し、手動調整値をどのロジックに組み込むべきか特定する。
## Phase 2: 仕様書の分割作成
出力先: `docs/dev/dev_mas-136_manual_plan_source.md`
**【重要】絶対に1回のツール呼び出しで全内容を出力せず、以下のStepに分割して実行すること。**
### Step 2-1: 骨格の作成 (File Write)
- `docs/_internal/dev_spec_prompt_template.md` に記載のセクション構成に基づき、見出しのみの骨格を作成する。
### Step 2-2: 前半セクションの追記 (File Edit または Bash)
※アーキテクトからの指示:
- **目的**: パイプライン等から自動計算される売上計画に対し、トップダウンでの手動調整を可能にし、計画策定の柔軟性を高めることを明記する。
- **修正方針**: 以下のアーキテクチャ方針を具体的に記述すること。
- **`ManualPlanRepository`の新設**: `202_repository.js` に、51タブ(シートキー: `PL_PLAN_MANUAL`)を読み込むための `ManualPlanRepository` を追加する。
- **DTOの定義**: `003_contracts.js` に、51タブの行データを表現する `ManualPlanDTO` を `@typedef` で定義する。プロパティは `科目名`, `対象年月`, `調整金額`, `摘要` 等を想定。
- **既存関数の再利用**: `ManualPlanRepository.findAll()` の実装では、既存のヘルパー関数 `readSheetAsDtos_` を再利用し、車輪の再発明を避けることを明記する。データ正規化には `Utils.parseDateToYm` と `Utils.parseAmt` を使用する。
- **データマートへの統合**: 既存の売上計画データマートビルダーが `ManualPlanRepository.findAll()` を呼び出し、取得した調整値をパイプラインベースの計画値に**加算**するロジックを追記する方針を記述する。
- **注意事項**:
- 「手動調整値の上書きルール」については、既存の計画値に「加算/減算」する方式を採用することを明記する。「上書き」方式は採用しない。
- 51タブのデータは、ユーザーが手動でクリアしない限り、データマート更新のたびに毎回適用される仕様であることを記載する。
### Step 2-3a: エッジケース〜人間検討事項の追記 (File Edit または Bash)
※アーキテテクトからの指示:
- **エッジケース**: 以下の仕様をテーブル形式で網羅的に記述すること。
- **マイナス値の扱い**: 許容する。売上計画の減額調整として扱う。
- **必須項目の欠損**: `科目名` または `対象年月` が空の行は、計算対象から除外(スキップ)する。
- **重複時の挙動**: 同一の `科目名` と `対象年月` を持つ行が複数存在する場合、`調整金額` を**合算**して処理する。
- **マスタ未登録の科目**: 科目マスタ(`11_mst_account`)に存在しない科目名が指定された行は、計算対象から除外(スキップ)し、ログに警告を出力する。
- **二重計上の防止**:
- 本件は読み込み対応のため、書き込み時のロックはスコープ外と明記。
- ただし、データマート更新処理全体が複数同時に実行されることを想定し、呼び出し元(データマートビルダー)で `LockService` を用いて排他制御を行うことを「推奨事項」として記載する。
- **人間が検討すべき事項**:
- `TODO_future.md` から転記した「手動調整値の上書きルール」に対し、「本仕様書で『加算/減算方式』として定義済みのため、即実装可能」と結論を記載する。
- **実データ検証**:
- `Human-in-the-Loop`の観点から、実装後の動作確認フローとして「51タブにテストデータ(プラス値、マイナス値、重複データ)を入力 → データマート更新を実行 → 関連する財務諸表タブ(6x_pl_xxx)で計画値が正しく変動したことを目視確認する」という手順を具体的に記述する。
### Step 2-3b: 実装プロンプト〜変更履歴の追記 (File Edit または Bash)
- 実装プロンプトは、上記で定義した修正方針とエッジケースをClaude Codeが忠実に実装できるよう、具体的かつ自己完結的に記述すること。
- 修正対象ファイルとして `202_repository.js`, `003_contracts.js`, および売上計画データマートビルダー(Phase 1で特定したファイル)を明記する。
### Step 2-4: 仕様書作成プロンプトの記録 (File Edit または Bash)
- 仕様書末尾に `<details>` タグを設け、この `<instruction>` 全文を追記すること。
</instruction>