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 = `
-
-
- |
- |
- |
-
- |
+ |
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+ ${
+ 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 += ``;
}
@@ -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();
});