MAS-137: 未払法人税等の計算・計上(対話形式の決算処理メニュー / MAS-073 派生)
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-137 |
| カテゴリ | 経費・仕訳(決算処理) |
| Phase | P1 |
| 優先度 | ★★ |
| 所要時間 | 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.js の auto_tax セクションが月次ベースで「みなし法人税」を YTD 差分方式で計算しているだけで、期末の決算整理仕訳として 42_trn_journal に固定計上する公式フローは未整備であった。また既存 MAS-073 関連の 2 仕様書 (_formal / _transition) はいずれも「正式仕訳が既に存在する前提」でのマート分類や移行パスを定義しているのみで、仕訳生成そのもののフローがギャップとして残っていた。本案件はこのギャップを埋める。
現在のコード
| ファイル / 行 | 現状 |
|---|---|
000_infra/002_constants.js L21-30 | TAX_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-30 | dmCalcBracketTax_(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-308 | onOpen() は 🚀 BizLP メニュー(操作パネル / 自動起動トリガー管理)のみ。決算処理用メニューは未定義 |
200_data/202_repository.js L286-297 | JournalRepository.append(dtos) で 42_trn_journal 末尾に JournalEntryDTO[] を追記可能。本案件で仕訳生成に使用 |
400_domain/410_subledger_engine.js L26-43 | generateTrnId_(trnSheet, YYYYMMDD, offset) で TRN_YYYYMMDD_NNNN を発番。本案件から参照のみで再利用(変更禁止) |
400_domain/ 配下 | 既存最大ファイル番号は 407_rpa_orchestrator.js。本案件の新規ファイルは 408_corporate_tax.js とする |
docs/dev/dev_mas-073_corporate_tax_formal.md | B/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.md | B/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() からメニュー経由で呼び出される)
処理フロー:
対象会計年度の入力ダイアログ —
SpreadsheetApp.getUi().promptで年度を入力させる。未入力時は前年度を既定、数値でない場合はui.alertで中断。税引前当期純利益の取得 —
Utils.getSheetByKey("FS_PL", "92_fs_pl")で取得。存在しなければユーザーに「📊 マート更新 > 財務3表の更新」の実行を促して中断。A 列を走査し、装飾除去後の比較String(row[0]).replace(/^[✨\s]+/, "").indexOf("税引前当期純利益") === 0で行を特定。年度合計列の特定 — ヘッダー行 1 行目で
合計/通期/YTDを含む列を最左優先で検索。見つからない場合は最終列を年度合計とみなす。値はNumber(String(v).replace(/[¥,]/g, ""))で通貨書式を剥がしてパース。課税所得の決定 — 税引前利益 > 0 の場合はそのまま課税所得、<= 0 の場合はゼロ(赤字ブランチは均等割のみ計上)。
税額計算 — 3 要素を分離計算:
- 国税(法人税等・国税実効) =
dmCalcBracketTax_(profit, Constants.TAX_RATES.brackets).national(既存ヘルパー再利用) - 地方税所得割 =
dmCalcBracketTax_(profit, Constants.TAX_RATES.brackets).local - 地方税均等割 = 年額
Constants.TAX_RATES.localMinimumAnnualを事業月数で按分(下記)
- 国税(法人税等・国税実効) =
事業月数の算出 — 設立初年度 (
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 円。既存仕訳の重複チェック —
JournalRepository.findAll().dtosを走査し、dto.摘要に【自動】法人税等計上 FY + fiscalYearを含む DTO が存在するかを確認。存在する場合はui.alert(ButtonSet.YES_NO)で再計算の可否を確認。NO なら return。旧仕訳の削除・無効化は手動対応とし、ダイアログで案内する。計算結果プレビューダイアログ — 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 に追記します。よろしいですか?科目マスタ存在確認 —
AccountRepository.findAsMap()で法人税、住民税及び事業税/租税公課/未払法人税等の 3 科目が登録済であることを確認。未登録があればui.alertで中断し、ユーザーに科目追加を促す。仕訳追記 —
JournalRepository.append([dto])で 1 レコードを追記。generateTrnId_(trnSheet, yyyymmdd)でTRN_YYYYMMDD_NNNNを発番。完了通知 —
Utils.toastResult("runCorporateTaxCalculation", "FY" + fiscalYear + " 法人税等 ¥" + total.toLocaleString() + " を 42_trn_journal に追記しました (取引ID=" + trnId + ")")でトースト通知。
生成する JournalEntryDTO の内容:
| フィールド | 値 |
|---|---|
取引ID | generateTrnId_(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.jsがAudit.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.js | onOpen() に 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.json | nav 配列に本仕様書を追加 | サイドバー表示順のみ変更 |
注意事項
dmCalcBracketTax_の参照境界 — 本案件では既存ヘルパーdmCalcBracketTax_(603_datamart_pl.jsL14)を外部参照するのみで変更しない。GAS は全関数がグローバルスコープで共有されるため技術的には参照可能だが、関数名末尾のアンダースコアは module-private の慣習。将来Utils.calcBracketTax()等への公開化が望ましい(人間検討事項 1 参照)。- 事業年度の期末日定義 — 本プロジェクトは 8 月起点の会計期間を採用(
603_datamart_pl.jsL147, L150)。FY2025 = 2025-08-01 〜 2026-07-31。仕訳の発生日(P/L計上日)は期末日new Date(fiscalYear + 1, 6, 31)= 翌年 7 月 31 日とする。他の会計期間起点を使う場合はConstants.FISCAL_START_MONTHの定義追加が必要だが、本案件のスコープ外。 - 均等割の月割計算アルゴリズム —
603_datamart_pl.jsL144-164 の月次配分と独立して実装する。本案件は 年額 → 事業月数で按分した合計を一括計上、マート側は 月次 YTD 差分 方式。切捨ルールは共通(Math.floor(annualMin * months / 12 / 100) * 100)。両者の年額合計は一致するため、整合性テストをtestLocalMinimumTaxFoundingYearで担保する。 取引ID発番キャッシュのリセット —generateTrnId_は_cacheに当日の最大採番を保持する。本案件で追記後にgenerateTrnId_._cache = {};でリセットし、後続の他 RPA バッチとの衝突を防ぐ。- サイドバーへのボタン追加を行わない理由 — 決算処理は年 1 回の低頻度イベントであり、日常運用サイドバーに混在させると誤起動リスクが高まる。
onOpen()メニューに限定することで「明示的にメニューを開いてから実行」という 2 段階の操作を強制し、誤起動を防止する設計。将来、決算関連の処理(特別損失計上・繰延税金資産計上など)が増えた場合のみサイドバー化を検討(人間検討事項 5 参照)。 - 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 からのみ実行可能です"); }で明示的に拒否する。 - 赤字時の仕訳科目切替 — 赤字時は借方科目を「法人税、住民税及び事業税」→「租税公課」に切替える。税法上、均等割は損益計算書の租税公課(販管費)で処理することが一般的(中小企業会計指針 第72項 参照)。
11_mst_accountに両科目が登録されている前提。 - 税率マスタの年度依存 —
Constants.TAX_RATESは単一テーブルのみ管理しており、年度による税率改定(法改正)には未対応。本案件では「現時点の税率でfiscalYearの税額を計算する」設計とし、過去年度の正確な税率再現は将来対応(人間検討事項 2 参照)。 - 消費税の扱い — 法人税等は消費税課税対象外。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(..) で NaN → ui.alert("税引前当期純利益の値が数値として取得できません (取得値: XXX)") → return | (仕訳生成せず) |
| 既存の同年度仕訳が存在 | ui.alert(ButtonSet.YES_NO) で重複確認 → NO なら return、YES なら追記継続(旧仕訳は手動無効化を案内) | 通常ケース(2 レコード目として追記) |
| 科目マスタに「法人税、住民税及び事業税」/「租税公課」/「未払法人税等」いずれか未登録 | ui.alert("科目マスタに {科目名} が未登録です") → return | (仕訳生成せず) |
SpreadsheetApp.getUi() が例外(トリガー/Web アプリ経由) | throw new Error("本機能はスプレッドシート UI からのみ実行可能です") | (仕訳生成せず、例外で中断) |
| ユーザーが対象年度ダイアログで CANCEL / CLOSE | return(何も起こらない) | (仕訳生成せず) |
| ユーザーが計算結果プレビューで CANCEL / CLOSE | return(計算のみで仕訳追記スキップ) | (仕訳生成せず) |
| 税額合計が 0 円(赤字 + 設立前月の異常ケース等) | 均等割=0 のため実質的に全ゼロ。ダイアログで「計上額がゼロのため仕訳生成をスキップします」を表示し return | (仕訳生成せず) |
税額計算の検算例(FY2025 通常ケース・通期・利益 1,200 万円)
| 項目 | 計算 | 金額 |
|---|---|---|
| 国税 (第1ブラケット) | 8,000,000 × 0.165 | 1,320,000 |
| 国税 (第2ブラケット) | 4,000,000 × 0.256 | 1,024,000 |
| 国税 合計 | 2,344,000 | |
| 地方税・所得割 (第1ブラケット) | 8,000,000 × 0.049 | 392,000 |
| 地方税・所得割 (第2ブラケット) | 4,000,000 × 0.080 | 320,000 |
| 地方税・所得割 合計 | 712,000 | |
| 地方税・均等割 | 70,000(通期) | 70,000 |
| 未払法人税等 合計 | 3,126,000 |
税額計算の検算例(FY2025 設立初年度・赤字)
| 項目 | 計算 | 金額 |
|---|---|---|
| 国税 | dmCalcBracketTax_ が赤字ガードで 0 を返す | 0 |
| 地方税・所得割 | 同上 | 0 |
| 地方税・均等割 | Math.floor(70000 × 9 / 12 / 100) × 100 | 52,500 |
| 未払法人税等 合計(=租税公課で計上) | 52,500 |
実データ検証
実装前に MCP またはシート直接確認で以下を検証する:
92_fs_plシートの現状確認- A 列の行ラベル一覧を取得し、「税引前当期純利益」行の位置を確認(絵文字
✨装飾の有無、全角/半角スペースの違いを含む) - ヘッダー行(1 行目)で年度合計列のラベル(
合計/通期/YTD)の実際の表記を確認し、正規表現マッチング仕様を確定させる - 本案件ドメインモジュール側で採用する「行ラベル装飾除去」「合計列特定」アルゴリズムの前提と一致するか照合
- A 列の行ラベル一覧を取得し、「税引前当期純利益」行の位置を確認(絵文字
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.jsL21-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.jsのauto_taxセクションがhasManualTax=trueを検出し、みなし法人税の月次自動計算がゼロになっていること
- 追記された
関連ドキュメント
| ドキュメント | 関連箇所 |
|---|---|
docs/dev/dev_mas-073_corporate_tax_formal.md | B/S 側の未払法人税等分類修正仕様。本案件で追記する仕訳が正しく分類されるために必須 |
docs/dev/dev_mas-073_corporate_tax_transition.md | みなし→手動 INV への移行ガイド。本案件は経路が異なる(直接 JournalRepository.append)が、移行思想(手動計上で auto_tax を無効化)は共通 |
000_infra/002_constants.js | TAX_RATES マスタ(本案件で参照) |
000_infra/003_contracts.js | JournalEntryDTO の型定義(L97-129) |
200_data/202_repository.js | JournalRepository.append()(L291-297)、AccountRepository.findAsMap()(L323-341) |
400_domain/410_subledger_engine.js | generateTrnId_(L26-43)— 本案件で参照 |
600_report/603_datamart_pl.js | dmCalcBracketTax_(L14-30)、均等割月割ロジック(L144-164)— 本案件で参照および思想参照 |
100_config/101_sys_config.js | onOpen()(L299-308)への追加箇所 |
docs/prd.md | プロダクトポリシー(Human-in-the-Loop の必須化) |
| 中小企業会計指針 第72項 | 租税公課の仕訳処理ルール(赤字時の均等割) |
人間が検討すべき事項
docs/_internal/TODO_future.md の MAS-073 行に記載された「人間が検討すべき事項」:
- 税理士との納付タイミング・仕訳パターンの確認 — 本案件で生成される仕訳は「期末一括計上」パターンだが、税理士によっては「中間申告で半額を先に計上」「事業税のみ翌期損金」など異なる仕訳パターンを推奨する場合がある。運用開始前に税理士レビューで仕訳パターンの妥当性を検証すること。
追加で検討すべき事項(実装過程で判明したもの):
dmCalcBracketTax_の共通化 — 現在は603_datamart_pl.js内のローカルヘルパー(末尾アンダースコア)として定義されており、本案件で外部参照する。将来、000_infra/004_utils.jsにUtils.calcBracketTax(income, brackets)として移動し、マート側も新ヘルパーを呼ぶ形にリファクタすべきか。影響範囲が大きいため別案件として検討。- 年度依存の税率テーブル — 法改正により
TAX_RATES.bracketsは改訂される可能性あり。現状は単一テーブルのみなので、過去年度の仕訳を後から再計算する場合に不整合が生じる。Constants.TAX_RATES_HISTORY = { 2024: {...}, 2025: {...} }のような年度別テーブル化が必要か検討。 - 月割計算の会計期間起点の共通化 —
603_datamart_pl.jsL147 の8月起点ハードコードは環境依存。Constants.FISCAL_START_MONTH = 8を定義して両者が参照する設計に変更すべきか(本案件のスコープ外)。 - Action B パイプラインとの接続 — 本案件は仕訳を直接
42_trn_journalに追記するが、dev_mas-073_corporate_tax_transition.mdの思想では「手動 INV → Action A/B」経由が望ましい。両者のどちらを正式ルートとするか、運用方針の統一が必要。現時点では「計算支援 = 本案件」「納付 = Action B 経由」と棲み分ける設計を想定。 - サイドバーへのメニュー追加の是非 —
onOpen()限定としたが、頻度の低い決算処理を隠すことでユーザーが「機能の存在を忘れる」リスクもある。将来の運用実績を踏まえ、サイドバーに「💰 決算処理」セクションを追加するか判断。 - 重複仕訳の自動無効化 — 現仕様では重複検出時に YES で追記し、旧仕訳の無効化はユーザー手動対応。運用負荷を考慮し、YES 選択時に旧仕訳の
有効フラグを自動で FALSE に更新する機能を追加すべきか(JournalRepository.saveの実装が必要)。 - 事業税の特別法人事業税との分離 — 現在
TAX_RATES.brackets.localは「住民税法人税割 + 事業税所得割 + 特別法人事業税」の合算実効税率。税理士によっては内訳の仕訳分離を要求する場合があり、その際はbracketsを{ local_resident, local_enterprise, local_special }に細分化する必要あり。 - みなし法人税 (
auto_tax_national/auto_tax_local) との重複防止 — 本案件で仕訳追記後、マート再計算時にhasManualTax=trueとなり、みなし月次計算がゼロ化される(既存ロジック603_datamart_pl.jsL135-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.7 | P/L シート読み取りロジック・税額計算アルゴリズム・Human-in-the-Loop フロー設計に高い推論力が必要 |
| Step 1 新規モジュール実装 | Claude Sonnet 4.6 | ダイアログフロー・重複検出・DTO 構築等の中程度の判断が必要 |
| Step 2 onOpen() 拡張 | Claude Haiku 4.5 | 3 行追加の定型作業 |
| Step 3 監査ログ追加 | Claude Haiku 4.5 | Utils.logInfo 呼び出しの追加のみ |
| Step 4 テストケース追加 | Claude Haiku 4.5 | 期待値転記のみ |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-19 | 初版作成 |
仕様書作成プロンプト
展開して表示
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
- 拡張思考の使い分け: Phase 1で設計を確定させ、Phase 2では出力に徹する。
- テキスト報告の禁止: 説明は1文以内。直ちに tool を呼ぶ。
- 4-5 分割の Write/Edit 実行: 2-1(骨格)/2-2(前半)/2-3a(後半)/2-3b(プロンプト)/2-4(記録)に分割。
- 各 Step で何を書くかを具体指示: 出力時に設計判断を再考しない。
====================================================================== あなたはGAS会計システムのシニア開発者兼仕様書ライターです。 CLIエージェント「Claude Code」として、案件 MAS-073「未払法人税等の適切な処理」の開発仕様書を作成してください。
Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)
docs/_internal/TODO_future.mdで案件 MAS-073 の要件を把握。- 関連するGASコードやマスタをツールで深く調査。特に
000_infra/002_constants.jsのTAX_RATESオブジェクトの構造と、200_data/202_repository.jsのJournalRepository.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()に追加する。 - 処理は、ユーザーがメニューから起動する対話形式のフローとすること。
- 新規メニュー「💰 決算処理 → 法人税等の計算・計上」を
- 処理フロー:
- 実行時、ダイアログで対象の会計年度をユーザーに選択させる。
- 対象年度のP/L(例:
92_fs_plシート)から税引前当期純利益を取得し、これを課税所得の基礎とする。 002_constants.jsのTAX_RATESオブジェクトをマスターデータとして使用し、法人税、住民税及び事業税を計算する。- 800万円を境とする累進課税ブラケットを適用すること。
- 地方税均等割 (
localMinimumAnnual) を加算すること。
- 計算後、ユーザーに確認ダイアログを提示する。(詳細はStep 2-3aで指示)
- ユーザー承認後、
JournalRepository.append()を使用して42_trn_journalシートに決算整理仕訳を追記する。- 借方:
法人税、住民税及び事業税 - 貸方:
未払法人税等
- 借方:
- 二重計上防止策:
- 仕訳を追記する前に、
42_trn_journal内に同一年度の法人税等計上仕訳が既に存在しないか確認すること。 - 識別のため、摘要に「【自動】法人税等計上 FYxxxx」のような一意な文字列を含めること。
- 既存の仕訳が存在する場合は、ユーザーに再計算して上書きするか確認するフローを設けること。
- 仕訳を追記する前に、
Step 2-3a: エッジケース〜人間検討事項の追記 (File Edit または Bash)
※アーキテクトからの指示:
- エッジケーステーブルの作成: 以下のシナリオを網羅したエッジケーステーブルを仕様書に含めること。
- 課税所得がマイナス(赤字)の場合:
- 所得に応じた税額(法人税、住民税所得割、事業税)はゼロとして計算する。
- ただし、地方税の均等割 (
localMinimumAnnual) は赤字でも発生するため、この金額のみを計上する。 - 仕訳科目は「租税公課 / 未払法人税等」とする。
- 設立初年度の場合:
Constants.TAX_RATES.foundingYearとfoundingMonthを参照し、事業年度の月数を正確に計算する。- 地方税均等割は、算出した事業月数に応じて月割り計算すること (
年額 * 事業月数 / 12)。
- 課税所得がマイナス(赤字)の場合:
- 人間が検討すべき事項 (Human-in-the-Loop):
- この機能は会計の根幹に関わるため、計算結果をユーザーが検証するフローを必須とすること。
- 仕訳を書き込む前に、必ず以下の情報を含む確認ダイアログをユーザーに提示し、明示的な承認を得ること。
- 計算の基礎となった課税所得額
- 計算された税額の内訳(国税、地方税所得割、地方税均等割、合計額)
- 生成される仕訳のプレビュー(勘定科目と金額)
Step 2-3b: 実装プロンプト〜変更履歴の追記 (File Edit または Bash)
Step 2-4: 仕様書作成プロンプトの記録 (File Edit または Bash)
末尾に <details> を設け、この <instruction> 全文を追記。