概要
| 項目 | 内容 |
|---|
| 案件ID | MAS-090 |
| カテゴリ | 会計指針 (営業外収益・法人税等) |
| Phase | P3 |
| 優先度 | ★ |
| 所要時間 | 3〜4時間 |
| 対象ファイル (新規) | 400_domain/408_withholding_tax.js |
| 対象ファイル (追記) | 000_infra/002_constants.js, templates/operations_sidebar.html |
| 参照ファイル (読み取りのみ) | 200_data/202_repository.js, 000_infra/003_contracts.js, 000_infra/004_utils.js, 100_config/101_sys_config.js |
| 前提マスタ整備 (別作業) | 11_mst_account に 仮払法人税等 (B/S 流動資産) を追加する D-系マイグレーション |
| 前提案件 | なし(独立機能) |
目的
銀行普通預金に着金する受取利息は、金融機関側で 所得税 15%+復興特別所得税 0.315% (合算 15.315%) + 道府県民税利子割 5% = 合計 20.315% が源泉徴収された「ネット額」で入金される。現状は 42_trn_journal にネット額のまま手動計上しており、
中小企業会計指針第 21 項(税金の取扱い)に準拠した「グロス額で受取利息を計上し、源泉徴収税額は仮払法人税等として分離」という処理が行われていない。
本案件では以下の UI 機能を追加することで、Human-in-the-Loop で 1 クリックで「ネット 1 行 → グロス利息 1 行 + 源泉税 1 行 + 元仕訳の振替取消 1 行」の展開を実現する。
- 受取利息のグロス表示により、期間比較・税務申告用の数値がクリーンになる
- 源泉徴収税額を
仮払法人税等 として分離することで、期末の法人税等確定申告時に税額控除が正しく適用できる
- 元仕訳は物理削除せず 仕訳振替方式 で残すため、監査証跡 (Audit Trail) が保全される
現在のコード
42_trn_journal の既存スキーマ (101_sys_config.js L656)
'TRN_JOUR': {
headers: [
"取引ID", "発生日(P/L計上日)", "決済日_計画", "決済日_実績",
"収支区分", "取引先名", "科目名", "税区分",
"外貨金額", "通貨",
"税抜金額_実績", "消費税額_実績", "税込金額_実績",
"組織名", "PJ名", "決済手段",
"仕訳ステータス", // "自動計上" | "手動" | "仕訳振替"
"証憑URL", "摘要",
"管理ID", "CF支払ID", "CF支払ID枝番",
"収支区分コード", "取引先コード", "法人番号",
"主科目コード", "税区分コード",
"組織コード", "PJコード", "決済手段コード",
"仕訳ステータスコード"
],
color: "#38761d"
}
重要発見: 42_trn_journal には 有効フラグ 列が存在しない。
| # | 確認済み事項 | 影響 |
|---|
| 1 | JournalEntryDTO (000_infra/003_contracts.js L97-129) に 有効フラグ プロパティがない | 元仕訳を論理削除するには別手段が必要 |
| 2 | JournalRepository.findAll/save/append (200_data/202_repository.js L259-298) は全フィールドを DTO 変換するが 有効フラグ は扱わない | 採用できる「取消し」は (a) 物理削除、(b) 仕訳振替方式 のいずれか |
| 3 | 既存の 仕訳ステータス には "仕訳振替" という値が流通している (400_domain/410_subledger_engine.js L150-166) | 本仕様では "仕訳振替" を再利用する |
現状の起票フロー(ネット額のまま記録)
(1) 銀行利息着金 (例: ¥797 = ¥1000 - 20.315%)
↓ 手動または Action B
(2) 42_trn_journal に1行
収支区分=収入 科目名=受取利息 税抜金額_実績=797 仕訳ステータス=手動(or 自動計上)
↓
(3) 92_fs_pl の「受取利息」にネット額 ¥797 が反映される ❌ グロス表示されない
法人税等確定申告の「源泉徴収税額の控除」¥203 を使えない ❌
既存定数の構造 (000_infra/002_constants.js L9-30)
/** @deprecated TAX_RATES を使用してください */
DEFAULT_TAX_RATE: 0.30,
ALLOWANCE_RATE: 0.006,
TAX_RATES: {
brackets: [
{ upTo: 8000000, national: 0.165, local: 0.049 },
{ upTo: Infinity, national: 0.256, local: 0.080 },
],
localMinimumAnnual: 70000,
foundingYear: 2025,
foundingMonth: 11,
},
本機能で追加する WITHHOLDING_TAX_RATE_INTEREST: 0.20315 は TAX_RATES とは独立した単一数値のため、TAX_RATES ブロックの 直後 (L30 の閉じ }, と L32 の MONTH_ITERATION_LIMIT の間、L31 空行の位置) にトップレベル定数として追加する。
// 101_sys_config.js
function onOpen() {
ui.createMenu('🚀 BizLP')
.addItem('操作パネルを開く', 'openOperationsSidebar')
.addSeparator()
.addItem('✅ 自動起動を有効化', 'installAutoOpenSidebarTrigger')
.addItem('🚫 自動起動を無効化', 'uninstallAutoOpenSidebarTrigger')
.addToUi();
}
全操作はサイドバー (templates/operations_sidebar.html) に集約されている。よってメニュー項目の追加先は onOpen() ではなくサイドバー HTML になる。既存の「📒 経理業務 (RPA / Action)」セクション (L32-43) の末尾、processSettlementClearings (Action B) ボタン (L42) の 直後 に新ボタンを追加する。これは「決済仕訳が Action B で生成された後に、受取利息のグロスアップ展開を実行する」という時系列的に自然な配置である。
修正方針
ファイルごとの変更一覧
| # | ファイル | 変更内容 | 目安行数 |
|---|
| 1 | 000_infra/002_constants.js | L31 空行位置に WITHHOLDING_TAX_RATE_INTEREST: 0.20315 を追加 | +2 |
| 2 | 400_domain/408_withholding_tax.js | 新規作成。WithholdingTaxService.applyInterestWithholdingTax() を公開 | +180 |
| 3 | templates/operations_sidebar.html | L42 の直後に 💰 受取利息の源泉税展開 ボタンを追加 | +1 |
| 4 | 前提: 11_mst_account に 仮払法人税等 (コード 157 等, 諸表区分=BS, 大分類=資産, 表示区分=その他流動資産) を追加 | D-系マイグレーションで別 PR 対応 | — |
グロスアップ計算式
// net: 銀行着金額 (ユーザーが選択した行の 税抜金額_実績)
// rate: Constants.WITHHOLDING_TAX_RATE_INTEREST = 0.20315
var netAmt = Number(selectedRow['税抜金額_実績']) || 0;
var rate = Constants.WITHHOLDING_TAX_RATE_INTEREST;
var grossAmt = Math.floor(netAmt / (1 - rate)); // 端数は切り捨て(所得税法基本通達 181-223)
var taxAmt = grossAmt - netAmt; // 差引き(taxAmt = Math.floor(netAmt * rate / (1-rate)) と同値だが誤差を排除するため差分で算出)
検算例: netAmt = 797 → grossAmt = floor(797 / 0.79685) = floor(1000.188...) = 1000、taxAmt = 1000 - 797 = 203。実務上の合致。
UI フロー (Human-in-the-Loop)
【事前】ユーザーは 42_trn_journal で「受取利息・ネット額」の1行を行選択する
↓
【Step 1】サイドバー「💰 受取利息の源泉税展開」ボタン押下
↓ google.script.run で applyInterestWithholdingTax を呼ぶ
【Step 2】LockService.tryLock(10000) で排他ロック取得
↓ タイムアウト時はエラーダイアログ
【Step 3】選択行バリデーション
- 選択行数 === 1 (0行/複数行はエラー)
- 科目名 === '受取利息' (Phase 1 で科目マスタ L210 確認済み)
- 仕訳ステータス !== '仕訳振替' (既に展開済みの行を再処理しない)
- 税抜金額_実績 > 0
↓
【Step 4】確認ダイアログ表示(OK/Cancel)
┌────────────────────────────────────┐
│ 受取利息の源泉税展開 │
│ │
│ 元仕訳 : TRN_yyyyMMdd_NNNN │
│ ネット額 ¥797 │
│ │
│ グロス利息 : ¥1,000 (収入) │
│ 源泉税 : ¥ 203 (仮払法人税等) │
│ 元仕訳取消: ¥797 (仕訳振替で相殺) │
│ │
│ 合計 3 行を 42_trn_journal に追記 │
│ 元仕訳はそのまま残ります(監査証跡)│
└────────────────────────────────────┘
↓ OK
【Step 5】JournalRepository.append([row1, row2, row3]) で 3 行追記
↓
【Step 6】LockService.releaseLock() → toast 通知 + Utils.logInfo + Utils.auditLog
アトミック処理と3つの追記仕訳
JournalRepository.append(dtos) に渡す DTO 配列は以下の 3 要素(全て 42_trn_journal の 1 行ずつに対応):
追記1: 元仕訳の振替取消 (Reverse)
| フィールド | 値 |
|---|
| 取引ID | RpaCommon.generateTrnId(dateStr, 0) で新規発番 |
| 発生日(P/L計上日) | 元仕訳と同じ |
| 収支区分 | '支出' (元が収入なので逆) |
| 取引先名 | 元仕訳と同じ |
| 科目名 | '受取利息' |
| 税区分 | '非課税' |
| 税抜金額_実績 | netAmt (元と同額、収支区分が逆なので実質相殺) |
| 消費税額_実績 | 0 |
| 税込金額_実績 | netAmt |
| 組織名 / PJ名 / 決済手段 | 元仕訳と同じ |
| 仕訳ステータス | '仕訳振替' |
| 管理ID | 元仕訳の 取引ID (トレーサビリティ) |
| 摘要 | '[S-18 元仕訳取消] ' + 元仕訳の摘要 |
追記2: グロス利息 (Gross-up revenue)
| フィールド | 値 |
|---|
| 取引ID | RpaCommon.generateTrnId(dateStr, 1) |
| 収支区分 | '収入' |
| 科目名 | '受取利息' |
| 税区分 | '非課税' |
| 税抜金額_実績 | grossAmt |
| 税込金額_実績 | grossAmt |
| 仕訳ステータス | '手動' (グロスアップは人間承認済みのため手動扱い) |
| 管理ID | 元仕訳の 取引ID |
| 摘要 | '[S-18 グロス利息] ' + 元仕訳の摘要 |
追記3: 源泉徴収税 (Withholding tax prepaid)
| フィールド | 値 |
|---|
| 取引ID | RpaCommon.generateTrnId(dateStr, 2) |
| 収支区分 | '支出' |
| 科目名 | '仮払法人税等' ← 科目マスタに事前登録必須 |
| 税区分 | '対象外' |
| 税抜金額_実績 | taxAmt |
| 税込金額_実績 | taxAmt |
| 仕訳ステータス | '手動' |
| 管理ID | 元仕訳の 取引ID |
| 摘要 | '[S-18 源泉徴収税] ' + 元仕訳の摘要 + ' (税率 20.315%)' |
会計恒等式: 追記1 (支出 netAmt) + 追記2 (収入 grossAmt) + 追記3 (支出 taxAmt) = 収入 grossAmt - netAmt - taxAmt = 0 円 (grossAmt - netAmt = taxAmt なので)。つまり元仕訳 (収入 netAmt) と合算すると、純額は netAmt のまま維持されつつ、P/L 上はグロス grossAmt / B/S 上は taxAmt が仮払法人税等として計上される。
書き込み方式の比較と採用
| 方式 | 処理 | 採用 | 理由 |
|---|
A. append(3 DTOs) | 末尾3行追記のみ。元仕訳はそのまま | ✅ | 最小アトミック (1回の setValues)、監査証跡保全、JournalRepository.save() の全クリア再書込を回避 |
B. save(全DTOs) | 元行の削除 + 新規3行を含めて全置換 | ❌ | writeDtosToSheet_ が clearContent → 全行 setValues で大規模データ時に数秒〜十数秒かかる |
C. 元行セル直接更新 + append(2 DTOs) | 有効フラグ=FALSE 直接書込 + 2行追記 | ❌ | 42_trn_journal に 有効フラグ が存在しないため実施不可 |
採用 = A。LockService で排他した上で 1 回の append を呼ぶ。
既存コンポーネントの再利用
| モジュール | 関数 | 用途 |
|---|
JournalRepository (202) | append(dtos) | 末尾3行追記 |
Contracts (003) | toRow(headers, dto) は JournalRepository.append 内で自動呼出 | ヘッダー順に値をマッピング |
RpaCommon (400) | 既存 _invIdCache と同様の _trnIdCache を新規追加(または Utils.getIdPrefixConfig + 直接採番) | TRN_yyyyMMdd_NNNN 採番 |
Constants (002) | WITHHOLDING_TAX_RATE_INTEREST (新規) / getParam | 税率定数 / 03_sys_params 経由の科目名フォールバック |
Utils (004) | logInfo / toastResult / auditLog | 統一ログ |
LockService | getScriptLock() + tryLock(10000) + releaseLock() | 排他制御 |
影響範囲
- 変更ファイル: 3 ファイル (新規 1 + 追記 2)
- 変更行数: 約 +185 行
- 既存動作への影響: なし
42_trn_journal への新規追記のみで、元仕訳を 物理削除しない
601_datamart_ingest 以降のマート処理は 仕訳振替 行を既存ルールで処理するのみ
- 既存の Action A / Action B ロジックは無変更
- マート側での数値変化:
- 92_fs_pl の「受取利息」は ネット → グロス に増加する
- 91_fs_bs の「仮払法人税等」(新規表示)に
taxAmt が積み上がる
- 84_cf_daily_plan / 85_cf_daily_actual の受取利息ラインはグロス表示に変わる
- 実行タイミング: 銀行利息着金後、ユーザーが任意のタイミングで実行
- 実行頻度見積もり: 小規模法人では年 2〜4 回(普通預金利息は半期または年払いが多い)
注意事項
仮払法人税等 が科目マスタに未登録の場合: 処理開始時に AccountRepository.findAsMap() で存在確認し、未登録ならエラーダイアログを出して中断する。仕様書としては D-系マイグレーションで事前に登録する前提とし、未登録時は「運用開始前に 11_mst_account に追加してください」と明示する。
仕訳振替 行を再実行しない: 選択行の 仕訳ステータス === '仕訳振替' ならエラー中断する。これは「既に MAS-090 展開済みの振替取消行」や、他の仕訳振替処理で作られた行を誤って二次処理するのを防ぐ。
- 元仕訳の
管理ID の扱い: 元仕訳の 取引ID を 3 行追記すべての 管理ID にコピーすることで、42_trn_journal のフィルタで「管理ID=元TRN_ID」とすれば「元 + 3行追記」を全て一覧できる(監査・ロールバック用)。
- 選択行数の判定:
SpreadsheetApp.getActiveSheet().getActiveRange() の getNumRows() で 1 行かチェック。複数行はアラート中断(将来の拡張で一括処理対応可能)。
LockService のタイムアウト値: 10 秒。他の Action A/B や RPA と被らない前提だが、同時実行でロック取得失敗した場合は「他処理が実行中です」とダイアログ表示。
- 所得税法基本通達の端数処理: 源泉徴収税額は 1 円未満切捨て が原則 (所得税法 120 条関連)。
Math.floor を使用。
- 非課税科目のため消費税は 0: 受取利息・源泉税とも
税区分='非課税' または '対象外' で 消費税額_実績=0。
Constants.getParam() による科目名の外部化: 将来の科目名変更 (例: 仮払法人税等 → 仮払税金) に対応するため、ハードコード値を 03_sys_params からのフォールバックに置き換える設計を推奨する。
INTEREST_INCOME_ACCOUNT (デフォルト '受取利息')
WITHHOLDING_TAX_ACCOUNT (デフォルト '仮払法人税等')
- 複数銀行口座・複数利息行がある場合: 1 行ずつ選択 → 実行を繰り返す運用とする。将来要件として「年度まとめて一括展開」機能は別案件で検討。
- CF (84_cf_daily_plan) への影響: 追記1 (仕訳振替・支出) と追記2 (収入) の合算で 元仕訳のネット額を相殺 → グロスで表示 される形になる。追記3 (仮払法人税等) は B/S 資産科目のため CF には現金移動として載らない(正しい挙動)。
エッジケース
| # | 条件 | 挙動 | 理由 |
|---|
| 1 | アクティブシートが 42_trn_journal 以外 で実行された | エラーダイアログ「42_trn_journal タブでこの操作を実行してください」→ 処理中断 | 誤操作防止。他タブの行を誤って展開しない |
| 2 | 選択行数が 0 行 (カーソルがヘッダー行 row=1) | エラーダイアログ「展開対象の行を 1 行選択してください」→ 処理中断 | 必須入力 |
| 3 | 選択行数が 2 行以上 | エラーダイアログ「1 行ずつ選択して実行してください」→ 処理中断 | バッチ処理は別案件。1 行ずつの処理を強制 |
| 4 | 選択行の 科目名 !== '受取利息' | エラーダイアログ「対象科目は『受取利息』のみです(現在: XXXX)」→ 処理中断 | 誤操作防止。源泉税の計算式が他科目では成立しない |
| 5 | 選択行の 仕訳ステータス === '仕訳振替' | エラーダイアログ「この行は既に仕訳振替処理済みです」→ 処理中断 | 二重実行防止。MAS-090 で追記1として作成された取消行を誤って再処理しない |
| 6 | 選択行の 税抜金額_実績 <= 0 もしくは非数値 | エラーダイアログ「金額が 0 以下または不正です」→ 処理中断 | 不正入力。グロスアップ計算が意味をなさない |
| 7 | 収支区分 !== '収入' | エラーダイアログ「収支区分が『収入』の行のみ展開可能です」→ 処理中断 | 受取利息は必ず収入 |
| 8 | 11_mst_account に 仮払法人税等 が未登録 | エラーダイアログ「科目マスタに『仮払法人税等』を登録してください」→ 処理中断 | 下流 (601_datamart_ingest) で未登録科目エラーが起きる前にブロック |
| 9 | LockService.tryLock(10000) がタイムアウト | エラーダイアログ「他の処理が実行中です。数秒後に再実行してください」→ 処理中断 | 同時実行時の排他制御 |
| 10 | 確認ダイアログで キャンセル が押された | 何もせず終了 | Human-in-the-Loop の基本 |
| 11 | 源泉税額に端数発生 (例: netAmt=1000 → grossAmt = floor(1000/0.79685) = 1254、taxAmt = 254) | Math.floor() で 1 円未満切捨て | 所得税法 120 条・基本通達 181-223 準拠 |
| 12 | JournalRepository.append() 実行中に例外発生 | try/catch で捕捉 → LockService.releaseLock() → エラーダイアログ + Utils.logError | 処理中断時もロック確実解放 |
| 13 | 選択行の 発生日(P/L計上日) が空 | エラーダイアログ「発生日が未入力です」→ 処理中断 | 3 行追記の発生日に使えない |
| 14 | 追記3 の 仮払法人税等 が後日法人税確定申告で相殺されず残る | 期末マート側で警告(MAS-073 未払法人税等の処理と連動) | MAS-073 と MAS-090 の連動は別案件扱い。現段階では B/S 上に計上するのみ |
| 15 | ユーザーが誤って 42_trn_journal の 集計行・空行 を選択 | エラーダイアログ「取引ID が空の行は選択できません」→ 処理中断 | 取引ID フィールドが空なら弾く |
実データ検証
11_mst_account (科目マスタ) の事前確認結果
| 科目名 | コード | 諸表区分 | 大分類 | 表示区分 | 税区分 | 登録状況 | 備考 |
|---|
| 受取利息 | 600 | PL | 収益 | 財務収益 (営業外収益) | 非課税 | ✅ 登録済 | 本案件の対象科目。正確な文字列: "受取利息" |
| 仮払法人税等 | 157 (予約) | BS | 資産 | その他流動資産 | 対象外 | ❌ 未登録 | 実装前にマスタ登録が必須。D-系マイグレーションまたは手動登録 |
| 法人税、住民税及び事業税 | 800 | PL | 費用 | 法人税等 | 対象外 | ✅ 登録済 | 代替案として検討したが、本来は期末計上科目のため MAS-090 では使わない |
| 租税公課 | 527 | PL | 費用 | 一般管理費 (販管費) | 対象外 | ✅ 登録済 | 代替案として検討可能(簡易処理)。ただし税務上は分離が望ましい |
| 預り金 | 208 | BS | 負債 | その他流動負債 | 対象外 | ✅ 登録済 | 源泉徴収税の 支払義務者側 (給与源泉等) で使用。受領者側 (本案件) では使わない |
仮払法人税等 追加手順 (前提マイグレーション)
11_mst_account シート末尾に1行追加:
| A: TRUE | B: 157 | C: BS | D: 資産 | E: その他流動資産
| F: 仮払法人税等 | G: 仮払法人税等 | H: 固定費 | I: 対象外
| J: 仮払法人税等
| K: 法人税・源泉所得税等のうち当期に前払いしたもの。期末に未払法人税等と相殺する
または、800_ops/ 配下に 8XX_migration_d07_withholding_account.js を新規作成し、冪等性付きで自動追加する(D-01〜D-03 マイグレーションと同じパターン)。
03_sys_params の追加候補キー (将来拡張・任意)
| キー | デフォルト値 | 用途 |
|---|
INTEREST_INCOME_ACCOUNT | 受取利息 | 対象科目名。将来の科目名変更時の切り替え |
WITHHOLDING_TAX_ACCOUNT | 仮払法人税等 | 源泉税の相手勘定。法人税、住民税及び事業税 / 租税公課 等への切替可能にするため |
WITHHOLDING_TAX_RATE_INTEREST | 0.20315 | 源泉税率のパラメータ化(法改正対応)。Constants.WITHHOLDING_TAX_RATE_INTEREST より優先 |
関連ドキュメント
人間が検討すべき事項
TODO_future.md 由来の項目
- 銀行口座の利息発生頻度・金額の事前確認
- 現状の普通預金利息の年間総額・発生頻度 (半期 / 年次) を把握する
- 金額が年数百円レベルなら重要性基準で簡易処理 (ネット計上のまま) でも許容される可能性
- 金額が数万円以上なら本機能の本格運用価値が高い
本仕様書策定時に発見された追加検討事項
- 源泉税の相手勘定 (科目選定) 🔴 最重要
- 本仕様書では
仮払法人税等 を推奨しているが、以下の選択肢がある:
- (a)
仮払法人税等 (新設・BS流動資産) — 正統。期末に 未払法人税等 と相殺し税額控除を適用
- (b)
法人税、住民税及び事業税 (既存・PL費用) — 年末に直接費用計上する簡易処理
- (c)
租税公課 (既存・PL販管費) — さらに簡易。税務上の区分は不正確
- 顧問税理士との確認必須。小規模法人で法人税還付が発生するかで (a) vs (b) が決まる
- 国税と地方税 (利子割 5%) を 1 科目に合算するか分離するか
- 所得税 15.315% + 道府県民税利子割 5% = 20.315%
- 税理士側では分離 (
仮払法人税等 / 仮払都道府県民税利子割) を推奨する場合がある
- 本仕様書は合算 1 仕訳で実装するが、分離要件が出たら追記4行目を追加する拡張が必要
- 科目名をコードハードコード vs
03_sys_params 経由
- 推奨:
03_sys_params の INTEREST_INCOME_ACCOUNT / WITHHOLDING_TAX_ACCOUNT キーから取得
- 実装コスト少、将来の科目名変更や税理士レビューでの科目切替に柔軟対応
- デメリット: パラメータ未登録時のフォールバックロジック (
Constants.getParam(key, '受取利息')) が必須
42_trn_journal に 有効フラグ 列を追加するか (全体波及)
- 本案件では 追加しない 方針(仕訳振替方式で解決)
- 将来的に他案件 (MAS-087 元データ修正→下流再同期 等) で
有効フラグ が必要になれば、DDL全体更新で追加する
- いったん
仕訳振替 方式で MAS-090 を実装し、後続案件の要求を見てから DDL 変更の要否を判断
- 一括展開 (複数行同時処理) 機能の要否
- 本仕様では 1 行選択のみ対応
- 年度末に銀行全口座の利息をまとめて展開したい要求が出れば別案件で追加
- 実行履歴の永続化
Utils.auditLog() で 98_audit_log にログ記録する想定
- 展開済み元仕訳を
42_trn_journal の 管理ID 経由で後追い可能にすることで、二重実行防止と監査証跡を両立
- 前提マイグレーションの実施タイミング
- 実装 PR に
800_ops/8XX_migration_d07_withholding_account.js を同梱するか、別 PR で先行させるか
- 推奨: 別 PR で先行(D-系マイグレーションは独立性が高いため)
- MAS-073 (未払法人税等) との連携
- MAS-073 実装時に
仮払法人税等 を 未払法人税等 から差し引く決算仕訳ロジックを追加する必要あり
- 本案件単独では B/S に
仮払法人税等 が積み上がり続けるだけ。期末処理は別スコープ
実装プロンプト(Claude Code 用)
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
CLIエージェントである「Claude Code」として、以下の指示に従い、案件 MAS-090「受取利息の源泉所得税(グロス計上+仮払法人税振替)」を実装してください。
## 実行前タスク(コンテキストの読み込み)
実装前に**必ず**以下のファイルを読み込み、現在の実装を正確に把握してください。
1. `CLAUDE.md` — プロジェクトルール(ファイル担当マトリクス、コーディング規約、GAS ファイル番号体系)
2. `000_infra/002_constants.js` — `TAX_RATES` の構造(L21-30)、`getParam()` 実装(L147-167)
3. `000_infra/003_contracts.js` — `JournalEntryDTO` の全フィールド名(L95-129)。**`有効フラグ` が存在しないことを必ず確認**
4. `200_data/202_repository.js` — `JournalRepository.findAll/save/append`(L259-298)、`AccountRepository.findAsMap`(L315-349)
5. `400_domain/400_rpa_common.js` — ID採番パターン(`generateInvId` 参考、L25-41)
6. `100_config/101_sys_config.js` — `TRN_JOUR` スキーマ定義(L656)で `42_trn_journal` に `有効フラグ` が無いことを確認。`onOpen()`(L299-308)
7. `templates/operations_sidebar.html` — サイドバーボタン構造(L32-43 が経理業務セクション)
8. `docs/master/mst_account.md` — `受取利息` (L210, 科目コード600) 登録済、`仮払法人税等` 未登録を確認
9. `docs/dev/dev_mas-090_withholding_tax_interest.md` — 本仕様書(設計決定事項を全て含む)
## 修正対象ファイル
以下の 3 ファイルのみ変更してください。他ファイルは変更不要です。
- 追記: `000_infra/002_constants.js` (定数1件追加)
- **新規作成**: `400_domain/408_withholding_tax.js` (サービス本体)
- 追記: `templates/operations_sidebar.html` (サイドバーボタン1件追加)
## 前提作業(別PRで先行実施)
以下は本PRのスコープ外ですが、本機能の運用開始前に必ず実施されていることを前提とします。
- `11_mst_account` に `仮払法人税等` を追加(コード157、BS/資産/その他流動資産/対象外)
- 追加方法: 手動登録 or `800_ops/8XX_migration_d07_withholding_account.js` を新規作成して冪等マイグレーション実行
## 実装内容
### Step 1: 定数追加(`000_infra/002_constants.js`)
`TAX_RATES` ブロックの閉じ `},` (L30)の**直後**、`MONTH_ITERATION_LIMIT` (L33)の直前に以下を追加:
```javascript
/** 受取利息の源泉徴収税率 (所得税15.315% + 道府県民税利子割5% = 20.315%) */
WITHHOLDING_TAX_RATE_INTEREST: 0.20315,
```
### Step 2: 新規ファイル `400_domain/408_withholding_tax.js`
以下のコードを作成してください:
```javascript
/**
* =========================================================
* 408_withholding_tax.js — 受取利息 源泉徴収税のグロス計上
* =========================================================
* 依存: JournalRepository (202), AccountRepository (202),
* Constants (002), Utils (004)
*
* S-18: 銀行利息のネット着金額を「グロス利息 + 仮払法人税等 + 元仕訳取消」
* の 3 仕訳に展開する Human-in-the-Loop UI 機能。
*/
var WithholdingTaxService = {
/** 受取利息の源泉税展開(サイドバーから呼ばれる公開API) */
applyInterestWithholdingTax: function() {
return applyInterestWithholdingTax();
},
};
/**
* 選択された受取利息行(ネット額)を、グロス利息+源泉税+元仕訳取消の3仕訳に展開する。
* UIから呼ばれる前提(サイドバーのボタン)。
*/
function applyInterestWithholdingTax() {
const FUNC = 'applyInterestWithholdingTax';
const ui = SpreadsheetApp.getUi();
const ss = SpreadsheetApp.getActiveSpreadsheet();
const lock = LockService.getScriptLock();
if (!lock.tryLock(10000)) {
ui.alert('🚨 他の処理が実行中', '数秒後に再実行してください。', ui.ButtonSet.OK);
return;
}
try {
// (1) アクティブシート確認
const sheet = ss.getActiveSheet();
if (sheet.getName() !== '42_trn_journal') {
ui.alert('🚨 対象タブ違い', '42_trn_journal タブでこの操作を実行してください。', ui.ButtonSet.OK);
return;
}
// (2) 選択行のバリデーション
const range = sheet.getActiveRange();
if (!range || range.getNumRows() !== 1 || range.getRow() === 1) {
ui.alert('🚨 行選択エラー', '展開対象の行を 1 行選択してください(ヘッダー行は不可)。', ui.ButtonSet.OK);
return;
}
const row = range.getRow();
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0]
.map(function(h) { return String(h).trim(); });
const rowValues = sheet.getRange(row, 1, 1, headers.length).getValues()[0];
const dto = Contracts.toDto(headers, rowValues);
const trnId = String(dto['取引ID'] || '').trim();
if (!trnId) {
ui.alert('🚨 空行', '取引ID が空の行は選択できません。', ui.ButtonSet.OK);
return;
}
const accountName = String(dto['科目名'] || '').trim();
const INTEREST_ACC = Constants.getParam('INTEREST_INCOME_ACCOUNT', '受取利息');
if (accountName !== INTEREST_ACC) {
ui.alert('🚨 科目違い', '対象科目は「' + INTEREST_ACC + '」のみです(現在: ' + accountName + ')。', ui.ButtonSet.OK);
return;
}
const status = String(dto['仕訳ステータス'] || '').trim();
if (status === '仕訳振替') {
ui.alert('🚨 処理済', 'この行は既に仕訳振替処理済みです。', ui.ButtonSet.OK);
return;
}
const direction = String(dto['収支区分'] || '').trim();
if (direction !== '収入') {
ui.alert('🚨 収支区分違い', '収支区分が「収入」の行のみ展開可能です(現在: ' + direction + ')。', ui.ButtonSet.OK);
return;
}
const netAmt = Number(dto['税抜金額_実績']) || 0;
if (netAmt <= 0) {
ui.alert('🚨 金額エラー', '金額が 0 以下または不正です(現在: ' + netAmt + ')。', ui.ButtonSet.OK);
return;
}
const occurDate = dto['発生日(P/L計上日)'];
if (!occurDate) {
ui.alert('🚨 発生日未入力', '発生日(P/L計上日) が未入力の行は展開できません。', ui.ButtonSet.OK);
return;
}
// (3) 科目マスタで『仮払法人税等』登録確認
const WH_ACC = Constants.getParam('WITHHOLDING_TAX_ACCOUNT', '仮払法人税等');
const acctMap = AccountRepository.findAsMap();
if (!acctMap[WH_ACC]) {
ui.alert('🚨 科目マスタ未登録',
'11_mst_account に「' + WH_ACC + '」を登録してから実行してください。',
ui.ButtonSet.OK);
return;
}
// (4) グロスアップ計算
const rate = Constants.WITHHOLDING_TAX_RATE_INTEREST;
const grossAmt = Math.floor(netAmt / (1 - rate));
const taxAmt = grossAmt - netAmt;
// (5) 確認ダイアログ
const msg = '元仕訳: ' + trnId + ' (ネット額 ¥' + netAmt.toLocaleString() + ')\n\n'
+ '以下 3 行を 42_trn_journal に追記します:\n\n'
+ ' ① [元仕訳取消] 支出 ' + INTEREST_ACC + ' ¥' + netAmt.toLocaleString() + ' (仕訳振替)\n'
+ ' ② [グロス利息] 収入 ' + INTEREST_ACC + ' ¥' + grossAmt.toLocaleString() + '\n'
+ ' ③ [源泉徴収税] 支出 ' + WH_ACC + ' ¥' + taxAmt.toLocaleString() + '\n\n'
+ '※ 元仕訳は削除せず残します(監査証跡)。\n'
+ '※ 全 3 行の 管理ID = ' + trnId + ' でトレース可能です。\n\n'
+ '実行してよろしいですか?';
const confirm = ui.alert('💰 受取利息 源泉税展開 (S-18)', msg, ui.ButtonSet.OK_CANCEL);
if (confirm !== ui.Button.OK) return;
// (6) 3 仕訳の DTO を作成
const dateStr = Utilities.formatDate(
(occurDate instanceof Date) ? occurDate : new Date(occurDate),
'Asia/Tokyo', 'yyyyMMdd');
const newTrnIds = generateTrnIds_(dateStr, 3);
const reverseDto = {
'取引ID': newTrnIds[0],
'発生日(P/L計上日)': occurDate,
'決済日_計画': dto['決済日_計画'],
'決済日_実績': dto['決済日_実績'],
'収支区分': '支出',
'取引先名': dto['取引先名'],
'科目名': INTEREST_ACC,
'税区分': '非課税',
'通貨': dto['通貨'] || 'JPY',
'税抜金額_実績': netAmt,
'消費税額_実績': 0,
'税込金額_実績': netAmt,
'組織名': dto['組織名'],
'PJ名': dto['PJ名'],
'決済手段': dto['決済手段'],
'仕訳ステータス': '仕訳振替',
'摘要': '[S-18 元仕訳取消] ' + String(dto['摘要'] || ''),
'管理ID': trnId,
};
const grossDto = {
'取引ID': newTrnIds[1],
'発生日(P/L計上日)': occurDate,
'決済日_計画': dto['決済日_計画'],
'決済日_実績': dto['決済日_実績'],
'収支区分': '収入',
'取引先名': dto['取引先名'],
'科目名': INTEREST_ACC,
'税区分': '非課税',
'通貨': dto['通貨'] || 'JPY',
'税抜金額_実績': grossAmt,
'消費税額_実績': 0,
'税込金額_実績': grossAmt,
'組織名': dto['組織名'],
'PJ名': dto['PJ名'],
'決済手段': dto['決済手段'],
'仕訳ステータス': '手動',
'摘要': '[S-18 グロス利息] ' + String(dto['摘要'] || ''),
'管理ID': trnId,
};
const taxDto = {
'取引ID': newTrnIds[2],
'発生日(P/L計上日)': occurDate,
'決済日_計画': dto['決済日_計画'],
'決済日_実績': dto['決済日_実績'],
'収支区分': '支出',
'取引先名': dto['取引先名'],
'科目名': WH_ACC,
'税区分': '対象外',
'通貨': dto['通貨'] || 'JPY',
'税抜金額_実績': taxAmt,
'消費税額_実績': 0,
'税込金額_実績': taxAmt,
'組織名': dto['組織名'],
'PJ名': dto['PJ名'],
'決済手段': dto['決済手段'],
'仕訳ステータス': '手動',
'摘要': '[S-18 源泉徴収税] ' + String(dto['摘要'] || '') + ' (税率 ' + (rate * 100).toFixed(3) + '%)',
'管理ID': trnId,
};
// (7) 3 行を末尾追記(全置換ではなく append で O(3) 書込)
const added = JournalRepository.append([reverseDto, grossDto, taxDto]);
// (8) ログ・通知
Utils.logInfo(FUNC,
'TRN展開: src=' + trnId + ' net=' + netAmt + ' gross=' + grossAmt + ' tax=' + taxAmt
+ ' 追記行数=' + added);
try {
Utils.auditLog('INSERT', '42_trn_journal', trnId, '', FUNC,
{ netAmt: netAmt, grossAmt: grossAmt, taxAmt: taxAmt },
{ reverse: newTrnIds[0], gross: newTrnIds[1], tax: newTrnIds[2] },
'S-18 受取利息 源泉税展開');
} catch(e) { /* auditLog 未実装環境ではスキップ */ }
ss.toast('✅ 3 行を追記しました (grossAmt=¥' + grossAmt.toLocaleString()
+ ' / taxAmt=¥' + taxAmt.toLocaleString() + ')',
'S-18 完了', 8);
} catch (e) {
Utils.logError(FUNC, e);
SpreadsheetApp.getUi().alert('🚨 エラー発生', FUNC + ': ' + e.message, SpreadsheetApp.getUi().ButtonSet.OK);
} finally {
try { lock.releaseLock(); } catch(e) {}
}
}
/**
* TRN ID を指定件数まとめて採番する。
* 既存の 42_trn_journal を走査して当日最大番号を算出し、連番で返す。
* @private
*/
function generateTrnIds_(dateStr, count) {
const result = JournalRepository.findAll();
const prefix = 'TRN_' + dateStr + '_';
let maxNum = 0;
for (let i = 0; i < result.dtos.length; i++) {
const id = String(result.dtos[i]['取引ID'] || '');
if (id.indexOf(prefix) === 0) {
const num = parseInt(id.substring(prefix.length), 10);
if (!isNaN(num) && num > maxNum) maxNum = num;
}
}
const ids = [];
for (let j = 0; j < count; j++) {
ids.push(prefix + String(maxNum + 1 + j).padStart(4, '0'));
}
return ids;
}
```
### Step 3: サイドバーボタン追加(`templates/operations_sidebar.html`)
L42 の `<button class="btn" onclick="run('processSettlementClearings', this)">✅ 消込済STL→仕訳 (Action B)</button>` の**直後**に以下の 1 行を追加:
```html
<button class="btn" onclick="run('applyInterestWithholdingTax', this)">💰 受取利息の源泉税展開</button>
```
## 制約(必ず守ること)
1. `42_trn_journal` には **`有効フラグ` 列が存在しない**。元仕訳の論理削除を行わず、**仕訳振替方式で追記3行**で解決する
2. `JournalRepository.save()` ではなく `JournalRepository.append()` を使う(全置換クリアを回避)
3. `JournalEntryDTO` のフィールド名は `000_infra/003_contracts.js` のヘッダーと**完全一致**させる(全角・半角・括弧・アンダースコア含む)。特に:
- `取引ID`(ハイフンなし)
- `発生日(P/L計上日)`(全角カッコ)
- `税抜金額_実績`(アンダースコア)
- `仕訳ステータス`
- `管理ID`
4. 科目名は `Constants.getParam('INTEREST_INCOME_ACCOUNT', '受取利息')` 経由で取得する(ハードコード禁止)
5. 源泉税率は `Constants.WITHHOLDING_TAX_RATE_INTEREST` を参照する(マジックナンバー禁止)
6. `LockService.tryLock(10000)` で排他ロック、`finally` で必ず `releaseLock()`
7. 1 行選択の強制(`range.getNumRows() !== 1` または `range.getRow() === 1` はエラー)
8. `仕訳ステータス === '仕訳振替'` の行は二重実行防止のためエラーで中断
9. グロスアップ計算: `grossAmt = Math.floor(netAmt / (1 - rate))`, `taxAmt = grossAmt - netAmt`(切り捨て)
10. `AccountRepository.findAsMap()` で `仮払法人税等` の登録を確認してからでなければ処理実行不可
## エッジケース(必ず全 15 件ハンドリング)
(仕様書「エッジケース」セクションの表をそのまま転記)
| # | 条件 | 挙動 |
|---|------|------|
| 1 | `42_trn_journal` 以外で実行 | エラーダイアログ・中断 |
| 2 | 選択 0 行 (ヘッダー行) | エラーダイアログ・中断 |
| 3 | 選択 2 行以上 | エラーダイアログ・中断 |
| 4 | 科目名 !== '受取利息' | エラーダイアログ・中断 |
| 5 | 仕訳ステータス === '仕訳振替' | エラーダイアログ・中断 |
| 6 | 税抜金額_実績 <= 0 | エラーダイアログ・中断 |
| 7 | 収支区分 !== '収入' | エラーダイアログ・中断 |
| 8 | 仮払法人税等 未登録 | エラーダイアログ・中断 |
| 9 | LockService タイムアウト | エラーダイアログ・中断 |
| 10 | 確認ダイアログ キャンセル | 無処理終了 |
| 11 | 源泉税額端数 | Math.floor 切捨て |
| 12 | append 例外 | try/catch + releaseLock + エラーダイアログ |
| 13 | 発生日 空 | エラーダイアログ・中断 |
| 14 | 仮払法人税等 期末未相殺 | 現段階は B/S 計上のみ (MAS-073 連動は別案件) |
| 15 | 取引ID 空行 | エラーダイアログ・中断 |
## 実データ検証(使用する正確な文字列)
- 対象科目: `"受取利息"` (11_mst_account 登録済、コード600、非課税、財務収益)
- 相手勘定: `"仮払法人税等"` (要事前登録、コード157、BS/資産/その他流動資産/対象外)
- 仕訳ステータス: `"仕訳振替"` / `"手動"` (既存流通値)
- 税率定数名: `Constants.WITHHOLDING_TAX_RATE_INTEREST` (新規追加)
## 動作確認(`npm run push:dev` 後)
1. `11_mst_account` の最終行に `仮払法人税等` を手動追加する(または D-07 マイグレーション実行)
2. `42_trn_journal` に受取利息のテストデータ 1 行を挿入する
- 例: `取引ID=TRN_20260419_9999`, `発生日=2026-04-19`, `収支区分=収入`, `科目名=受取利息`, `税抜金額_実績=797`, `仕訳ステータス=手動`
3. サイドバー「🚀 操作パネルを開く」→「💰 受取利息の源泉税展開」を押す
4. 行未選択で実行 → 「展開対象の行を 1 行選択してください」エラー確認
5. 42_trn_journal の他科目行(例: `売上高`)を選択して実行 → 「対象科目は『受取利息』のみです」エラー確認
6. テスト行を選択して実行 → 確認ダイアログ表示
- 期待: 「① 支出 受取利息 ¥797 / ② 収入 受取利息 ¥1,000 / ③ 支出 仮払法人税等 ¥203」
7. OK 押下 → 42_trn_journal に 3 行追記される
8. 追記された 3 行の `管理ID` 列が全て `TRN_20260419_9999` (元取引ID)になっていることを確認
9. 追記1 行の `仕訳ステータス=仕訳振替` 確認
10. 同じテスト行をもう一度選択して実行 → 「この行は既に仕訳振替処理済みです」エラー(※ 実は元行は変更されていないが、追記1行を再選択した場合にエラーになることを確認)
11. 「📊 財務3表の更新」実行 → 92_fs_pl の「受取利息」が `¥1,000` (グロス) に、91_fs_bs の「仮払法人税等」が `¥203` に反映されることを確認
12. `98_audit_log` に INSERT ログが記録されていることを確認
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|
定数追加 (002_constants.js) | Claude Haiku 4.5 | 仕様書で行番号・コード完全定義済。判断要素なし |
新規ファイル 408_withholding_tax.js 作成 | Claude Sonnet 4.6 | グロスアップ計算・LockService パターン・DTO構築の複合ロジック。仕様書のコード例をほぼそのまま書き写す判断 |
| サイドバー HTML 追記 | Claude Haiku 4.5 | 仕様書で挿入位置・コード完全定義済 |
| 動作確認 (dev環境 GAS エディタ実行) | ユーザー手動 | 12 項目の手動検証フロー。GAS 認可・行選択の動的操作が必要 |
| 前提マイグレーション作成 (別PR) | Claude Sonnet 4.6 | 既存 804-807 マイグレーションと同パターンだが、冪等性と重複チェックに中程度の判断必要 |
変更履歴
仕様書作成プロンプト
展開して表示
<instruction>
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
1. **拡張思考の使い分け**: Phase 1(設計)ではフル活用し、ファイル名・関数名・行番号・エッジケース一覧・固有名詞を完全確定させる。Phase 2(清書)の各 Step 内では最小限に抑え、Phase 1 で確定した内容の書き下しに徹する。出力途中で再考しない。
2. **テキスト報告の禁止**: 「〜を作成します」等の text のみで tool_use なしに turn を終了しない。説明は 1 文以内。直ちに tool を呼ぶ。
3. **4-5 分割の Write/Edit 実行**: Step 2-1(骨格 ~20行) / 2-2(概要〜注意事項 ~300行) / 2-3a(エッジケース〜人間検討事項 ~200行) / 2-3b(実装プロンプト〜変更履歴 ~250行) / 2-4(`<details>` プロンプト全文記録) に分割。1 回の Write/Edit は約 300 行以内。
4. **各 Step で何を書くかを具体指示**: Phase 1 で設計判断を完全確定させてから Phase 2 に進む。Phase 2 実行中に「本当にこれで良かったか」と再考しない。
======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 S-18「受取利息の源泉所得税」の開発仕様書を作成してください。
開発仕様書を新規作成したら、`docs/_config.json` の `nav` 配列の適切なセクションにも必ず追記してください。
---
## Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)
以下をすべてツールで読み込み、Phase 2 で設計判断を再考しなくて済む状態にする。
**Grep は「どこにあるか」の発見まで。「どう書くか」の判断は必ず Read で行う。**
名前や記憶から「〜用の配列だろう」「〜というメニューがあったはず」と推測した瞬間に手を止めて Read する。
### 1-A: 案件定義
- `docs/_internal/TODO_future.md` で S-18 の行を検索し、案件名・概要・人間が検討すべき事項を取得する。
### 1-B: 既存仕様書フォーマットの確認
- `docs/dev/dev_mas-085_consistency_check.md` を 1 件 Read し、セクション構成とフォーマットを把握する。
### 1-C: 関連コードの調査(Read で構造を裏取り)
1. **`000_infra/002_constants.js`** を Read する
- `TAX_RATES`, `SHEET_DEFAULTS` 等の既存定数の構造(オブジェクト形式 / 配列 / ネスト深さ)を確認する
- `WITHHOLDING_TAX_RATE_INTEREST` が未定義であることを確認し、追加する正確な位置(行番号)を決定する(`TAX_RATES` ブロックの隣か、トップレベルか)
- `getParam()` メソッドの実装を確認し、`03_sys_params` から値を取得するパターンを把握する
2. **`000_infra/003_contracts.js`** を Read する
- `JournalEntryDTO` (`@typedef`) の**全フィールド名**を確認する
- `取引ID`, `有効フラグ`, `管理ID`, `税抜金額_実績`, `収支区分`, `科目名` が存在するか、正確なフィールド名(全角/半角、スペース等)を確認する
3. **`200_data/202_repository.js`** を Read する
- `JournalRepository.findAll()`, `save()`, `append()` の実装を確認する
- `save()` が `clearContent()` → 全行 `setValues()` の全置換方式であることを確認し、大規模データでのリスクを把握する
- 代替案として「元行セル直接更新(`getRange().setValue(false)`)+ `JournalRepository.append()` 2件」アプローチとの比較を検討し、どちらを採用するか Phase 2 前に確定する
4. **`100_config/101_sys_config.js`** を Read する
- `onOpen()` 関数内のメニュー構造(階層・メニュー項目名・実在する文字列)を **Read で確認**する。メニュー名を記憶や推測で書かない
- 「受取利息の源泉税を計算」を追加する正確な挿入位置(行番号)を特定する
- `LockService` を使用している既存の実装パターンがあれば参照する(ない場合は `LockService.getScriptLock()` / `lock.tryLock(10000)` / `lock.releaseLock()` パターンを採用する)
5. **`400_domain/407_rpa_orchestrator.js` または `400_domain/410_subledger_engine.js`** を Read する
- 既存の `JournalEntryDTO` 生成パターン(フィールドの設定方法、デフォルト値)を確認する
- 取引 ID 発番の既存パターン(`RpaCommon` の関数名、`Utils.getIdPrefixConfig()` の使用方法)を確認する
- 新関数をこのファイルに追記するか、新規ファイル(`400_domain/408_withholding_tax.js` 等)を作成するかを決定する
6. **MCP で `11_mst_account`(科目マスタ)の実データを確認する**
- `受取利息` が科目として登録されているか、**正確な文字列**(全角スペース等)を取得する
- 源泉税の相手勘定として使用する科目(`仮払法人税等` 等)が登録されているか、**正確な文字列**を取得する
- 登録がない場合は「実装前にマスタ登録が必要」として実データ検証セクションと人間が検討すべき事項に記載する
### 1-D: Phase 2 前に確定させる設計事項
以下を Phase 1 の調査結果から確定させ、Phase 2 では書き下しに徹する:
- 新関数を追加するファイルパスと関数名
- `101_sys_config.js` の `onOpen()` 内のメニュー追加行番号と正確なメニュー文字列
- `002_constants.js` への `WITHHOLDING_TAX_RATE_INTEREST: 0.20315` の追加行番号
- `JournalEntryDTO` の `有効フラグ` フィールドの正確な文字列
- 科目マスタで確認した `受取利息` と源泉税相手勘定の正確な科目名
- `save()` vs「元行直接更新 + `append()`」のどちらを採用するか
- グロスアップ計算式: `grossAmt = Math.floor(netAmt / (1 - 0.20315))`、`taxAmt = grossAmt - netAmt`(端数切り捨て)
---
## Phase 2: 仕様書の分割作成
出力先: `docs/dev/dev_mas-090_withholding_tax_interest.md`
### Step 2-1: 骨格の作成 (Write, ~20行)
全セクションの見出しのみを含む骨格ファイルを作成する。セクション:
`概要 / 目的 / 現在のコード / 修正方針 / 影響範囲 / 注意事項 / エッジケース / 実データ検証 / 関連ドキュメント / 人間が検討すべき事項 / 実装プロンプト(Claude Code 用) / 推奨実行モデル / 変更履歴 / 仕様書作成プロンプト`
### Step 2-2: 概要〜注意事項の追記 (Edit または Bash heredoc, ~300行)
以下の内容を記述する:
- **概要テーブル**: 案件ID=S-18, カテゴリ, Phase, 優先度, 所要時間, 対象ファイル(Phase 1 で確定したファイルパスを列挙), 前提案件
- **目的**: 受取利息のネット額計上をグロス利息 + 源泉税の 2 仕訳に展開する UI 機能の追加。二重計上防止のための元仕訳無効化を含む
- **現在のコード**: 現状は `42_trn_journal` に受取利息をネット金額で手動記録しており、源泉税相当額の計上漏れが発生しうる。具体的な問題箇所(シート名・フィールド名)を記載する
- **修正方針**:
- **追加ファイル・追加行番号**: Phase 1 で確定したファイルパスと行番号を明記する
- **定数追加**: `002_constants.js` の Phase 1 で確定した行番号に `WITHHOLDING_TAX_RATE_INTEREST: 0.20315` を追加する
- **メニュー追加**: `101_sys_config.js` の `onOpen()` 内、Phase 1 で特定した行番号の直後に、Phase 1 で確認した実在する形式に従ってメニュー項目を追加する
- **UIフロー**: ユーザーが `42_trn_journal` シートで対象行(`科目名 === '受取利息'`(Phase 1 で確認した正確な文字列)かつ `有効フラグ === true`)を 1 行選択 → メニュー実行 → グロス額・源泉税額・生成される 2 仕訳の内容をダイアログ表示 → 承認後に処理実行(Human-in-the-Loop)
- **グロスアップ計算**: `grossAmt = Math.floor(netAmt / (1 - Constants.WITHHOLDING_TAX_RATE_INTEREST))`、`taxAmt = grossAmt - netAmt`
- **アトミック処理** (`LockService.getScriptLock()` で全体を囲み、`lock.tryLock(10000)` でロック取得失敗時はエラーダイアログを表示して中断):
1. 元仕訳の `有効フラグ`(Phase 1 で確認した正確なフィールド名)を `false` に設定する
2. 新レコード1(グロス利息): `収支区分: '収入'`、`科目名: Phase 1 で確認した正確な文字列`、`税抜金額_実績: grossAmt`、`管理ID: 元の取引ID`、その他フィールドは元仕訳から引き継ぐ
3. 新レコード2(源泉税): `収支区分: '支出'`、`科目名: Phase 1 で確認した正確な文字列`、`税抜金額_実績: taxAmt`、`管理ID: 元の取引ID`
4. Phase 1 で確定した書き込みアプローチ(`save()` or 元行直接更新 + `append()`)で反映する
- **既存コンポーネントの再利用**: `JournalRepository.findAll()` / `save()` or `append()`(`202_repository.js`)、`Constants.WITHHOLDING_TAX_RATE_INTEREST`(`002_constants.js`)、`Utils.logInfo` / `Utils.toastResult`(`004_utils.js`)
- **影響範囲**: 変更ファイル・変更量・既存動作への影響(`JournalRepository.save()` を使用する場合は `42_trn_journal` 全データ書き戻しが発生することを明記する)
- **注意事項**:
- `有効フラグ` が `false` の行は処理対象外(二重実行防止)。選択行の `有効フラグ` を必ず確認してから実行する
- 選択行数が 1 以外の場合はアラートを表示して中断する
- `科目名` が対象科目以外の場合はアラートを表示して中断する
- `JournalRepository.save()` を採用した場合、シート全データを一旦クリアするため処理時間が長くなりうる。大規模データ環境では「元行セル直接更新 + `append()`」方式への切り替えを検討する
- 科目名はコードにハードコードせず、`Constants.getParam()` で `03_sys_params` から取得する設計を推奨する(将来の科目名変更に対応)
### Step 2-3a: エッジケース〜人間が検討すべき事項の追記 (Edit または Bash, ~200行)
以下の内容を記述する:
- **エッジケーステーブル** (| 条件 | 挙動 | 理由 |):
- 選択行が 0 行または 2 行以上 | エラーダイアログ表示・処理中断 | 1 行ずつの処理を強制
- 選択行の `科目名` が対象科目以外 | エラーダイアログ表示・処理中断 | 誤操作防止
- 選択行の `有効フラグ` が `false` | エラーダイアログ表示・処理中断 | 二重実行防止
- 選択行の `税抜金額_実績` が 0 以下 | エラーダイアログ表示・処理中断 | 不正入力
- `LockService.tryLock()` がタイムアウト | エラーダイアログ表示・処理中断 | 同時実行の排他制御
- 源泉税額の端数(例: netAmt=1000 → grossAmt=1254, taxAmt=254) | `Math.floor()` で切り捨て | 所得税法の規定に準拠
- **実データ検証**:
- MCP で確認した `受取利息` の正確な科目名(コードに埋め込む文字列・未登録なら登録手順)
- MCP で確認した源泉税相手勘定の正確な科目名(未登録なら登録手順)
- `03_sys_params` に科目名パラメータを追加する場合のキー名案(例: `INTEREST_INCOME_ACCOUNT`, `WITHHOLDING_TAX_ACCOUNT`)
- **関連ドキュメント**: テーブル形式で関連仕様書リンクを記載する
- **人間が検討すべき事項**: `TODO_future.md` からの転記 + 以下を追記:
- 源泉税の相手勘定科目(`仮払法人税等` 等)が適切か、また国税と地方税を合算で 1 科目に計上する方針で問題ないか → 顧問税理士への確認が必要
- 科目名をコードにハードコードするか `03_sys_params` 経由にするかの設計方針を確定する必要がある
- 銀行口座の利息発生頻度・金額の事前確認(本機能の利用頻度の見積もり)
### Step 2-3b: 実装プロンプト〜変更履歴の追記 (Edit または Bash, ~250行)
以下の内容を記述する:
- **実装プロンプト(行頭 4 スペースインデント、バッククォートで囲まない)**:
Phase 1〜Step 2-3a で確定した設計方針を反映した、別セッションにコピペしても動作する自己完結的な実装指示。以下を含める:
- 実行前タスク(読み込むファイルと確認ポイント)
- 修正対象ファイル(ファイルパスを「のみ」または「への追記」で明示)
- 実装内容(関数名・挿入行番号・グロスアップ計算式・LockService パターン・コード例を含む)
- 制約(`有効フラグ=false` 行のスキップ、1 行のみ選択の強制、`有効フラグ` と `管理ID` フィールドの正確な文字列)
- エッジケーステーブル(Step 2-3a のテーブルを転記)
- 実データ検証(使用する科目名の正確な文字列)
- 動作確認手順(`npm run push:dev` 後の番号付き検証手順。使用するメニュー名は Phase 1 で確認した実在する文字列のみ引用する)
- **推奨実行モデルテーブル**:
| 工程 | 推奨モデル | 理由 |
|------|----------|------|
| 実装(定数追加・関数追加・メニュー追加) | Claude Sonnet 4.6 | 複数ファイル横断・会計ロジック(グロスアップ計算・LockService)の理解が必要 |
- **変更履歴テーブル**: `| 2026-04-19 | 初版作成 |`
### Step 2-4: 仕様書作成プロンプトの記録 (Edit または Bash)
仕様書末尾の「仕様書作成プロンプト」セクションに、`<details><summary>展開して表示</summary>` ブロックで本 `<instruction>` 全文を記録する(最重量工程のため必ず独立 Step で実行する)。
---
## Phase 3: 後処理(登録・コミット)
### 3-A: `docs/_config.json` への追記
`docs/_config.json` を Read して既存エントリの番号体系を確認してから、重複しない番号で以下を追加する(§E.2 バグ修正・バリデーション、または案件カテゴリに応じた適切なセクション):
```json
{ "file": "dev/dev_mas-090_withholding_tax_interest.md", "title": "E.2.X MAS-090 受取利息の源泉所得税" }
```
### 3-B: `docs/_internal/changelog.md` への追記
`changelog.md` のヘッダー直後に以下を追記する:
```
| 2026-04-19 | [dev_mas-090_withholding_tax_interest.md](dev_mas-090_withholding_tax_interest.md) | 初版作成。受取利息のネット計上をグロス利息+源泉税2仕訳に展開するUI機能の仕様書 |
```
### 3-C: コミット&プッシュ
```bash
git add docs/dev/dev_mas-090_withholding_tax_interest.md docs/_config.json docs/_internal/changelog.md
git commit -m "docs: MAS-090 受取利息の源泉所得税 開発仕様書を作成
受取利息のネット計上をグロス利息+源泉税の2仕訳に展開するUI機能の仕様書。
Human-in-the-Loop設計・LockService排他制御・源泉税率定数化(WITHHOLDING_TAX_RATE_INTEREST)を含む。
https://claude.ai/code/session_XXXXX"
git push -u origin docs/dev-MAS-090
```
</instruction>