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 = `${acc.account_code}${acc.account_name}`;
+ 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 @@
${
line.credit > 0 ? line.credit.toLocaleString() : ""
} |
+ ${line.line_description || ""} |
`;
tbody.appendChild(tr);
@@ -137,6 +139,7 @@
合計 |
${totalDebit.toLocaleString()} |
${totalCredit.toLocaleString()} |
+ |
`;
tbody.appendChild(totalRow);
}