Files
njts-accounting-core/frontend/journal-entry.html
FNOS-WIN11ENT\choshoukaku b6751ced18 20251215滴修正
2025-12-15 14:49:59 +09:00

325 lines
9.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>仕訳入力(複数行・消費税自動)</title>
<style>
body { font-family: sans-serif; margin: 24px; }
table { border-collapse: collapse; width: 100%; margin-top: 12px; }
th, td { border: 1px solid #ccc; padding: 6px; text-align: center; }
th { background: #f5f5f5; }
input, select, button { padding: 6px; }
.right { text-align: right; }
.error { color: #c00; margin-top: 8px; }
.ok { color: #090; margin-top: 8px; }
.row { display: flex; gap: 12px; align-items: center; margin: 8px 0; }
.row > * { flex: 1; }
</style>
</head>
<body>
<h2><div class="row">
<div>
<label>消費税 端数処理</label>
<select id="roundingMode">
<option value="round">四捨五入</option>
<option value="floor">切捨て</option>
<option value="ceil">切上げ</option>
</select>
</div>
</div></h2>
<div class="row">
<div>
<label>仕訳日</label>
<input type="date" id="entryDate" />
</div>
<div style="flex:2">
<label>摘要</label>
<input type="text" id="description" placeholder="例:売上計上/経費支払" style="width:100%" />
</div>
</div>
<div class="row">
<div>
<label>仮払消費税等 勘定</label>
<select id="taxDebitAccount"></select>
</div>
<div>
<label>仮受消費税等 勘定</label>
<select id="taxCreditAccount"></select>
</div>
</div>
<table id="linesTable">
<thead>
<tr>
<th>科目</th>
<th>借方</th>
<th>貸方</th>
<th>税区分</th>
<th>税方向</th>
<th>操作</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr>
<th>合計</th>
<th id="debitTotal" class="right">0</th>
<th id="creditTotal" class="right">0</th>
<th colspan="3"></th>
</tr>
</tfoot>
</table>
<button onclick="addRow()"> 行追加</button>
<button onclick="submitEntry()">登録</button>
<div id="result"></div>
<script>
const API_BASE = "http://127.0.0.1:18080";
let accounts = [];
const TAX_OPTIONS = [
{ value: "none", label: "非課税" },
{ value: "10-excl", label: "10%(税抜)" },
{ value: "10-incl", label: "10%(税込)" },
{ value: "8-excl", label: "8%(税抜)" },
{ value: "8-incl", label: "8%(税込)" }
];
const TAX_DIRECTIONS = [
{ value: "karibarai", label: "仮払(仕入・経費側)" },
{ value: "kariuke", label: "仮受(売上側)" }
];
// ------------------------
// 初期化
// ------------------------
async function init() {
const res = await fetch(`${API_BASE}/accounts`);
accounts = await res.json();
// 税勘定候補を抽出
const taxDebitSel = document.getElementById("taxDebitAccount");
const taxCreditSel = document.getElementById("taxCreditAccount");
const taxDebitCandidates = accounts.filter(a => a.account_name.includes("仮払消費税"));
const taxCreditCandidates = accounts.filter(a => a.account_name.includes("仮受消費税"));
// 仮払
fillSelect(taxDebitSel, taxDebitCandidates.length ? taxDebitCandidates : accounts);
// 仮受
fillSelect(taxCreditSel, taxCreditCandidates.length ? taxCreditCandidates : accounts);
addRow();
addRow();
recalc();
}
function fillSelect(sel, list) {
sel.innerHTML = "";
list.forEach(a => {
const opt = document.createElement("option");
opt.value = a.account_id;
opt.textContent = `${a.account_code} ${a.account_name}`;
sel.appendChild(opt);
});
}
// ------------------------
// 行追加・削除
// ------------------------
function addRow() {
const tbody = document.querySelector("#linesTable tbody");
const tr = document.createElement("tr");
tr.innerHTML = `
<td>
<select class="account">
${accounts.map(a =>
`<option value="${a.account_id}">${a.account_code} ${a.account_name}</option>`
).join("")}
</select>
</td>
<td><input type="number" min="0" class="debit" oninput="onDebitInput(this)" /></td>
<td><input type="number" min="0" class="credit" oninput="onCreditInput(this)" /></td>
<td>
<select class="tax">
${TAX_OPTIONS.map(t => `<option value="${t.value}">${t.label}</option>`).join("")}
</select>
</td>
<td>
<select class="taxdir">
${TAX_DIRECTIONS.map(t => `<option value="${t.value}">${t.label}</option>`).join("")}
</select>
</td>
<td><button onclick="removeRow(this)">削除</button></td>
`;
tbody.appendChild(tr);
}
function removeRow(btn) {
btn.closest("tr").remove();
recalc();
}
function onDebitInput(input) {
const credit = input.closest("tr").querySelector(".credit");
if (input.value) credit.value = "";
recalc();
}
function onCreditInput(input) {
const debit = input.closest("tr").querySelector(".debit");
if (input.value) debit.value = "";
recalc();
}
// ------------------------
// 合計再計算(税は送信時に展開するので、ここは本体行のみ)
// ------------------------
function recalc() {
let debitTotal = 0;
let creditTotal = 0;
document.querySelectorAll("#linesTable tbody tr").forEach(tr => {
const d = Number(tr.querySelector(".debit").value || 0);
const c = Number(tr.querySelector(".credit").value || 0);
debitTotal += d;
creditTotal += c;
});
document.getElementById("debitTotal").textContent = debitTotal.toLocaleString();
document.getElementById("creditTotal").textContent = creditTotal.toLocaleString();
}
// ------------------------
// 税計算(四捨五入)
// ------------------------
function calcTaxSplit(amount, rate, inclusive) {
if (!rate) return { base: amount, tax: 0 };
if (!inclusive) {
// 税抜amount = 本体
const tax = applyRounding(amount * rate);
return { base: amount, tax };
} else {
// 税込amount = 総額
const base = applyRounding(amount / (1 + rate));
const tax = amount - base; // 差額は必ず一致させる
return { base, tax };
}
}
function parseTaxOption(opt) {
if (opt === "none") return { rate: 0, inclusive: false };
const [rateStr, mode] = opt.split("-");
const rate = rateStr === "10" ? 0.10 : 0.08;
return { rate, inclusive: mode === "incl" };
}
// ------------------------
// 送信(税行を自動付与)
// ------------------------
async function submitEntry() {
const result = document.getElementById("result");
result.textContent = "";
result.className = "";
const entryDate = document.getElementById("entryDate").value;
const desc = document.getElementById("description").value;
if (!entryDate) return showError("仕訳日を入力してください");
if (!desc) return showError("摘要を入力してください");
const taxDebitAccountId = Number(document.getElementById("taxDebitAccount").value);
const taxCreditAccountId = Number(document.getElementById("taxCreditAccount").value);
const baseLines = [];
const taxLines = [];
document.querySelectorAll("#linesTable tbody tr").forEach(tr => {
const accountId = Number(tr.querySelector(".account").value);
const debit = Number(tr.querySelector(".debit").value || 0);
const credit = Number(tr.querySelector(".credit").value || 0);
const taxOpt = tr.querySelector(".tax").value;
const taxDir = tr.querySelector(".taxdir").value;
// 空行は無視
if (debit === 0 && credit === 0) return;
const { rate, inclusive } = parseTaxOption(taxOpt);
if (debit > 0) {
const { base, tax } = calcTaxSplit(debit, rate, inclusive);
baseLines.push({ account_id: accountId, debit: base, credit: 0 });
if (tax > 0) {
if (taxDir === "karibarai") {
taxLines.push({ account_id: taxDebitAccountId, debit: tax, credit: 0 });
} else {
taxLines.push({ account_id: taxCreditAccountId, debit: 0, credit: tax });
}
}
} else {
const { base, tax } = calcTaxSplit(credit, rate, inclusive);
baseLines.push({ account_id: accountId, debit: 0, credit: base });
if (tax > 0) {
if (taxDir === "karibarai") {
taxLines.push({ account_id: taxDebitAccountId, debit: tax, credit: 0 });
} else {
taxLines.push({ account_id: taxCreditAccountId, debit: 0, credit: tax });
}
}
}
});
const lines = [...baseLines, ...taxLines];
if (lines.length < 2)
return showError("仕訳行は2行以上必要です");
// 借貸チェック(送信前に最終確認)
const tot = lines.reduce((s, l) => {
s.debit += Number(l.debit || 0);
s.credit += Number(l.credit || 0);
return s;
}, {debit:0, credit:0});
if (tot.debit !== tot.credit)
return showError(`借方合計(${tot.debit})と貸方合計(${tot.credit})が一致していません`);
const payload = { entry_date: entryDate, description: desc, lines };
try {
const res = await fetch(`${API_BASE}/journal-entries`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const data = await res.json();
if (!res.ok) return showError(data.detail || "登録失敗");
showOk(`登録完了ID: ${data.journal_entry_id}`);
} catch {
showError("通信エラー");
}
}
function showError(msg) {
const r = document.getElementById("result");
r.textContent = msg;
r.className = "error";
}
function showOk(msg) {
const r = document.getElementById("result");
r.textContent = msg;
r.className = "ok";
}
// 合計の初期表示用
document.addEventListener("input", () => recalc());
init();
</script>
</body>
</html>