173 lines
6.0 KiB
JavaScript
173 lines
6.0 KiB
JavaScript
// === 仕訳登録 画面用ユーティリティ ===
|
||
|
||
// 税率を 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";
|
||
});
|