概要

項目内容
案件IDMAS-136
カテゴリパイプライン / FP&A
PhaseP2
優先度
所要時間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: ManualPlanDTO003_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: ManualPlanRepository202_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.jsManualPlanDTO@typedef を追加+15 行
200_data/202_repository.jsManualPlanRepository を末尾に追加+35 行
600_report/601_datamart_ingest.jsdmIngestManualPlanData_ を追加+55 行
600_report/602_datamart_main.js計画合流ブロックに呼び出し追加+5 行
100_config/101_sys_config.jsSYS_CONFIGPL_PLAN_MANUAL キー登録(事前条件。51タブが未存在なら DDL 対応が必要だが、本仕様書のスコープ外)既存前提

既存動作への影響:

  • 51 タブが未存在の場合、_getSheetnull を返し、findAll は空配列を返す → 既存の計画パイプラインは一切変わらない(後方互換)
  • 51 タブにデータがあっても、マスタ未登録・空行・期間外のものは全てスキップされるため、予期しない副作用は起きない
  • CF 計画にも加算される(INV と同一の月・税抜金額で計上されるため、自動的に P/L・B/S・CF すべての計画が一貫して変動)

注意事項

  1. 51 タブのシート実体は本仕様書のスコープ外。既存 51_list_pipeline_plan(パイプライン展開明細ビュー)とは別の、新規タブ 51_pl_plan_manual を想定している。実装前にシート名・シートキー・ヘッダー構成を SYS_CONFIG と DDL(101_sys_config.js)で整備するサブタスクが別途必要(本仕様書では前提条件として扱う)。
  2. 「加算方式」を厳守。上書き方式(計画値をゼロリセットしてから再計算)は既存パイプライン・INV の計画値を破壊するため採用しない。Repository 内でも save メソッドは実装しない(読み取り専用)。
  3. 毎回マート更新で適用される。ユーザーが 51 タブの値をクリアしない限り、次月以降のマート更新でも同じ調整が積み上がる。「一時的シナリオ」を試したい場合はユーザーが手動で有効フラグ=FALSE か行削除を行う運用とする。
  4. 税区分は税抜=税込で扱う。手動調整値は簡略化のため税込=税抜として計画に合流する(本来の仕訳では消費税額 = 税抜 × 10% だが、計画段階でのトップダウン調整としては丸めを優先)。税抜ベースで記入する運用ルールを仕様書に明記。
  5. source: 'MANUAL_PLAN' フィールドは既存イベントには存在しない新フィールド。dmProcessAllEvents_ 以降の処理で未参照でも無害(既存コードは 科目名・pYm・税抜金額・諸表区分 等のフィールドしか見ない)。将来の監査ログで「この計画値は手動調整由来」を識別するために残す。
  6. 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 に存在するが 有効フラグ = FALSEAccountRepository.findAsMap() がスキップするため未登録と同じ扱い既存仕様を継承
12PJ名 が空「全社共通」を自動設定PJ 別損益(78 タブ)に引きずられない汎用的な調整を表現
13摘要 が空【手動調整】 のプレフィックスのみ付与(内容なし)トレーサビリティ確保。イベントには必ず由来を示すタグを付ける
1451 タブ自体が存在しない_getSheetnull を返し、findAll は空配列。合流ゼロ件で既存パイプライン動作と一致後方互換。MAS-079 未導入環境でも破綻しない
1551 タブにデータ行がゼロ同上、合流ゼロ件readSheetAsDtos_ が空配列を返す既存挙動に従う

二重計上の防止

  • 本案件は読み込み対応のため、書き込み時のロックはスコープ外ManualPlanRepositorysave / append を持たない(意図的)。
  • ただし、データマート更新処理 (dmUpdateAll_) 全体が複数同時に実行されるシナリオ(複数ユーザーの同時メニュー操作、トリガーの競合)を想定し、呼び出し元のデータマートビルダー側で LockService.getScriptLock() による排他制御を行うことを推奨事項として記載する(別案件で対応済みの場合は確認のみ)。
  • 51 タブの値自体が変わらない限り、データマートの冪等性は保証される(毎回同じ調整が同じ月に加算されるだけ)。

実データ検証

Human-in-the-Loop の観点から、実装後の動作確認フローを以下に定める。必ず dev 環境で一巡させてから prod に展開すること

検証フロー

  1. 前提準備
    • setupAllSchemas を実行し、51 タブ(シートキー PL_PLAN_MANUAL / 実体 51_pl_plan_manual)がヘッダー 有効フラグ / 科目名 / 対象年月 / 調整金額 / 摘要 / PJ名 / 組織名 で作成されていることを確認
    • 11_mst_account で検証に使用する科目(例: 売上高)が有効フラグ=TRUE で登録されていることを確認
  2. テストデータ投入
    • 行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 スキップテスト)
  3. マート更新実行
    • 🔄 データマート更新 → 全マート再構築 メニューを押下
  4. 期待値検証
    • 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 ログ
  5. CF・B/S への波及確認
    • 83_cf_monthly_plan(CF 計画タブ)で、2026-05 の営業活動キャッシュインが対応額だけ増えていること(入金ラグは INV 既定と同じ扱い)
    • 73_bs_monthly_plan(B/S 計画タブ)で、2026-05 の売掛金 or 現預金が対応額だけ増えていること

検証ポイント(MCP での事前確認)

  • シートキー PL_PLAN_MANUAL01_sys_configSYS_CONFIG シート)に登録済みか
  • 51 タブの実ヘッダー が DTO プロパティ名(有効フラグ / 科目名 / 対象年月 / 調整金額 / 摘要 / PJ名 / 組織名)と完全一致しているか(列順は Repository 層で吸収されるが、文字列完全一致は必須)
  • 11_mst_account に存在する科目の日本語表記(例: 売上高売上 など揺れがないか)

関連ドキュメント

仕様書関連箇所
docs/spec/spec_pipeline_plan.mddmIngestPipelinePlanData_ の設計(本仕様書の合流方式の元パターン)
docs/spec/spec_plan_bs_fix.md計画 B/S のフィルター仕様(手動調整値が B/S 計画にも波及する前提)
docs/dev/dev_mas-078_pipeline_cf_integration.mdMAS-078 CF 合流。本案件と同じく「計画パイプラインに第 N ソースを合流」する実装パターン
CLAUDE.mdコーディング規約・Repository パターン・会計ロジック(科目マスタ未登録=エラー等)
docs/spec/spec_dashboard.md51 タブの位置付け(ダッシュボード系シートの全体像)

人間が検討すべき事項

TODO_future.md からの転記

  • 手動調整値の上書きルール(リセットタイミング等) → 本仕様書で**「加算/減算方式」として定義済み**。51 タブの値はユーザーが手動でクリアしない限り、データマート更新のたびに毎回適用される。即実装可能(追加の仕様合意不要)。

調査で判明した追加検討事項

  • シート実体 51_pl_plan_manual の新設: 既存の 51_list_pipeline_plan(パイプライン展開明細ビュー)とタブ番号が重複する可能性あり。実装前にタブ番号割り当て(例: 5151b 等の renumber、もしくは既存 51 を退避)を docs/spec/spec_tab_renumber.md でレビューする必要がある。
  • DDL 対応: 本仕様書は読み込み専用だが、新規タブとして 51_pl_plan_manualsetupAllSchemas で自動生成させる 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>