let accounts = []; let retainedEarnings = null; let isLocked = false; // 科目グループ定義(小計付き) const GROUPS = [ { key: "asset", label: "資産", subGroups: [ { label: "現金・預金合計", codes: ["101", "102", "103", "104"] }, { label: "売上債権合計", codes: ["201"] }, { label: "流動資産合計", codes: [ "101", "102", "103", "104", "201", "202", "203", "204", "205", "206", ], }, { label: "固定資産合計", codes: [ "301", "401", "402", "403", "404", "405", "406", "407", "408", "410", ], }, ], }, { key: "liability", label: "負債", subGroups: [ { label: "仕入債務合計", codes: ["501"] }, { label: "流動負債合計", codes: ["501", "502", "503", "504", "505", "506", "507", "508"], }, { label: "固定負債合計", codes: ["509"] }, ], }, { key: "equity", label: "純資産", subGroups: [ { label: "株主資本合計", codes: ["601", "602"] }, { label: "純資産合計", codes: ["601", "602"] }, ], }, ]; function formatNumber(n) { return Number(n || 0).toLocaleString(); } function parseNumber(str) { // カンマを除去して数値に変換 return Number(String(str).replace(/,/g, "")) || 0; } function recalcTotals() { let debit = 0; let credit = 0; // 先算:不含繰越利益剰余金 accounts.forEach((a) => { if (a.account_name === "繰越利益剰余金" || a.account_name === "繰越利益") return; debit += Number(a.opening_debit || 0); credit += Number(a.opening_credit || 0); }); const diff = debit - credit; console.log("Debug: debit=", debit, "credit=", credit, "diff=", diff); console.log("Debug: retainedEarnings=", retainedEarnings); // 自动计算 繰越利益剰余金 if (retainedEarnings) { if (diff > 0) { retainedEarnings.opening_debit = 0; retainedEarnings.opening_credit = diff; } else { retainedEarnings.opening_debit = -diff; retainedEarnings.opening_credit = 0; } console.log("Debug: retainedEarnings after calc=", retainedEarnings); // DOM入力フィールドを更新 const retainedIndex = accounts.indexOf(retainedEarnings); const inputs = document.querySelectorAll("#balanceTable tbody input"); inputs.forEach((input) => { const match = input .getAttribute("onchange") ?.match(/accounts\[(\d+)\]\.opening_(debit|credit)/); if (match && parseInt(match[1]) === retainedIndex) { const field = match[2]; // "debit" or "credit" input.value = formatNumber(retainedEarnings[`opening_${field}`]); } }); } // 再算一次(包含繰越利益) let finalDebit = 0; let finalCredit = 0; accounts.forEach((a) => { finalDebit += Number(a.opening_debit || 0); finalCredit += Number(a.opening_credit || 0); }); document.getElementById("totalDebit").innerText = formatNumber(finalDebit); document.getElementById("totalCredit").innerText = formatNumber(finalCredit); document.getElementById("diff").innerText = formatNumber( finalDebit - finalCredit ); // 小計行を更新 updateSubtotalRows(); } function updateSubtotalRows() { GROUPS.forEach((group) => { group.subGroups.forEach((subGroup) => { const subGroupTotal = accounts .filter((acc) => subGroup.codes.includes(acc.account_code)) .reduce( (sum, acc) => ({ debit: sum.debit + Number(acc.opening_debit || 0), credit: sum.credit + Number(acc.opening_credit || 0), }), { debit: 0, credit: 0 } ); // 対応する小計行を探して更新 const rows = document.querySelectorAll("#balanceTable tbody tr"); rows.forEach((row) => { const labelCell = row.cells[1]; if (labelCell && labelCell.textContent === subGroup.label) { row.cells[2].textContent = formatNumber(subGroupTotal.debit); row.cells[3].textContent = formatNumber(subGroupTotal.credit); } }); }); }); } async function loadOpeningBalances() { const year = document.getElementById("fiscalYear").value; const data = await apiGet("/opening-balances", { fiscal_year: year }); accounts = data; retainedEarnings = accounts.find( (a) => a.account_name === "繰越利益剰余金" || a.account_name === "繰越利益" ); const tbody = document.querySelector("#balanceTable tbody"); tbody.innerHTML = ""; GROUPS.forEach((group) => { // 分组标题行 const headerTr = document.createElement("tr"); headerTr.innerHTML = ` ${group.label} `; tbody.appendChild(headerTr); const groupAccounts = accounts.filter((a) => a.account_type === group.key); groupAccounts.forEach((a, idx) => { const i = accounts.indexOf(a); const isRetained = a.account_name === "繰越利益剰余金" || a.account_name === "繰越利益"; const tr = document.createElement("tr"); tr.innerHTML = ` ${a.account_code} ${a.account_name} `; tbody.appendChild(tr); // 小計行の追加 group.subGroups.forEach((subGroup) => { const isLastInSubGroup = subGroup.codes.includes(a.account_code) && !groupAccounts .slice(idx + 1) .some((nextAcc) => subGroup.codes.includes(nextAcc.account_code)); if (isLastInSubGroup) { const subGroupTotal = accounts .filter((acc) => subGroup.codes.includes(acc.account_code)) .reduce( (sum, acc) => ({ debit: sum.debit + Number(acc.opening_debit || 0), credit: sum.credit + Number(acc.opening_credit || 0), }), { debit: 0, credit: 0 } ); const totalTr = document.createElement("tr"); totalTr.style.backgroundColor = "#d0d0d0"; totalTr.style.fontWeight = "bold"; totalTr.innerHTML = ` ${subGroup.label} ${formatNumber(subGroupTotal.debit)} ${formatNumber(subGroupTotal.credit)} `; tbody.appendChild(totalTr); } }); }); }); // 年度锁定チェック fetch( `http://127.0.0.1:18080/opening-balances/lock-status?fiscal_year=${year}` ) .then((res) => res.json()) .then((data) => { isLocked = data.is_locked; document.querySelector( "button[onclick='saveOpeningBalances()']" ).disabled = isLocked; if (isLocked) { alert("この会計年度の期首残高は確定されています。"); } }); recalcTotals(); } async function saveOpeningBalances() { if (isLocked) { alert("この会計年度の期首残高は確定されているため、保存できません。"); return; } const year = Number(document.getElementById("fiscalYear").value); const balances = accounts // ❗ 排除 繰越利益剰余金 .filter( (a) => a.account_name !== "繰越利益剰余金" && a.account_name !== "繰越利益" && (Number(a.opening_debit) !== 0 || Number(a.opening_credit) !== 0) ) .map((a) => ({ account_id: a.account_id, debit: Number(a.opening_debit || 0), credit: Number(a.opening_credit || 0), })); try { await fetch("http://127.0.0.1:18080/opening-balances", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fiscal_year: year, balances: balances, }), }).then(async (res) => { if (!res.ok) { const data = await res.json(); throw new Error(data.detail); } }); alert("期首残高を保存しました"); } catch (e) { alert(e.message); } }