概要

項目内容
案件IDMAS-148
カテゴリ自動入力パイプライン(即効)
PhaseP1.5
優先度★★★
所要時間2-3時間
実装ステータス📝 仕様書段階・実装未着手 (2026-04-28 監査時点)
対象ファイル100_config/101_sys_config.js(メニュー追加), 400_domain/410_subledger_engine.js(統合関数追加)
前提案件なし(Action A/B/マート更新は全て実装済み)

目的

月次締めに必要な3つの操作(Action A → Action B → マート更新)を1つのメニュー項目で連続実行できるようにし、締め作業のUXを改善する。事前バリデーション(未承認INV・未消込STLの警告)を付与することで、差戻し漏れや処理忘れも防止する。

現在のコード

月次締めに必要な3つの操作(手動で順次実行)

現在、月次締めは以下の3つのメニュー操作を順番に手動実行する必要がある:

順番メニュー関数ファイル:行番号
1📒 経理業務 → 承認済INVを仕訳台帳へ転記 (Action A)processInvoiceApprovals()410_subledger_engine.js:106
2📒 経理業務 → 消込済STLを仕訳台帳へ転記 (Action B)processSettlementClearings()410_subledger_engine.js:352
3📊 マート更新 → 財務3表の更新buildBudgetTrendDataMart()602_datamart_main.js:157

Action A のエントリーポイント(410_subledger_engine.js L106)

function processInvoiceApprovals() {
  const FUNC = 'processInvoiceApprovals';
  try {
    const invSheet = Utils.getSheetByKey('WRK_INVC', '32_wrk_invoice');
    // ...
    // 完了時:
    SpreadsheetApp.getActiveSpreadsheet().toast(
      `🎉 ${newTrnRows.length}件のINV承認を仕訳台帳へ転記しました${stlMsg}`, '処理完了', 5
    );
  } catch (e) {
    console.error(FUNC, e.message, e.stack);
    SpreadsheetApp.getUi().alert(`🚨 ${FUNC} でエラーが発生しました`, e.message, SpreadsheetApp.getUi().ButtonSet.OK);
  }
}

Action B のエントリーポイント(410_subledger_engine.js L352)

function processSettlementClearings() {
  const FUNC = 'processSettlementClearings';
  try {
    const bankSheet = Utils.getSheetByKey('WRK_BANK', '33_wrk_bank');
    // ...
    // 完了時:
    SpreadsheetApp.getActiveSpreadsheet().toast(
      `🎉 ${processCount}件のSTL消込を仕訳台帳へ転記しました`, '処理完了', 5
    );
  } catch (e) {
    console.error(FUNC, e.message, e.stack);
    SpreadsheetApp.getUi().alert(`🚨 ${FUNC} でエラーが発生しました`, e.message, SpreadsheetApp.getUi().ButtonSet.OK);
  }
}

マート更新のエントリーポイント(602_datamart_main.js L157)

function buildBudgetTrendDataMart(overrideBoundary) {
  const FUNC = 'buildBudgetTrendDataMart';
  try {
    // ... 約200行のマート構築ロジック
    ss.toast('🎉 マート更新完了 ...', '完了', 5);
  } catch (e) {
    Utils.logError(FUNC, e);
    SpreadsheetApp.getUi().alert(`🚨 ${FUNC} でエラーが発生しました`, e.message, SpreadsheetApp.getUi().ButtonSet.OK);
  }
}

現在のメニュー構造(101_sys_config.js L299-377)

function onOpen() {
  const ui = SpreadsheetApp.getUi();
  ui.createMenu('📒 経理業務')
    // ... RPA 6種 ...
    .addItem('📋 承認済INVを仕訳台帳へ転記 (Action A)', 'processInvoiceApprovals')   // L311
    .addItem('✅ 消込済STLを仕訳台帳へ転記 (Action B)', 'processSettlementClearings') // L312
    .addToUi();
  // ...
  ui.createMenu('📊 マート更新')
    .addItem('財務3表(P/L・B/S・C/F)の更新', 'buildBudgetTrendDataMart')              // L329
    // ...

修正方針

設計思想

既存の processInvoiceApprovals() / processSettlementClearings() / buildBudgetTrendDataMart() はそのまま維持し、これらを順次呼び出すオーケストレーター関数 runMonthlyClose() を新設する。各関数の内部ロジックには一切手を加えない。

ただし、既存関数は内部で try-catch してエラー時に ui.alert を出す設計のため、オーケストレーターからはエラーの発生を直接検知できない。そこで、各関数の実行前後でデータ状態(TRN行数等)を比較し、処理の進行を確認する。

Step 1: 事前バリデーション関数 preCloseValidation_() の追加

410_subledger_engine.js の末尾にプライベート関数として追加。32_wrk_invoice / 33_wrk_bank を走査し、月次締め前に確認すべき状態を集計する。

/**
 * I-04: 月次締め事前バリデーション
 * @return {{ warnings: string[], pendingActionA: number, pendingActionB: number }}
 */
function preCloseValidation_() {
  var ss = getWebSpreadsheet_();
  var warnings = [];

  // 32_wrk_invoice チェック
  var invSheet = Utils.getSheetByKey('WRK_INVC', '32_wrk_invoice');
  var pendingActionA = 0;
  var unapprovedCount = 0;
  if (invSheet) {
    var invData = invSheet.getDataRange().getValues();
    var invIdx = buildHeaderIndex_(invData[0]);
    var iFlag = invIdx['有効フラグ'];
    var iStatus = invIdx['請求ステータス'];
    var iJnl = invIdx['自動仕訳JNL_ID'];
    for (var i = 1; i < invData.length; i++) {
      if (iFlag !== undefined && (invData[i][iFlag] === false ||
          String(invData[i][iFlag]).toUpperCase() === 'FALSE')) continue;
      var status = String(invData[i][iStatus] || '').trim();
      var jnlId = String(invData[i][iJnl] || '').trim();
      if (status === '承認済' && !jnlId) pendingActionA++;
      if (status === '未処理' || status === '未承認') unapprovedCount++;
    }
  }

  // 33_wrk_bank チェック
  var bankSheet = Utils.getSheetByKey('WRK_BANK', '33_wrk_bank');
  var pendingActionB = 0;
  var unsettledCount = 0;
  if (bankSheet) {
    var bankData = bankSheet.getDataRange().getValues();
    var bankIdx = buildHeaderIndex_(bankData[0]);
    var bFlag = bankIdx['有効フラグ'];
    var bStatus = bankIdx['決済ステータス'];
    var bJnl = bankIdx['自動仕訳JNL_ID'];
    for (var j = 1; j < bankData.length; j++) {
      if (bFlag !== undefined && (bankData[j][bFlag] === false ||
          String(bankData[j][bFlag]).toUpperCase() === 'FALSE')) continue;
      var bStat = String(bankData[j][bStatus] || '').trim();
      var bJnlId = String(bankData[j][bJnl] || '').trim();
      if (bStat === '消込済' && !bJnlId) pendingActionB++;
      if (bStat === '未処理') unsettledCount++;
    }
  }

  if (unapprovedCount > 0)
    warnings.push('⚠️ 未承認INV: ' + unapprovedCount + '件(承認を確認してください)');
  if (unsettledCount > 0)
    warnings.push('⚠️ 未消込STL: ' + unsettledCount + '件(消込を確認してください)');

  return {
    warnings: warnings,
    pendingActionA: pendingActionA,
    pendingActionB: pendingActionB
  };
}

Step 2: オーケストレーター関数 runMonthlyClose() の追加

410_subledger_engine.js の末尾(preCloseValidation_() の後)に追加。

/**
 * I-04: ワンクリック月次締め
 * Action A → Action B → マート更新 を連続実行する
 */
function runMonthlyClose() {
  var FUNC = 'runMonthlyClose';
  var ui = SpreadsheetApp.getUi();
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var summary = [];

  try {
    // Phase 0: 事前バリデーション
    Utils.logInfo(FUNC, '月次締め開始 — 事前チェック');
    var validation = preCloseValidation_();

    // 警告がある場合はユーザーに確認
    if (validation.warnings.length > 0) {
      var warningMsg = '以下の未処理データがあります:\n\n'
        + validation.warnings.join('\n')
        + '\n\nこのまま月次締めを続行しますか?\n'
        + '(未承認・未消込のデータは締め処理の対象外です)';
      var confirm = ui.alert('📋 月次締め — 事前チェック', warningMsg, ui.ButtonSet.YES_NO);
      if (confirm !== ui.Button.YES) {
        Utils.logInfo(FUNC, 'ユーザーによりキャンセル');
        return;
      }
    }

    // Phase 1: Action A(承認済INV→仕訳台帳)
    if (validation.pendingActionA > 0) {
      ss.toast('📋 Step 1/3: Action A 実行中...', '月次締め', 30);
      processInvoiceApprovals();
      summary.push('✅ Action A: ' + validation.pendingActionA + '件のINVを転記');
    } else {
      summary.push('⏭️ Action A: 対象INVなし(スキップ)');
    }

    // Phase 2: Action B(消込済STL→仕訳台帳)
    if (validation.pendingActionB > 0) {
      ss.toast('✅ Step 2/3: Action B 実行中...', '月次締め', 30);
      processSettlementClearings();
      summary.push('✅ Action B: ' + validation.pendingActionB + '件のSTLを転記');
    } else {
      summary.push('⏭️ Action B: 対象STLなし(スキップ)');
    }

    // Phase 3: マート更新
    ss.toast('📊 Step 3/3: マート更新中...', '月次締め', 60);
    buildBudgetTrendDataMart();
    summary.push('✅ マート更新: 財務3表(P/L・B/S・C/F)を再構築');

    // 完了サマリー
    Utils.logInfo(FUNC, '月次締め完了');
    ui.alert('🎉 月次締め完了',
      summary.join('\n') + '\n\n'
      + '環境: ' + Env.name().toUpperCase(),
      ui.ButtonSet.OK);

  } catch (e) {
    Utils.logError(FUNC, e);
    ui.alert('🚨 月次締めでエラーが発生しました',
      '処理済み:\n' + (summary.length > 0 ? summary.join('\n') : '(なし)')
      + '\n\nエラー: ' + e.message
      + '\n\n途中で停止しました。各処理を個別に実行してください。',
      ui.ButtonSet.OK);
  }
}

Step 3: メニューへの追加

101_sys_config.jsonOpen() に新メニュー項目を追加。「📒 経理業務」メニューの先頭に配置する。

ui.createMenu('📒 経理業務')
  .addItem('📊 月次締め実行 (A→B→マート)', 'runMonthlyClose')    // I-04: 追加
  .addSeparator()                                                  // I-04: 追加
  .addItem('🤖 全RPA一括実行(SaaS+HC+CAPEX+FIN+Adhoc)', 'generateAllRpaInvoices')
  // ... 以下変更なし

影響範囲

変更対象変更内容変更量
400_domain/410_subledger_engine.jspreCloseValidation_() + runMonthlyClose() 追加~100行
100_config/101_sys_config.jsメニュー項目2行追加(.addItem + .addSeparator2行
  • 既存動作への影響: なし。既存の processInvoiceApprovals() / processSettlementClearings() / buildBudgetTrendDataMart() は一切変更しない。個別実行も従来通り可能
  • 新規関数はグローバル1つ + プライベート1つ: runMonthlyClose() はメニューから呼び出す公開関数。preCloseValidation_() は末尾アンダースコア付きのプライベート関数

注意事項

  1. GAS 6分実行制限: Action A + Action B + マート更新の合計実行時間が6分を超えるとGASがタイムアウトする。現状の実データ量(INV 数百件、STL 数百件)では問題ないが、データ量が増加した場合は MAS-194(長時間処理の分割実行)と連携が必要
  2. 既存関数の try-catch: Action A/B は内部で try-catch してエラー時に ui.alert を出す。オーケストレーターの catch では捕捉できないが、ui.alert がユーザーに表示されるため実用上問題ない。ただしその場合、後続の処理(Action B やマート更新)は引き続き実行される
  3. 冪等性: Action A/B は JNL_ID の有無で処理済み判定するため、2回実行しても安全(0件処理になるだけ)。マート更新も全データを再構築するため冪等
  4. toast の上書き: 各 Phase で ss.toast() を使って進捗表示するが、既存関数内の toast と上書きし合う。表示は最後の toast が残る(機能上問題なし)
  5. Action A/B の「対象なし」判定: preCloseValidation_() で事前に件数を確認し、0件ならスキップ。ただしバリデーション後にユーザーが別タブでデータを変更する可能性はゼロではない(実用上無視可能)

エッジケース

条件動作理由
未承認INV・未消込STLが存在警告ダイアログ表示 → YES で続行、NO でキャンセル差戻し漏れの防止。未承認・未消込は処理対象外であることを明示
Action A/B の対象が0件スキップしてサマリーに「⏭️ 対象なし」と表示不要な処理を避けてマート更新に進む
Action A 実行中にエラーAction A の ui.alert でエラー表示後、Action B・マート更新が続行Action A の内部 catch がエラーを吸収するため。部分的な処理結果はサマリーに反映
全処理が正常完了サマリーダイアログに各 Phase の結果を一覧表示ユーザーが処理結果を一目で確認できる
GAS 6分タイムアウトGAS ランタイムが強制終了。catch に到達しない現状のデータ量では発生しない。将来的に MAS-194 で対応
事前チェックでキャンセル何も実行しないユーザーが未承認INV等を先に処理するため
dev 環境で実行正常動作。サマリーに「環境: DEV」と表示Env.name() で判定

実データ検証(MCP でのデータ確認が必要な場合)

確認項目確認方法理由
32_wrk_invoice の請求ステータス値MCP で 請求ステータス 列のユニーク値を確認バリデーションの条件(「承認済」「未処理」「未承認」)が実データと一致するか
33_wrk_bank の決済ステータス値MCP で 決済ステータス 列のユニーク値を確認バリデーションの条件(「消込済」「未処理」)が実データと一致するか
3処理の合計実行時間Action A/B/マート更新をそれぞれ計測GAS 6分制限の余裕度を把握

関連ドキュメント

仕様書関連箇所
CLAUDE.md変更時の動作確認テスト: 400_domain/401_bat_rpa.js → HC RPA テスト, 403_subledger_engine.js → Action A → Action B テスト
spec_pl.mdP/L計上のフィルタ条件(承認済・決済完了のみ)
spec_bs.mdB/S計上のフィルタ条件
410_subledger_engine.jsL106: Action A, L352: Action B
602_datamart_main.jsL157: マート更新メインエントリーポイント
dev_mas-094_boundary_month_selector.md基準年月の手動指定(月次締めでは自動計算を使用)

人間が検討すべき事項

  • GAS 6分実行制限への対策: 現状のデータ量では問題ないが、INV/STL が数千件に増加した場合、3処理の合計が6分を超える可能性がある。MAS-194(長時間処理の分割実行)と連携して、タイムアウト時に残りの処理を自動スケジュールする仕組みが将来的に必要(TODO_future.md から転記)
  • 途中エラー時のリカバリー方針: Action A でエラーが発生した場合、Action B やマート更新を続行すべきか中断すべきか。現設計では Action A/B の内部 catch がエラーを吸収するため、実質的に続行される。これが望ましいかはデータの整合性リスクに依存する
  • 事前バリデーションの項目拡張: 将来的に MAS-149(取込進捗ダッシュボード)や MAS-183(月次決算チェックリスト)と統合し、「CC未マッチ件数」「領収書未処理件数」などのチェック項目を追加できる設計になっている

実装プロンプト(Claude Code 用)

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-148「ワンクリック月次締め」を実装してください。

## 実行前タスク
以下のファイルを読み込んでください:
1. `400_domain/410_subledger_engine.js` — `processInvoiceApprovals()` (L106) と `processSettlementClearings()` (L352) の全体構造。エラーハンドリング(try-catch + ui.alert)のパターンを確認
2. `600_report/602_datamart_main.js` — `buildBudgetTrendDataMart()` (L157) の関数シグネチャと完了通知(L368)
3. `100_config/101_sys_config.js` — `onOpen()` (L299) のメニュー構築。「📒 経理業務」メニュー (L301-313) の先頭に追加する
4. `000_infra/004_utils.js` — `Utils.logInfo()` / `Utils.logError()` / `Utils.getSheetByKey()` のシグネチャ
5. `000_infra/001_env.js` — `Env.name()` のシグネチャ
6. `CLAUDE.md`
7. `docs/dev/dev_mas-148_oneclick_monthly_close.md`

## 修正対象ファイル
- `400_domain/410_subledger_engine.js` — 末尾に2関数追加
- `100_config/101_sys_config.js` — メニュー項目追加(2行)

## 実装内容

### A: preCloseValidation_() の追加(410_subledger_engine.js 末尾)
32_wrk_invoice と 33_wrk_bank を走査し、以下を集計するプライベート関数:
- `pendingActionA`: 承認済 かつ JNL_ID 空の INV 件数(Action A 対象)
- `pendingActionB`: 消込済 かつ JNL_ID 空の STL 件数(Action B 対象)
- `warnings`: 未承認INV件数・未消込STL件数の警告メッセージ配列
ヘッダー参照は `buildHeaderIndex_()` を使用(列番号ハードコード禁止)。
有効フラグ=FALSE の行はスキップ。

### B: runMonthlyClose() の追加(410_subledger_engine.js 末尾、preCloseValidation_ の後)
1. `preCloseValidation_()` で事前チェック
2. warnings がある場合は `ui.alert(YES_NO)` で続行確認。NO ならリターン
3. pendingActionA > 0 なら `processInvoiceApprovals()` を実行。0件ならスキップ
4. pendingActionB > 0 なら `processSettlementClearings()` を実行。0件ならスキップ
5. `buildBudgetTrendDataMart()` を引数なしで実行(常に実行)
6. 完了後にサマリーダイアログ(`ui.alert`)で各 Phase の結果を表示
各 Phase の前に `ss.toast()` で進捗を表示(「Step 1/3: Action A 実行中...」等)。
全体を try-catch で囲み、エラー時は処理済み内容 + エラーメッセージを表示。

### C: メニュー追加(101_sys_config.js L301-302)
「📒 経理業務」メニューの先頭(`generateAllRpaInvoices` の前)に追加:
```js
.addItem('📊 月次締め実行 (A→B→マート)', 'runMonthlyClose')
.addSeparator()
```

## 制約
- 既存の `processInvoiceApprovals()` / `processSettlementClearings()` / `buildBudgetTrendDataMart()` は一切変更しない
- `preCloseValidation_()` の列参照は必ずヘッダー名ベース(`buildHeaderIndex_` 使用)
- 有効フラグ=FALSE の行は全処理でスキップ
- 新規関数は `410_subledger_engine.js` の末尾(`processSettlementClearings` 関数の後)に追加

## 動作確認
`npm run push:dev` 後:
1. スプレッドシートをリロードし、「📒 経理業務」メニューの先頭に「📊 月次締め実行 (A→B→マート)」が表示されること
2. 未承認INVまたは未消込STLがある状態で実行 → 警告ダイアログが表示されること
3. 警告ダイアログで「いいえ」 → 何も実行されないこと
4. 警告ダイアログで「はい」(または警告なし) → Action A → Action B → マート更新が順次実行されること
5. 完了後にサマリーダイアログが表示され、各 Phase の結果(処理件数 or スキップ)が確認できること
6. Action A/B の対象が0件の場合 → スキップされてマート更新のみ実行されること
7. 2回連続実行 → 2回目は Action A/B とも「対象なし(スキップ)」になること(冪等性)

### 拡張思考の使用状況

| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| preCloseValidation_ 実装 | なし | 32/33タブの走査 + 条件カウントの単純ロジック |
| runMonthlyClose 実装 | なし | 既存3関数の順次呼び出し + UI制御 |
| メニュー追加 | なし | addItem + addSeparator の2行追加 |

推奨実行モデル

工程推奨モデル理由
仕様書作成(本ドキュメント)Claude Opus 4.6既存3関数のエラーハンドリングパターン分析、オーケストレーター設計の判断が必要
実装Claude Haiku 4.5仕様書でコードが完全に定義済み。2関数追加 + メニュー2行の機械的作業
動作確認ユーザー手動GASエディタでのメニュー操作・ダイアログ確認が必要

変更履歴

日付変更内容
2026-04-16初版作成
2026-04-16テンプレート準拠で全面改訂。コード調査に基づき行番号・エラーハンドリングパターンを反映。事前バリデーション・エッジケース・実データ検証セクション追加