This commit is contained in:
FNOS-WIN11ENT\choshoukaku
2025-12-15 17:57:49 +09:00
parent b6751ced18
commit 88163423e0
5 changed files with 603 additions and 470 deletions

View File

@@ -0,0 +1,172 @@
// === 仕訳登録 画面用ユーティリティ ===
// 税率を 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";
});