This commit is contained in:
FNOS-WIN11ENT\choshoukaku
2025-12-18 15:30:39 +09:00
parent 88163423e0
commit 87fa1aaa1f
12 changed files with 742 additions and 192 deletions

View File

@@ -22,7 +22,7 @@
<div class="row">
<label>仕訳日</label>
<input type="date" id="entryDate" />
<input type="date" id="entryDate" onchange="onEntryDateChanged()"/>
</div>
<div class="row">
@@ -37,6 +37,14 @@
<label>仮受消費税等 勘定</label>
<select id="taxReceivedSelect"></select>
</div>
<div style="margin-bottom: 10px;">
<label>消費税 端数処理:</label>
<select id="roundingMode">
<option value="round">四捨五入</option>
<option value="floor">切捨て</option>
<option value="ceil">切上げ</option>
</select>
</div>
<table id="linesTable">
<thead>
@@ -57,7 +65,43 @@
<button onclick="submitJournal()">登録</button>
</div>
<h3>月次锁定管理</h3>
<div style="margin-bottom: 8px;">
<label>年度:</label>
<input type="number" id="lockYear" value="2025" style="width:80px;">
<label>月份:</label>
<input type="number" id="lockMonth" min="1" max="12" value="6" style="width:60px;">
</div>
<button onclick="lockMonth()">🔒 锁定</button>
<button onclick="unlockMonth()">🔓 解锁</button>
<div id="lockResult" style="margin-top:10px;color:#333;"></div>
<div style="margin-top:12px;">
<button onclick="refreshMonthLocks()">🔄 刷新锁定列表</button>
<pre id="lockList" style="background:#f7f7f7;padding:8px;"></pre>
</div>
<div id="monthLockBanner"
style="display:none;
margin:8px 0;
padding:6px 10px;
background:#ffecec;
border:1px solid #ffaaaa;
color:#a00000;">
🔒 该月份已锁定,不能编辑仕訳
</div>
<script>
let rowSeq = 0; // 各行唯一ID
const API = "http://127.0.0.1:18080";
let accounts = [];
@@ -115,10 +159,16 @@ function setupTaxSelectors() {
// -----------------------------
// 行追加
// -----------------------------
function addRow(isTaxRow = false, taxInfo = null) {
function addRow(isTaxRow = false, taxInfo = null, parentId = null) {
const tbody = document.querySelector("#linesTable tbody");
const tr = document.createElement("tr");
if (isTaxRow) tr.classList.add("tax-row");
if (!isTaxRow) {
tr.dataset.rowId = String(++rowSeq);
} else {
tr.classList.add("tax-row");
tr.dataset.parentId = parentId;
}
tr.innerHTML = `
<td>
@@ -149,26 +199,46 @@ function addRow(isTaxRow = false, taxInfo = null) {
tbody.appendChild(tr);
if (!isTaxRow) {
tr.querySelector(".debit").addEventListener("input", () => handleTax(tr));
tr.querySelector(".credit").addEventListener("input", () => handleTax(tr));
tr.querySelector(".taxType").addEventListener("change", () => handleTax(tr));
tr.querySelector(".taxDirection").addEventListener("change", () => handleTax(tr));
}
// 税行の場合,填值
if (isTaxRow && taxInfo) {
const acc = tr.querySelector(".account");
acc.value = taxInfo.accountId || "";
if (taxInfo.side === "debit") tr.querySelector(".debit").value = taxInfo.amount;
if (taxInfo.side === "credit") tr.querySelector(".credit").value = taxInfo.amount;
}
// 普通行:绑定事件
if (!isTaxRow) {
const recalc = () => handleTax(tr);
tr.querySelector(".debit").addEventListener("input", recalc);
tr.querySelector(".credit").addEventListener("input", recalc);
tr.querySelector(".taxType").addEventListener("change", recalc);
tr.querySelector(".taxDirection").addEventListener("change", recalc);
}
return tr;
}
// 删除该行的所有税行
function removeTaxRowsOf(row) {
const id = row.dataset.rowId;
if (!id) return;
document.querySelectorAll(`tr.tax-row[data-parent-id="${id}"]`).forEach(r => r.remove());
}
function removeRow(btn) {
const row = btn.closest("tr");
removeTaxRowsOf(row);
row.remove();
}
// -----------------------------
// 税行 自動生成/更新10%/8%のみ、四捨五入)
// -----------------------------
function handleTax(row) {
removeTaxRow(row);
// 清理旧税行
removeTaxRowsOf(row);
const taxType = row.querySelector(".taxType").value;
const taxDir = row.querySelector(".taxDirection").value;
@@ -176,28 +246,48 @@ function handleTax(row) {
const credit = Number(row.querySelector(".credit").value || 0);
if (taxType === "none" || taxDir === "none") return;
const baseAmount = debit || credit;
if (!baseAmount) return;
const totalAmount = debit || credit;
if (!totalAmount) return;
const rate = taxType === "8" ? 0.08 : 0.10;
const taxAmount = Math.round(baseAmount * rate);
const roundingMode = document.getElementById("roundingMode").value;
// 税込 → 税額逆算
const rawTax = totalAmount * rate / (1 + rate);
let taxAmount;
switch (roundingMode) {
case "floor": taxAmount = Math.floor(rawTax); break;
case "ceil": taxAmount = Math.ceil(rawTax); break;
default: taxAmount = Math.round(rawTax);
}
const parentId = row.dataset.rowId;
if (taxDir === "paid") {
const taxAcc = document.getElementById("taxPaidSelect").value;
if (!taxAcc) { alert("仮払消費税等 勘定を選択してください"); return; }
addRow(true, { accountId: taxAcc, amount: taxAmount, side: "debit" });
if (!taxAcc) return alert("仮払消費税等 勘定を選択してください");
addRow(true, { accountId: taxAcc, amount: taxAmount, side: "debit" }, parentId);
}
if (taxDir === "received") {
const taxAcc = document.getElementById("taxReceivedSelect").value;
if (!taxAcc) { alert("仮受消費税等 勘定を選択してください"); return; }
addRow(true, { accountId: taxAcc, amount: taxAmount, side: "credit" });
if (!taxAcc) return alert("仮受消費税等 勘定を選択してください");
addRow(true, { accountId: taxAcc, amount: taxAmount, side: "credit" }, parentId);
}
}
// 税行は常に「直後の1行」を使う
function removeTaxRow(row) {
const next = row.nextElementSibling;
if (next && next.classList.contains("tax-row")) next.remove();
const id = row.dataset.rowId;
if (!id) return;
document
.querySelectorAll(`tr.tax-row[data-parent-id="${id}"]`)
.forEach(tr => tr.remove());
}
function removeRow(btn) {
@@ -254,6 +344,137 @@ async function submitJournal() {
alert(`登録完了ID=${result.journal_entry_id}`);
location.reload();
}
const API_BASE = "http://127.0.0.1:18080";
async function callApi(path) {
try {
const res = await fetch(`${API_BASE}${path}`, { method: "POST" });
const text = await res.text();
let data = {};
try { data = text ? JSON.parse(text) : {}; } catch { data = { detail: text }; }
if (!res.ok) {
throw new Error(data.detail || `HTTP ${res.status}`);
}
return data;
} catch (e) {
throw new Error(e.message || "请求失败");
}
}
async function lockMonth() {
const year = document.getElementById("lockYear").value;
const month = document.getElementById("lockMonth").value;
const el = document.getElementById("lockResult");
el.innerText = "处理中...";
try {
await callApi(`/month-locks/lock?fiscal_year=${year}&month=${month}`);
el.innerText = `${year}${month}月 已锁定`;
await refreshMonthLocks(); // 可选:自动刷新列表
} catch (e) {
el.innerText = `❌ 错误:${e.message}`;
}
}
async function unlockMonth() {
const year = document.getElementById("lockYear").value;
const month = document.getElementById("lockMonth").value;
const el = document.getElementById("lockResult");
el.innerText = "处理中...";
try {
await callApi(`/month-locks/unlock?fiscal_year=${year}&month=${month}`);
el.innerText = `${year}${month}月 已解锁`;
await refreshMonthLocks(); // 可选:自动刷新列表
} catch (e) {
el.innerText = `❌ 错误:${e.message}`;
}
}
async function refreshMonthLocks() {
const el = document.getElementById("lockList");
el.textContent = "读取中...";
try {
const res = await fetch(`${API_BASE}/month-locks`);
const data = await res.json();
// 只显示 locked 的,避免太长
const locked = (data || []).filter(x => x.is_locked);
if (locked.length === 0) {
el.textContent = "(当前没有锁定月份)";
return;
}
el.textContent = locked
.map(x => `${x.fiscal_year}-${String(x.month).padStart(2, "0")} locked_by=${x.locked_by ?? ""}`)
.join("\n");
} catch (e) {
el.textContent = `读取失败:${e.message || e}`;
}
}
async function checkMonthLockByDate(dateStr) {
if (!dateStr) return false;
const d = new Date(dateStr);
const year = d.getFullYear();
const month = d.getMonth() + 1;
const res = await fetch(`${API_BASE}/month-locks`);
const data = await res.json();
return (data || []).some(
x => x.fiscal_year === year && x.month === month && x.is_locked
);
}
async function onEntryDateChanged() {
const dateStr = document.getElementById("entryDate").value;
const locked = await checkMonthLockByDate(dateStr);
const banner = document.getElementById("monthLockBanner");
if (locked) {
banner.style.display = "block";
setJournalReadonly(true);
} else {
banner.style.display = "none";
setJournalReadonly(false);
}
}
function setJournalReadonly(readonly) {
// 保存按钮
const saveBtn = document.getElementById("saveButton");
if (saveBtn) saveBtn.disabled = readonly;
// 行追加按钮
const addRowBtn = document.getElementById("addRowButton");
if (addRowBtn) addRowBtn.disabled = readonly;
// 所有“删除行”按钮
document.querySelectorAll(".delete-row-btn").forEach(btn => {
btn.disabled = readonly;
});
// 所有仕訳输入项(金额 / 科目等)
document.querySelectorAll(".journal-input").forEach(input => {
input.disabled = readonly;
});
}
</script>
</body>