概要

項目内容
案件IDMAS-137
カテゴリ経費・仕訳(決算処理)
PhaseP1
優先度★★
所要時間3-4時間(コード実装)+ 税理士レビュー
対象ファイル400_domain/408_corporate_tax.js(新規作成)
100_config/101_sys_config.js(onOpen にメニュー追加)
900_test/901_test_runner.js(テストケース追加)
前提案件MAS-073(正式処理 / 移行ガイド)— B/S 分類・移行パスを前提にメニュー化

目的

ユーザーがメニューから起動する対話形式のフローで、確定申告時の法人税等の計算→プレビュー→承認→仕訳追記をエンドツーエンドで支援する。

現状は 603_datamart_pl.jsauto_tax セクションが月次ベースで「みなし法人税」を YTD 差分方式で計算しているだけで、期末の決算整理仕訳として 42_trn_journal に固定計上する公式フローは未整備であった。また既存 MAS-073 関連の 2 仕様書 (_formal / _transition) はいずれも「正式仕訳が既に存在する前提」でのマート分類や移行パスを定義しているのみで、仕訳生成そのもののフローがギャップとして残っていた。本案件はこのギャップを埋める。

現在のコード

ファイル / 行現状
000_infra/002_constants.js L21-30TAX_RATES = { brackets:[{upTo:8M, national:0.165, local:0.049},{upTo:Infinity, national:0.256, local:0.080}], localMinimumAnnual:70000, foundingYear:2025, foundingMonth:11 } が定義済。本案件ではこのマスタをそのまま利用する(変更なし)
600_report/603_datamart_pl.js L14-30dmCalcBracketTax_(income, brackets) が実装済。income <= 0{national:0, local:0} を返す赤字ガード込み。本案件からこのヘルパーを参照のみで再利用する(変更禁止)
600_report/603_datamart_pl.js L144-164均等割の月割配分ロジックが月次マート用に実装済。本案件の「年額を事業月数で按分した合計一括計上」とはアルゴリズムを共通化せず、同じ切捨ルール(Math.floor(annualMin * months / 12 / 100) * 100)だけ踏襲する
100_config/101_sys_config.js L299-308onOpen()🚀 BizLP メニュー(操作パネル / 自動起動トリガー管理)のみ。決算処理用メニューは未定義
200_data/202_repository.js L286-297JournalRepository.append(dtos)42_trn_journal 末尾に JournalEntryDTO[] を追記可能。本案件で仕訳生成に使用
400_domain/410_subledger_engine.js L26-43generateTrnId_(trnSheet, YYYYMMDD, offset)TRN_YYYYMMDD_NNNN を発番。本案件から参照のみで再利用(変更禁止)
400_domain/ 配下既存最大ファイル番号は 407_rpa_orchestrator.js。本案件の新規ファイルは 408_corporate_tax.js とする
docs/dev/dev_mas-073_corporate_tax_formal.mdB/S 側(604_datamart_bs.js 等)の未払法人税等カテゴリ修正仕様。本案件で生成される仕訳がこの修正後コードで正しく分類されることが前提
docs/dev/dev_mas-073_corporate_tax_transition.mdみなし(auto_tax) → 手動 INV (Action A/B 経由) への移行ガイド。本案件は 手動 INV を経由せず、直接 42_trn_journal に仕訳を追記 する点で経路が異なる

既存 MAS-073 関連仕様書との棲み分け

仕様書スコープ本案件との関係
dev_mas-073_corporate_tax_formal.mdB/S マートの「未払法人税等」分類修正前提案件。本案件で追記する仕訳がマート側で正しく分類される必要がある
dev_mas-073_corporate_tax_transition.mdみなし→手動 INV → Action A/B への移行経路並行案件。経路が異なる(本案件は JournalRepository.append で直接追記)
本案件 dev_mas-137_corporate_tax_decision_menu.md決算時の対話的な法人税等計算→承認→仕訳追記計算+生成フローを提供。上記 2 件は生成後の下流処理を担保

修正方針

4 Step 構成で実装する。

Step 1 — 新規ドメインモジュール 400_domain/408_corporate_tax.js 作成

公開関数: runCorporateTaxCalculation()onOpen() からメニュー経由で呼び出される)

処理フロー:

  1. 対象会計年度の入力ダイアログSpreadsheetApp.getUi().prompt で年度を入力させる。未入力時は前年度を既定、数値でない場合は ui.alert で中断。

  2. 税引前当期純利益の取得Utils.getSheetByKey("FS_PL", "92_fs_pl") で取得。存在しなければユーザーに「📊 マート更新 > 財務3表の更新」の実行を促して中断。A 列を走査し、装飾除去後の比較 String(row[0]).replace(/^[✨\s]+/, "").indexOf("税引前当期純利益") === 0 で行を特定。

  3. 年度合計列の特定 — ヘッダー行 1 行目で 合計 / 通期 / YTD を含む列を最左優先で検索。見つからない場合は最終列を年度合計とみなす。値は Number(String(v).replace(/[¥,]/g, "")) で通貨書式を剥がしてパース。

  4. 課税所得の決定 — 税引前利益 > 0 の場合はそのまま課税所得、<= 0 の場合はゼロ(赤字ブランチは均等割のみ計上)。

  5. 税額計算 — 3 要素を分離計算:

    • 国税(法人税等・国税実効) = dmCalcBracketTax_(profit, Constants.TAX_RATES.brackets).national(既存ヘルパー再利用)
    • 地方税所得割 = dmCalcBracketTax_(profit, Constants.TAX_RATES.brackets).local
    • 地方税均等割 = 年額 Constants.TAX_RATES.localMinimumAnnual を事業月数で按分(下記)
  6. 事業月数の算出 — 設立初年度 (fiscalYear === Constants.TAX_RATES.foundingYear) の場合のみ月割計算:

    var fiscalMonths = 12;
    if (fiscalYear === Constants.TAX_RATES.foundingYear) {
      // 会計期間 8月起点 (603_datamart_pl.js L147, L150 の仕様に合わせる)
      fiscalMonths = 12 - (Constants.TAX_RATES.foundingMonth - 8);
      if (fiscalMonths <= 0) fiscalMonths = 12;
    }
    var adjustedMinTax = Math.floor(Constants.TAX_RATES.localMinimumAnnual * fiscalMonths / 12 / 100) * 100;
    

    設立 2 年目以降は fiscalMonths = 12 → 均等割 = 70,000 円。

  7. 既存仕訳の重複チェックJournalRepository.findAll().dtos を走査し、dto.摘要【自動】法人税等計上 FY + fiscalYear を含む DTO が存在するかを確認。存在する場合は ui.alert(ButtonSet.YES_NO) で再計算の可否を確認。NO なら return。旧仕訳の削除・無効化は手動対応とし、ダイアログで案内する。

  8. 計算結果プレビューダイアログ — OK/CANCEL 形式で以下の整形済みメッセージを表示し、OK 以外で return:

    【計算基礎】
    対象会計年度: FY{fiscalYear}
    税引前当期純利益 (92_fs_pl より): ¥{profit.toLocaleString()}
    課税所得 (赤字の場合はゼロ): ¥{taxableIncome.toLocaleString()}
    
    【税額内訳】
    国税 (法人税等・実効): ¥{national.toLocaleString()}
    地方税・所得割: ¥{localIncome.toLocaleString()}
    地方税・均等割 (事業月数={fiscalMonths}ヶ月): ¥{adjustedMinTax.toLocaleString()}
    ─────────────────────────
    合計 (未払法人税等): ¥{total.toLocaleString()}
    
    【生成予定の決算整理仕訳】
    借方: {debitAccount} ¥{total.toLocaleString()}
    貸方: 未払法人税等 ¥{total.toLocaleString()}
    (赤字時は借方=「租税公課」、黒字時は借方=「法人税、住民税及び事業税」)
    
    摘要: 【自動】法人税等計上 FY{fiscalYear} (税引前利益 ¥{profit.toLocaleString()} ベース)
    
    この内容で 42_trn_journal に追記します。よろしいですか?
    
  9. 科目マスタ存在確認AccountRepository.findAsMap()法人税、住民税及び事業税 / 租税公課 / 未払法人税等 の 3 科目が登録済であることを確認。未登録があれば ui.alert で中断し、ユーザーに科目追加を促す。

  10. 仕訳追記JournalRepository.append([dto]) で 1 レコードを追記。generateTrnId_(trnSheet, yyyymmdd)TRN_YYYYMMDD_NNNN を発番。

  11. 完了通知Utils.toastResult("runCorporateTaxCalculation", "FY" + fiscalYear + " 法人税等 ¥" + total.toLocaleString() + " を 42_trn_journal に追記しました (取引ID=" + trnId + ")") でトースト通知。

生成する JournalEntryDTO の内容:

フィールド
取引IDgenerateTrnId_(trnSheet, yyyymmdd) で発番
発生日(P/L計上日)対象会計年度の期末日 = new Date(fiscalYear + 1, 6, 31) (8月起点会計期間における翌年 7 月 31 日)
決済日_計画 / 決済日_実績空欄(未払計上のため未決済)
収支区分支出
取引先名国税庁・地方自治体(固定文字列)
科目名黒字: 法人税、住民税及び事業税 / 赤字: 租税公課
税区分対象外
税抜金額_実績 / 税込金額_実績national + localIncome + adjustedMinTax(消費税なし)
消費税額_実績0
仕訳ステータス自動計上
摘要【自動】法人税等計上 FY + fiscalYear + (税引前利益 ¥ + profit.toLocaleString() + ベース)
その他(組織名 / PJ名 / 決済手段 / 証憑URL / 各種コード)空欄(決算整理仕訳のため)

Step 2 — onOpen() 拡張 (100_config/101_sys_config.js L302-307)

既存 🚀 BizLP メニュー作成の 直後(L307 の addToUi() の次の行)に以下を追加:

ui.createMenu("💰 決算処理")
  .addItem("法人税等の計算・計上", "runCorporateTaxCalculation")
  .addToUi();

サイドバーには追加しない — 本プロジェクトは templates/operations_sidebar.html を日常運用のメイン UI としているが、決算処理は頻度が低く誤起動のリスクも高いため、意図的に GAS 標準メニューに分離する設計(注意事項 5 参照)。

Step 3 — 監査ログ・シーケンスログの追加

runCorporateTaxCalculation 内で以下のロギングを行う:

  • Utils.logInfo(FUNC, "fiscalYear=" + fiscalYear + ", profit=" + profit + ", total=" + total) を各主要ステップで出力
  • 仕訳追記成功後: 802_audit.jsAudit.log を公開していれば Audit.log(FUNC, { fiscalYear, profit, totalTax, trnId }) で監査ログ保存
  • Audit 名前空間が未定義の場合はスキップ(typeof Audit !== "undefined" && typeof Audit.log === "function" でガード)

Step 4 — テストケース追加 (900_test/901_test_runner.js)

以下 4 件を追加:

  • testCalcBracketTaxProgressive() — 利益 1,200 万 → 国税 = 800万×0.165 + 400万×0.256 = 1,320,000 + 1,024,000 = 2,344,000、地方税所得割 = 800万×0.049 + 400万×0.080 = 392,000 + 320,000 = 712,000 の一致確認。
  • testCalcBracketTaxLoss() — 利益 -100万 → dmCalcBracketTax_ 結果は {national:0, local:0}、均等割のみ 70,000 円計上。
  • testLocalMinimumTaxFoundingYear() — 設立初年度(FY2025、11 月設立、8 月起点会計期間)の事業月数 = 12 - (11 - 8) = 9 ヶ月 → 均等割 = Math.floor(70000 * 9 / 12 / 100) * 100 = 52,500 円。
  • testDuplicateJournalGuard()JournalRepository.findAll() をモックし「FY2025 既存仕訳あり」を返して、重複検出ロジックがフラグを正しく立てることを単体検証する(SpreadsheetApp.getUi はモック化できないため、ダイアログ表示の検証は手動試験で担保)。

影響範囲

ファイル / シート変更種別影響
400_domain/408_corporate_tax.js新規作成(既存最大 407 +1)他モジュールへの影響なし。関数名 runCorporateTaxCalculation のグローバル公開
100_config/101_sys_config.jsonOpen() に 3 行追加既存の 🚀 BizLP メニューには影響なし。💰 決算処理 メニューが追加される
600_report/603_datamart_pl.js変更なし(dmCalcBracketTax_ を外部参照)月次マート計算への影響なし
200_data/202_repository.js変更なし(JournalRepository.append を呼ぶのみ)既存 Repository の挙動は不変
000_infra/002_constants.js変更なし(TAX_RATES を参照するのみ)定数定義への影響なし
42_trn_journal シート実行ごとに 1 行追記既存行への影響なし。重複チェックで同一 FY の多重追記は防止
900_test/901_test_runner.jsテストケース 4 件追加既存テストへの影響なし
docs/_config.jsonnav 配列に本仕様書を追加サイドバー表示順のみ変更

注意事項

  1. dmCalcBracketTax_ の参照境界 — 本案件では既存ヘルパー dmCalcBracketTax_603_datamart_pl.js L14)を外部参照するのみで変更しない。GAS は全関数がグローバルスコープで共有されるため技術的には参照可能だが、関数名末尾のアンダースコアは module-private の慣習。将来 Utils.calcBracketTax() 等への公開化が望ましい(人間検討事項 1 参照)。
  2. 事業年度の期末日定義 — 本プロジェクトは 8 月起点の会計期間を採用(603_datamart_pl.js L147, L150)。FY2025 = 2025-08-01 〜 2026-07-31。仕訳の 発生日(P/L計上日) は期末日 new Date(fiscalYear + 1, 6, 31) = 翌年 7 月 31 日とする。他の会計期間起点を使う場合は Constants.FISCAL_START_MONTH の定義追加が必要だが、本案件のスコープ外。
  3. 均等割の月割計算アルゴリズム603_datamart_pl.js L144-164 の月次配分と独立して実装する。本案件は 年額 → 事業月数で按分した合計を一括計上、マート側は 月次 YTD 差分 方式。切捨ルールは共通(Math.floor(annualMin * months / 12 / 100) * 100)。両者の年額合計は一致するため、整合性テストを testLocalMinimumTaxFoundingYear で担保する。
  4. 取引ID 発番キャッシュのリセットgenerateTrnId__cache に当日の最大採番を保持する。本案件で追記後に generateTrnId_._cache = {}; でリセットし、後続の他 RPA バッチとの衝突を防ぐ。
  5. サイドバーへのボタン追加を行わない理由 — 決算処理は年 1 回の低頻度イベントであり、日常運用サイドバーに混在させると誤起動リスクが高まる。onOpen() メニューに限定することで「明示的にメニューを開いてから実行」という 2 段階の操作を強制し、誤起動を防止する設計。将来、決算関連の処理(特別損失計上・繰延税金資産計上など)が増えた場合のみサイドバー化を検討(人間検討事項 5 参照)。
  6. Human-in-the-Loop の必須化 — 本機能は決算数値に直接影響するため、ui.alert(ButtonSet.OK_CANCEL) による明示的な承認なしで仕訳を追記してはならない。SpreadsheetApp.getUi() が取得できない実行コンテキスト(トリガー経由 / Web アプリ経由)では 例外で中断 する(MAS-025 のように try/catch で握り潰す設計は NG)。try { var ui = SpreadsheetApp.getUi(); } catch (e) { throw new Error("本機能はスプレッドシート UI からのみ実行可能です"); } で明示的に拒否する。
  7. 赤字時の仕訳科目切替 — 赤字時は借方科目を「法人税、住民税及び事業税」→「租税公課」に切替える。税法上、均等割は損益計算書の租税公課(販管費)で処理することが一般的(中小企業会計指針 第72項 参照)。11_mst_account に両科目が登録されている前提。
  8. 税率マスタの年度依存Constants.TAX_RATES は単一テーブルのみ管理しており、年度による税率改定(法改正)には未対応。本案件では「現時点の税率で fiscalYear の税額を計算する」設計とし、過去年度の正確な税率再現は将来対応(人間検討事項 2 参照)。
  9. 消費税の扱い — 法人税等は消費税課税対象外。DTO の 税区分 = 対象外消費税額_実績 = 0、税抜金額_実績 === 税込金額_実績 とする。

エッジケース

条件挙動生成される仕訳
税引前利益 > 0(通常ケース)累進ブラケットで国税+地方税所得割を計算し、均等割を加算借方: 法人税、住民税及び事業税 / 貸方: 未払法人税等
税引前利益 = 0国税=0、地方税所得割=0、均等割のみ計上借方: 租税公課 / 貸方: 未払法人税等(全額=均等割)
税引前利益 < 0(赤字)dmCalcBracketTax_{national:0, local:0} を返す(既存ヘルパーのガード)。均等割のみ計上借方: 租税公課 / 貸方: 未払法人税等(全額=均等割)
税引前利益が丁度 8,000,000 円第1ブラケット上限で全額、national=1,320,000 / local=392,000通常ケース
税引前利益が 8,000,001 円(ブラケット境界+1)第1ブラケット 800万 + 第2ブラケット 1 円 → 第1=1,320,000 + 0.256≒0 (四捨五入) / 第1地方=392,000 + 0.080≒0通常ケース(ブラケット境界で nationalTax が僅増、既存 Math.round の丸め挙動に準拠)
設立初年度 (fiscalYear === TAX_RATES.foundingYear)fiscalMonths = 12 - (foundingMonth - 8) で事業月数算出 → 均等割を月割(100 円未満切捨)通常ケース(均等割のみ調整)
設立初年度で設立月 ≤ 8 月fiscalMonths <= 0 → 12 に丸め戻し(通期営業とみなす)通常ケース(均等割=年額70,000)
92_fs_pl シート未生成ui.alert("92_fs_pl が存在しません。先に財務3表の更新を実行してください") → return(仕訳生成せず)
税引前当期純利益行が見つからないui.alert("92_fs_pl 内に税引前当期純利益行が見つかりません") → return(仕訳生成せず)
年度合計列が見つからないヘッダー行で合計列未検出時は最終列を採用。それでも数値化できなければ ui.alert → return(仕訳生成せず)
年度合計値が数値でない(セル空欄・文字列)Number(..)NaNui.alert("税引前当期純利益の値が数値として取得できません (取得値: XXX)") → return(仕訳生成せず)
既存の同年度仕訳が存在ui.alert(ButtonSet.YES_NO) で重複確認 → NO なら return、YES なら追記継続(旧仕訳は手動無効化を案内)通常ケース(2 レコード目として追記)
科目マスタに「法人税、住民税及び事業税」/「租税公課」/「未払法人税等」いずれか未登録ui.alert("科目マスタに {科目名} が未登録です") → return(仕訳生成せず)
SpreadsheetApp.getUi() が例外(トリガー/Web アプリ経由)throw new Error("本機能はスプレッドシート UI からのみ実行可能です")(仕訳生成せず、例外で中断)
ユーザーが対象年度ダイアログで CANCEL / CLOSEreturn(何も起こらない)(仕訳生成せず)
ユーザーが計算結果プレビューで CANCEL / CLOSEreturn(計算のみで仕訳追記スキップ)(仕訳生成せず)
税額合計が 0 円(赤字 + 設立前月の異常ケース等)均等割=0 のため実質的に全ゼロ。ダイアログで「計上額がゼロのため仕訳生成をスキップします」を表示し return(仕訳生成せず)

税額計算の検算例(FY2025 通常ケース・通期・利益 1,200 万円)

項目計算金額
国税 (第1ブラケット)8,000,000 × 0.1651,320,000
国税 (第2ブラケット)4,000,000 × 0.2561,024,000
国税 合計2,344,000
地方税・所得割 (第1ブラケット)8,000,000 × 0.049392,000
地方税・所得割 (第2ブラケット)4,000,000 × 0.080320,000
地方税・所得割 合計712,000
地方税・均等割70,000(通期)70,000
未払法人税等 合計3,126,000

税額計算の検算例(FY2025 設立初年度・赤字)

項目計算金額
国税dmCalcBracketTax_ が赤字ガードで 0 を返す0
地方税・所得割同上0
地方税・均等割Math.floor(70000 × 9 / 12 / 100) × 10052,500
未払法人税等 合計(=租税公課で計上)52,500

実データ検証

実装前に MCP またはシート直接確認で以下を検証する:

  • 92_fs_pl シートの現状確認

    • A 列の行ラベル一覧を取得し、「税引前当期純利益」行の位置を確認(絵文字 装飾の有無、全角/半角スペースの違いを含む)
    • ヘッダー行(1 行目)で年度合計列のラベル(合計 / 通期 / YTD)の実際の表記を確認し、正規表現マッチング仕様を確定させる
    • 本案件ドメインモジュール側で採用する「行ラベル装飾除去」「合計列特定」アルゴリズムの前提と一致するか照合
  • 42_trn_journal の現状確認

    • 既に手動で法人税等の仕訳が入力されているか(摘要に 法人税未払法人税等 を含むレコード件数)
    • 本案件の重複チェックキー 【自動】法人税等計上 FYxxxx と衝突しないことの確認
    • 取引ID の最大採番(TRN_YYYYMMDD_NNNN 形式)を確認し、generateTrnId_ の発番で衝突しないことの確認
  • 11_mst_account の現状確認

    • 法人税、住民税及び事業税 / 租税公課 / 未払法人税等 の 3 科目が登録済であることを確認
    • 各科目の stmt / cat が想定通りであること(法人税、住民税及び事業税 → stmt=P/L, cat=税金 / 未払法人税等 → stmt=B/S, cat=流動負債 / 租税公課 → stmt=P/L, cat=販管費)
  • TAX_RATES の現状確認

    • Constants.TAX_RATES.brackets / localMinimumAnnual / foundingYear / foundingMonth の値が 002_constants.js L21-30 と一致することを確認
    • 本番環境・開発環境の Env.isDev() 判定により、税率値が環境依存で変わらないこと(002_constants.js は環境非依存)を確認
  • 実行後の検算

    • 追記された 42_trn_journal レコードの 税抜金額_実績 が「国税 + 地方税所得割 + 地方税均等割」と一致すること
    • 次回 buildBudgetTrendDataMart(マート更新)実行後、91_fs_bs の流動負債「未払法人税等」に本仕訳の金額が計上されていること(dev_mas-073_corporate_tax_formal.md の B/S 分類修正と併せて検証)
    • 603_datamart_pl.jsauto_tax セクションが hasManualTax=true を検出し、みなし法人税の月次自動計算がゼロになっていること

関連ドキュメント

ドキュメント関連箇所
docs/dev/dev_mas-073_corporate_tax_formal.mdB/S 側の未払法人税等分類修正仕様。本案件で追記する仕訳が正しく分類されるために必須
docs/dev/dev_mas-073_corporate_tax_transition.mdみなし→手動 INV への移行ガイド。本案件は経路が異なる(直接 JournalRepository.append)が、移行思想(手動計上で auto_tax を無効化)は共通
000_infra/002_constants.jsTAX_RATES マスタ(本案件で参照)
000_infra/003_contracts.jsJournalEntryDTO の型定義(L97-129)
200_data/202_repository.jsJournalRepository.append()(L291-297)、AccountRepository.findAsMap()(L323-341)
400_domain/410_subledger_engine.jsgenerateTrnId_(L26-43)— 本案件で参照
600_report/603_datamart_pl.jsdmCalcBracketTax_(L14-30)、均等割月割ロジック(L144-164)— 本案件で参照および思想参照
100_config/101_sys_config.jsonOpen()(L299-308)への追加箇所
docs/prd.mdプロダクトポリシー(Human-in-the-Loop の必須化)
中小企業会計指針 第72項租税公課の仕訳処理ルール(赤字時の均等割)

人間が検討すべき事項

docs/_internal/TODO_future.md の MAS-073 行に記載された「人間が検討すべき事項」:

  • 税理士との納付タイミング・仕訳パターンの確認 — 本案件で生成される仕訳は「期末一括計上」パターンだが、税理士によっては「中間申告で半額を先に計上」「事業税のみ翌期損金」など異なる仕訳パターンを推奨する場合がある。運用開始前に税理士レビューで仕訳パターンの妥当性を検証すること。

追加で検討すべき事項(実装過程で判明したもの):

  1. dmCalcBracketTax_ の共通化 — 現在は 603_datamart_pl.js 内のローカルヘルパー(末尾アンダースコア)として定義されており、本案件で外部参照する。将来、000_infra/004_utils.jsUtils.calcBracketTax(income, brackets) として移動し、マート側も新ヘルパーを呼ぶ形にリファクタすべきか。影響範囲が大きいため別案件として検討。
  2. 年度依存の税率テーブル — 法改正により TAX_RATES.brackets は改訂される可能性あり。現状は単一テーブルのみなので、過去年度の仕訳を後から再計算する場合に不整合が生じる。Constants.TAX_RATES_HISTORY = { 2024: {...}, 2025: {...} } のような年度別テーブル化が必要か検討。
  3. 月割計算の会計期間起点の共通化603_datamart_pl.js L147 の 8月起点 ハードコードは環境依存。Constants.FISCAL_START_MONTH = 8 を定義して両者が参照する設計に変更すべきか(本案件のスコープ外)。
  4. Action B パイプラインとの接続 — 本案件は仕訳を直接 42_trn_journal に追記するが、dev_mas-073_corporate_tax_transition.md の思想では「手動 INV → Action A/B」経由が望ましい。両者のどちらを正式ルートとするか、運用方針の統一が必要。現時点では「計算支援 = 本案件」「納付 = Action B 経由」と棲み分ける設計を想定。
  5. サイドバーへのメニュー追加の是非onOpen() 限定としたが、頻度の低い決算処理を隠すことでユーザーが「機能の存在を忘れる」リスクもある。将来の運用実績を踏まえ、サイドバーに「💰 決算処理」セクションを追加するか判断。
  6. 重複仕訳の自動無効化 — 現仕様では重複検出時に YES で追記し、旧仕訳の無効化はユーザー手動対応。運用負荷を考慮し、YES 選択時に旧仕訳の 有効フラグ を自動で FALSE に更新する機能を追加すべきか(JournalRepository.save の実装が必要)。
  7. 事業税の特別法人事業税との分離 — 現在 TAX_RATES.brackets.local は「住民税法人税割 + 事業税所得割 + 特別法人事業税」の合算実効税率。税理士によっては内訳の仕訳分離を要求する場合があり、その際は brackets{ local_resident, local_enterprise, local_special } に細分化する必要あり。
  8. みなし法人税 (auto_tax_national / auto_tax_local) との重複防止 — 本案件で仕訳追記後、マート再計算時に hasManualTax=true となり、みなし月次計算がゼロ化される(既存ロジック 603_datamart_pl.js L135-137)。この挙動は正しいが、ユーザーに「仕訳計上後はマートを再計算してください」と明示的に案内するガイドが必要。

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

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-073「未払法人税等の計算・計上(対話形式の決算処理メニュー)」を実装してください。

## 実行前タスク
- `000_infra/002_constants.js` を Read し、`TAX_RATES`(L21-30)の全フィールドを確認する:`brackets` / `localMinimumAnnual` / `foundingYear` / `foundingMonth`
- `600_report/603_datamart_pl.js` を Read し、`dmCalcBracketTax_`(L14-30)の引数・戻り値・赤字ガード動作と、均等割月割ロジック(L144-164)の月数算出式を確認する
- `200_data/202_repository.js` を Read し、`JournalRepository.append()`(L291-297)と `AccountRepository.findAsMap()`(L323-341)の戻り値を確認する
- `400_domain/410_subledger_engine.js` を Read し、`generateTrnId_`(L26-43)の引数(trnSheet, dateStr, offset)と発番ロジック・キャッシュ動作を確認する
- `100_config/101_sys_config.js` を Read し、`onOpen()`(L299-308)の現状(`🚀 BizLP` メニューのみ)を確認する
- `000_infra/003_contracts.js` を Read し、`JournalEntryDTO`(L97-129)の全フィールドと必須項目を確認する
- `000_infra/004_utils.js` を Read し、`Utils.getSheetByKey` / `Utils.logInfo` / `Utils.toastResult` の使用方法を確認する

## 修正対象ファイル
1. `400_domain/408_corporate_tax.js` — 新規作成(既存最大 407 +1、関数 `runCorporateTaxCalculation` を公開)
2. `100_config/101_sys_config.js` — `onOpen()` の `🚀 BizLP` メニュー追加直後(L307 の `addToUi()` の次の行)に `💰 決算処理` メニューを追加
3. `900_test/901_test_runner.js` — テストケース 4 件追加

## 実装内容

### 1. 新規モジュール `400_domain/408_corporate_tax.js`

```js
// ==========================================
// 💰 決算処理: 法人税等の計算・計上 (S-01)
// ==========================================

function runCorporateTaxCalculation() {
  var FUNC = "runCorporateTaxCalculation";
  var ui;
  try { ui = SpreadsheetApp.getUi(); }
  catch (e) { throw new Error("本機能はスプレッドシート UI からのみ実行可能です"); }

  // Step 1: 対象会計年度の入力
  var defaultFy = new Date().getFullYear() - 1;
  var resp = ui.prompt("💰 決算処理 - 法人税等計算",
    "対象の会計年度を西暦4桁で入力してください (例: 2025)。未入力時は前年度 (" + defaultFy + ") を採用します。",
    ui.ButtonSet.OK_CANCEL);
  if (resp.getSelectedButton() !== ui.Button.OK) return;
  var fyStr = String(resp.getResponseText() || "").trim();
  var fiscalYear = fyStr ? parseInt(fyStr, 10) : defaultFy;
  if (isNaN(fiscalYear) || fiscalYear < 2000 || fiscalYear > 2100) {
    ui.alert("会計年度が不正です: " + fyStr); return;
  }

  // Step 2: 税引前当期純利益の取得
  var fsPl = Utils.getSheetByKey("FS_PL", "92_fs_pl");
  if (!fsPl) {
    ui.alert("92_fs_pl シートが存在しません。先に 📊 マート更新 > 財務3表の更新 を実行してください。"); return;
  }
  var data = fsPl.getDataRange().getValues();
  var headerRow = data[0];
  var profitRowIdx = -1;
  for (var i = 1; i < data.length; i++) {
    var label = String(data[i][0] || "").replace(/^[✨\s]+/, "");
    if (label.indexOf("税引前当期純利益") === 0) { profitRowIdx = i; break; }
  }
  if (profitRowIdx < 0) { ui.alert("92_fs_pl 内に税引前当期純利益行が見つかりません。"); return; }

  // Step 3: 年度合計列の特定
  var totalColIdx = -1;
  for (var c = 1; c < headerRow.length; c++) {
    var h = String(headerRow[c]);
    if (h.indexOf("合計") >= 0 || h.indexOf("通期") >= 0 || h.indexOf("YTD") >= 0) { totalColIdx = c; break; }
  }
  if (totalColIdx < 0) totalColIdx = headerRow.length - 1;

  var rawVal = data[profitRowIdx][totalColIdx];
  var profit = Number(String(rawVal).replace(/[¥,]/g, ""));
  if (isNaN(profit)) { ui.alert("税引前当期純利益の値が数値として取得できません (取得値: " + rawVal + ")"); return; }

  // Step 4-6: 税額計算
  var taxable = profit > 0 ? profit : 0;
  var br = dmCalcBracketTax_(taxable, Constants.TAX_RATES.brackets);
  var national = br.national;
  var localIncome = br.local;

  var fiscalMonths = 12;
  if (fiscalYear === Constants.TAX_RATES.foundingYear) {
    fiscalMonths = 12 - (Constants.TAX_RATES.foundingMonth - 8);
    if (fiscalMonths <= 0) fiscalMonths = 12;
  }
  var adjustedMinTax = Math.floor(Constants.TAX_RATES.localMinimumAnnual * fiscalMonths / 12 / 100) * 100;
  var total = national + localIncome + adjustedMinTax;

  if (total === 0) {
    ui.alert("計上額がゼロのため仕訳生成をスキップします。"); return;
  }

  // Step 7: 重複チェック
  var existing = JournalRepository.findAll();
  var dupKey = "【自動】法人税等計上 FY" + fiscalYear;
  var existingTrn = null;
  for (var k = 0; k < existing.dtos.length; k++) {
    if (String(existing.dtos[k].摘要 || "").indexOf(dupKey) >= 0) {
      existingTrn = existing.dtos[k].取引ID; break;
    }
  }
  if (existingTrn) {
    var r = ui.alert("重複仕訳の警告",
      "FY" + fiscalYear + " の法人税等計上仕訳は既に存在します (" + existingTrn + ")。再計算して追記しますか?\n※ 旧仕訳の削除・無効化は手動で行う必要があります。",
      ui.ButtonSet.YES_NO);
    if (r !== ui.Button.YES) return;
  }

  // Step 8: 科目マスタ存在確認
  var acctMap = AccountRepository.findAsMap();
  var debitAccount = profit > 0 ? "法人税、住民税及び事業税" : "租税公課";
  var required = [debitAccount, "未払法人税等"];
  for (var a = 0; a < required.length; a++) {
    if (!acctMap[required[a]]) {
      ui.alert("科目マスタ (11_mst_account) に「" + required[a] + "」が未登録です。追加してから再実行してください。"); return;
    }
  }

  // Step 9: プレビュー確認
  var fmt = function(n) { return "¥" + Math.round(n).toLocaleString(); };
  var msg =
    "【計算基礎】\n" +
    "対象会計年度: FY" + fiscalYear + "\n" +
    "税引前当期純利益 (92_fs_pl より): " + fmt(profit) + "\n" +
    "課税所得 (赤字の場合はゼロ): " + fmt(taxable) + "\n\n" +
    "【税額内訳】\n" +
    "国税 (法人税等・実効): " + fmt(national) + "\n" +
    "地方税・所得割: " + fmt(localIncome) + "\n" +
    "地方税・均等割 (事業月数=" + fiscalMonths + "ヶ月): " + fmt(adjustedMinTax) + "\n" +
    "─────────────────────────\n" +
    "合計 (未払法人税等): " + fmt(total) + "\n\n" +
    "【生成予定の決算整理仕訳】\n" +
    "借方: " + debitAccount + " " + fmt(total) + "\n" +
    "貸方: 未払法人税等 " + fmt(total) + "\n\n" +
    "摘要: " + dupKey + " (税引前利益 " + fmt(profit) + " ベース)\n\n" +
    "この内容で 42_trn_journal に追記します。よろしいですか?";
  var confirm = ui.alert("決算整理仕訳の確認", msg, ui.ButtonSet.OK_CANCEL);
  if (confirm !== ui.Button.OK) return;

  // Step 10: 仕訳追記
  var trnSheet = Utils.getSheetByKey("TRN_JOUR", "42_trn_journal");
  var now = new Date();
  var dateStr = Utilities.formatDate(now, "Asia/Tokyo", "yyyyMMdd");
  if (generateTrnId_._cache) delete generateTrnId_._cache[dateStr];
  var trnId = generateTrnId_(trnSheet, dateStr);
  var dto = {
    "取引ID": trnId,
    "発生日(P/L計上日)": new Date(fiscalYear + 1, 6, 31),
    "決済日_計画": "", "決済日_実績": "",
    "収支区分": "支出",
    "取引先名": "国税庁・地方自治体",
    "科目名": debitAccount,
    "税区分": "対象外",
    "税抜金額_実績": total, "消費税額_実績": 0, "税込金額_実績": total,
    "組織名": "", "PJ名": "", "決済手段": "",
    "仕訳ステータス": "自動計上",
    "証憑URL": "",
    "摘要": dupKey + " (税引前利益 " + fmt(profit) + " ベース)"
  };
  JournalRepository.append([dto]);
  generateTrnId_._cache = {};

  Utils.logInfo(FUNC, "fiscalYear=" + fiscalYear + ", profit=" + profit + ", total=" + total + ", trnId=" + trnId);
  if (typeof Audit !== "undefined" && typeof Audit.log === "function") {
    Audit.log(FUNC, { fiscalYear: fiscalYear, profit: profit, national: national, localIncome: localIncome, minTax: adjustedMinTax, total: total, trnId: trnId });
  }
  Utils.toastResult(FUNC, "FY" + fiscalYear + " 法人税等 " + fmt(total) + " を 42_trn_journal に追記しました (取引ID=" + trnId + ")");
}
```

### 2. `onOpen()` 拡張(`100_config/101_sys_config.js` L307 の `.addToUi()` の次の行)

```js
ui.createMenu("💰 決算処理")
  .addItem("法人税等の計算・計上", "runCorporateTaxCalculation")
  .addToUi();
```

### 3. テストケース追加(`900_test/901_test_runner.js`)

```js
function testCalcBracketTaxProgressive() {
  var result = dmCalcBracketTax_(12000000, Constants.TAX_RATES.brackets);
  assert(result.national === 2344000, "national tax expected 2344000 got " + result.national);
  assert(result.local === 712000, "local tax expected 712000 got " + result.local);
}

function testCalcBracketTaxLoss() {
  var result = dmCalcBracketTax_(-1000000, Constants.TAX_RATES.brackets);
  assert(result.national === 0 && result.local === 0, "loss must yield zero tax");
}

function testLocalMinimumTaxFoundingYear() {
  var months = 12 - (Constants.TAX_RATES.foundingMonth - 8);
  if (months <= 0) months = 12;
  var expected = Math.floor(Constants.TAX_RATES.localMinimumAnnual * months / 12 / 100) * 100;
  assert(expected === 52500, "FY2025 founding year min tax expected 52500 got " + expected);
}

function testDuplicateJournalGuard() {
  var dtos = [
    { 取引ID: "TRN_20260101_0001", 摘要: "【自動】法人税等計上 FY2025 (税引前利益 ¥10,000,000 ベース)" }
  ];
  var hit = dtos.some(function(d) { return String(d.摘要 || "").indexOf("【自動】法人税等計上 FY2025") >= 0; });
  assert(hit === true, "duplicate detection must be true");
}
```

## 制約
- `600_report/603_datamart_pl.js` の `dmCalcBracketTax_` / 均等割月割ロジックは変更禁止(参照のみ)
- `000_infra/002_constants.js` の `TAX_RATES` は変更禁止(本案件の入力マスタ)
- `200_data/202_repository.js` の既存 Repository は変更禁止
- `400_domain/410_subledger_engine.js` の `generateTrnId_` は参照のみ
- `Constants.TAX_RATES.localMinimumAnnual` の 100 円未満切捨て(`Math.floor(... / 100) * 100`)を必ず適用
- Human-in-the-Loop のため、ダイアログ承認なしでは絶対に `JournalRepository.append` を呼ばないこと
- `SpreadsheetApp.getUi()` 取得失敗時は try/catch で握り潰さず、`throw new Error` で明示的に中断すること
- 列参照はヘッダー名ベース(列番号ハードコード禁止)。A 列の行ラベルは絵文字 `✨` と全角/半角スペースを剥がしてから比較

## エッジケース
- 税引前利益 <= 0 → 赤字ブランチ(均等割のみ、借方=租税公課)
- 設立初年度(FY2025)→ 均等割を月割(事業月数 9 ヶ月 → 52,500 円)
- `92_fs_pl` シート未生成 → マート更新を促すダイアログで中断
- 税引前当期純利益行 / 年度合計列が見つからない → 中断ダイアログ
- 既存同年度仕訳あり → YES/NO で再追記の可否確認
- 科目マスタ未登録 → 中断ダイアログ(科目追加を促す)
- UI 取得不可(トリガー/Web アプリ経由)→ 例外で中断
- 税額合計 = 0 円(赤字 + 設立前月等)→ 「計上額ゼロのためスキップ」で中断

## 動作確認
1. `npm run push:dev` で開発用 GAS にデプロイ
2. シートを開き直して `onOpen()` を発火させ、メニューバーに `💰 決算処理` が表示されることを確認
3. 「📊 マート更新 > 財務3表の更新」を実行して `92_fs_pl` を生成
4. `💰 決算処理 > 法人税等の計算・計上` をクリック → 対象年度ダイアログが表示されること
5. 前年度 (未入力 OK) で OK → 計算結果プレビューダイアログに税額内訳・仕訳プレビューが表示されること
6. OK → `42_trn_journal` 末尾に `TRN_YYYYMMDD_NNNN` の 1 行が追記されること
7. 同一年度で再実行 → 重複警告ダイアログが表示されること (YES で上書き追記 / NO で中断)
8. マートを再更新 → `91_fs_bs` の流動負債「未払法人税等」に金額反映 / `92_fs_pl` のみなし法人税がゼロ化(`hasManualTax=true` 経路)
9. トリガー経由で実行 → `SpreadsheetApp.getUi()` 例外で中断(仕訳生成しない)
10. テスト: `🧪 テスト > 全テスト実行` で `testCalcBracketTaxProgressive` 等 4 件が PASS すること

### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| 新規モジュール実装 | あり | ダイアログフロー設計、P/L シート読み取りロジック、重複検出 |
| onOpen() 拡張 | なし | 3 行追加の定型作業 |
| テストケース追加 | なし | 期待値を Phase 1 の検算表から転記 |

推奨実行モデル

工程推奨モデル理由
仕様書作成(本ドキュメント)Claude Opus 4.7P/L シート読み取りロジック・税額計算アルゴリズム・Human-in-the-Loop フロー設計に高い推論力が必要
Step 1 新規モジュール実装Claude Sonnet 4.6ダイアログフロー・重複検出・DTO 構築等の中程度の判断が必要
Step 2 onOpen() 拡張Claude Haiku 4.53 行追加の定型作業
Step 3 監査ログ追加Claude Haiku 4.5Utils.logInfo 呼び出しの追加のみ
Step 4 テストケース追加Claude Haiku 4.5期待値転記のみ

変更履歴

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

仕様書作成プロンプト

展開して表示

【タイムアウト回避・実行原則(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(
    記録)に分割。
  4. 各 Step で何を書くかを具体指示: 出力時に設計判断を再考しない。

====================================================================== あなたはGAS会計システムのシニア開発者兼仕様書ライターです。 CLIエージェント「Claude Code」として、案件 MAS-073「未払法人税等の適切な処理」の開発仕様書を作成してください。

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

  1. docs/_internal/TODO_future.md で案件 MAS-073 の要件を把握。
  2. 関連するGASコードやマスタをツールで深く調査。特に 000_infra/002_constants.jsTAX_RATES オブジェクトの構造と、200_data/202_repository.jsJournalRepository.append() の使い方を理解すること。

Phase 2: 仕様書の分割作成

出力先: docs/dev/dev_mas-137_corporate_tax_decision_menu.md 【重要】絶対に1回のツール呼び出しで全内容を出力せず、以下のStepに分割して実行すること。

Step 2-1: 骨格の作成 (File Write)

Step 2-2: 前半セクションの追記 (File Edit または Bash)

※アーキテクトからの指示:

  • アーキテクチャ方針:
    • 新規メニュー「💰 決算処理 → 法人税等の計算・計上」をonOpen()に追加する。
    • 処理は、ユーザーがメニューから起動する対話形式のフローとすること。
  • 処理フロー:
    1. 実行時、ダイアログで対象の会計年度をユーザーに選択させる。
    2. 対象年度のP/L(例: 92_fs_plシート)から税引前当期純利益を取得し、これを課税所得の基礎とする。
    3. 002_constants.jsTAX_RATES オブジェクトをマスターデータとして使用し、法人税、住民税及び事業税を計算する。
      • 800万円を境とする累進課税ブラケットを適用すること。
      • 地方税均等割 (localMinimumAnnual) を加算すること。
    4. 計算後、ユーザーに確認ダイアログを提示する。(詳細はStep 2-3aで指示)
    5. ユーザー承認後、JournalRepository.append() を使用して42_trn_journalシートに決算整理仕訳を追記する。
      • 借方: 法人税、住民税及び事業税
      • 貸方: 未払法人税等
  • 二重計上防止策:
    • 仕訳を追記する前に、42_trn_journal内に同一年度の法人税等計上仕訳が既に存在しないか確認すること。
    • 識別のため、摘要に「【自動】法人税等計上 FYxxxx」のような一意な文字列を含めること。
    • 既存の仕訳が存在する場合は、ユーザーに再計算して上書きするか確認するフローを設けること。

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

※アーキテクトからの指示:

  • エッジケーステーブルの作成: 以下のシナリオを網羅したエッジケーステーブルを仕様書に含めること。
    • 課税所得がマイナス(赤字)の場合:
      • 所得に応じた税額(法人税、住民税所得割、事業税)はゼロとして計算する。
      • ただし、地方税の均等割 (localMinimumAnnual) は赤字でも発生するため、この金額のみを計上する。
      • 仕訳科目は「租税公課 / 未払法人税等」とする。
    • 設立初年度の場合:
      • Constants.TAX_RATES.foundingYearfoundingMonth を参照し、事業年度の月数を正確に計算する。
      • 地方税均等割は、算出した事業月数に応じて月割り計算すること (年額 * 事業月数 / 12)。
  • 人間が検討すべき事項 (Human-in-the-Loop):
    • この機能は会計の根幹に関わるため、計算結果をユーザーが検証するフローを必須とすること。
    • 仕訳を書き込む前に、必ず以下の情報を含む確認ダイアログをユーザーに提示し、明示的な承認を得ること。
      1. 計算の基礎となった課税所得額
      2. 計算された税額の内訳(国税、地方税所得割、地方税均等割、合計額)
      3. 生成される仕訳のプレビュー(勘定科目と金額)

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

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

末尾に <details> を設け、この <instruction> 全文を追記。

Phase 3: _config.json への追記と構文チェック