403 lines
14 KiB
JavaScript
403 lines
14 KiB
JavaScript
// ===============================================
|
||
// trial-balance.js - 試算表データ読み込みスクリプト
|
||
// ===============================================
|
||
console.log("🔧 trial-balance.js ファイルが読み込まれました");
|
||
|
||
// 防止重复初始化
|
||
let trialBalanceInitialized = false;
|
||
|
||
// 初期読込関数
|
||
function initTrialBalance() {
|
||
// 已经初始化过就直接返回
|
||
if (trialBalanceInitialized) {
|
||
console.log("⚠️ initTrialBalance() はすでに実行済みです");
|
||
return;
|
||
}
|
||
|
||
trialBalanceInitialized = true;
|
||
console.log("✓ initTrialBalance() 関数が実行されました");
|
||
|
||
const API_URL = `/trial-balance`;
|
||
|
||
// デフォルト期間を計算(5/31決算:6月開始~現在月末)
|
||
function getDefaultPeriod() {
|
||
const today = new Date();
|
||
const currentYear = today.getFullYear();
|
||
const currentMonth = today.getMonth() + 1; // 1-12
|
||
|
||
let fiscalStartYear, fiscalEndYear;
|
||
|
||
// 6月~5月が1会計年度
|
||
if (currentMonth >= 6) {
|
||
// 6月以降:当年6月1日から
|
||
fiscalStartYear = currentYear;
|
||
fiscalEndYear = currentYear;
|
||
} else {
|
||
// 1-5月:前年6月1日から
|
||
fiscalStartYear = currentYear - 1;
|
||
fiscalEndYear = currentYear;
|
||
}
|
||
|
||
const dateFrom = `${fiscalStartYear}-06-01`;
|
||
|
||
// 現在月の末日を計算
|
||
const lastDayOfMonth = new Date(fiscalEndYear, currentMonth, 0).getDate();
|
||
const dateTo = `${fiscalEndYear}-${String(currentMonth).padStart(
|
||
2,
|
||
"0",
|
||
)}-${String(lastDayOfMonth).padStart(2, "0")}`;
|
||
|
||
return { dateFrom, dateTo };
|
||
}
|
||
|
||
// 試算表データを読み込む関数
|
||
function loadTrialBalance(dateFrom, dateTo) {
|
||
const url = `${API_URL}?date_from=${dateFrom}&date_to=${dateTo}`;
|
||
console.log("読み込み URL:", url);
|
||
|
||
// 加載中のステータス表示
|
||
const loadingEl = document.getElementById("loadingStatus");
|
||
if (loadingEl) loadingEl.style.display = "inline";
|
||
|
||
fetch(url)
|
||
.then((r) => {
|
||
console.log("API Response Status:", r.status);
|
||
if (!r.ok) throw new Error("API 返回错误: " + r.status);
|
||
return r.json();
|
||
})
|
||
.then((data) => {
|
||
console.log("試算表 API データ:", data);
|
||
|
||
// 期間情報を更新
|
||
updatePeriodInfo(dateFrom, dateTo);
|
||
|
||
const tbody = document.querySelector("#trialBalanceTable tbody");
|
||
if (!tbody) {
|
||
console.error("未找到 #trialBalanceTable tbody");
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = "";
|
||
|
||
// =========================================
|
||
// 科目タイプ判定関数(日本企業損益計算書対応)
|
||
// =========================================
|
||
function getAccountType(accountCode) {
|
||
if (!accountCode) return "unknown";
|
||
const code = parseInt(accountCode);
|
||
|
||
// 営業収益(売上高)
|
||
if (code === 701) return "operating_revenue";
|
||
|
||
// 非営業収益(受取利息、雑収入)
|
||
if (code === 702 || code === 709) return "non_operating_revenue";
|
||
|
||
// 売上原価(仕入高)
|
||
if (code === 801) return "cogs";
|
||
|
||
// 営業費用(仕入高以外の800-899、ただし520-599は除外)
|
||
if (code >= 802 && code <= 899) return "operating_expense";
|
||
|
||
// 法人税等(504)- 損益計算に含めない
|
||
if (code === 504) return "corporate_tax";
|
||
|
||
// 消費税等(505)- 損益計算に含めない
|
||
if (code === 505) return "consumption_tax";
|
||
|
||
// その他は貸借科目
|
||
return "balance_sheet";
|
||
}
|
||
|
||
// =========================================
|
||
// 各タイプの合計集計(日本企業損益計算書対応)
|
||
// =========================================
|
||
let operatingRevenue = 0; // 701: 売上高(貸方)
|
||
let cogs = 0; // 801: 売上原価(借方)
|
||
let operatingExpenses = 0; // 802-820: 営業費用(借方)
|
||
let nonOperatingRevenue = 0; // 702, 709: 営業外収益(貸方)
|
||
let corporateTax = 0; // 504: 法人税等(貸方)
|
||
let consumptionTax = 0; // 505: 消費税(貸方)
|
||
|
||
// 税理士資料形式:P/L科目の原始借方・貸方合計
|
||
let plTotalDebit = 0; // 全費用科目の借方合計(801-820)
|
||
let plTotalCredit = 0; // 全収益科目の貸方合計(701-709)
|
||
|
||
let bsDebit = 0; // 貸借科目合計(借方)
|
||
let bsCredit = 0; // 貸借科目合計(貸方)
|
||
|
||
data.accounts.forEach((row) => {
|
||
const code = row.account_code;
|
||
const debit = Number(row.debit ?? 0);
|
||
const credit = Number(row.credit ?? 0);
|
||
const type = getAccountType(code);
|
||
|
||
if (type === "operating_revenue") {
|
||
operatingRevenue += credit - debit;
|
||
plTotalCredit += credit;
|
||
} else if (type === "non_operating_revenue") {
|
||
nonOperatingRevenue += credit - debit;
|
||
plTotalCredit += credit;
|
||
} else if (type === "cogs") {
|
||
cogs += debit - credit;
|
||
plTotalDebit += debit;
|
||
} else if (type === "operating_expense") {
|
||
operatingExpenses += debit - credit;
|
||
plTotalDebit += debit;
|
||
} else if (type === "balance_sheet") {
|
||
bsDebit += debit;
|
||
bsCredit += credit;
|
||
}
|
||
});
|
||
|
||
// 日本企業損益計算書の標準フロー
|
||
// ※ 504(未払法人税等)、505(未払消費税等) は負債科目のため損益計算に含めない
|
||
const grossProfit = operatingRevenue - cogs; // 売上総利益 = 売上高 - 売上原価
|
||
const operatingProfit = grossProfit - operatingExpenses; // 営業利益 = 売上総利益 - 営業費用
|
||
const ordinaryProfit = operatingProfit + nonOperatingRevenue; // 経常利益 = 営業利益 + 営業外収益
|
||
// 801-820科目の合計を計算用の変量
|
||
let operatingExpense801_820Debit = 0;
|
||
let operatingExpense801_820Credit = 0;
|
||
|
||
// 税引前当期純利益 = 経常利益(特別損益の分類がないため)
|
||
const netProfit = ordinaryProfit; // 当期純利益 = 経常利益(法人税等は損益計算に含めない)
|
||
|
||
data.accounts.forEach((row) => {
|
||
const code = row.account_code;
|
||
const debit = Number(row.debit ?? 0);
|
||
const credit = Number(row.credit ?? 0);
|
||
const opening = Number(row.opening_balance ?? 0);
|
||
const closing = Number(row.closing_balance ?? 0);
|
||
|
||
// =========================
|
||
// 行タイプ判定
|
||
// =========================
|
||
const isTotalRow = !row.account_code; // null / "" / undefined 全覆盖
|
||
|
||
const tr = document.createElement("tr");
|
||
|
||
if (isTotalRow) {
|
||
tr.classList.add("total-row");
|
||
}
|
||
|
||
// indent フラグに基づくクラス追加
|
||
if (row.indent) {
|
||
tr.classList.add("indent-row");
|
||
}
|
||
|
||
const ratio = row.ratio ?? "0.00";
|
||
|
||
// =========================
|
||
// 借方・貸方は全科目共通でそのまま表示
|
||
// =========================
|
||
const displayDebit = debit ? debit.toLocaleString() : "";
|
||
const displayCredit = credit ? credit.toLocaleString() : "";
|
||
|
||
// 期末残高のフォーマット(マイナス値も表示)
|
||
let displayClosing = "";
|
||
if (closing !== 0) {
|
||
displayClosing = closing.toLocaleString();
|
||
}
|
||
|
||
tr.innerHTML = `
|
||
<td class="left">${row.account_code ?? ""}</td>
|
||
<td class="left">${row.account_name ?? ""}</td>
|
||
<td class="right">${opening ? opening.toLocaleString() : ""}</td>
|
||
<td class="right">${displayDebit}</td>
|
||
<td class="right">${displayCredit}</td>
|
||
<td class="right">${displayClosing}</td>
|
||
<td class="right">${ratio}</td>
|
||
`;
|
||
|
||
tbody.appendChild(tr);
|
||
});
|
||
|
||
// フッター合計は削除
|
||
|
||
// 加載中ステータスを非表示
|
||
const loadingEl = document.getElementById("loadingStatus");
|
||
if (loadingEl) loadingEl.style.display = "none";
|
||
})
|
||
.catch((err) => {
|
||
console.error("試算表読取失敗:", err);
|
||
const tbody = document.querySelector("#trialBalanceTable tbody");
|
||
if (tbody) {
|
||
tbody.innerHTML = `<tr><td colspan="7" style="text-align:center; color:red;">エラー: ${err.message}</td></tr>`;
|
||
}
|
||
// 加載中ステータスを非表示
|
||
const loadingEl = document.getElementById("loadingStatus");
|
||
if (loadingEl) loadingEl.style.display = "none";
|
||
alert(
|
||
`試算表読取失敗: ${err.message}\n\nコンソールログ(F12)を確認してください`,
|
||
);
|
||
});
|
||
}
|
||
|
||
// 期間情報を更新する関数
|
||
function updatePeriodInfo(dateFrom, dateTo) {
|
||
const fromDate = new Date(dateFrom);
|
||
const toDate = new Date(dateTo);
|
||
|
||
const fromYear = fromDate.getFullYear() - 2018; // 令和変換(2019年=令和1年)
|
||
const toYear = toDate.getFullYear() - 2018;
|
||
|
||
const periodText = `令和${fromYear}年 ${
|
||
fromDate.getMonth() + 1
|
||
}月 ${fromDate.getDate()}日 ~ 令和${toYear}年 ${
|
||
toDate.getMonth() + 1
|
||
}月 ${toDate.getDate()}日`;
|
||
document.getElementById("periodInfo").textContent = periodText;
|
||
|
||
// thead内の期間情報も更新(印刷用)
|
||
const theadPeriodInfo = document.getElementById("theadPeriodInfo");
|
||
if (theadPeriodInfo) {
|
||
theadPeriodInfo.textContent = periodText;
|
||
}
|
||
}
|
||
|
||
// 年度ドロップダウンを読み込む
|
||
async function loadFiscalYears() {
|
||
try {
|
||
const res = await fetch("/trial-balance/fiscal-years");
|
||
if (!res.ok) return;
|
||
const years = await res.json();
|
||
const sel = document.getElementById("fiscalYear");
|
||
if (!sel) return;
|
||
years.forEach((fy) => {
|
||
const opt = document.createElement("option");
|
||
opt.value = JSON.stringify({
|
||
date_from: fy.date_from,
|
||
date_to: fy.date_to,
|
||
});
|
||
opt.textContent = fy.label;
|
||
sel.appendChild(opt);
|
||
});
|
||
} catch (e) {
|
||
console.warn("年度一覧取得失敗:", e);
|
||
}
|
||
}
|
||
loadFiscalYears();
|
||
|
||
// 年度選択 → 開始・終了日を自動セット(ユーザーは自由に変更可能)
|
||
document.getElementById("fiscalYear").addEventListener("change", function () {
|
||
if (!this.value) return;
|
||
try {
|
||
const { date_from, date_to } = JSON.parse(this.value);
|
||
document.getElementById("dateFrom").value = date_from;
|
||
document.getElementById("dateTo").value = date_to;
|
||
} catch (e) {
|
||
/* ignore */
|
||
}
|
||
});
|
||
|
||
// 檢索按鈕的事件
|
||
document.getElementById("btnSearch").addEventListener("click", () => {
|
||
const dateFrom = document.getElementById("dateFrom").value;
|
||
const dateTo = document.getElementById("dateTo").value;
|
||
|
||
if (!dateFrom || !dateTo) {
|
||
alert("開始年月日と終了年月日を入力してください");
|
||
return;
|
||
}
|
||
|
||
if (dateFrom > dateTo) {
|
||
alert("開始年月日は終了年月日より前の日付を指定してください");
|
||
return;
|
||
}
|
||
|
||
loadTrialBalance(dateFrom, dateTo);
|
||
});
|
||
|
||
// 全年度表示ボタンのイベント
|
||
document.getElementById("btnFullYear").addEventListener("click", () => {
|
||
const currentYear = new Date().getFullYear();
|
||
const dateFrom = `${currentYear}-01-01`;
|
||
const dateTo = `${currentYear}-12-31`;
|
||
|
||
document.getElementById("dateFrom").value = dateFrom;
|
||
document.getElementById("dateTo").value = dateTo;
|
||
|
||
loadTrialBalance(dateFrom, dateTo);
|
||
});
|
||
|
||
// 印刷ボタンのイベント(最新データを読み込んでから印刷)
|
||
document.getElementById("btnPrint").addEventListener("click", () => {
|
||
const dateFrom = document.getElementById("dateFrom").value;
|
||
const dateTo = document.getElementById("dateTo").value;
|
||
|
||
if (!dateFrom || !dateTo) {
|
||
alert("開始年月日と終了年月日を入力してください");
|
||
return;
|
||
}
|
||
|
||
if (dateFrom > dateTo) {
|
||
alert("開始年月日は終了年月日より前の日付を指定してください");
|
||
return;
|
||
}
|
||
|
||
// 最新データを読込んでから印刷
|
||
loadTrialBalance(dateFrom, dateTo);
|
||
|
||
// データ読込完了を待って印刷開始(少し遅延させる)
|
||
setTimeout(() => {
|
||
setupPrintDate(); // 打印前更新日期
|
||
window.print();
|
||
}, 800);
|
||
});
|
||
|
||
// 初期読み込み(5/31決算に対応)
|
||
try {
|
||
const defaultPeriod = getDefaultPeriod();
|
||
document.getElementById("dateFrom").value = defaultPeriod.dateFrom;
|
||
document.getElementById("dateTo").value = defaultPeriod.dateTo;
|
||
loadTrialBalance(defaultPeriod.dateFrom, defaultPeriod.dateTo);
|
||
console.log("✓ 初期読み込み完了");
|
||
} catch (err) {
|
||
console.error("初期読み込みエラー:", err);
|
||
}
|
||
}
|
||
|
||
// 複数の方式で初期化を実行(iOS互換性を向上)
|
||
document.addEventListener("DOMContentLoaded", initTrialBalance);
|
||
window.addEventListener("load", initTrialBalance);
|
||
if (document.readyState === "loading") {
|
||
document.addEventListener("DOMContentLoaded", initTrialBalance);
|
||
} else {
|
||
// ページがすでに読み込まれている場合、すぐに実行
|
||
setTimeout(initTrialBalance, 100);
|
||
}
|
||
|
||
// 打印日期和页码设置
|
||
// ===============================================
|
||
function setupPrintDate() {
|
||
const printDateEl = document.getElementById("printDate");
|
||
if (!printDateEl) return;
|
||
|
||
// 设置打印日期
|
||
const today = new Date();
|
||
const year = today.getFullYear();
|
||
const month = String(today.getMonth() + 1).padStart(2, "0");
|
||
const date = String(today.getDate()).padStart(2, "0");
|
||
const dateStr = `${year}年${month.replace(/^0/, "")}月${date.replace(
|
||
/^0/,
|
||
"",
|
||
)}日`;
|
||
|
||
printDateEl.textContent = `印刷日:${dateStr}`;
|
||
|
||
// thead内の印刷日も更新
|
||
const theadPrintDate = document.getElementById("theadPrintDate");
|
||
if (theadPrintDate) {
|
||
theadPrintDate.textContent = `印刷日:${dateStr}`;
|
||
console.log("✓ 印刷日を更新しました:", theadPrintDate.textContent);
|
||
} else {
|
||
console.warn("⚠️ theadPrintDate要素が見つかりません");
|
||
}
|
||
}
|
||
|
||
// 页面加载时设置一次(仅一次)
|
||
if (document.readyState === "loading") {
|
||
document.addEventListener("DOMContentLoaded", setupPrintDate, { once: true });
|
||
} else {
|
||
setupPrintDate();
|
||
}
|