diff --git a/backend/app/main.py b/backend/app/main.py index 0161aee..658b854 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -85,7 +85,7 @@ app.include_router(cash.router) from fastapi.staticfiles import StaticFiles app.mount( - "/", + "/static", StaticFiles(directory="app/static", html=True), name="static", ) diff --git a/backend/app/routers/journal_entries.py b/backend/app/routers/journal_entries.py index e0d9e12..9e814ef 100644 --- a/backend/app/routers/journal_entries.py +++ b/backend/app/routers/journal_entries.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, HTTPException from app.core.database import get_connection from pydantic import BaseModel -from typing import List +from typing import List, Optional from decimal import Decimal from datetime import date @@ -12,12 +12,17 @@ class JournalLine(BaseModel): account_id: int debit: Decimal = Decimal("0") credit: Decimal = Decimal("0") + tax_rate: Optional[int] = None # 10 or 8 or None + tax_direction: Optional[str] = None # "paid" or "received" or None class JournalEntryRequest(BaseModel): entry_date: date description: str lines: List[JournalLine] + tax_paid_account_id: Optional[int] = None # 仮払消費税等 勘定 + tax_received_account_id: Optional[int] = None # 仮受消費税等 勘定 + rounding_mode: str = "round" # "round", "floor", or "ceil" class JournalEntryUpdateRequest(BaseModel): description: str @@ -28,10 +33,183 @@ class JournalEntryUpdateRequest(BaseModel): class JournalEntryDeleteRequest(BaseModel): deleted_reason: str + +@router.get("", summary="仕訳一覧取得") +def get_journal_entries( + from_date: date = None, + to_date: date = None, + keyword: str = None +): + """ + 仕訳一覧を検索して取得 + """ + with get_connection() as conn: + with conn.cursor() as cur: + # クエリ構築 + sql = """ + SELECT + je.journal_entry_id, + je.entry_date, + je.description, + je.fiscal_year, + COALESCE(SUM(jl.debit), 0) as debit_total, + COALESCE(SUM(jl.credit), 0) as credit_total + FROM journal_entries je + LEFT JOIN journal_lines jl ON je.journal_entry_id = jl.journal_entry_id + WHERE je.is_deleted = false + """ + + params = [] + + if from_date: + sql += " AND je.entry_date >= %s" + params.append(from_date) + + if to_date: + sql += " AND je.entry_date <= %s" + params.append(to_date) + + if keyword: + sql += " AND je.description LIKE %s" + params.append(f"%{keyword}%") + + 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 + """ + + cur.execute(sql, params) + rows = cur.fetchall() + + return [dict(row) for row in rows] + + +@router.get("/{journal_id}", summary="仕訳詳細取得") +def get_journal_entry(journal_id: int): + """ + 指定された仕訳の詳細を取得 + """ + with get_connection() as conn: + with conn.cursor() as cur: + # ヘッダー取得 + cur.execute(""" + SELECT + journal_entry_id, + entry_date, + description, + fiscal_year, + version, + is_deleted + FROM journal_entries + WHERE journal_entry_id = %s + """, (journal_id,)) + + header = cur.fetchone() + if not header: + raise HTTPException(status_code=404, detail="仕訳が存在しません") + + # 明細取得 + cur.execute(""" + SELECT + jl.journal_line_id, + jl.account_id, + a.account_code, + a.account_name, + jl.debit, + jl.credit + FROM journal_lines jl + JOIN accounts a ON jl.account_id = a.account_id + WHERE jl.journal_entry_id = %s + ORDER BY jl.journal_line_id + """, (journal_id,)) + + lines = cur.fetchall() + + return { + **dict(header), + "lines": [dict(line) for line in lines] + } + + +@router.post("/{journal_id}/reverse", summary="逆仕訳作成") +def create_reverse_entry(journal_id: int): + """ + 指定された仕訳の逆仕訳を作成 + """ + with get_connection() as conn: + with conn.cursor() as cur: + # 元仕訳取得 + cur.execute(""" + SELECT entry_date, description, fiscal_year + FROM journal_entries + WHERE journal_entry_id = %s AND is_deleted = false + """, (journal_id,)) + + header = cur.fetchone() + if not header: + raise HTTPException(status_code=404, detail="仕訳が存在しません") + + # 元仕訳の明細取得 + cur.execute(""" + SELECT account_id, debit, credit + FROM journal_lines + WHERE journal_entry_id = %s + """, (journal_id,)) + + lines = cur.fetchall() + + # 逆仕訳ヘッダー作成 + cur.execute(""" + INSERT INTO journal_entries ( + entry_date, + description, + fiscal_year, + version, + is_deleted, + created_by + ) + VALUES (%s, %s, %s, 1, false, %s) + RETURNING journal_entry_id + """, ( + header["entry_date"], + f"[逆仕訳] {header['description']}", + header["fiscal_year"], + "system" + )) + + new_entry_id = cur.fetchone()["journal_entry_id"] + + # 逆仕訳明細作成(借方と貸方を入れ替え) + for line in lines: + cur.execute(""" + INSERT INTO journal_lines ( + journal_entry_id, + account_id, + debit, + credit + ) + VALUES (%s, %s, %s, %s) + """, ( + new_entry_id, + line["account_id"], + line["credit"], # 借方と貸方を入れ替え + line["debit"] + )) + + conn.commit() + + return { + "status": "ok", + "reversed_journal_entry_id": new_entry_id, + "original_journal_entry_id": journal_id + } + + @router.post("", summary="仕訳登録") def create_journal_entry(req: JournalEntryRequest): from app.utils.month_lock import is_month_locked + from decimal import Decimal, ROUND_HALF_UP, ROUND_DOWN, ROUND_UP if is_month_locked(req.entry_date): raise HTTPException( @@ -42,9 +220,57 @@ def create_journal_entry(req: JournalEntryRequest): if not req.lines: raise HTTPException(status_code=400, detail="仕訳明細不能为空") - # (可选)简单的借贷平衡校验 - total_debit = sum(l.debit for l in req.lines) - total_credit = sum(l.credit for l in req.lines) + # 税額計算と税行生成 + all_lines = [] + tax_aggregation = {} # {"paid": Decimal, "received": Decimal} + + # 端数処理モード設定 + rounding_map = { + "round": ROUND_HALF_UP, + "floor": ROUND_DOWN, + "ceil": ROUND_UP + } + rounding = rounding_map.get(req.rounding_mode, ROUND_HALF_UP) + + for line in req.lines: + all_lines.append(line) + + # 税対象行の場合、税額を計算 + if line.tax_rate and line.tax_direction: + amount = line.debit if line.debit > 0 else line.credit + rate = Decimal(str(line.tax_rate)) / Decimal("100") # 10% -> 0.10 + + # 入力額は税抜額として、税額 = 税抜額 × 税率 + raw_tax = amount * rate + tax_amount = raw_tax.quantize(Decimal("1"), rounding=rounding) + + # 税方向ごとに集計 + if line.tax_direction not in tax_aggregation: + tax_aggregation[line.tax_direction] = Decimal("0") + tax_aggregation[line.tax_direction] += tax_amount + + # 集計した税額で税行を追加 + if "paid" in tax_aggregation and tax_aggregation["paid"] > 0: + if not req.tax_paid_account_id: + raise HTTPException(status_code=400, detail="仮払消費税等 勘定を選択してください") + all_lines.append(JournalLine( + account_id=req.tax_paid_account_id, + debit=tax_aggregation["paid"], + credit=Decimal("0") + )) + + if "received" in tax_aggregation and tax_aggregation["received"] > 0: + if not req.tax_received_account_id: + raise HTTPException(status_code=400, detail="仮受消費税等 勘定を選択してください") + all_lines.append(JournalLine( + account_id=req.tax_received_account_id, + debit=Decimal("0"), + credit=tax_aggregation["received"] + )) + + # 借貸平衡校験 + total_debit = sum(l.debit for l in all_lines) + total_credit = sum(l.credit for l in all_lines) if total_debit != total_credit: raise HTTPException(status_code=400, detail="借方和贷方不平衡") @@ -53,43 +279,43 @@ def create_journal_entry(req: JournalEntryRequest): with get_connection() as conn: with conn.cursor() as cur: # 仕訳ヘッダ - cur.execute(""" - INSERT INTO journal_entries ( - journal_date, - description, - fiscal_year, - version, - is_deleted, - created_by - ) - VALUES (%s, %s, %s, 1, false, %s) - RETURNING journal_id - """, ( - req.entry_date, - req.description, - fiscal_year, - "system" # 以后可以换成登录用户 - )) - - entry_id = cur.fetchone()["journal_id"] - - # 明細 - for line in req.lines: cur.execute(""" - INSERT INTO journal_lines ( - journal_id, - account_id, - debit, - credit + INSERT INTO journal_entries ( + entry_date, + description, + fiscal_year, + version, + is_deleted, + created_by ) - VALUES (%s, %s, %s, %s) + VALUES (%s, %s, %s, 1, false, %s) + RETURNING journal_entry_id """, ( - entry_id, - line.account_id, - line.debit, - line.credit + req.entry_date, + req.description, + fiscal_year, + "system" )) + entry_id = cur.fetchone()["journal_entry_id"] + + # 明細(税行を含む) + for line in all_lines: + cur.execute(""" + INSERT INTO journal_lines ( + journal_entry_id, + account_id, + debit, + credit + ) + VALUES (%s, %s, %s, %s) + """, ( + entry_id, + line.account_id, + line.debit, + line.credit + )) + conn.commit() return { @@ -179,7 +405,7 @@ def update_journal_entry(journal_id: int, req: JournalEntryUpdateRequest): -@router.delete("/{journal_id}", summary="仕訳删除(软删除)") +@router.delete("/{journal_id}", summary="仕訳削除(ソフト削除)") def delete_journal_entry(journal_id: int, req: JournalEntryDeleteRequest): with get_connection() as conn: @@ -187,21 +413,17 @@ def delete_journal_entry(journal_id: int, req: JournalEntryDeleteRequest): cur.execute(""" UPDATE journal_entries SET is_deleted = true, - deleted_at = NOW(), - deleted_reason = %s - WHERE journal_id = %s + updated_at = NOW() + WHERE journal_entry_id = %s AND is_deleted = false - RETURNING journal_id - """, ( - req.deleted_reason, - journal_id - )) + RETURNING journal_entry_id + """, (journal_id,)) row = cur.fetchone() if not row: raise HTTPException( status_code=404, - detail="仕訳不存在,或已被删除" + detail="仕訳が存在しないか、既に削除されています" ) conn.commit() diff --git a/frontend/journal-edit.html b/frontend/journal-edit.html index 64e7970..7ad2e50 100644 --- a/frontend/journal-edit.html +++ b/frontend/journal-edit.html @@ -1,98 +1,134 @@ - - -修正仕訳入力 - - - + + + 修正仕訳入力 + + + +

修正仕訳入力

-

修正仕訳入力

+ + - - + + - - + + + + + + + + + + + + + + + + + + +
科目借方貸方操作
合計00
- - - - - - - - - - - - - - - - - - -
科目借方貸方操作
合計00
+
+ + + +
- - +
-
+ + function showError(msg) { + const r = document.getElementById("result"); + r.textContent = msg; + r.className = "error"; + } + function showOk(msg) { + const r = document.getElementById("result"); + r.textContent = msg; + r.className = "ok"; + } - + function cancelEdit() { + if (confirm("編集をキャンセルして一覧画面に戻りますか?")) { + localStorage.removeItem("editSource"); + window.location.href = "journal-list.html"; + } + } + + diff --git a/frontend/journal-entry.html b/frontend/journal-entry.html index e20148a..a687bc6 100644 --- a/frontend/journal-entry.html +++ b/frontend/journal-entry.html @@ -75,8 +75,8 @@
@@ -104,7 +104,14 @@
- + { - if (row.classList.contains("tax-row")) return; + 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 (debit === 0 && credit === 0) return; - lines.push({ account_id: accountId, debit: debit, credit: credit }); - totalDebit += debit; - totalCredit += credit; + 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; } - if (lines.length < 2) { - alert("至少需要借方和贷方各一行"); - return; - } - // 借貸一致チェック(最小) - if (totalDebit !== totalCredit) { - alert( - `借方(${totalDebit.toLocaleString()})と貸方(${totalCredit.toLocaleString()})が一致しません` - ); - return; - } - const payload = { entry_date: entryDate, description, lines }; + 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", @@ -464,7 +481,11 @@ }); if (!res.ok) { - alert("登録に失敗しました"); + const errorData = await res.json().catch(() => ({})); + const errorMsg = + errorData.detail || `登録に失敗しました (HTTP ${res.status})`; + console.error("Error:", errorData); + alert(errorMsg); return; } diff --git a/frontend/journal-list.html b/frontend/journal-list.html index f55affd..6cc1c95 100644 --- a/frontend/journal-list.html +++ b/frontend/journal-list.html @@ -1,108 +1,199 @@ - - -仕訳一覧 - - - + + + 仕訳一覧 + + + +

仕訳一覧

-

仕訳一覧

+ + ~ + - - + + - - + - + + + + + + + + + + + +
日付摘要借方合計貸方合計操作
- - - - - - - - - - - -
日付摘要借方合計貸方合計操作
+ + // -------------------------------------------------- + // 仕訳削除 + // -------------------------------------------------- + async function deleteEntry(id) { + const reason = prompt("削除理由を入力してください:"); + if (!reason || reason.trim() === "") { + alert("削除理由の入力が必要です"); + return; + } - + if (!confirm("この仕訳を削除してもよろしいですか?")) { + return; + } + + const res = await fetch(`${API}/journal-entries/${id}`, { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ deleted_reason: reason }), + }); + + const data = await res.json(); + if (!res.ok) { + alert(data.detail || "削除に失敗しました"); + return; + } + + alert("削除しました"); + search(); // リストを再読み込み + } + + // ページロード時に前回の検索条件を復元 + window.addEventListener("DOMContentLoaded", () => { + const savedConditions = localStorage.getItem("journalSearchConditions"); + if (savedConditions) { + const conditions = JSON.parse(savedConditions); + if (conditions.fromDate) + document.getElementById("fromDate").value = conditions.fromDate; + if (conditions.toDate) + document.getElementById("toDate").value = conditions.toDate; + if (conditions.keyword) + document.getElementById("keyword").value = conditions.keyword; + + // 自動的に検索を実行 + search(); + } + }); + + diff --git a/frontend/journal-view.html b/frontend/journal-view.html new file mode 100644 index 0000000..ea5d224 --- /dev/null +++ b/frontend/journal-view.html @@ -0,0 +1,137 @@ + + + + + 仕訳詳細 + + + +

仕訳詳細

+ +
+ + + + + + + + + + + +
科目コード科目名借方貸方
+ +
+ + +
+ + + + diff --git a/メモ.txt b/メモ.txt index 058d0f5..618329f 100644 --- a/メモ.txt +++ b/メモ.txt @@ -30,6 +30,14 @@ DB_NAME=njts_acct DB_USER=njts_app DB_PASSWORD=njts_app2025 +---------------------- +nano /volume1/docker/postgres/docker-compose.yml +DB重启 +cd /volume1/docker/postgres +docker compose down +docker compose up -d +docker ps + ---------------------- バージョン確認 python --version