diff --git a/backend/app/main.py b/backend/app/main.py index 658b854..553727d 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -16,8 +16,8 @@ app = FastAPI( app.add_middleware( CORSMiddleware, - allow_origins=["*"], # 本地开发先用 * - allow_credentials=True, + allow_origins=["*"], + allow_credentials=False, allow_methods=["*"], allow_headers=["*"], ) diff --git a/backend/app/modules/trial_balance/service.py b/backend/app/modules/trial_balance/service.py index 83bd5e7..5848311 100644 --- a/backend/app/modules/trial_balance/service.py +++ b/backend/app/modules/trial_balance/service.py @@ -91,6 +91,11 @@ def fetch_trial_balance(date_from: str, date_to: str): credit = D(row["credit"]) closing = opening + debit - credit + + # 負債・資本は絶対値で表示(見やすくするため) + if acc["account_type"] in ("liability", "equity"): + opening = abs(opening) + closing = abs(closing) account_data[code] = { "account_id": aid, diff --git a/backend/app/routers/journal_entries.py b/backend/app/routers/journal_entries.py index 9e814ef..dcf21c8 100644 --- a/backend/app/routers/journal_entries.py +++ b/backend/app/routers/journal_entries.py @@ -38,7 +38,8 @@ class JournalEntryDeleteRequest(BaseModel): def get_journal_entries( from_date: date = None, to_date: date = None, - keyword: str = None + keyword: str = None, + account_id: int = None ): """ 仕訳一覧を検索して取得 @@ -73,6 +74,14 @@ def get_journal_entries( sql += " AND je.description LIKE %s" params.append(f"%{keyword}%") + if account_id: + sql += """ AND je.journal_entry_id IN ( + SELECT DISTINCT journal_entry_id + FROM journal_lines + WHERE account_id = %s + )""" + params.append(account_id) + sql += """ GROUP BY je.journal_entry_id, je.entry_date, je.description, je.fiscal_year ORDER BY je.entry_date DESC, je.journal_entry_id DESC @@ -116,7 +125,9 @@ def get_journal_entry(journal_id: int): a.account_code, a.account_name, jl.debit, - jl.credit + jl.credit, + jl.tax_rate, + jl.tax_direction FROM journal_lines jl JOIN accounts a ON jl.account_id = a.account_id WHERE jl.journal_entry_id = %s @@ -301,19 +312,35 @@ def create_journal_entry(req: JournalEntryRequest): # 明細(税行を含む) for line in all_lines: + # tax_directionの値を変換: paid->INPUT, received->OUTPUT, none->NONE + tax_dir_db = None + if line.tax_direction: + if line.tax_direction == 'paid': + tax_dir_db = 'INPUT' + elif line.tax_direction == 'received': + tax_dir_db = 'OUTPUT' + elif line.tax_direction == 'none': + tax_dir_db = 'NONE' + else: + tax_dir_db = line.tax_direction.upper() + cur.execute(""" INSERT INTO journal_lines ( journal_entry_id, account_id, debit, - credit + credit, + tax_rate, + tax_direction ) - VALUES (%s, %s, %s, %s) + VALUES (%s, %s, %s, %s, %s, %s) """, ( entry_id, line.account_id, line.debit, - line.credit + line.credit, + line.tax_rate, + tax_dir_db )) conn.commit() diff --git a/frontend/journal-entry.html b/frontend/journal-entry.html index 5d31e44..bcbec3d 100644 --- a/frontend/journal-entry.html +++ b/frontend/journal-entry.html @@ -177,7 +177,12 @@ - + + + + @@ -200,6 +205,17 @@ const API = "http://127.0.0.1:18080"; + // 千分位カンマフォーマット関数 + function formatNumberInput(input) { + let value = input.value.replace(/,/g, ""); // 既存のカンマを削除 + if (value === "" || isNaN(value)) { + input.value = ""; + return; + } + // 数値をカンマ区切りに変換 + input.value = Number(value).toLocaleString(); + } + let accounts = []; const ACCOUNT_GROUPS = [ @@ -269,6 +285,7 @@ ); setupTaxSelectors(); + setupSearchAccountSelector(); addRow(); document.getElementById("entryDate").value = new Date() @@ -283,6 +300,32 @@ init(); + // ----------------------------- + // 検索用科目セレクタ + // ----------------------------- + function setupSearchAccountSelector() { + const select = document.getElementById("searchAccountId"); + select.innerHTML = ''; + + ACCOUNT_GROUPS.forEach((group) => { + const optgroup = document.createElement("optgroup"); + optgroup.label = group.label; + + 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); + }); + + select.appendChild(optgroup); + }); + } + // ----------------------------- // 摘要履歴管理 // ----------------------------- @@ -358,35 +401,35 @@ } tr.innerHTML = ` - - - - - - - + ${buildAccountOptions()} - - - - - ${ - isTaxRow ? "" : `` - } - `; + + + + + + + + + + ${ + isTaxRow ? "" : `` + } + `; tbody.appendChild(tr); @@ -436,8 +479,12 @@ const taxType = row.querySelector(".taxType").value; const taxDir = row.querySelector(".taxDirection").value; - const debit = Number(row.querySelector(".debit").value || 0); - const credit = Number(row.querySelector(".credit").value || 0); + const debit = Number( + row.querySelector(".debit").value.replace(/,/g, "") || 0 + ); + const credit = Number( + row.querySelector(".credit").value.replace(/,/g, "") || 0 + ); if (taxType === "none" || taxDir === "none") return; @@ -503,86 +550,145 @@ // 登録(税行自動生成版) // ----------------------------- async function submitJournal() { - const entryDate = document.getElementById("entryDate").value; - const description = document.getElementById("description").value.trim(); + try { + const entryDate = document.getElementById("entryDate").value; + const description = document + .getElementById("description") + .value.trim(); - if (!entryDate) { - alert("仕訳日を入力してください"); - return; - } - - // 明細収集(税情報も含む) - const rows = document.querySelectorAll("#linesTable tbody tr"); - const lines = []; - - rows.forEach((row) => { - if (row.classList.contains("tax-row")) return; // 税行は無視 - - const accountId = Number(row.querySelector(".account").value); - const debit = Number(row.querySelector(".debit").value || 0); - const credit = Number(row.querySelector(".credit").value || 0); - const taxType = row.querySelector(".taxType").value; - const taxDirection = row.querySelector(".taxDirection").value; - - if (debit > 0 && credit > 0) { - alert("同一行不能同时填写借方和贷方"); - throw new Error("invalid line"); + if (!entryDate) { + alert("仕訳日を入力してください"); + return; } - if (debit === 0 && credit === 0) return; - const lineData = { - account_id: accountId, - debit: debit, - credit: credit, + // 明細収集(税情報も含む) + const rows = document.querySelectorAll("#linesTable tbody tr"); + const lines = []; + + rows.forEach((row) => { + if (row.classList.contains("tax-row")) return; // 税行は無視 + + const accountId = Number(row.querySelector(".account").value); + const debit = Number( + row.querySelector(".debit").value.replace(/,/g, "") || 0 + ); + const credit = Number( + row.querySelector(".credit").value.replace(/,/g, "") || 0 + ); + const taxType = row.querySelector(".taxType").value; + const taxDirection = row.querySelector(".taxDirection").value; + + if (debit > 0 && credit > 0) { + alert("同一行不能同时填写借方和贷方"); + throw new Error("invalid line"); + } + if (debit === 0 && credit === 0) return; + + const lineData = { + account_id: accountId, + debit: debit, + credit: credit, + }; + + // 税情報を追加 + if (taxType !== "none" && taxDirection !== "none") { + lineData.tax_rate = parseInt(taxType); + lineData.tax_direction = taxDirection; + } + + lines.push(lineData); + }); + + if (lines.length === 0) { + alert("仕訳明細がありません"); + return; + } + + // 税科目(仮払消費税等・仮受消費税等)が存在するかチェック + const hasTaxAccount = lines.some((line) => { + const account = accounts.find( + (a) => a.account_id === line.account_id + ); + return ( + account && + (account.account_name.includes("仮払消費税") || + account.account_name.includes("仮受消費税")) + ); + }); + + // 税科目が存在し、かつ税情報を持つ行がある場合は警告 + const hasTaxInfo = lines.some( + (line) => line.tax_rate != null && line.tax_direction != null + ); + + if (hasTaxAccount && hasTaxInfo) { + if ( + !confirm( + "警告:仮払消費税等・仮受消費税等の行がすでに存在していますが、他の行に税区分・税方向が設定されています。\n" + + "このまま登録すると税が二重計算され、借貸不平衡になります。\n\n" + + "税区分・税方向をすべて「対象外」に設定してから登録することをお勧めします。\n\n" + + "このまま登録しますか?" + ) + ) { + return; + } + } + + const payload = { + entry_date: entryDate, + description, + lines, + tax_paid_account_id: + Number(document.getElementById("taxPaidSelect").value) || null, + tax_received_account_id: + Number(document.getElementById("taxReceivedSelect").value) || + null, + rounding_mode: document.getElementById("roundingMode").value, }; - // 税情報を追加 - if (taxType !== "none" && taxDirection !== "none") { - lineData.tax_rate = parseInt(taxType); - lineData.tax_direction = taxDirection; + console.log("送信するデータ:", JSON.stringify(payload, null, 2)); + console.log("明細行数:", lines.length); + lines.forEach((line, index) => { + console.log(`行${index + 1}:`, line); + }); + + const res = await fetch(`${API}/journal-entries`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + if (!res.ok) { + const errorData = await res.json().catch(() => ({})); + const errorMsg = + errorData.detail || `登録に失敗しました (HTTP ${res.status})`; + console.error("Error:", errorData); + alert(errorMsg); + return; } - lines.push(lineData); - }); + const result = await res.json(); - if (lines.length === 0) { - alert("仕訳明細がありません"); - return; + // 摘要を履歴に保存 + saveDescriptionToHistory(description); + + alert(`登録完了(ID=${result.journal_entry_id})`); + + // フォームをクリア + document.getElementById("description").value = ""; + const tbody = document.querySelector("#linesTable tbody"); + tbody.innerHTML = ""; + rowSeq = 0; + addRow(); + + // 仕訳検索を自動実行して最新の登録状況を表示 + searchJournals(); + } catch (error) { + console.error("送信エラー:", error); + alert( + `エラーが発生しました: ${error.message}\n\nサーバーが起動しているか確認してください。` + ); } - - const payload = { - entry_date: entryDate, - description, - lines, - tax_paid_account_id: - Number(document.getElementById("taxPaidSelect").value) || null, - tax_received_account_id: - Number(document.getElementById("taxReceivedSelect").value) || null, - rounding_mode: document.getElementById("roundingMode").value, - }; - - const res = await fetch(`${API}/journal-entries`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - - if (!res.ok) { - const errorData = await res.json().catch(() => ({})); - const errorMsg = - errorData.detail || `登録に失敗しました (HTTP ${res.status})`; - console.error("Error:", errorData); - alert(errorMsg); - return; - } - - const result = await res.json(); - - // 摘要を履歴に保存 - saveDescriptionToHistory(description); - - alert(`登録完了(ID=${result.journal_entry_id})`); - location.reload(); } const API_BASE = "http://127.0.0.1:18080"; @@ -731,8 +837,8 @@ .filter((a) => group.codes.includes(a.account_code)) .forEach((a) => { html += ``; + ${a.account_code} ${a.account_name} + `; }); html += ``; }); @@ -747,8 +853,8 @@ html += ``; others.forEach((a) => { html += ``; + ${a.account_code} ${a.account_name} + `; }); html += ``; } @@ -763,6 +869,7 @@ const from = document.getElementById("searchFromDate").value; const to = document.getElementById("searchToDate").value; const key = document.getElementById("searchKeyword").value; + const accountId = document.getElementById("searchAccountId").value; // 検索条件を保存 localStorage.setItem( @@ -771,6 +878,7 @@ fromDate: from, toDate: to, keyword: key, + accountId: accountId, }) ); @@ -778,6 +886,7 @@ if (from) qs.append("from_date", from); if (to) qs.append("to_date", to); if (key) qs.append("keyword", key); + if (accountId) qs.append("account_id", accountId); const res = await fetch(`${API}/journal-entries?${qs.toString()}`); const data = await res.json(); @@ -788,21 +897,25 @@ data.forEach((e) => { const tr = document.createElement("tr"); tr.innerHTML = ` - ${e.entry_date} - ${e.description} - ${e.debit_total.toLocaleString()} - ${e.credit_total.toLocaleString()} - - - - - - - `; + ${e.entry_date} + ${e.description} + ${e.debit_total.toLocaleString()} + ${e.credit_total.toLocaleString()} + + + + + + + `; tbody.appendChild(tr); }); } @@ -824,10 +937,8 @@ const data = await res.json(); - // 日付は今日の日付をデフォルトに(必要に応じて変更可能) - document.getElementById("entryDate").value = new Date() - .toISOString() - .slice(0, 10); + // 日付をコピー元の仕訳日に設定 + document.getElementById("entryDate").value = data.entry_date; // 摘要をコピー document.getElementById("description").value = data.description || ""; @@ -837,7 +948,8 @@ tbody.innerHTML = ""; rowSeq = 0; - // 仕訳明細をコピー + // 仕訳明細をすべてコピー(税行も含む) + // 税の自動計算機能は実行しない(税区分・税方向を「対象外」に設定) if (data.lines && data.lines.length > 0) { data.lines.forEach((line) => { const tr = addRow(); @@ -848,26 +960,25 @@ accountSelect.value = line.account_id; } - // 借方・貸方金額を設定 + // 借方・貸方金額を設定(カンマフォーマット付き) const debitInput = tr.querySelector(".debit"); const creditInput = tr.querySelector(".credit"); if (debitInput && line.debit) { - debitInput.value = line.debit; + debitInput.value = Number(line.debit).toLocaleString(); } if (creditInput && line.credit) { - creditInput.value = line.credit; + creditInput.value = Number(line.credit).toLocaleString(); } - // 税区分を設定 + // 税区分・税方向は「対象外」に設定(自動計算を防ぐため) const taxTypeSelect = tr.querySelector(".taxType"); const taxDirSelect = tr.querySelector(".taxDirection"); - if (line.tax_rate && taxTypeSelect) { - taxTypeSelect.value = String(line.tax_rate); + if (taxTypeSelect) { + taxTypeSelect.value = "none"; // 対象外 } - - if (line.tax_direction && taxDirSelect) { - taxDirSelect.value = line.tax_direction; + if (taxDirSelect) { + taxDirSelect.value = "none"; // 対象外 } }); } @@ -875,8 +986,12 @@ // ページ上部の仕訳入力フォームにスクロール window.scrollTo({ top: 0, behavior: "smooth" }); + console.log("コピーされたデータ:", data); // デバッグ用 + console.log( + `${data.lines.length}行をコピーしました。税区分・税方向はすべて「対象外」に設定されています。` + ); alert( - "仕訳を複製しました。日付や金額などを修正して登録してください。" + `仕訳を複製しました(${data.lines.length}行)。\n税の自動計算を防ぐため、すべての行の税区分・税方向を「対象外」に設定しています。\n必要に応じて修正してから登録してください。` ); } catch (error) { console.error("Error:", error); @@ -962,7 +1077,7 @@ searchJournals(); // リストを再読み込み } - // ページロード後に前回の検索条件を復元 + // ページロード後に検索条件を設定 window.addEventListener("DOMContentLoaded", () => { // デフォルト期間を設定(5/31決算:6月1日~現在) const today = new Date(); @@ -981,29 +1096,23 @@ const defaultFromDate = `${fiscalStartYear}-06-01`; const defaultToDate = today.toISOString().slice(0, 10); + // 常にデフォルト期間を設定(会計年度に合わせるため) + document.getElementById("searchFromDate").value = defaultFromDate; + document.getElementById("searchToDate").value = defaultToDate; + + // 摘要と科目のみ前回の検索条件を復元 const savedConditions = localStorage.getItem("journalSearchConditions"); if (savedConditions) { const conditions = JSON.parse(savedConditions); - if (conditions.fromDate) - document.getElementById("searchFromDate").value = - conditions.fromDate; - else - document.getElementById("searchFromDate").value = defaultFromDate; - - if (conditions.toDate) - document.getElementById("searchToDate").value = conditions.toDate; - else document.getElementById("searchToDate").value = defaultToDate; - if (conditions.keyword) document.getElementById("searchKeyword").value = conditions.keyword; - - // 自動的に検索を実行 - searchJournals(); - } else { - // 保存された条件がない場合はデフォルト値を設定 - document.getElementById("searchFromDate").value = defaultFromDate; - document.getElementById("searchToDate").value = defaultToDate; + if (conditions.accountId) + document.getElementById("searchAccountId").value = + conditions.accountId; } + + // ページロード時に自動的に検索を実行して最新データを表示 + searchJournals(); }); diff --git a/frontend/journal-view.html b/frontend/journal-view.html index ea5d224..d4c9608 100644 --- a/frontend/journal-view.html +++ b/frontend/journal-view.html @@ -128,7 +128,7 @@ } function goBack() { - window.location.href = "journal-list.html"; + window.location.href = "journal-entry.html"; } loadJournalEntry(); diff --git a/frontend/trial-balance.html b/frontend/trial-balance.html index b6f403f..8897817 100644 --- a/frontend/trial-balance.html +++ b/frontend/trial-balance.html @@ -6,12 +6,14 @@