This commit is contained in:
admin
2026-03-03 00:01:43 +09:00
parent 3b028b8fc0
commit 068903b933
31 changed files with 3204 additions and 889 deletions

View File

@@ -44,6 +44,7 @@
.account-search-container {
position: relative;
width: 100%;
box-sizing: border-box;
}
.account-search-input {
@@ -52,6 +53,7 @@
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.account-search-input:focus {
@@ -84,6 +86,9 @@
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
text-align: left;
display: block;
width: 100%;
}
.account-option:hover,
@@ -94,11 +99,14 @@
.account-option-strong {
font-weight: bold;
color: #333;
display: inline;
margin-right: 6px;
}
.account-option-light {
color: #999;
font-size: 12px;
color: #555;
font-size: 14px;
display: inline;
}
</style>
</head>
@@ -241,43 +249,81 @@
const container = inputElement.closest(".account-search-container");
const dropdown = container.querySelector(".account-dropdown");
inputElement.addEventListener("input", (e) => {
const searchText = e.target.value.toLowerCase().trim();
// 科目選択履歴を保存journal-entry.htmlと共通のlocalStorageキー
function saveAccountToRecentEdit(accountId) {
if (!accountId) return;
let recent = JSON.parse(localStorage.getItem("recentAccounts") || "[]");
recent = recent.filter((id) => id !== accountId);
recent.unshift(accountId);
if (recent.length > 20) recent = recent.slice(0, 20);
localStorage.setItem("recentAccounts", JSON.stringify(recent));
}
if (!searchText) {
dropdown.classList.remove("active");
return;
}
function makeLbl(text) {
const lbl = document.createElement("div");
lbl.style.cssText = "padding:3px 12px;font-size:11px;color:#888;background:#f5f5f5;font-weight:bold;border-bottom:1px solid #e0e0e0;";
lbl.textContent = text;
return lbl;
}
// 过滤账户
const filtered = accounts.filter((acc) => {
const code = acc.account_code.toLowerCase();
const name = acc.account_name.toLowerCase();
return code.includes(searchText) || name.includes(searchText);
});
// 生成下拉建议
dropdown.innerHTML = "";
if (filtered.length === 0) {
dropdown.innerHTML = `<div style="padding: 8px 12px; color: #999;">該当する科目がありません</div>`;
} else {
filtered.slice(0, 20).forEach((acc) => {
const option = document.createElement("div");
option.className = "account-option";
option.innerHTML = `
<span class="account-option-strong">${acc.account_code}</span>
<span class="account-option-light">${acc.account_name}</span>
`;
option.addEventListener("click", () => {
inputElement.value = `${acc.account_code} ${acc.account_name}`;
inputElement.dataset.accountId = acc.account_id;
dropdown.classList.remove("active");
});
dropdown.appendChild(option);
// 显示科目下拉列表的函数
function showAccountList(searchText = "") {
const isFiltered = searchText.length > 0;
let filtered = accounts;
if (isFiltered) {
filtered = accounts.filter((acc) => {
const code = acc.account_code.toLowerCase();
const name = acc.account_name.toLowerCase();
return code.includes(searchText) || name.includes(searchText);
});
}
dropdown.innerHTML = "";
function appendOpt(acc) {
const option = document.createElement("div");
option.className = "account-option";
option.innerHTML = `<span class="account-option-strong">${acc.account_code}</span><span class="account-option-light">${acc.account_name}</span>`;
option.addEventListener("click", () => {
inputElement.value = `${acc.account_code} ${acc.account_name}`;
inputElement.dataset.accountId = acc.account_id;
dropdown.classList.remove("active");
saveAccountToRecentEdit(acc.account_id);
});
dropdown.appendChild(option);
}
if (filtered.length === 0) {
dropdown.innerHTML = `<div style="padding: 8px 12px; color: #999;">該当する科目がありません</div>`;
} else if (isFiltered) {
const sorted = filtered.slice().sort((a, b) => a.account_code.localeCompare(b.account_code));
sorted.slice(0, 50).forEach(appendOpt);
} else {
const recentIds = JSON.parse(localStorage.getItem("recentAccounts") || "[]");
const recentItems = recentIds.map((id) => filtered.find((a) => a.account_id === id)).filter(Boolean);
const recentSet = new Set(recentIds);
const otherItems = filtered
.filter((a) => !recentSet.has(a.account_id))
.sort((a, b) => a.account_code.localeCompare(b.account_code));
if (recentItems.length > 0) {
dropdown.appendChild(makeLbl("⭐ 最近使った科目"));
recentItems.forEach(appendOpt);
const sep = document.createElement("div");
sep.style.cssText = "border-top:1px solid #e8e8e8;margin:2px 0;";
dropdown.appendChild(sep);
}
dropdown.appendChild(makeLbl("📋 すべての科目"));
otherItems.slice(0, 50).forEach(appendOpt);
}
dropdown.classList.add("active");
}
// 入力時:フィルター
inputElement.addEventListener("input", (e) => {
const searchText = e.target.value.toLowerCase().trim();
showAccountList(searchText);
});
// クリック以外の場所のドロップダウンを非表示
@@ -287,11 +333,9 @@
}
});
// フォーカス時にドロップダウンを表示(既存値がある場合)
// フォーカス時:すべての科目を表示
inputElement.addEventListener("focus", () => {
if (inputElement.value) {
dropdown.classList.add("active");
}
showAccountList(inputElement.value.toLowerCase().trim());
});
}
@@ -386,13 +430,20 @@
if (line) {
// Find the account and populate the input
const account = accounts.find(a => a.account_id === line.account_id);
const account = accounts.find(
(a) => a.account_id === line.account_id,
);
if (account) {
accountInput.value = `${account.account_code} ${account.account_name}`;
accountInput.dataset.accountId = account.account_id;
}
tr.querySelectorAll("input")[1].value = line.debit || "";
tr.querySelectorAll("input")[2].value = line.credit || "";
// type="number" のみを取得
const numberInputs = tr.querySelectorAll("input[type='number']");
// Decimal 値を数値に変換してから代入
const debitVal = Number(line.debit) || 0;
const creditVal = Number(line.credit) || 0;
numberInputs[0].value = debitVal > 0 ? String(debitVal) : "";
numberInputs[1].value = creditVal > 0 ? String(creditVal) : "";
}
// 入力時に合計を自動更新
@@ -414,8 +465,26 @@
let d = 0,
c = 0;
document.querySelectorAll("#linesTable tbody tr").forEach((tr) => {
d += Number(tr.querySelectorAll("input")[0].value || 0);
c += Number(tr.querySelectorAll("input")[1].value || 0);
// type="number" のみを取得(科目検索入力框を除外)
const numberInputs = tr.querySelectorAll("input[type='number']");
if (numberInputs.length >= 2) {
const debitStr = numberInputs[0]?.value;
const creditStr = numberInputs[1]?.value;
// 空または 0 を含む場合は 0、そうでなければ数値に変換
const debitVal =
debitStr && debitStr.trim()
? Number(debitStr.toString().replace(/,/g, ""))
: 0;
const creditVal =
creditStr && creditStr.trim()
? Number(creditStr.toString().replace(/,/g, ""))
: 0;
// NaN チェック
if (!isNaN(debitVal)) d += debitVal;
if (!isNaN(creditVal)) c += creditVal;
}
});
document.getElementById("debitTotal").textContent = d.toLocaleString();
document.getElementById("creditTotal").textContent = c.toLocaleString();
@@ -429,13 +498,28 @@
result.textContent = "";
result.className = "";
const entryDate = document.getElementById("entryDate").value;
const entryDateInput = document.getElementById("entryDate");
const entryDate = entryDateInput.value;
const desc = document.getElementById("description").value;
if (!entryDate || !desc) {
if (entryDateInput.validity.badInput) {
showError(
"日付が正しくありません。存在しない日付が入力されています11月31日は無効です",
);
return;
}
if (!entryDate && !desc) {
showError("日付と摘要は必須です");
return;
}
if (!entryDate) {
showError("日付を入力してください");
return;
}
if (!desc) {
showError("摘要を入力してください");
return;
}
const lines = [];
let d = 0,
@@ -444,8 +528,9 @@
document.querySelectorAll("#linesTable tbody tr").forEach((tr) => {
const accountInput = tr.querySelector(".account-search-input");
const accountId = Number(accountInput.dataset.accountId || 0);
const debit = Number(tr.querySelectorAll("input")[1].value || 0);
const credit = Number(tr.querySelectorAll("input")[2].value || 0);
const numberInputs = tr.querySelectorAll("input[type='number']");
const debit = Number(numberInputs[0]?.value || 0);
const credit = Number(numberInputs[1]?.value || 0);
if (debit === 0 && credit === 0) return;
lines.push({ account_id: accountId, debit, credit });
d += debit;

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,18 @@
// ===============================================
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`;
@@ -69,12 +79,90 @@ function initTrialBalance() {
tbody.innerHTML = "";
let totalOpening = 0;
let totalDebit = 0;
let totalCredit = 0;
let totalClosing = 0;
// =========================================
// 科目タイプ判定関数(日本企業損益計算書対応)
// =========================================
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);
@@ -85,14 +173,6 @@ function initTrialBalance() {
// =========================
const isTotalRow = !row.account_code; // null / "" / undefined 全覆盖
// 明细行だけ合計に含める
if (!isTotalRow) {
totalOpening += opening;
totalDebit += debit;
totalCredit += credit;
totalClosing += closing;
}
const tr = document.createElement("tr");
if (isTotalRow) {
@@ -106,32 +186,32 @@ function initTrialBalance() {
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">${debit ? debit.toLocaleString() : ""}</td>
<td class="right">${credit ? credit.toLocaleString() : ""}</td>
<td class="right">${closing ? closing.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);
});
// =========================
// フッター合計
// =========================
document.getElementById("totalOpening").textContent =
totalOpening.toLocaleString();
document.getElementById("totalDebit").textContent =
totalDebit.toLocaleString();
document.getElementById("totalCredit").textContent =
totalCredit.toLocaleString();
document.getElementById("totalClosing").textContent =
totalClosing.toLocaleString();
document.getElementById("totalRatio").textContent =
data.totals?.ratio ?? "100.00";
// フッター合計は削除
// 加載中ステータスを非表示
const loadingEl = document.getElementById("loadingStatus");
@@ -166,9 +246,15 @@ function initTrialBalance() {
toDate.getMonth() + 1
}${toDate.getDate()}`;
document.getElementById("periodInfo").textContent = periodText;
// thead内の期間情報も更新印刷用
const theadPeriodInfo = document.getElementById("theadPeriodInfo");
if (theadPeriodInfo) {
theadPeriodInfo.textContent = periodText;
}
}
// 検索ボタンのイベント
// 檢索按鈕的事件
document.getElementById("btnSearch").addEventListener("click", () => {
const dateFrom = document.getElementById("dateFrom").value;
const dateTo = document.getElementById("dateTo").value;
@@ -198,6 +284,31 @@ function initTrialBalance() {
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();
@@ -219,3 +330,38 @@ if (document.readyState === "loading") {
// ページがすでに読み込まれている場合、すぐに実行
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();
}

View File

@@ -83,6 +83,10 @@
td.right {
text-align: right;
}
td.center,
th.center {
text-align: center;
}
tr.total-row td {
background-color: #d8d8d8 !important;
font-weight: bold;
@@ -91,10 +95,171 @@
tr.indent-row td:nth-child(2) {
padding-left: 40px !important;
}
tfoot td {
font-weight: bold;
background: #e0e0e0;
border-top: 2px solid #333;
.print-button {
background: #ff9800;
color: white;
border: none;
padding: 8px 16px;
margin-left: 10px;
border-radius: 3px;
cursor: pointer;
font-size: 14px;
}
.print-button:hover {
background: #e68900;
}
/* 画面表示時thead内の印刷用ヘッダー行を非表示 */
tr.print-header-title,
tr.print-header-period,
tr.print-header-meta {
display: none;
}
.print-header-cell {
border: none !important;
background: none !important;
background-color: white !important;
padding: 2px 5px !important;
}
@media print {
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
color-adjust: exact !important;
}
body {
margin: 0;
padding: 0;
font-size: 11px;
}
/* 画面用の要素を印刷時に隠す */
#printDate,
h2,
.period-info,
.back-button,
.search-form,
.print-button,
#loadingStatus {
display: none !important;
}
/* thead内の印刷用ヘッダー行を表示 */
tr.print-header-title,
tr.print-header-period,
tr.print-header-meta {
display: table-row !important;
}
.print-header-cell {
border: none !important;
background: none !important;
background-color: white !important;
padding: 3px 5px !important;
}
/* 印刷時の単位・印刷日のスタイリング */
tr.print-header-meta th {
border: none !important;
background: none !important;
padding: 2px 5px !important;
text-align: right !important;
}
tr.print-header-meta th:last-child {
text-align: right !important;
}
tr.print-header-meta div {
text-align: right !important;
font-size: 10px !important;
font-weight: normal !important;
display: block !important;
width: 100% !important;
}
#theadPrintDate {
text-align: right !important;
align-items: center !important;
flex-wrap: wrap !important;
gap: 20px !important;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 8px;
}
thead {
display: table-header-group;
page-break-after: avoid;
}
tfoot {
display: none !important;
}
th {
background: #e8e8e8 !important;
font-weight: bold;
padding: 5px 6px;
border: 1px solid #666;
page-break-inside: avoid;
font-size: 11px;
}
td {
border: 1px solid #666;
padding: 4px 5px;
font-size: 10px;
white-space: nowrap;
}
tr.total-row td {
background-color: #d8d8d8 !important;
font-weight: bold;
border-top: 2px solid #333;
page-break-inside: avoid;
}
tbody tr {
page-break-inside: avoid;
}
tr.indent-row td:nth-child(2) {
padding-left: 20px !important;
}
td.left {
text-align: left;
white-space: normal;
}
td.right {
text-align: right;
white-space: nowrap;
}
td.center,
th.center {
text-align: center;
}
}
@page {
size: A4 portrait;
margin-top: 10mm;
margin-bottom: 20mm;
margin-left: 10mm;
margin-right: 10mm;
@bottom-center {
content: "第 " counter(page) " 页";
font-size: 10px;
}
}
</style>
</head>
@@ -115,6 +280,7 @@
>
← 首ページに戻る
</button>
<h2>残高試算表(貸借・損益)</h2>
<div
@@ -133,18 +299,68 @@
<button id="btnSearch">検索</button>
<button id="btnFullYear" class="secondary">全年度表示</button>
<button id="btnPrint" class="print-button">印刷</button>
</div>
<div class="period-info">
<span id="periodInfo">令和7年 1月 1日 令和7年 12月31日</span>
<span style="margin-left: 20px">単位: 円</span>
<span style="margin-left: 20px; float: right">単位: 円</span>
</div>
<table id="trialBalanceTable">
<thead>
<tr class="print-header-title">
<th
colspan="7"
class="print-header-cell"
style="text-align: center; font-size: 14px; font-weight: bold"
>
残高試算表(貸借・損益)
</th>
</tr>
<tr class="print-header-period">
<th
colspan="7"
class="print-header-cell"
style="text-align: center; font-size: 11px; font-weight: normal"
id="theadPeriodInfo"
></th>
</tr>
<tr class="print-header-meta">
<th colspan="5" class="print-header-cell"></th>
<th
colspan="2"
class="print-header-cell"
style="text-align: right !important; padding: 2px 5px"
>
<div
style="
text-align: right !important;
font-size: 10px;
line-height: 1.2;
display: block;
width: 100%;
"
id="theadPrintDate"
>
印刷日2026年2月25日
</div>
<div
style="
text-align: right !important;
font-size: 10px;
line-height: 1.2;
display: block;
width: 100%;
"
>
単位:円
</div>
</th>
</tr>
<tr>
<th class="left">科目コード</th>
<th class="left">科目名称</th>
<th class="center">科目名称</th>
<th>期首残高</th>
<th>借方発生額</th>
<th>貸方発生額</th>
@@ -155,16 +371,6 @@
<tbody>
<!-- JS 动态插入 -->
</tbody>
<tfoot>
<tr>
<td colspan="2" class="left">合計</td>
<td id="totalOpening">0</td>
<td id="totalDebit">0</td>
<td id="totalCredit">0</td>
<td id="totalClosing">0</td>
<td id="totalRatio">100.00</td>
</tr>
</tfoot>
</table>
<script src="js/trial-balance.js"></script>