From 99d16d8219f710854752d655972083c75cb915d5 Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 4 Mar 2026 00:35:51 +0900 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/routers/journal_entries.py | 128 +++- backend/sql/add_line_description.sql | 24 + frontend/journal-edit.html | 37 +- frontend/journal-entry.html | 890 ++++++++++++++++++++++++- frontend/journal-view.html | 5 +- 5 files changed, 1012 insertions(+), 72 deletions(-) create mode 100644 backend/sql/add_line_description.sql diff --git a/backend/app/routers/journal_entries.py b/backend/app/routers/journal_entries.py index 656cbdc..a13fda4 100644 --- a/backend/app/routers/journal_entries.py +++ b/backend/app/routers/journal_entries.py @@ -7,6 +7,26 @@ from datetime import date router = APIRouter(prefix="/journal-entries", tags=["仕訳"]) +# journal_lines に line_description カラムが存在するかキャッシュ +_line_description_column_exists: Optional[bool] = None + +def _check_line_description_column() -> bool: + """journal_lines テーブルに line_description カラムが存在するか確認(キャッシュあり)""" + global _line_description_column_exists + if _line_description_column_exists is None: + try: + with get_connection() as conn: + with conn.cursor() as cur: + cur.execute(""" + SELECT 1 FROM information_schema.columns + WHERE table_name = 'journal_lines' + AND column_name = 'line_description' + """) + _line_description_column_exists = cur.fetchone() is not None + except Exception: + _line_description_column_exists = False + return _line_description_column_exists + class JournalLine(BaseModel): account_id: int @@ -14,6 +34,7 @@ class JournalLine(BaseModel): credit: Decimal = Decimal("0") tax_rate: Optional[int] = None # 10 or 8 or None tax_direction: Optional[str] = None # "paid" or "received" or None + line_description: Optional[str] = None # 行摘要(任意) class JournalEntryRequest(BaseModel): @@ -205,7 +226,9 @@ def get_journal_entry(journal_id: int): raise HTTPException(status_code=404, detail="仕訳が存在しません") # 明細取得 - cur.execute(""" + has_line_desc = _check_line_description_column() + line_desc_col = ", jl.line_description" if has_line_desc else "" + cur.execute(f""" SELECT jl.journal_line_id, jl.account_id, @@ -215,6 +238,7 @@ def get_journal_entry(journal_id: int): jl.credit, jl.tax_rate, jl.tax_direction + {line_desc_col} FROM journal_lines jl JOIN accounts a ON jl.account_id = a.account_id WHERE jl.journal_entry_id = %s @@ -493,6 +517,7 @@ def create_journal_entry(req: JournalEntryRequest): entry_id = cur.fetchone()["journal_entry_id"] # ④ 插入交易明細 + has_line_desc = _check_line_description_column() for line in all_lines: # 轉換 tax_direction tax_dir_db = None @@ -504,24 +529,46 @@ def create_journal_entry(req: JournalEntryRequest): else: tax_dir_db = line.tax_direction.upper() - cur.execute(""" - INSERT INTO journal_lines ( - journal_entry_id, - account_id, - debit, - credit, - tax_rate, - tax_direction - ) - VALUES (%s, %s, %s, %s, %s, %s) - """, ( - entry_id, - line.account_id, - line.debit, - line.credit, - line.tax_rate, - tax_dir_db - )) + if has_line_desc: + cur.execute(""" + INSERT INTO journal_lines ( + journal_entry_id, + account_id, + debit, + credit, + tax_rate, + tax_direction, + line_description + ) + VALUES (%s, %s, %s, %s, %s, %s, %s) + """, ( + entry_id, + line.account_id, + line.debit, + line.credit, + line.tax_rate, + tax_dir_db, + line.line_description or None + )) + else: + cur.execute(""" + INSERT INTO journal_lines ( + journal_entry_id, + account_id, + debit, + credit, + tax_rate, + tax_direction + ) + VALUES (%s, %s, %s, %s, %s, %s) + """, ( + entry_id, + line.account_id, + line.debit, + line.credit, + line.tax_rate, + tax_dir_db + )) conn.commit() @@ -543,6 +590,7 @@ def create_journal_entry(req: JournalEntryRequest): entry_id = cur.fetchone()["journal_entry_id"] # 插入交易明細 + has_line_desc = _check_line_description_column() for line in all_lines: # 轉換 tax_direction tax_dir_db = None @@ -554,19 +602,35 @@ def create_journal_entry(req: JournalEntryRequest): else: tax_dir_db = line.tax_direction.upper() - cur.execute(""" - INSERT INTO journal_lines ( - journal_entry_id, account_id, debit, credit, - tax_rate, tax_direction - ) VALUES (%s, %s, %s, %s, %s, %s) - """, ( - entry_id, - line.account_id, - line.debit, - line.credit, - line.tax_rate, - tax_dir_db - )) + if has_line_desc: + cur.execute(""" + INSERT INTO journal_lines ( + journal_entry_id, account_id, debit, credit, + tax_rate, tax_direction, line_description + ) VALUES (%s, %s, %s, %s, %s, %s, %s) + """, ( + entry_id, + line.account_id, + line.debit, + line.credit, + line.tax_rate, + tax_dir_db, + line.line_description or None + )) + else: + cur.execute(""" + INSERT INTO journal_lines ( + journal_entry_id, account_id, debit, credit, + tax_rate, tax_direction + ) VALUES (%s, %s, %s, %s, %s, %s) + """, ( + entry_id, + line.account_id, + line.debit, + line.credit, + line.tax_rate, + tax_dir_db + )) conn.commit() diff --git a/backend/sql/add_line_description.sql b/backend/sql/add_line_description.sql new file mode 100644 index 0000000..91c0e48 --- /dev/null +++ b/backend/sql/add_line_description.sql @@ -0,0 +1,24 @@ +-- ============================================================================ +-- マイグレーション: journal_lines テーブルに line_description カラム追加 +-- 実行方法: admin または postgres 超级用户で実行 +-- psql -h 192.168.0.61 -p 55432 -U admin -d njts_acct -f add_line_description.sql +-- ============================================================================ + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'journal_lines' AND column_name = 'line_description' + ) THEN + ALTER TABLE journal_lines ADD COLUMN line_description TEXT; + RAISE NOTICE '✓ line_description カラムを journal_lines に追加しました'; + ELSE + RAISE NOTICE '✓ line_description カラムはすでに存在しています'; + END IF; +END $$; + +-- 確認 +SELECT column_name, data_type, is_nullable +FROM information_schema.columns +WHERE table_name = 'journal_lines' +ORDER BY ordinal_position; diff --git a/frontend/journal-edit.html b/frontend/journal-edit.html index fd367bb..be367ec 100644 --- a/frontend/journal-edit.html +++ b/frontend/journal-edit.html @@ -28,6 +28,10 @@ button { padding: 6px; } + #linesTable td input { + box-sizing: border-box; + width: 100%; + } .right { text-align: right; } @@ -122,10 +126,11 @@ - - - - + + + + + @@ -135,6 +140,7 @@ +
科目借方貸方操作科目借方貸方摘要(行)操作
0 0
@@ -277,7 +283,11 @@ filtered = accounts.filter((acc) => { const code = acc.account_code.toLowerCase(); const name = acc.account_name.toLowerCase(); - return code.includes(searchText) || name.includes(searchText); + return ( + code.includes(searchText) || + name.includes(searchText) || + `${code} ${name}`.includes(searchText) + ); }); } @@ -426,8 +436,9 @@
- - + + + `; @@ -453,6 +464,9 @@ const creditVal = Number(line.credit) || 0; numberInputs[0].value = debitVal > 0 ? String(debitVal) : ""; numberInputs[1].value = creditVal > 0 ? String(creditVal) : ""; + // 摘要(行)を設定 + const memoInput = tr.querySelector(".line-memo-input"); + if (memoInput) memoInput.value = line.line_description || ""; } // 入力時に合計を自動更新 @@ -540,8 +554,15 @@ const numberInputs = tr.querySelectorAll("input[type='number']"); const debit = Number(numberInputs[0]?.value || 0); const credit = Number(numberInputs[1]?.value || 0); + const lineDesc = + tr.querySelector(".line-memo-input")?.value.trim() || undefined; if (debit === 0 && credit === 0) return; - lines.push({ account_id: accountId, debit, credit }); + lines.push({ + account_id: accountId, + debit, + credit, + ...(lineDesc ? { line_description: lineDesc } : {}), + }); d += debit; c += credit; }); diff --git a/frontend/journal-entry.html b/frontend/journal-entry.html index 6298c42..aa65b4e 100644 --- a/frontend/journal-entry.html +++ b/frontend/journal-entry.html @@ -342,14 +342,20 @@
- - +
@@ -406,6 +412,14 @@ + +
@@ -453,6 +467,257 @@
+ +
+ + ▼ 仕訳詳細入力(複数行・借貸直接入力) + + +
+ +
+ + +
+ + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + +
+ 科目 + + 借方金額 + + 貸方金額 + + 摘要(行) +
+ 合計 + + 0 + + 0 + + +
+ + +
+ + + + +
+
+
+ +
+
{ const code = acc.account_code.toLowerCase(); const name = acc.account_name.toLowerCase(); - return code.includes(searchText) || name.includes(searchText); + return ( + code.includes(searchText) || + name.includes(searchText) || + `${code} ${name}`.includes(searchText) + ); }); renderDropdown(filtered, true); @@ -895,6 +1175,17 @@ // 摘要履歴を読み込み loadDescriptionHistory(); + // 摘要検索ドロップダウンをセットアップ + setupDescriptionSearch(); + + // 詳細入力セクション初期化 + setupDetailDescriptionSearch(); + detailClearAll(); + // 今日の日付をデフォルト設定 + document.getElementById("detailEntryDate").value = new Date() + .toISOString() + .slice(0, 10); + console.log("✓ init() 完成"); } @@ -1006,7 +1297,11 @@ const filtered = accounts.filter((acc) => { const code = acc.account_code.toLowerCase(); const name = acc.account_name.toLowerCase(); - return code.includes(searchText) || name.includes(searchText); + return ( + code.includes(searchText) || + name.includes(searchText) || + `${code} ${name}`.includes(searchText) + ); }); renderSearchDropdown(filtered, true); @@ -1043,17 +1338,7 @@ // 摘要履歴管理 // ----------------------------- function loadDescriptionHistory() { - const history = JSON.parse( - localStorage.getItem("descriptionHistory") || "[]", - ); - const datalist = document.getElementById("descriptionHistory"); - datalist.innerHTML = ""; - - history.forEach((desc) => { - const option = document.createElement("option"); - option.value = desc; - datalist.appendChild(option); - }); + // 旧datalist方式の互換用に残す(現在はカスタムドロップダウンで管理) } function saveDescriptionToHistory(description) { @@ -1067,22 +1352,564 @@ history = history.filter((d) => d !== description); history.unshift(description); - // 最大100件まで保存 - if (history.length > 100) { - history = history.slice(0, 100); + // 最大500件まで保存 + if (history.length > 500) { + history = history.slice(0, 500); } localStorage.setItem("descriptionHistory", JSON.stringify(history)); - loadDescriptionHistory(); + } + + // 摘要検索ドロップダウン + function setupDescriptionSearch() { + const input = document.getElementById("description"); + const dropdown = document.getElementById("descriptionDropdown"); + if (!input || !dropdown) return; + + let debounceTimer = null; + + function makeSectionLabel(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; + } + + function addOption(text, onSelect) { + const opt = document.createElement("div"); + opt.className = "account-option"; + opt.style.cssText = "font-size:14px;"; + opt.textContent = text; + opt.addEventListener("mousedown", (ev) => { + ev.preventDefault(); + input.value = text; + dropdown.classList.remove("active"); + if (onSelect) onSelect(); + }); + dropdown.appendChild(opt); + } + + function renderDropdown(historyItems, apiItems) { + dropdown.innerHTML = ""; + + // 充てる候補なし + if (historyItems.length === 0 && apiItems.length === 0) { + dropdown.classList.remove("active"); + return; + } + + if (historyItems.length > 0) { + dropdown.appendChild(makeSectionLabel("🗒️ 入力履歴")); + historyItems.slice(0, 20).forEach((t) => addOption(t)); + } + + if (apiItems.length > 0) { + const sep = document.createElement("div"); + sep.style.cssText = "border-top:1px solid #e8e8e8;margin:2px 0;"; + if (historyItems.length > 0) dropdown.appendChild(sep); + dropdown.appendChild(makeSectionLabel("📝 過去の仕訳から")); + apiItems.slice(0, 20).forEach((t) => addOption(t)); + } + + dropdown.classList.add("active"); + } + + async function doSearch(text) { + const history = JSON.parse( + localStorage.getItem("descriptionHistory") || "[]", + ); + const historyFiltered = text + ? history.filter((d) => + d.toLowerCase().includes(text.toLowerCase()), + ) + : history.slice(0, 30); + + // APIから過去仕訳の摘要を検索 + let apiItems = []; + try { + const qs = new URLSearchParams(); + if (text) qs.append("keyword", text); + qs.append("limit", "50"); + const res = await fetch(`${API}/journal-entries?${qs.toString()}`); + if (res.ok) { + const data = await res.json(); + const entries = data.items ?? data; + const historySet = new Set(historyFiltered); + const seen = new Set(); + apiItems = entries + .map((e) => e.description) + .filter( + (d) => d && !historySet.has(d) && !seen.has(d) && seen.add(d), + ) + .slice(0, 20); + } + } catch (_) { + // APIエラーは無視 + } + + renderDropdown(historyFiltered, apiItems); + } + + input.addEventListener("input", () => { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => doSearch(input.value.trim()), 250); + }); + + input.addEventListener("focus", () => { + doSearch(input.value.trim()); + }); + + input.addEventListener("click", () => { + if (!dropdown.classList.contains("active")) { + doSearch(input.value.trim()); + } + }); + + document.addEventListener("click", (e) => { + if (!input.parentElement.contains(e.target)) { + dropdown.classList.remove("active"); + } + }); + + // Enterキー・Tabキーでドロップダウンを閉じる + input.addEventListener("keydown", (e) => { + if (e.key === "Enter" || e.key === "Tab") { + dropdown.classList.remove("active"); + } + }); } // ----------------------------- // (setupTaxSelectors, addRow, handleTax, updateTotals は新UIでは不要) // ----------------------------- - // ----------------------------- - // 登録(簡易入力版) - // ----------------------------- + // ============================================================ + // 仕訳詳細入力(多行対応) + // ============================================================ + let detailRowSeq = 0; + + function detailAddLine() { + const tbody = document.getElementById("detailLinesTbody"); + const rowId = ++detailRowSeq; + const tr = document.createElement("tr"); + tr.id = `detailRow_${rowId}`; + tr.innerHTML = ` + + + + + + + + + + + + + + + + `; + tbody.appendChild(tr); + + // 科目検索セットアップ + const input = tr.querySelector(".detail-account-input"); + setupDetailAccountSearch(input); + + detailUpdateTotals(); + return tr; + } + + function detailRemoveLine(rowId) { + const tr = document.getElementById(`detailRow_${rowId}`); + if (tr) tr.remove(); + detailUpdateTotals(); + } + + function detailGetNumber(input) { + return Number((input.value || "0").replace(/,/g, "")) || 0; + } + + function detailUpdateTotals() { + const tbody = document.getElementById("detailLinesTbody"); + let debitSum = 0, + creditSum = 0; + tbody.querySelectorAll("tr").forEach((tr) => { + debitSum += detailGetNumber(tr.querySelector(".detail-debit")); + creditSum += detailGetNumber(tr.querySelector(".detail-credit")); + }); + + const fmt = (n) => n.toLocaleString(); + document.getElementById("detailDebitTotal").textContent = fmt(debitSum); + document.getElementById("detailCreditTotal").textContent = + fmt(creditSum); + + const el = document.getElementById("detailBalanceStatus"); + if (debitSum === 0 && creditSum === 0) { + el.textContent = ""; + } else if (debitSum === creditSum) { + el.textContent = "✅ 貸借一致"; + el.style.color = "#28a745"; + } else { + const diff = Math.abs(debitSum - creditSum); + el.textContent = `⚠ 差額 ${diff.toLocaleString()} 円`; + el.style.color = "#dc3545"; + } + } + + function setupDetailAccountSearch(inputElement) { + const container = inputElement.closest(".account-search-container"); + const dropdown = container.querySelector(".account-dropdown"); + + 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; + } + + function appendOpt(acc) { + const opt = document.createElement("div"); + opt.className = "account-option"; + opt.innerHTML = ``; + opt.addEventListener("mousedown", (ev) => { + ev.preventDefault(); + inputElement.value = `${acc.account_code} ${acc.account_name}`; + inputElement.dataset.accountId = acc.account_id; + dropdown.classList.remove("active"); + saveAccountToRecent(acc.account_id); + }); + dropdown.appendChild(opt); + } + + function renderDropdown(list, isFiltered) { + dropdown.innerHTML = ""; + if (list.length === 0) { + dropdown.innerHTML = + '
該当する科目がありません
'; + dropdown.classList.add("active"); + return; + } + if (isFiltered) { + list + .slice() + .sort((a, b) => a.account_code.localeCompare(b.account_code)) + .slice(0, 50) + .forEach(appendOpt); + } else { + const recentIds = JSON.parse( + localStorage.getItem("recentAccounts") || "[]", + ); + const recentItems = recentIds + .map((id) => list.find((a) => a.account_id === id)) + .filter(Boolean); + const recentSet = new Set(recentIds); + const otherItems = list + .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 q = e.target.value.toLowerCase().trim(); + inputElement.dataset.accountId = ""; + const filtered = q + ? accounts.filter( + (a) => + a.account_code.toLowerCase().includes(q) || + a.account_name.toLowerCase().includes(q) || + `${a.account_code.toLowerCase()} ${a.account_name.toLowerCase()}`.includes( + q, + ), + ) + : accounts; + renderDropdown(filtered, !!q); + }); + + inputElement.addEventListener("focus", () => { + const q = inputElement.value.toLowerCase().trim(); + renderDropdown( + q + ? accounts.filter( + (a) => + a.account_code.toLowerCase().includes(q) || + a.account_name.toLowerCase().includes(q) || + `${a.account_code.toLowerCase()} ${a.account_name.toLowerCase()}`.includes( + q, + ), + ) + : accounts, + !!q, + ); + }); + + inputElement.addEventListener("click", () => { + if (!dropdown.classList.contains("active")) { + const q = inputElement.value.toLowerCase().trim(); + renderDropdown( + q + ? accounts.filter( + (a) => + a.account_code.toLowerCase().includes(q) || + a.account_name.toLowerCase().includes(q) || + `${a.account_code.toLowerCase()} ${a.account_name.toLowerCase()}`.includes( + q, + ), + ) + : accounts, + !!q, + ); + } + }); + + document.addEventListener("click", (e) => { + if (!container.contains(e.target)) + dropdown.classList.remove("active"); + }); + } + + // 詳細入力の摘要オートコンプリート + function setupDetailDescriptionSearch() { + const input = document.getElementById("detailDescription"); + const dropdown = document.getElementById("detailDescriptionDropdown"); + if (!input || !dropdown) return; + + let timer = null; + + function makeSectionLabel(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; + } + + function addOpt(text) { + const opt = document.createElement("div"); + opt.className = "account-option"; + opt.style.fontSize = "14px"; + opt.textContent = text; + opt.addEventListener("mousedown", (ev) => { + ev.preventDefault(); + input.value = text; + dropdown.classList.remove("active"); + }); + dropdown.appendChild(opt); + } + + async function doSearch(text) { + const history = JSON.parse( + localStorage.getItem("descriptionHistory") || "[]", + ); + const histFiltered = text + ? history.filter((d) => + d.toLowerCase().includes(text.toLowerCase()), + ) + : history.slice(0, 30); + let apiItems = []; + try { + const qs = new URLSearchParams(); + if (text) qs.append("keyword", text); + qs.append("limit", "50"); + const res = await fetch(`${API}/journal-entries?${qs.toString()}`); + if (res.ok) { + const data = await res.json(); + const entries = data.items ?? data; + const hSet = new Set(histFiltered); + const seen = new Set(); + apiItems = entries + .map((e) => e.description) + .filter((d) => d && !hSet.has(d) && !seen.has(d) && seen.add(d)) + .slice(0, 20); + } + } catch (_) {} + + dropdown.innerHTML = ""; + if (histFiltered.length === 0 && apiItems.length === 0) { + dropdown.classList.remove("active"); + return; + } + if (histFiltered.length > 0) { + dropdown.appendChild(makeSectionLabel("🗒️ 入力履歴")); + histFiltered.slice(0, 20).forEach(addOpt); + } + if (apiItems.length > 0) { + if (histFiltered.length > 0) { + const s = document.createElement("div"); + s.style.cssText = "border-top:1px solid #e8e8e8;margin:2px 0;"; + dropdown.appendChild(s); + } + dropdown.appendChild(makeSectionLabel("📝 過去の仕訳から")); + apiItems.forEach(addOpt); + } + dropdown.classList.add("active"); + } + + input.addEventListener("input", () => { + clearTimeout(timer); + timer = setTimeout(() => doSearch(input.value.trim()), 250); + }); + input.addEventListener("focus", () => doSearch(input.value.trim())); + input.addEventListener("click", () => { + if (!dropdown.classList.contains("active")) + doSearch(input.value.trim()); + }); + input.addEventListener("keydown", (e) => { + if (e.key === "Enter" || e.key === "Tab") + dropdown.classList.remove("active"); + }); + document.addEventListener("click", (e) => { + if (!input.parentElement.contains(e.target)) + dropdown.classList.remove("active"); + }); + } + + async function submitDetailJournal() { + const dateInput = document.getElementById("detailEntryDate"); + const entryDate = dateInput.value; + const description = document + .getElementById("detailDescription") + .value.trim(); + + if (dateInput.validity.badInput) { + alert( + "日付が正しくありません。存在しない日付が入力されています(例:11月31日は無効です)", + ); + return; + } + if (!entryDate) { + alert("仕訳日を入力してください"); + return; + } + if (!description) { + alert("摘要を入力してください"); + return; + } + + const lines = []; + let debitSum = 0, + creditSum = 0; + let valid = true; + + document.querySelectorAll("#detailLinesTbody tr").forEach((tr) => { + const accountInput = tr.querySelector(".detail-account-input"); + const accountId = Number(accountInput?.dataset.accountId || 0); + const debit = detailGetNumber(tr.querySelector(".detail-debit")); + const credit = detailGetNumber(tr.querySelector(".detail-credit")); + + if (debit === 0 && credit === 0) return; // 空行スキップ + + if (!accountId) { + alert("科目が選択されていない行があります"); + valid = false; + return; + } + if (debit > 0 && credit > 0) { + alert("1行に借方と貸方の両方を入力することはできません"); + valid = false; + return; + } + + const lineDesc = + tr.querySelector(".detail-line-memo")?.value.trim() || null; + lines.push({ + account_id: accountId, + debit, + credit, + line_description: lineDesc || undefined, + }); + debitSum += debit; + creditSum += credit; + }); + + if (!valid) return; + if (lines.length < 2) { + alert("仕訳明細は2行以上必要です"); + return; + } + if (debitSum !== creditSum) { + alert( + `貸借が一致していません(差額:${Math.abs(debitSum - creditSum).toLocaleString()} 円)`, + ); + return; + } + + const payload = { + entry_date: entryDate, + description, + lines, + tax_paid_account_id: + taxPaidAccounts.length > 0 ? taxPaidAccounts[0].account_id : null, + tax_received_account_id: + taxReceivedAccounts.length > 0 + ? taxReceivedAccounts[0].account_id + : null, + rounding_mode: "floor", + }; + + try { + const res = await fetch(`${API}/journal-entries`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (!res.ok) { + const err = await res.json(); + throw new Error(err.detail || "登録失敗"); + } + + saveDescriptionToHistory(description); + alert("仕訳を登録しました"); + + const keep = document.getElementById("detailKeepAfterSubmit").checked; + if (!keep) { + detailClearAll(); + } + // keep=true の場合は仕訳日・摘要・明細・金額すべてそのまま保持 + + searchJournals(); + } catch (e) { + alert("登録エラー: " + e.message); + } + } + + function detailClearAll() { + document.getElementById("detailLinesTbody").innerHTML = ""; + document.getElementById("detailDescription").value = ""; + document.getElementById("detailEntryDate").value = new Date() + .toISOString() + .slice(0, 10); + detailRowSeq = 0; + detailUpdateTotals(); + // デフォルト2行追加 + detailAddLine(); + detailAddLine(); + } + async function submitJournal() { try { const entryDateInput = document.getElementById("entryDate"); @@ -1216,7 +2043,8 @@ taxReceivedAccounts.length > 0 ? taxReceivedAccounts[0].account_id : null, - rounding_mode: "floor", + rounding_mode: + document.getElementById("entryRoundingMode")?.value || "floor", }; console.log("送信するデータ:", JSON.stringify(payload, null, 2)); diff --git a/frontend/journal-view.html b/frontend/journal-view.html index 3d530c5..7d32046 100644 --- a/frontend/journal-view.html +++ b/frontend/journal-view.html @@ -44,10 +44,11 @@ - + + @@ -123,6 +124,7 @@ + `; tbody.appendChild(tr); @@ -137,6 +139,7 @@ + `; tbody.appendChild(totalRow); }
科目コード科目コード 科目名 借方 貸方摘要(行)
${ line.credit > 0 ? line.credit.toLocaleString() : "" }${line.line_description || ""}合計 ${totalDebit.toLocaleString()} ${totalCredit.toLocaleString()}