修正
This commit is contained in:
172
frontend/js/journal-entry.js
Normal file
172
frontend/js/journal-entry.js
Normal 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";
|
||||
});
|
||||
Reference in New Issue
Block a user