// === 仕訳登録 画面用ユーティリティ === // 税率を permil(‰)で表現:10%→100‰, 8%→80‰ // 小数誤差を避けるため、整数演算ベースで処理 const TAX_PERMIL = { "10": 100, // 10% "8": 80 // 8% }; // 補助: 四捨五入(整数) function roundInt(x) { return Math.round(x); } // 補助: 税抜 = round( 税込 × 1000 / (1000 + ratePermil) ) function calcExclFromIncl(inclAmount, ratePermil) { const denom = 1000 + ratePermil; return roundInt((inclAmount * 1000) / denom); } // 補助: 税額 = 税込 − 税抜(税込入力時) function calcTaxFromIncl(inclAmount, ratePermil) { const excl = calcExclFromIncl(inclAmount, ratePermil); return inclAmount - excl; } // 補助: 税額 = round( 税抜 × ratePermil / 1000 )(税抜入力時) function calcTaxFromExcl(exclAmount, ratePermil) { return roundInt((exclAmount * ratePermil) / 1000); } // 仕訳行タイプ判定(会計実務ルール) function isForceInclAccount(accountCodeOrName) { // 売掛金/買掛金/未収入金/未払金 は税込固定 const keywords = ["売掛金", "買掛金", "未収入金", "未払金"]; return keywords.some(k => accountCodeOrName.includes(k)); } // 税方向→仮受(売上) or 仮払(仕入) function taxAccountNameByDirection(direction) { return direction === "仮受" ? "仮受消費税等" : "仮払消費税等"; } // 状態管理(税額手修正フラグ) let manualTaxOverridden = false; // UIイベント: 税額手修正が行われたら自動連動解除 function onTaxAmountEdited() { manualTaxOverridden = true; const badge = document.querySelector("#taxManualBadge"); if (badge) badge.style.display = "inline-block"; } // メイン: 画面上の入力からプレビュー行を生成(複数税率もOK) function buildJournalPreview({ lines, // [{accountId, accountName, dc, amount, taxRate, taxApplicable, subAccountId}] taxMode, // "税込" | "税抜" taxDirection // "仮受" | "仮払" }) { // 税率ごとに集計(補助科目単位で集約) const buckets = new Map(); // key = `${rate}-${subAccountId || ''}-${taxDirection}` let debitTotal = 0; let creditTotal = 0; const normalLines = []; // 税対象外や通常科目 for (const ln of lines) { const isForceIncl = isForceInclAccount(ln.accountName); const mode = isForceIncl ? "税込" : taxMode; // 強制税込 if (ln.dc === "D") debitTotal += ln.amount; else creditTotal += ln.amount; normalLines.push(ln); if (!ln.taxApplicable || !ln.taxRate) continue; // 対象外は課税計算しない const ratePermil = TAX_PERMIL[String(ln.taxRate)]; if (ratePermil == null) continue; // 税額の計算(自動計算のみ。手修正済ならここで足さない) if (!manualTaxOverridden) { let taxAmount = 0; if (mode === "税込") { // 税抜 = round( 税込 ÷(1+税率) ) → 税額=差額 const excl = calcExclFromIncl(ln.amount, ratePermil); taxAmount = ln.amount - excl; } else { // 税抜 → 税額 = round( 税抜 × 税率 ) taxAmount = calcTaxFromExcl(ln.amount, ratePermil); } const key = `${ln.taxRate}-${ln.subAccountId || ""}-${taxDirection}`; const prev = buckets.get(key) || 0; buckets.set(key, prev + taxAmount); } } // 税行を作成(税率・補助科目ごと・税方向ごとに1行) const taxLines = []; for (const [key, amt] of buckets.entries()) { const [rate, subAccountId, direction] = key.split("-"); if (amt === 0) continue; const taxAccount = taxAccountNameByDirection(direction); // 仮受は「貸方」、仮払は「借方」 const dc = direction === "仮受" ? "C" : "D"; taxLines.push({ accountName: taxAccount, dc, amount: amt, taxRate: Number(rate), taxApplicable: true, subAccountId: subAccountId || null, locked: true, // 科目/税区分/方向ロック amountEditable: true // 金額のみ編集可 }); if (dc === "D") debitTotal += amt; else creditTotal += amt; } // プレビュー結果 return { lines: [...normalLines, ...taxLines], debitTotal, creditTotal, balanced: debitTotal === creditTotal }; } // 登録ボタン押下時:サーバーに渡すDTOを組み立て function buildPostPayload(uiState) { const preview = buildJournalPreview(uiState); // バリデーション:貸借一致 if (!preview.balanced && !manualTaxOverridden) { throw new Error("貸借が一致していません。税額手修正が必要な場合は、税額を調整して再試行してください。"); } // 税行が必要なのに無い場合はエラー(対象外以外で税率がある) const needTax = uiState.lines.some(l => l.taxApplicable && l.taxRate); const hasTaxLine = preview.lines.some(l => l.accountName === "仮受消費税等" || l.accountName === "仮払消費税等"); if (needTax && !hasTaxLine && !manualTaxOverridden) { throw new Error("課税対象ですが消費税行がありません。"); } return { taxMode: uiState.taxMode, // "税込" | "税抜" taxDirection: uiState.taxDirection, // "仮受" | "仮払" manualTaxOverridden, // trueなら後端は自動再計算しない lines: preview.lines.map(l => ({ accountId: l.accountId, accountName: l.accountName, dc: l.dc, // "D" | "C" amount: l.amount, // 円(整数) taxApplicable: !!l.taxApplicable, taxRate: l.taxRate || null, // 8/10/null subAccountId: l.subAccountId || null, isTaxLine: (l.accountName === "仮受消費税等" || l.accountName === "仮払消費税等"), lockAccountMeta: !!l.locked, amountEditable: !!l.amountEditable })) }; } // 画面初期化で税額手修正バッジ非表示 document.addEventListener("DOMContentLoaded", () => { const badge = document.querySelector("#taxManualBadge"); if (badge) badge.style.display = "none"; });