概要

項目内容
案件IDMAS-114
カテゴリバリデーション追加(電帳法・消費税)
PhaseP2
優先度★★
所要時間3〜4時間
対象ファイル100_config/101_sys_config.jsDDL変更)/ 200_data/202_repository.js(PartnerRepository 追記)/ 400_domain/412_invoice_validator.js(新規作成)/ templates/operations_sidebar.html(メニューボタン追加)
参照ファイル000_infra/002_constants.jsCOLORS.WARN_RED_BG / COLORS.WHITE)/ 000_infra/003_contracts.jsInvoiceDTO @typedef)/ 000_infra/004_utils.jsUtils.toastResult / Utils.logError / Utils.auditLog / Utils.getSheetByKey
前提案件MAS-120(取引先マスタ拡張)のT番号列追加と重複しないこと。既に MAS-120 で登録番号(T番号)列が追加されている場合は Step 1 の 12_mst_partner 列追加をスキップする

目的

インボイス制度(適格請求書等保存方式)における仕入税額控除の要件を自動検証し、インボイス不備による税額控除否認リスクを防止する。具体的には以下の2観点を自動チェックし、NG 行をハイライト表示する(Human-in-the-Loop:修正は人間が行う):

  1. T番号(適格請求書発行事業者登録番号)の形式検証: 12_mst_partner に登録された T番号が /^T\d{13}$/(T + 半角数字13桁)に一致するか
  2. 税率計算の整合性検証: 32_wrk_invoice の「税抜金額_計画」と「消費税額_計画」から算出した実効税率が、標準税率(10%) または軽減税率(8%) の許容誤差±0.2% 以内に収まるか

35_wrk_receipt(領収書OCR)側にも T番号記録フィールドは存在するが、本機能は会計サブ元帳(INV)側の事後検証を目的とする。領収書OCR結果は別の入力経路として扱い、本機能では 32_wrk_invoice の「取引先名」と 12_mst_partner「登録番号(T番号)」の突合でT番号を解決する。

現在のコード

新規実装(既存のインボイス検証ロジックなし)

Phase 1 調査の結果、12_mst_partner および 32_wrk_invoice にはインボイス制度に関する検証ロジックが実装されていない。35_wrk_receipt.T番号 はOCR取込時に書き込まれるが、INV 起票後に形式検証を行う処理は存在しない。したがって本案件は以下3箇所の新規追加となる:

  • DDL: 12_mst_partner / 32_wrk_invoice に列追加
  • Repository: PartnerRepository を新設(既存 AccountRepository と同パターン)
  • Domain: 400_domain/412_invoice_validator.js を新規作成

メニュー登録の現状(100_config/101_sys_config.js L299-L308)

本プロジェクトのメニューは onOpen() で top-level メニューを 1 項目のみ登録し、実際の操作は templates/operations_sidebar.html のサイドバーに集約されている(MAS-085 / MAS-091 等と同じ運用)。したがって MAS-114 のエントリポイントは templates/operations_sidebar.html§⚙️ メンテナンス セクションへボタンを 1 行追加する方式を採用する。

// 100_config/101_sys_config.js L299-308
function onOpen() {
  const ui = SpreadsheetApp.getUi();
  // 全操作は右側サイドバーに集約 (狭い画面での top-level メニュー切り詰めを回避)
  ui.createMenu('🚀 BizLP')
    .addItem('操作パネルを開く', 'openOperationsSidebar')
    .addSeparator()
    .addItem('✅ 自動起動を有効化', 'installAutoOpenSidebarTrigger')
    .addItem('🚫 自動起動を無効化', 'uninstallAutoOpenSidebarTrigger')
    .addToUi();
}

サイドバー側の現状(templates/operations_sidebar.html L71-L76 抜粋):

<div class="section">
  <h3>⚙️ メンテナンス</h3>
  <button class="btn" onclick="run('runDataValidation', this)">✅ データ整合性チェック</button>
  <button class="btn warn" onclick="run('cleanupOrphanTrn', this)">🧹 孤立TRN削除</button>
  <button class="btn warn" onclick="run('cleanupDuplicateRows', this)">🧹 重複行削除</button>
  <button class="btn" onclick="run('checkInvStlConsistency', this)">🔎 INV/STL整合性</button>
</div>

修正方針

本機能は 3 Step 構成で実装する。Step 間の依存関係は Step 1 → Step 2 → Step 3 の一方向のみ。

Step 1: DDL 変更(100_config/101_sys_config.js

setupAllSchemas() 内 L643-L668 の schemas オブジェクトに以下の変更を加える。DDL 変更は増分適用(MAS-134)の恩恵を受けるため、列追加時は既存データは保持される。

(a)MST_PART12_mst_partner)の headers 配列末尾に「登録番号(T番号)」を追加

// 変更前(L645)
'MST_PART': { headers: ["有効フラグ","取引先コード","法人番号","略称_4文字","取引先名_正式","略称","銀行摘要名","UI用取引先名","取引先区分"], color: "#666666" },

// 変更後
'MST_PART': { headers: ["有効フラグ","取引先コード","法人番号","略称_4文字","取引先名_正式","略称","銀行摘要名","UI用取引先名","取引先区分","登録番号(T番号)"], color: "#666666" },

(b)WRK_INVC32_wrk_invoice)の headers 配列末尾に「インボイスチェック結果」「チェック結果詳細」を追加

// 変更前(L654 末尾)
..."自動仕訳JNL_ID","決済日_実績"], color: "#1155cc" },

// 変更後
..."自動仕訳JNL_ID","決済日_実績","インボイスチェック結果","チェック結果詳細"], color: "#1155cc" },

(c)setVali('MST_PART', 9, 'N', '12_mst_partner') の修正不要

L1215 の 取引先区分 → UI取引先区分 のプルダウン定義は列番号 9 固定で問題なし(登録番号は末尾の列 10 に追加されるため既存マッピングに影響しない)。

Step 2: PartnerRepository の新規実装(200_data/202_repository.js 末尾に追記)

AccountRepository(L301-L350)と同パターンで実装する。InvoiceRepository / OrderRepository の直後に AccountRepository が定義されているため、本 Repository は AccountRepository の後ろ(L350 以降)に追記する。

// =====================================================================
// PartnerRepository — 12_mst_partner (読み取り専用マスタ)
// =====================================================================

var PartnerRepository = {

  /** @private */
  _getSheet: function() {
    return Utils.getSheetByKey('MST_PART', '12_mst_partner');
  },

  /**
   * 全取引先マスタレコードを DTO 配列で取得する。
   * @returns {{ headers: string[], dtos: Object[] }}
   */
  findAll: function() {
    return readSheetAsDtos_(PartnerRepository._getSheet());
  },

  /**
   * 取引先名をキーとする「登録番号(T番号)」マップを取得する(キャッシュ付き)。
   * 有効フラグ=FALSE の行はスキップ。登録番号が空の取引先も map に入れない(未登録扱い)。
   * @returns {Object.<string, string>} { 取引先名: "T0000000000000" }
   */
  findAsTNumberMap: function() {
    if (PartnerRepository._cache) return PartnerRepository._cache;
    var result = PartnerRepository.findAll();
    var map = {};
    for (var i = 0; i < result.dtos.length; i++) {
      var dto = result.dtos[i];
      var flag = dto['有効フラグ'];
      if (flag === false || String(flag).toUpperCase() === 'FALSE') continue;
      var name = String(dto['取引先名_正式'] || '').trim();
      var tnum = String(dto['登録番号(T番号)'] || '').trim();
      if (name && tnum) map[name] = tnum;
    }
    PartnerRepository._cache = map;
    return map;
  },

  /** @private */
  _cache: null,

  /** キャッシュをリセットする */
  resetCache: function() {
    PartnerRepository._cache = null;
  },
};

キーとする列名の選定: 32_wrk_invoice.取引先名 と対応するマスタ列は 12_mst_partner.取引先名_正式。現行 Utils.normalizePartnerName()取引先名_正式 で照合しているため本 Repository も 取引先名_正式 をキーとする。

Step 3: InvoiceValidator の新規実装(400_domain/412_invoice_validator.js 新規作成)

ファイル番号の採番根拠: ls 400_domain/ で確認した実在ファイルは 400-407, 410, 420。他仕様書の予約状況(MAS-090 = 408, MAS-100 = 409, MAS-104 = 411)を踏まえ、本案件は 412 を採用する。

公開関数 runInvoiceValidation() を実装する。処理フロー:

runInvoiceValidation()
  ├─ [1] LockService.getScriptLock() で排他制御(tryLock 10秒)
  │     └─ 取得失敗時は ui.alert() で通知して return
  ├─ [2] InvoiceRepository.findAll() で { headers, dtos } を取得
  ├─ [3] PartnerRepository.findAsTNumberMap() でT番号マップを取得
  ├─ [4] dtos をループして検証ロジックを適用(有効フラグ=FALSE はスキップ)
  │     ├─ 税区分フィルタ: 非課税 / 対象外 → "対象外" としてマーク
  │     ├─ T番号ルックアップ(取引先名 → T番号)
  │     ├─ 未登録 → "要確認(T番号未登録)" としてマーク
  │     ├─ T番号形式チェック(/^T\d{13}$/)
  │     │   └─ 不一致 → "NG" + 「T番号形式不正」メッセージ
  │     └─ 税率計算
  │         ├─ 税抜=0 かつ 税額=0 → "OK"
  │         ├─ 税抜=0 かつ 税額≠0 → "NG"(ゼロ除算ガード)
  │         └─ 消費税額 / 税抜金額 の実効税率を 8%±0.2% / 10%±0.2% と照合
  ├─ [5] InvoiceRepository.save(dtos) で全置換書き戻し
  ├─ [6] save() 後にシートを再取得し、結果列の行ごとに setBackground()
  │     ├─ NG / 要確認 → Constants.COLORS.WARN_RED_BG
  │     └─ OK / 対象外 → Constants.COLORS.WHITE(前回実行の残留色を除去)
  ├─ [7] Utils.auditLog('RUN', '32_wrk_invoice', '', '', 'runInvoiceValidation', '', 'NG='+ngCount+' 要確認='+warnCount, '')
  ├─ [8] Utils.toastResult('runInvoiceValidation', 'チェック完了: NG ' + ngCount + '件, 要確認 ' + warnCount + '件')
  └─ finally: lock.releaseLock()

メニュー登録: templates/operations_sidebar.html§⚙️ メンテナンス セクション(既存 checkInvStlConsistency ボタン直後)に 1 行追加:

<button class="btn" onclick="run('runInvoiceValidation', this)">🧾 インボイス要件チェック</button>

影響範囲

項目内容
変更ファイル数4(DDL × 1、Repository × 1、Domain 新規 × 1、テンプレート × 1)
コード追加量概算 200 行(Domain 120 / Repository 40 / DDL 2 / HTML 1)
既存動作への影響InvoiceRepository.save() は既存の全請求データを上書きする。LockService 排他制御必須
スキーマ変更あり:12_mst_partner +1 列 / 32_wrk_invoice +2 列。初回デプロイ前に setupAllSchemas() 実行必須
既存機能との競合なし:DDL の列追加のみ(既存列の削除・順序変更なし)
パフォーマンス1回の実行で全 INV 行をスキャン。現状データ量(〜数百行)で数秒以内

注意事項

  1. 排他制御は LockService.getScriptLock() を使用するPropertiesService.getScriptProperties() は Env モジュール経由で環境依存値取得専用であり、ロック取得には使用しない(失敗パターン #4 参照)
  2. DDL 変更(setupAllSchemas() 実行)は本機能の初回デプロイ前に必ず実施する。列未追加の状態で本バリデーターを実行すると headers.indexOf('インボイスチェック結果') が -1 を返し、結果の書き込み先が存在せず save() 時に捨てられる
  3. InvoiceRepository.save() は全行を置換するため、実行前に LockService 排他制御が必須。他の RPA 起票処理(Action A/B)と同時実行されると最新書き込みが上書きされる
  4. writeDtosToSheet_()clearContent() のみでフォーマット(背景色・フォント色)を消去しない(200_data/202_repository.js L45)。save() 後に都度 setBackground() でリセットしないと前回実行時の WARN_RED_BG が残存し、混乱の元になる
  5. 列参照はヘッダー名ベースheaders.indexOf())で行う。列番号ハードコード禁止(CLAUDE.md コーディング規約)
  6. 有効フラグ=FALSE の行は全処理でスキップする(CLAUDE.md コーディング規約)。検証結果列も更新しない(既存値を保持)
  7. 自動修正は行わない(Human-in-the-Loop)。検証結果・詳細メッセージの書き込みと背景色ハイライトのみ。T番号の自動補完・税額の自動修正は禁止
  8. T番号形式は /^T\d{13}$/(T + 半角数字13桁)。全角の「T」や半角小文字の「t」は不一致扱いとする(適格請求書発行事業者公表サイトと同じ仕様)
  9. メニュー項目名・関数名・シート名は Read で実在確認した文字列のみ使用する。類似名の推測・造語禁止(失敗パターン #18-#20 参照)
  10. 税率判定の許容誤差は ±0.2%(丸め誤差・端数処理の吸収用)。より厳密な判定が必要な場合は 03_sys_params にパラメータ化する拡張を検討する(初版では固定値)

エッジケース

本案件は「検証結果の提示」に限定し自動修正を行わないため、各エッジケースは「チェック結果 + 詳細メッセージ + 背景色」の3属性で表現する。

#条件チェック結果チェック結果詳細の記載例背景色理由
E01有効フラグ = FALSE(処理スキップ)更新しない変更なし無効行は全処理でスキップ(CLAUDE.md 規約)
E02税区分 = "非課税" または "対象外"対象外—(空欄)WHITE仕入税額控除の対象外であり T番号・税率いずれも検証不要
E03税区分 = "課税" かつ 取引先名 = 空要確認取引先名が未入力ですWARN_RED_BGT番号ルックアップ不可。取引先名の入力漏れを通知
E04取引先名が T番号マップに未登録要確認T番号未登録(取引先: 「{取引先名}」)WARN_RED_BGインボイス未対応事業者の可能性あり。自動修正禁止。人間が 12_mst_partner を確認
E05登録済みだが T番号が /^T\d{13}$/ に不一致NGT番号形式不正(登録値: 「{実値}」)WARN_RED_BG適格請求書発行事業者番号は「T + 半角数字13桁」。国税庁公表サイトと同じ仕様
E06税抜金額 = 0 かつ 消費税額 = 0OK—(空欄)WHITEゼロ取引は適格と扱う(実務上は仕訳振替等で発生)
E07税抜金額 = 0 かつ 消費税額 ≠ 0NG税抜ゼロで税額あり(税額: {値}円)WARN_RED_BGゼロ除算回避のため税率計算をスキップ。入力不正
E08税抜金額 < 0 かつ 消費税額 < 0(返品・赤伝)OK(税率計算可)—(空欄)WHITE絶対値で税率計算(Math.abs(税額 / 税抜))し 8%/10% 判定
E09消費税額 / 税抜金額 が 10%±0.2% に収まるOK—(空欄)WHITE標準税率の正常値(許容誤差は丸め吸収用)
E10消費税額 / 税抜金額 が 8%±0.2% に収まるOK—(空欄)WHITE軽減税率の正常値
E11上記いずれの範囲にも収まらないNG税率不一致(計算値: {n.n}%)WARN_RED_BG消費税率の誤入力が疑われる。人間が確認して修正
E12税抜金額 or 消費税額 が数値でない(文字列・NaN)NG金額が数値でありませんWARN_RED_BGUtils.parseAmt() でパース失敗時の防御
E13登録番号(T番号) 列が 12_mst_partner に未追加(DDL 未実行)(実行中断)dtos[0] から 登録番号(T番号)hasOwnProperty で検査し、false なら ui.alert('DDL未実行。setupAllSchemas を先に実行してください') で return列未追加で実行すると全行「要確認」になり検知の意味を失うため先に中断する
E14「インボイスチェック結果」「チェック結果詳細」列が 32_wrk_invoice に未追加(DDL 未実行)(実行中断)E13 と同様に headers.indexOf() が -1 なら ui.alert して returnsave() しても書き込み先が無いため事前中断
E15既に NG / 要確認が書き込まれた行の再実行上書き更新—(上書き)新しい結果に応じた色毎回全行スキャンして上書き。save() 前に setBackground(Constants.COLORS.WHITE) で全行リセットし残留色を除去
E16同一取引先名が 12_mst_partner に複数行存在(有効フラグ=TRUE)最後に読み込んだ値で上書きfindAsTNumberMap() のループで後勝ち。人間側で重複解消を促すため Utils.logInfo でログ出力
E17LockService.tryLock(10000) で取得失敗(実行中断)ui.alert('別の処理が実行中です。しばらく待ってから再実行してください。') で returnAction A/B・RPA 起票と競合している可能性あり

実データ検証

以下は初回デプロイ前に MCP (Google Sheets MCP 経由) で事前確認すべき項目。Phase 1 では「既存コードにインボイス検証ロジックが無いこと」までは Grep で確認済みだが、実データの表記揺れや既存列の有無までは未確認。

  1. 12_mst_partner に「登録番号(T番号)」列が既に存在するか確認する

    • 既に MAS-120(取引先マスタ拡張)等で追加済みの可能性。setupAllSchemas() の DDL 適用は冪等だが、列順・列名が完全一致しない場合はリネーム処理が走る(L706-L713 の RENAME_MAP
  2. 32_wrk_invoice の「税区分」列の実際の格納値を確認する

    • InvoiceDTO 定義では "課税" | "非課税" | "対象外" の 3 値だが、実データでは「課税10%」「課税8%」「課税(軽減)」等の表記揺れの可能性。表記揺れが確認された場合は normalizeTaxCategory_(val) で正規化するか、String(val).indexOf('課税') === 0 のような前方一致判定に変更する
  3. テスト用データの存在確認

    • 以下4パターンが実データに存在するか、存在しない場合は手動投入する
      • 正常系: 取引先マスタに T番号登録済み、税率 10%
      • T番号未登録: 取引先マスタに登録されているが「登録番号(T番号)」列が空
      • T番号形式不正: ABC123T12345 等の形式不正値
      • 税率不一致: 税抜金額 100,000 / 消費税額 9,500(9.5% ≠ 10% ≠ 8%)
      • 税区分=対象外: 仕訳振替や減価償却等
  4. InvoiceDTO.取引先名12_mst_partner.取引先名_正式 の突合率を確認する

    • Utils.normalizePartnerName()略称 / 取引先名_正式 の両方で照合するが、本機能では 取引先名_正式 のみをキーとする。既存 INV の多くが略称のみで起票されている場合はマッチ率が低下する。実データを確認し、必要なら 略称 もキーとしてマップに登録する方針を再検討

関連ドキュメント

ドキュメント関連箇所
docs/dev/dev_mas-075_expense_date_validation.mdバリデーター系仕様書のフォーマット参考元
docs/dev/dev_mas-085_consistency_check.md整合性チェック + 背景色ハイライトのパターン参考元
docs/dev/dev_mas-086_tax_category_suggester.md消費税率パラメータを 03_sys_params 経由で取得する拡張の参考
docs/dev/dev_mas-089_consumption_tax_exclusive_method.md税抜方式切替との整合(課税事業者移行時の前提)
docs/dev/dev_mas-120_partner_payment_terms_autofill.md取引先マスタ拡張。T番号列の先行追加可能性
docs/spec/spec_receipt_import.mdOCR取込時の T番号抽出(35_wrk_receipt.T番号)
docs/_internal/failure_patterns.md失敗パターン #2(ゼロ除算)/ #18-#20(メニュー名・関数名・シート名の実在確認)
CLAUDE.mdデータアクセス規約(列参照ヘッダー名ベース・有効フラグ=FALSE スキップ)/ ファイル番号体系

人間が検討すべき事項

  1. 課税事業者への移行時期(TODO_future.md より転記): 本機能は適格請求書受領側(仕入税額控除)の検証を目的とする。自社がインボイス発行側となるタイミング(MAS-089 と連動)で、本機能の位置づけを「受領検証のみ」か「発行時の自社T番号検証」にも拡張するかを決定する必要がある。

  2. 許容誤差 0.2% の妥当性: 丸め誤差・端数処理を吸収するため ±0.2% としたが、大口取引では絶対額の乖離が許容可能な誤差を超える可能性がある。将来は「許容誤差を % と 絶対額(円) の OR 条件」に拡張するか、03_sys_params でパラメータ化する。

  3. T番号マップのキー選定: 初版は 取引先名_正式 をキーとするが、実運用では INV 起票時に「略称」だけが入力される場合がある。将来的に Utils.normalizePartnerName() 経由で 略称 / 取引先名_正式 両対応とするかを判断する(実データ検証の結果次第)。

  4. 自動修正の是非: 本初版は Human-in-the-Loop(検証結果の表示のみ)としたが、将来的に「T番号マップから自動補完」「税額の自動再計算」を検討する場合は、監査ログ・承認フロー(MAS-104 ワークフロー連携)との整合性を要検討。

  5. 国税庁公表データとの連携(将来拡張): T番号の形式チェックのみで「実在する登録事業者か」は検証しない。国税庁の適格請求書発行事業者公表サイト API(https://www.invoice-kohyo.nta.go.jp/)との連携は将来課題とする。

  6. 軽減税率 8% の扱い: 現状は「食料品・新聞等」向けの想定だが、自社の取引でどの程度発生するかは不明。実データで 8% 取引の有無を確認し、少ないようなら初期版では 10% のみ検証として簡略化することも検討可能。

  7. 「要確認」と「NG」の区別の妥当性: T番号未登録(E04)は「要確認」=相手先がインボイス未対応の可能性あり、T番号形式不正(E05)は「NG」=データ不整合、と区別した。運用開始後にチェック結果詳細をレビューし、区別が混乱を招く場合は「NG」に統合することも検討する。

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

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-114「インボイス適格請求書の要件チェック」を実装してください。
3 Step に分けて実装します。

## 実行前タスク
- `100_config/101_sys_config.js` を Read: `setupAllSchemas()` 内の 12_mst_partner と 32_wrk_invoice のスキーマ定義箇所(L643-L668 の `schemas` オブジェクト・行番号)、`onOpen()` のメニュー登録パターン(L299-L308)、サイドバー方式(`openOperationsSidebar()` 経由で `templates/operations_sidebar.html` を描画)であることを確認する
- `templates/operations_sidebar.html` を Read: `§⚙️ メンテナンス` セクション(L71-L76)の実在ボタン(`runDataValidation` / `cleanupOrphanTrn` / `cleanupDuplicateRows` / `checkInvStlConsistency`)を確認し、命名・絵文字の規則を把握する
- `200_data/202_repository.js` を Read: `AccountRepository.findAsMap()` の実装(L304-L350、PartnerRepository のひな型)と `InvoiceRepository.save(dtos)` の引数形式(DTO オブジェクトの配列を受け取り `writeDtosToSheet_()` でシートを全置換)を確認する
- `000_infra/002_constants.js` を Read: `Constants.COLORS.WARN_RED_BG = '#f4cccc'` と `Constants.COLORS.WHITE = '#FFFFFF'` の値を確認する
- `000_infra/003_contracts.js` を Read: `InvoiceDTO` のプロパティ名(`有効フラグ` / `税区分` / `税抜金額_計画` / `消費税額_計画` / `取引先名`)を確認する
- `000_infra/004_utils.js` を Read: `Utils.getSheetByKey('MST_PART', '12_mst_partner')` が使用可能であること(L349 で既使用)、`Utils.toastResult` / `Utils.auditLog` / `Utils.parseAmt` のシグネチャを確認する
- `ls 400_domain/` を Bash で実行し、412 が空きであること(400-407, 410, 420 のみ実在。408-409-411 は他案件の予約済のため衝突回避)を確認する

## 修正対象ファイル
- `100_config/101_sys_config.js`: DDL スキーマへの列追加(L645, L654)
- `200_data/202_repository.js`: `PartnerRepository` を末尾(L350 以降)に追記
- `400_domain/412_invoice_validator.js`: 新規作成
- `templates/operations_sidebar.html`: `§⚙️ メンテナンス` セクションにボタン 1 行追加

## 実装内容

### Step 1: DDL 変更(`100_config/101_sys_config.js`)
Read で確認した L645 / L654 を編集する。

**(a)L645 `MST_PART` の headers 末尾に「登録番号(T番号)」を追加**
既存末尾 `"取引先区分"` の直後にカンマ区切りで `"登録番号(T番号)"` を追加。

**(b)L654 `WRK_INVC` の headers 末尾に「インボイスチェック結果」「チェック結果詳細」を追加**
既存末尾 `"決済日_実績"` の直後にカンマ区切りで `"インボイスチェック結果","チェック結果詳細"` を追加。

**(c)`onOpen()` は変更不要**
本プロジェクトは top-level メニューを 1 項目のみ登録し操作はサイドバーに集約する方式。エントリポイントは後述 Step 3 の `templates/operations_sidebar.html` に追加する。メニュー名を推測・造語しない。

### Step 2: `PartnerRepository` 新規追記(`200_data/202_repository.js` 末尾)
`AccountRepository`(L304-L350)と同パターンで実装:

- `_getSheet()`: `Utils.getSheetByKey('MST_PART', '12_mst_partner')` を返す(Read で確認済みの実在キー)
- `findAll()`: `readSheetAsDtos_(PartnerRepository._getSheet())` を返すのみ
- `findAsTNumberMap()`: キャッシュ付き。有効フラグ=FALSE をスキップ。`取引先名_正式` をキー、`登録番号(T番号)` を値とする Object を返す。両方が非空の行のみ map に入れる。`_cache: null` と `resetCache()` も実装する

### Step 3: `InvoiceValidator` 新規作成(`400_domain/412_invoice_validator.js`)

**ファイル冒頭のコメント**:
```
// ==========================================
// InvoiceValidator — インボイス適格請求書の要件チェック (S-42)
// ==========================================
// 仕入税額控除の要件を自動検証し、T番号形式・税率計算の不整合を検知する。
// Human-in-the-Loop: 自動修正は行わず、検証結果の表示・ハイライトのみ。
```

**公開関数 `runInvoiceValidation()`**:

1. `var lock = LockService.getScriptLock();` + `if (!lock.tryLock(10000)) { SpreadsheetApp.getUi().alert('別の処理が実行中です。しばらく待ってから再実行してください。'); return; }` で排他制御

2. `try { ... } finally { lock.releaseLock(); }` で囲む

3. データ取得:
   - `var result = InvoiceRepository.findAll();`
   - `var headers = result.headers;`
   - `var dtos = result.dtos;`
   - `var tMap = PartnerRepository.findAsTNumberMap();`

4. DDL 未実行ガード(E13/E14):
   - `if (headers.indexOf('インボイスチェック結果') === -1 || headers.indexOf('チェック結果詳細') === -1) { SpreadsheetApp.getUi().alert('DDL未実行。setupAllSchemas を先に実行してください。'); return; }`

5. 検証ロジックのループ:
   ```js
   var T_REGEX = /^T\d{13}$/;
   var ngCount = 0, warnCount = 0, okCount = 0;
   for (var i = 0; i < dtos.length; i++) {
     var dto = dtos[i];
     var flag = dto['有効フラグ'];
     if (flag === false || String(flag).toUpperCase() === 'FALSE') continue;
     var taxCat = String(dto['税区分'] || '').trim();
     var partner = String(dto['取引先名'] || '').trim();
     var net = Utils.parseAmt(dto['税抜金額_計画']);
     var tax = Utils.parseAmt(dto['消費税額_計画']);
     var result, detail;
     // 税区分フィルタ
     if (taxCat === '非課税' || taxCat === '対象外') {
       result = '対象外'; detail = '';
     } else if (!partner) {
       result = '要確認'; detail = '取引先名が未入力です';
     } else if (!tMap.hasOwnProperty(partner)) {
       result = '要確認'; detail = 'T番号未登録(取引先: 「' + partner + '」)';
     } else if (!T_REGEX.test(tMap[partner])) {
       result = 'NG'; detail = 'T番号形式不正(登録値: 「' + tMap[partner] + '」)';
     } else if (net === 0 && tax === 0) {
       result = 'OK'; detail = '';
     } else if (net === 0 && tax !== 0) {
       result = 'NG'; detail = '税抜ゼロで税額あり(税額: ' + tax + '円)';
     } else {
       var rate = Math.abs(tax / net);
       if (Math.abs(rate - 0.10) <= 0.002) { result = 'OK'; detail = ''; }
       else if (Math.abs(rate - 0.08) <= 0.002) { result = 'OK'; detail = ''; }
       else { result = 'NG'; detail = '税率不一致(計算値: ' + (rate * 100).toFixed(1) + '%)'; }
     }
     dto['インボイスチェック結果'] = result;
     dto['チェック結果詳細'] = detail;
     if (result === 'NG') ngCount++;
     else if (result === '要確認') warnCount++;
     else okCount++;
   }
   ```

6. 書き戻し: `InvoiceRepository.save(dtos);`

7. 背景色リセット + ハイライト:
   - `var sheet = Utils.getSheetByKey('WRK_INVC', '32_wrk_invoice');`
   - `var resultCol = headers.indexOf('インボイスチェック結果') + 1;` / `var detailCol = headers.indexOf('チェック結果詳細') + 1;`
   - 最終データ行数を取得(`dtos.length`)
   - 全データ行をまず白で塗りつぶし: `sheet.getRange(2, resultCol, dtos.length, 2).setBackground(Constants.COLORS.WHITE);`
   - 次に NG / 要確認の行のみ赤でマーク: `for (i = 0; i < dtos.length; i++) { var r = dtos[i]['インボイスチェック結果']; if (r === 'NG' || r === '要確認') { sheet.getRange(i + 2, resultCol, 1, 2).setBackground(Constants.COLORS.WARN_RED_BG); } }`
   - 有効フラグ=FALSE の行はスキップしたため結果が空のまま。前回実行の色は WHITE で上書き済みなので残留なし

8. 監査ログ: `Utils.auditLog('RUN', '32_wrk_invoice', '', 'インボイスチェック結果', 'runInvoiceValidation', '', 'NG=' + ngCount + ' 要確認=' + warnCount + ' OK=' + okCount, '');`

9. 完了通知: `Utils.toastResult('runInvoiceValidation', 'チェック完了: NG ' + ngCount + '件, 要確認 ' + warnCount + '件');`

10. エラーハンドリング: `try {...}` の中で例外が発生したら `Utils.logError('runInvoiceValidation', e);` + `SpreadsheetApp.getUi().alert('エラー: ' + e.message);` でユーザーに通知し、`finally` でロック解放

### Step 3.5: メニュー登録(`templates/operations_sidebar.html`)

`§⚙️ メンテナンス` セクション(L71-L76)の `checkInvStlConsistency` ボタン直後に 1 行追加:

```html
<button class="btn" onclick="run('runInvoiceValidation', this)">🧾 インボイス要件チェック</button>
```

## 制約
- `LockService.getScriptLock()` を使用すること。`PropertiesService` はロック取得に使用しない(Env モジュール経由で環境依存値取得専用)
- 列番号ハードコード禁止(`headers.indexOf()` 使用)
- 有効フラグ=FALSE の行は処理をスキップ
- データの自動修正禁止(Human-in-the-Loop: 検証結果の表示・ハイライトのみ)
- メニュー名・関数名・シート名はコードを Read して実在する文字列のみ使用する(失敗パターン #18-#20 参照)
- ファイル番号 412 は `ls 400_domain/` で事前に空きを確認してから使用する(他案件の予約との衝突回避)

## エッジケース
仕様書「## エッジケース」テーブル(E01-E17)を参照。

## 動作確認
1. `npm run push:dev` でデプロイ
2. GAS エディタで `setupAllSchemas()` を実行し、`12_mst_partner` に「登録番号(T番号)」列、`32_wrk_invoice` に「インボイスチェック結果」「チェック結果詳細」列が追加されたことを確認
3. `12_mst_partner` にサンプルデータを入力:
   - 正常系: 取引先名_正式「適格事業者A」、登録番号「T1234567890123」
   - 形式不正: 取引先名_正式「不正事業者B」、登録番号「ABC123」
   - 未登録: 取引先名_正式「未対応事業者C」、登録番号を空のまま
4. `32_wrk_invoice` に以下の各パターンのテストデータを用意:
   - 課税(税率10%)・取引先名「適格事業者A」・税抜10000/税額1000 → OK
   - 課税(税率10%)・取引先名「不正事業者B」 → NG(T番号形式不正)
   - 課税(税率10%)・取引先名「未対応事業者C」 → 要確認(T番号未登録)
   - 課税(税率10%)・取引先名「適格事業者A」・税抜10000/税額950 → NG(税率不一致 9.5%)
   - 課税(軽減8%)・取引先名「適格事業者A」・税抜10000/税額800 → OK
   - 非課税・適格事業者A → 対象外
   - 有効フラグ=FALSE → スキップ(更新なし)
5. サイドバーから「🧾 インボイス要件チェック」を実行し、各行に正しいチェック結果・詳細が書き込まれることを確認
6. NG / 要確認 行の「インボイスチェック結果」「チェック結果詳細」列に `#f4cccc` の背景色、OK / 対象外 行は白背景が設定されていることを確認
7. テストデータの「不正事業者B」の登録番号を `T1234567890124` に修正して再実行し、前回の NG 背景色が白に正しくリセットされ結果が OK に変わることを確認
8. `98_audit_log` を開き、`RUN` / `runInvoiceValidation` の行が記録されていることを確認
9. `npm run push:prod` で本番デプロイ
10. 本番側でも `setupAllSchemas()` 実行 → 初回検証実行 → 結果確認

### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| 実行前タスク(Read/調査)| あり | DDL 挿入位置・PartnerRepository ひな型の確定 |
| Step 1 DDL 変更 | なし | 指定位置への文字列追加のみ |
| Step 2 PartnerRepository | なし | AccountRepository の横展開。完全定義済み |
| Step 3 InvoiceValidator | 最小限 | 仕様書の処理フロー・ガード節を書き下すのみ |
| Step 3.5 HTML ボタン追加 | なし | 1 行追加のみ |

推奨実行モデル

工程推奨モデル理由
Step 1: DDL 変更Claude Haiku既存スキーマ配列末尾への文字列追加のみ。判断要素なし
Step 2: PartnerRepository 追記Claude HaikuAccountRepository と同パターンの横展開。コード完全定義済み
Step 3: InvoiceValidator 新規作成Claude Sonnet複数 Repository 統合 + LockService + Range フォーマット操作 + ガード節の網羅
Step 3.5: サイドバー HTML ボタン追加Claude Haiku1 行追加のみ

変更履歴

日付変更内容
2026-04-19初版作成

仕様書作成プロンプト(再現性・監査性のため必ず記録)

展開して表示
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
1. **拡張思考の使い分け**: Phase 1(設計)ではフル活用し、ファイル名・関数名・行番号・エッジケース一覧・影響範囲をここで完全確定させる。Phase 2(清書)では各 Step 内の拡張思考を最小限に抑え、Phase 1 確定内容の書き下しに徹する。出力途中で再考しない。
2. **テキスト報告の禁止**: 「〜を作成します」等の text のみで tool_use なしに turn を終了しない。説明は 1 文以内。直ちに tool を呼ぶ。
3. **4-5 分割の Write/Edit 実行**: 2-1(骨格 ~20行)/ 2-2(概要〜注意事項 ~300行)/ 2-3a(エッジケース〜人間検討事項 ~200行)/ 2-3b(実装プロンプト〜変更履歴 ~250行)/ 2-4(`<details>` プロンプト全文記録)に分割。1 回の Write/Edit は約 300 行以内。
4. **各 Step で何を書くかを具体指示**: 設計判断を Phase 2 実行時に持ち込まない。各 Step の内容は下記に箇条書きで列挙済み。

======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 S-42「インボイス適格請求書の要件チェック」の開発仕様書を作成してください。
作成後は `docs/_config.json` の `nav` 配列(§E.2 バグ修正・バリデーション相当のセクション)に必ず追記すること。

---

## Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)

以下の順序で Read / Bash を実行し、Phase 2 で書く内容を確定させる。
**Grep は「どこにあるか」の発見まで。「どう書くか」の判断は必ず `Read`。**
仕様書に書く変数名・関数名・シート名・メニュー名・定数名は Read で裏取り済みのものに限定する(失敗パターン #18-#20 参照)。

1. **案件定義**: `docs/_internal/TODO_future.md` — S-42 の案件名・概要・人間が検討すべき事項を取得。
2. **コーディング規約**: `CLAUDE.md` — ファイル番号体系・データアクセス規約(列参照はヘッダー名ベース・有効フラグ=FALSE スキップ等)・会計ロジックルールを確認。
3. **既存仕様書テンプレート**: `docs/dev/dev_mas-075_expense_date_validation.md` または `docs/dev/dev_mas-085_consistency_check.md` — バリデーション系仕様書のフォーマットを把握。
4. **DTO定義**: `000_infra/003_contracts.js` — `InvoiceDTO`(`32_wrk_invoice` の列定義)のプロパティ名(特に「税区分」「税抜金額_計画」「消費税額_計画」「取引先名」「有効フラグ」)を確認。
5. **共通定数**: `000_infra/002_constants.js` — `Constants.COLORS.WARN_RED_BG` の値、`SHEET_DEFAULTS` の `32_wrk_invoice` / `12_mst_partner` エントリを確認。
6. **ユーティリティ**: `000_infra/004_utils.js` — `Utils.toastResult()` / `Utils.logError()` / `Utils.auditLog()` のシグネチャを確認。
7. **Repository 層**: `200_data/202_repository.js` — 以下を確認する。
   - `InvoiceRepository.findAll()` の戻り値形式(`{ headers: string[], dtos: Object[] }`)と `save(dtos)` の引数形式
   - `AccountRepository.findAsMap()` の実装(`PartnerRepository.findAsTNumberMap()` のひな型として使用)
   - `readSheetAsDtos_()` / `writeDtosToSheet_()` 内部ヘルパーの動作(`clearContent()` のみでフォーマットは残存する点を確認)
8. **バリデーター層**: `200_data/201_data_validator.js` — 既存バリデーション関数のパターンを確認。
9. **システム設定**: `100_config/101_sys_config.js` — 以下を確認する(行番号まで記録)。
   - `onOpen()` のメニュー登録パターン(実在するメニュー項目名の文字列を確認)
   - `setupAllSchemas()` 内の `12_mst_partner` と `32_wrk_invoice` のスキーマ定義箇所(列配列の形式・末尾への追加方法)
10. **400_domain 空き番号確認**: Bash で `ls 400_domain/` を実行し、既存ファイルの番号一覧を確認する。CLAUDE.md 記載の一覧(400〜407, 410, 420)と照合し、新規ファイル `408_invoice_validator.js` の番号が実際に空きであることを確認する。空いていない場合は次の空き番号を採用する。

---

## Phase 2: 仕様書の分割作成

**出力先**: `docs/dev/dev_mas-114_invoice_validation.md`(ファイル名の `S` は大文字。既存仕様書の命名規則に準拠)

### Step 2-1: 骨格の作成(File Write、~20行)

見出しのみ。本文空で可。以下の順序でセクション見出しを配置する:
`# S-42: インボイス適格請求書の要件チェック`
概要 / 目的 / 現在のコード / 修正方針 / 影響範囲 / 注意事項 / エッジケース / 実データ検証 / 関連ドキュメント / 人間が検討すべき事項 / 実装プロンプト(Claude Code 用)/ 推奨実行モデル / 変更履歴 / 仕様書作成プロンプト

### Step 2-2: 前半セクションの追記(File Edit または Bash heredoc、~300行)

(省略: 概要テーブル / 目的 / 現在のコード / 修正方針(3 Step 分割)/ 影響範囲 / 注意事項 を記載)

### Step 2-3a: エッジケース〜人間検討事項の追記(File Edit または Bash、~200行)

(省略: エッジケーステーブル / 実データ検証 / 関連ドキュメント / 人間が検討すべき事項 を記載)

### Step 2-3b: 実装プロンプト〜変更履歴の追記(File Edit または Bash、~250行)

**【注意】実装プロンプトはバッククォートで囲まず、行頭スペース4つのインデントで出力すること。**
(省略: 実装プロンプト全文 / 推奨実行モデルテーブル / 変更履歴テーブル を記載)

### Step 2-4: 仕様書作成プロンプトの記録(File Edit または Bash)

末尾に `<details><summary>展開して表示</summary>` で instruction 全文をそのまま貼り付ける。

---

## Phase 3: 保存と記録

1. **`docs/_config.json` への登録**(必須):
   `nav` 配列の §E.2(バグ修正・バリデーション)セクションに以下を追加:
   `{ "file": "dev/dev_mas-114_invoice_validation.md", "title": "E.2.X S-42 インボイス適格請求書の要件チェック" }`
   追加後、`cat docs/_config.json | python3 -m json.tool` 等で JSON 構文エラーがないことを確認する。

2. **`docs/_internal/changelog.md` への追記**:
   先頭行(ヘッダー直後)に追記:
   `| 2026-04-19 | [dev_mas-114_invoice_validation.md](dev_mas-114_invoice_validation.md) | 初版作成。インボイス適格請求書要件チェック機能の仕様書を作成 |`

3. **コミット&プッシュ**:
   git add docs/dev/dev_mas-114_invoice_validation.md docs/_internal/changelog.md docs/_config.json
   git commit -m "docs: S-42 インボイス適格請求書の要件チェックの開発仕様書を作成"
   git push -u origin $(git branch --show-current)