This commit is contained in:
admin
2026-02-21 10:51:25 +09:00
parent 584530937b
commit 3b028b8fc0
25 changed files with 1852 additions and 244 deletions

View File

@@ -96,6 +96,12 @@
style="margin-left: 10px; background: #28a745"
>試算表へ</a
>
<a
class="btn"
href="opening_balance.html"
style="margin-left: 10px; background: #fd7e14"
>期首残高入力へ</a
>
</p>
</section>

View File

@@ -39,6 +39,67 @@
color: #090;
margin-top: 8px;
}
/* 账户搜索下拉框相关样式 */
.account-search-container {
position: relative;
width: 100%;
}
.account-search-input {
width: 100%;
padding: 6px !important;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
}
.account-search-input:focus {
outline: none;
border-color: #4caf50;
box-shadow: 0 0 4px rgba(76, 175, 80, 0.3);
}
.account-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
}
.account-dropdown.active {
display: block;
}
.account-option {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
}
.account-option:hover,
.account-option.selected {
background-color: #e3f2fd;
}
.account-option-strong {
font-weight: bold;
color: #333;
}
.account-option-light {
color: #999;
font-size: 12px;
}
</style>
</head>
<body>
@@ -84,6 +145,187 @@
const API = "";
let accounts = [];
// 科目グループ定義(仕訳入力と同期)
const ACCOUNT_GROUPS = [
// 資産の部
{
label: "現金・預金",
codes: ["101", "102", "103", "104", "105"],
},
{
label: "売上債権",
codes: ["201", "202"],
},
{
label: "有価証券",
codes: ["301"],
},
{
label: "棚卸資産",
codes: ["210"],
},
{
label: "他流動資産",
codes: ["203", "204", "205", "206", "207", "208"],
},
{
label: "有形固定資産",
codes: ["401", "402", "403", "404", "405", "406", "407"],
},
{
label: "無形固定資産",
codes: ["408"],
},
{
label: "投資その他資産",
codes: ["410"],
},
// 負債の部
{
label: "仕入債務",
codes: ["501"],
},
{
label: "その他流動負債",
codes: ["502", "503", "504", "505", "506", "507", "508"],
},
{
label: "固定負債",
codes: ["509"],
},
// 資本の部
{
label: "資本金",
codes: ["601"],
},
{
label: "利益剰余金",
codes: ["602"],
},
// 収益・費用
{
label: "収益",
codes: ["701", "702", "703", "704", "705"],
},
{
label: "費用",
codes: [
"801",
"802",
"803",
"804",
"805",
"806",
"807",
"808",
"809",
"810",
"811",
"812",
"813",
"814",
"815",
"816",
"817",
"818",
"819",
"820",
],
},
];
// ========================================
// 账户搜索功能
// ========================================
function setupAccountSearch(inputElement, row) {
const container = inputElement.closest(".account-search-container");
const dropdown = container.querySelector(".account-dropdown");
inputElement.addEventListener("input", (e) => {
const searchText = e.target.value.toLowerCase().trim();
if (!searchText) {
dropdown.classList.remove("active");
return;
}
// 过滤账户
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);
});
}
dropdown.classList.add("active");
});
// クリック以外の場所のドロップダウンを非表示
document.addEventListener("click", (e) => {
if (!container.contains(e.target)) {
dropdown.classList.remove("active");
}
});
// フォーカス時にドロップダウンを表示(既存値がある場合)
inputElement.addEventListener("focus", () => {
if (inputElement.value) {
dropdown.classList.add("active");
}
});
}
// 科目選択肢をグループ分けして構築
function buildAccountOptions() {
let html = "";
ACCOUNT_GROUPS.forEach((group) => {
html += `<optgroup label="${group.label}">`;
accounts
.filter((a) => group.codes.includes(a.account_code))
.forEach((a) => {
html += `<option value="${a.account_id}">${a.account_code} ${a.account_name}</option>`;
});
html += `</optgroup>`;
});
// 未分組の科目を「その他」グループに追加
const groupedCodes = ACCOUNT_GROUPS.flatMap((g) => g.codes);
const others = accounts.filter(
(a) => !groupedCodes.includes(a.account_code),
);
if (others.length > 0) {
html += `<optgroup label="その他">`;
others.forEach((a) => {
html += `<option value="${a.account_id}">${a.account_code} ${a.account_name}</option>`;
});
html += `</optgroup>`;
}
return html;
}
// ------------------------------------
// 初期化
// ------------------------------------
@@ -122,14 +364,14 @@
tr.innerHTML = `
<td>
<select>
${accounts
.map(
(a) =>
`<option value="${a.account_id}">${a.account_code} ${a.account_name}</option>`,
)
.join("")}
</select>
<div class="account-search-container">
<input
type="text"
class="account-search-input account"
placeholder="科目を入力・検索"
autocomplete="off" />
<div class="account-dropdown"></div>
</div>
</td>
<td><input type="number" min="0"></td>
<td><input type="number" min="0"></td>
@@ -138,10 +380,19 @@
tbody.appendChild(tr);
// Setup account search for this row
const accountInput = tr.querySelector(".account-search-input");
setupAccountSearch(accountInput, tr);
if (line) {
tr.querySelector("select").value = line.account_id;
tr.querySelectorAll("input")[0].value = line.debit || "";
tr.querySelectorAll("input")[1].value = line.credit || "";
// Find the account and populate the input
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 || "";
}
// 入力時に合計を自動更新
@@ -191,9 +442,10 @@
c = 0;
document.querySelectorAll("#linesTable tbody tr").forEach((tr) => {
const accountId = Number(tr.querySelector("select").value);
const debit = Number(tr.querySelectorAll("input")[0].value || 0);
const credit = Number(tr.querySelectorAll("input")[1].value || 0);
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);
if (debit === 0 && credit === 0) return;
lines.push({ account_id: accountId, debit, credit });
d += debit;

View File

@@ -184,6 +184,74 @@
display: table-header-group;
}
}
/* 账户搜索下拉框相关样式 */
.account-search-container {
position: relative;
width: 100%;
}
.account-search-input {
width: 100%;
padding: 6px !important;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.account-search-input:focus {
outline: none;
border-color: #4caf50;
box-shadow: 0 0 4px rgba(76, 175, 80, 0.3);
}
.account-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ccc;
border-top: none;
border-radius: 0 0 4px 4px;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
}
.account-dropdown.active {
display: block;
}
.account-option {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
text-align: left;
display: block;
width: 100%;
}
.account-option:hover,
.account-option.selected {
background-color: #e3f2fd;
}
.account-option-strong {
font-weight: bold;
color: #333;
display: block;
}
.account-option-light {
color: #999;
font-size: 12px;
display: block;
}
</style>
</head>
<body>
@@ -285,46 +353,82 @@
></div>
</div>
<div style="margin-top: 10px; display: flex; gap: 8px">
<div style="margin-top: 10px; display: flex; gap: 8px; align-items: center">
<button onclick="addRow()"> 行追加</button>
<button onclick="submitJournal()">登録</button>
<label
style="
display: flex;
align-items: center;
gap: 6px;
margin-left: 20px;
cursor: pointer;
"
>
<input type="checkbox" id="keepDetailsAfterSubmit" />
<span style="font-size: 14px">登録後に明細を保持する</span>
</label>
</div>
<hr style="margin: 40px 0; border: none; border-top: 2px solid #ddd" />
<h3>月次锁定管理</h3>
<!-- 月次锁定管理(折叠式) -->
<details style="margin: 16px 0">
<summary
style="
cursor: pointer;
font-size: 18px;
font-weight: bold;
padding: 12px;
background: #f5f5f5;
border-radius: 4px;
user-select: none;
"
>
▶ 月次锁定管理
</summary>
<div style="margin-bottom: 8px">
<label>年度:</label>
<input
type="number"
id="lockYear"
value="2025"
min="1900"
max="2999"
style="width: 80px"
/>
<div
style="
padding: 16px;
background: #fafafa;
border-radius: 4px;
margin-top: 8px;
"
>
<div style="margin-bottom: 8px">
<label>年度:</label>
<input
type="number"
id="lockYear"
value="2025"
min="1900"
max="2999"
style="width: 80px"
/>
<label>月份:</label>
<input
type="number"
id="lockMonth"
min="1"
max="12"
value="6"
style="width: 60px"
/>
</div>
<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>
<button onclick="lockMonth()">🔒 锁定</button>
<button onclick="unlockMonth()">🔓 解锁</button>
<div id="lockResult" style="margin-top: 10px; color: #333"></div>
<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 style="margin-top: 12px">
<button onclick="refreshMonthLocks()">🔄 刷新锁定列表</button>
<pre id="lockList" style="background: #f7f7f7; padding: 8px"></pre>
</div>
</div>
</details>
<div
id="monthLockBanner"
@@ -370,9 +474,15 @@
<div class="search-form-row">
<label>科目</label>
<select id="searchAccountId">
<option value="">-- すべて --</option>
</select>
<div class="account-search-container">
<input
type="text"
id="searchAccountInput"
class="account-search-input"
placeholder="科目を入力・検索"
autocomplete="off" />
<div class="account-dropdown"></div>
</div>
</div>
<div class="search-form-row" style="grid-column: 1 / -1">
@@ -426,52 +536,68 @@
let accounts = [];
const ACCOUNT_GROUPS = [
// 資産の部
{
label: "💰 現金・預金",
label: "現金・預金",
codes: ["101", "102", "103", "104", "105"],
},
{
label: "📦 流動資産",
codes: ["201", "202", "203", "204", "205", "210"],
label: "売上債権",
codes: ["201", "202"],
},
{
label: "🏗️ 固定資産",
codes: [
"301",
"401",
"402",
"403",
"404",
"405",
"406",
"407",
"408",
],
label: "有価証券",
codes: ["301"],
},
{
label: "💳 負債",
codes: [
"501",
"502",
"503",
"504",
"505",
"506",
"507",
"508",
"509",
],
label: "棚卸資産",
codes: ["210"],
},
{
label: "🏢 純資産",
codes: ["601", "602"],
label: "他流動資産",
codes: ["203", "204", "205", "206", "207", "208"],
},
{
label: "📈 収益",
label: "有形固定資産",
codes: ["401", "402", "403", "404", "405", "406", "407"],
},
{
label: "無形固定資産",
codes: ["408"],
},
{
label: "投資その他資産",
codes: ["410"],
},
// 負債の部
{
label: "仕入債務",
codes: ["501"],
},
{
label: "その他流動負債",
codes: ["502", "503", "504", "505", "506", "507", "508"],
},
{
label: "固定負債",
codes: ["509"],
},
// 資本の部
{
label: "資本金",
codes: ["601"],
},
{
label: "利益剰余金",
codes: ["602"],
},
// 収益・費用
{
label: "収益",
codes: ["701", "702", "703", "704", "705"],
},
{
label: "📉 費用",
label: "費用",
codes: [
"801",
"802",
@@ -483,6 +609,16 @@
"808",
"809",
"810",
"811",
"812",
"813",
"814",
"815",
"816",
"817",
"818",
"819",
"820",
],
},
];
@@ -491,9 +627,88 @@
let taxReceivedAccounts = [];
let rowSeq = 0; // 行ID計数器
// -----------------------------
// ========================================
// 账户搜索功能
// ========================================
function setupAccountSearch(inputElement, row) {
const container = inputElement.closest(".account-search-container");
const dropdown = container.querySelector(".account-dropdown");
inputElement.addEventListener("input", (e) => {
const searchText = e.target.value.toLowerCase().trim();
if (!searchText) {
dropdown.classList.remove("active");
return;
}
// 过滤账户
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");
saveAccountToRecent(acc.account_id);
});
dropdown.appendChild(option);
});
}
dropdown.classList.add("active");
});
// クリック以外の場所のドロップダウンを非表示
document.addEventListener("click", (e) => {
if (!container.contains(e.target)) {
dropdown.classList.remove("active");
}
});
// フォーカス時にドロップダウンを表示(既存値がある場合)
inputElement.addEventListener("focus", () => {
if (inputElement.value) {
dropdown.classList.add("active");
}
});
}
// getSelectedAccountId 用来获取当前行选中的账户ID
function getSelectedAccountIdFromInput(inputElement) {
return inputElement.dataset.accountId || null;
}
// 建立一个全局函数来获取行中的账户ID
window.getAccountIdFromRow = function(row) {
const input = row.querySelector(".account-search-input");
return input.dataset.accountId || null;
};
// 建立一个全局函数来获取行中的账户文本
window.getAccountTextFromRow = function(row) {
const input = row.querySelector(".account-search-input");
return input.value || "";
};
// ========================================
// 初期化
// -----------------------------
// ========================================
async function init() {
console.log("🔧 init() 开始...");
const res = await fetch(`${API}/accounts`);
@@ -566,25 +781,77 @@
// 検索用科目セレクタ
// -----------------------------
function setupSearchAccountSelector() {
const select = document.getElementById("searchAccountId");
select.innerHTML = '<option value="">-- すべて --</option>';
// 新的可搜索输入框实现
const inputElement = document.getElementById("searchAccountInput");
if (!inputElement) {
console.log("searchAccountInput 不存在");
return;
}
const container = inputElement.closest(".account-search-container");
if (!container) return;
const dropdown = container.querySelector(".account-dropdown");
ACCOUNT_GROUPS.forEach((group) => {
const optgroup = document.createElement("optgroup");
optgroup.label = group.label;
inputElement.addEventListener("input", (e) => {
const searchText = e.target.value.toLowerCase().trim();
if (!searchText) {
dropdown.classList.remove("active");
inputElement.dataset.accountId = "";
return;
}
const groupAccounts = accounts.filter((a) =>
group.codes.includes(a.account_code),
);
groupAccounts.forEach((acc) => {
const option = document.createElement("option");
option.value = acc.account_id;
option.textContent = `${acc.account_code} ${acc.account_name}`;
optgroup.appendChild(option);
const filtered = accounts.filter((acc) => {
const code = acc.account_code.toLowerCase();
const name = acc.account_name.toLowerCase();
return code.includes(searchText) || name.includes(searchText);
});
select.appendChild(optgroup);
dropdown.innerHTML = "";
const all = document.createElement("div");
all.className = "account-option";
all.innerHTML = `<span class="account-option-strong">-- すべて --</span>`;
all.addEventListener("click", () => {
inputElement.value = "";
inputElement.dataset.accountId = "";
dropdown.classList.remove("active");
performSearch();
});
dropdown.appendChild(all);
if (filtered.length === 0) {
const none = document.createElement("div");
none.style.padding = "8px 12px";
none.style.color = "#999";
none.textContent = "該当する科目がありません";
dropdown.appendChild(none);
} else {
filtered.slice(0, 20).forEach((acc) => {
const opt = document.createElement("div");
opt.className = "account-option";
opt.innerHTML = `<span class="account-option-strong">${acc.account_code}</span><span class="account-option-light">${acc.account_name}</span>`;
opt.addEventListener("click", () => {
inputElement.value = `${acc.account_code} ${acc.account_name}`;
inputElement.dataset.accountId = acc.account_id;
dropdown.classList.remove("active");
performSearch();
});
dropdown.appendChild(opt);
});
}
dropdown.classList.add("active");
});
document.addEventListener("click", (e) => {
if (!container.contains(e.target)) {
dropdown.classList.remove("active");
}
});
inputElement.addEventListener("focus", () => {
if (inputElement.value) {
inputElement.dispatchEvent(new Event("input"));
}
});
}
@@ -671,9 +938,15 @@
tr.innerHTML = `
<td>
<select class="account" ${isTaxRow ? "disabled" : ""}>
${buildAccountOptions()}
</select>
<div class="account-search-container">
<input
type="text"
class="account-search-input account"
placeholder="科目を入力・検索"
autocomplete="off"
${isTaxRow ? "readonly" : ""} />
<div class="account-dropdown"></div>
</div>
</td>
<td><input type="text" class="debit right" inputmode="numeric" style="ime-mode: inactive;" ${
isTaxRow ? "readonly" : ""
@@ -705,8 +978,12 @@
// 税行の場合,填值
if (isTaxRow && taxInfo) {
const acc = tr.querySelector(".account");
acc.value = taxInfo.accountId || "";
const accountInput = tr.querySelector(".account-search-input");
const account = accounts.find(a => a.account_id === taxInfo.accountId);
if (account) {
accountInput.value = `${account.account_code} ${account.account_name}`;
accountInput.dataset.accountId = account.account_id;
}
if (taxInfo.side === "debit")
tr.querySelector(".debit").value = taxInfo.amount;
if (taxInfo.side === "credit")
@@ -718,7 +995,7 @@
const recalc = () => handleTax(tr);
const debitInput = tr.querySelector(".debit");
const creditInput = tr.querySelector(".credit");
const accountSelect = tr.querySelector(".account");
const accountInput = tr.querySelector(".account-search-input");
debitInput.addEventListener("input", recalc);
creditInput.addEventListener("input", recalc);
@@ -727,10 +1004,8 @@
debitInput.addEventListener("blur", updateTotals);
creditInput.addEventListener("blur", updateTotals);
// 科目選択時に履歴を保存
accountSelect.addEventListener("change", () => {
saveAccountToRecent(parseInt(accountSelect.value));
});
// 科目検索入力の処理
setupAccountSearch(accountInput, tr);
tr.querySelector(".taxType").addEventListener("change", recalc);
tr.querySelector(".taxDirection").addEventListener("change", recalc);
@@ -909,7 +1184,8 @@
rows.forEach((row) => {
if (row.classList.contains("tax-row")) return; // 税行は無視
const accountId = Number(row.querySelector(".account").value);
const accountInput = row.querySelector(".account-search-input");
const accountId = Number(accountInput.dataset.accountId || 0);
const debit = Number(
row.querySelector(".debit").value.replace(/,/g, "") || 0,
);
@@ -1015,18 +1291,28 @@
alert(`登録完了ID=${result.journal_entry_id}`);
// フォームをクリア
document.getElementById("description").value = "";
const tbody = document.querySelector("#linesTable tbody");
tbody.innerHTML = "";
rowSeq = 0;
addRow();
// 「登録後に明細を保持する」チェックボックスを確認
const keepDetails = document.getElementById(
"keepDetailsAfterSubmit",
).checked;
// 借方合計・貸方合計を0にリセット
document.getElementById("totalDebit").textContent = "0";
document.getElementById("totalCredit").textContent = "0";
document.getElementById("balanceStatus").textContent = "";
document.getElementById("balanceStatus").style.background = "";
if (!keepDetails) {
// チェックボックスが未選中の場合、フォームをクリア(従来の挙動)
document.getElementById("description").value = "";
const tbody = document.querySelector("#linesTable tbody");
tbody.innerHTML = "";
rowSeq = 0;
addRow();
// 借方合計・貸方合計を0にリセット
document.getElementById("totalDebit").textContent = "0";
document.getElementById("totalCredit").textContent = "0";
document.getElementById("balanceStatus").textContent = "";
document.getElementById("balanceStatus").style.background = "";
} else {
// チェックボックスが選中の場合、何もクリアしない(日付、摘要、明細をすべて保持)
// ユーザーは日付だけを修正してから次の仕訳を登録する
}
// 仕訳検索を自動実行して最新の登録状況を表示
searchJournals();
@@ -1447,13 +1733,19 @@
}
}
// 検索結果を表に表示
// 搜索函数(在选择搜索科目时被触发)
function performSearch() {
searchJournals();
}
// 检索结果を表に表示
async function searchJournals() {
try {
const from = document.getElementById("searchFromDate").value;
const to = document.getElementById("searchToDate").value;
const key = document.getElementById("searchKeyword").value;
const accountId = document.getElementById("searchAccountId").value;
const accountInput = document.getElementById("searchAccountInput");
const accountId = accountInput ? accountInput.dataset.accountId : "";
const accountSide =
document.getElementById("searchAccountSide").value;
const includeHistory = document.getElementById(
@@ -1525,15 +1817,14 @@
if (key) {
conditions.push(`摘要: ${key}`);
}
const accountSelect = document.getElementById("searchAccountId");
const accountName =
accountSelect.options[accountSelect.selectedIndex]?.text || "";
if (accountName && accountName !== "-- すべて --") {
const accountInput = document.getElementById("searchAccountInput");
const accountName = accountInput ? accountInput.value : "";
if (accountName && accountName.trim() !== "") {
conditions.push(`科目: ${accountName}`);
}
const sideSelect = document.getElementById("searchAccountSide");
const sideName =
sideSelect.options[sideSelect.selectedIndex]?.text || "";
sideSelect ? sideSelect.options[sideSelect.selectedIndex]?.text : "";
if (sideName && sideName !== "-- すべて --") {
conditions.push(`方向: ${sideName}`);
}
@@ -1696,9 +1987,11 @@
const tr = addRow();
// 科目を設定
const accountSelect = tr.querySelector(".account");
if (accountSelect) {
accountSelect.value = line.account_id;
const accountInput = tr.querySelector(".account-search-input");
const account = accounts.find(a => a.account_id === line.account_id);
if (accountInput && account) {
accountInput.value = `${account.account_code} ${account.account_name}`;
accountInput.dataset.accountId = account.account_id;
}
// 借方・貸方金額を設定(カンマフォーマット付き)
@@ -1835,9 +2128,18 @@
if (
conditions.accountId !== undefined &&
conditions.accountId !== null
)
document.getElementById("searchAccountId").value =
conditions.accountId;
) {
const accountInput = document.getElementById("searchAccountInput");
if (accountInput && accounts && accounts.length > 0) {
const account = accounts.find(
(a) => a.account_id === conditions.accountId,
);
if (account) {
accountInput.value = `${account.account_code} ${account.account_name}`;
accountInput.dataset.accountId = account.account_id;
}
}
}
if (
conditions.accountSide !== undefined &&
conditions.accountSide !== null

View File

@@ -4,10 +4,20 @@ async function apiGet(path, params = {}) {
const query = new URLSearchParams(params).toString();
const url = `${API_BASE}${path}?${query}`;
console.log(`📡 API GET: ${url}`);
const res = await fetch(url);
console.log(`📊 Response status: ${res.status}`);
if (!res.ok) {
const data = await res.json();
console.error(`❌ API Error:`, data);
throw new Error(data.detail || "APIエラー");
}
return res.json();
const jsonData = await res.json();
console.log(`✅ API Response:`, jsonData);
return jsonData;
}

View File

@@ -1,15 +1,22 @@
let accounts = [];
let retainedEarnings = null;
let isLocked = false;
let isDataLoaded = false; // ★ データ読込済みフラグ(保存時の検証用)
// 科目グループ定義(小計付き)
// 科目グループ定義(小計付き)- 試算表と同じ順序
const GROUPS = [
{
key: "asset",
label: "資産",
subGroups: [
{ label: "現金・預金合計", codes: ["101", "102", "103", "104"] },
{ label: "売上債権合計", codes: ["201"] },
{ label: "現金・預金合計", codes: ["101", "102", "103", "104", "105"] },
{ label: "売上債権合計", codes: ["201", "202"] },
{ label: "有価証券合計", codes: ["301"] },
{ label: "棚卸資産合計", codes: ["210"] },
{
label: "他流動資産合計",
codes: ["203", "204", "205", "206", "207", "208"],
},
{
label: "流動資産合計",
codes: [
@@ -17,14 +24,25 @@ const GROUPS = [
"102",
"103",
"104",
"105",
"201",
"202",
"203",
"204",
"205",
"206",
"207",
"208",
"210",
"301",
],
},
{
label: "有形固定資産合計",
codes: ["401", "402", "403", "404", "405", "406", "407"],
},
{ label: "無形固定資産合計", codes: ["408"] },
{ label: "投資その他の資産合計", codes: ["301", "410"] },
{
label: "固定資産合計",
codes: [
@@ -40,6 +58,35 @@ const GROUPS = [
"410",
],
},
{
label: "資産合計",
codes: [
"101",
"102",
"103",
"104",
"105",
"201",
"202",
"203",
"204",
"205",
"206",
"207",
"208",
"210",
"301",
"401",
"402",
"403",
"404",
"405",
"406",
"407",
"408",
"410",
],
},
],
},
{
@@ -52,13 +99,17 @@ const GROUPS = [
codes: ["501", "502", "503", "504", "505", "506", "507", "508"],
},
{ label: "固定負債合計", codes: ["509"] },
{
label: "負債合計",
codes: ["501", "502", "503", "504", "505", "506", "507", "508", "509"],
},
],
},
{
key: "equity",
label: "純資産",
subGroups: [
{ label: "株主資本合計", codes: ["601", "602"] },
{ label: "資本合計", codes: ["601"] },
{ label: "純資産合計", codes: ["601", "602"] },
],
},
@@ -163,102 +214,67 @@ function updateSubtotalRows() {
async function loadOpeningBalances() {
const year = document.getElementById("fiscalYear").value;
const data = await apiGet("/opening-balances", { fiscal_year: year });
console.log(`🔄 Loading opening balances for year: ${year}`);
accounts = data;
retainedEarnings = accounts.find(
(a) => a.account_name === "繰越利益剰余金" || a.account_name === "繰越利益",
);
try {
const data = await apiGet("/opening-balances", { fiscal_year: year });
const tbody = document.querySelector("#balanceTable tbody");
tbody.innerHTML = "";
console.log(`✓ Received data:`, data);
console.log(`✓ Total accounts: ${data.length}`);
GROUPS.forEach((group) => {
// 分组标题行
const headerTr = document.createElement("tr");
headerTr.innerHTML = `
<td colspan="4" style="background:#ddd; font-weight:bold;">
${group.label}
</td>
`;
tbody.appendChild(headerTr);
accounts = data;
retainedEarnings = accounts.find(
(a) =>
a.account_name === "繰越利益剰余金" || a.account_name === "繰越利益",
);
const groupAccounts = accounts.filter((a) => a.account_type === group.key);
const tbody = document.querySelector("#balanceTable tbody");
console.log(`📌 tbody element:`, tbody);
groupAccounts.forEach((a, idx) => {
const i = accounts.indexOf(a);
tbody.innerHTML = "";
// シンプルに全科目を表示(グループ分けは後で)
console.log(`🔄 Starting to render ${data.length} accounts...`);
accounts.forEach((a, idx) => {
const isRetained =
a.account_name === "繰越利益剰余金" || a.account_name === "繰越利益";
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${a.account_code}</td>
<td>${a.account_name}</td>
<td>
<input type="text" value="${formatNumber(a.opening_debit)}"
${isRetained ? "readonly" : ""}
onchange="accounts[${i}].opening_debit=parseNumber(this.value); this.value=formatNumber(accounts[${i}].opening_debit); recalcTotals();">
</td>
<td>
<input type="text" value="${formatNumber(a.opening_credit)}"
${isRetained ? "readonly" : ""}
onchange="accounts[${i}].opening_credit=parseNumber(this.value); this.value=formatNumber(accounts[${i}].opening_credit); recalcTotals();">
</td>
`;
<td>${a.account_code}</td>
<td>${a.account_name}</td>
<td>
<input type="text" value="${formatNumber(a.opening_debit)}"
${isRetained ? "readonly" : ""}
onchange="accounts[${idx}].opening_debit=parseNumber(this.value); this.value=formatNumber(accounts[${idx}].opening_debit); recalcTotals();">
</td>
<td>
<input type="text" value="${formatNumber(a.opening_credit)}"
${isRetained ? "readonly" : ""}
onchange="accounts[${idx}].opening_credit=parseNumber(this.value); this.value=formatNumber(accounts[${idx}].opening_credit); recalcTotals();">
</td>
`;
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 = `
<td></td>
<td>${subGroup.label}</td>
<td>${formatNumber(subGroupTotal.debit)}</td>
<td>${formatNumber(subGroupTotal.credit)}</td>
`;
tbody.appendChild(totalTr);
}
});
});
});
// 年度锁定チェック
fetch(`/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();
console.log(`✅ Table rendered!`, tbody.innerHTML.substring(0, 100));
isDataLoaded = true; // ★ データ読込済みフラグを設定
recalcTotals();
} catch (e) {
console.error("❌ Error loading opening balances:", e);
alert(`読込エラー: ${e.message}`);
}
}
async function saveOpeningBalances() {
// ★ 重要:データを読込まずに保存するのを防止
if (!isDataLoaded) {
alert("先に「読込」ボタンで期首残高を読み込んでください。");
return;
}
if (isLocked) {
alert("この会計年度の期首残高は確定されているため、保存できません。");
return;
@@ -279,6 +295,12 @@ async function saveOpeningBalances() {
credit: Number(a.opening_credit || 0),
}));
// ★ 重要データが全て0の場合、保存を拒否
if (balances.length === 0) {
alert("全科目の残高が0です。期首残高データを入力してください。");
return;
}
try {
await fetch(`/opening-balances`, {
method: "POST",
@@ -295,6 +317,8 @@ async function saveOpeningBalances() {
});
alert("期首残高を保存しました");
// メインメニューに移動
location.href = "index.html";
} catch (e) {
alert(e.message);
}

View File

@@ -99,6 +99,11 @@ function initTrialBalance() {
tr.classList.add("total-row");
}
// indent フラグに基づくクラス追加
if (row.indent) {
tr.classList.add("indent-row");
}
const ratio = row.ratio ?? "0.00";
tr.innerHTML = `

View File

@@ -39,10 +39,23 @@
<br />
<button onclick="saveOpeningBalances()">保存</button>
<button
onclick="location.href = 'trial-balance.html'"
class="back-button"
onclick="saveOpeningBalances()"
style="
margin: 0 10px 0 0;
padding: 8px 16px;
background: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
"
>
保存
</button>
<button
onclick="location.href = 'index.html'"
style="
margin: 0 0 20px 0;
padding: 8px 16px;
@@ -54,7 +67,7 @@
font-size: 14px;
"
>
試算表に戻る
メインメニューに戻る
</button>
<script src="js/api.js"></script>

View File

@@ -88,6 +88,9 @@
font-weight: bold;
border-top: 2px solid #666;
}
tr.indent-row td:nth-child(2) {
padding-left: 40px !important;
}
tfoot td {
font-weight: bold;
background: #e0e0e0;