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 @@
-
-
-修正仕訳入力
-
-
-
+
+
+ 修正仕訳入力
+
+
+
+ 修正仕訳入力
-修正仕訳入力
+
+
-
-
+
+
-
-
+
+
+
+ | 科目 |
+ 借方 |
+ 貸方 |
+ 操作 |
+
+
+
+
+
+ | 合計 |
+ 0 |
+ 0 |
+ |
+
+
+
-
-
-
- | 科目 |
- 借方 |
- 貸方 |
- 操作 |
-
-
-
-
-
- | 合計 |
- 0 |
- 0 |
- |
-
-
-
+
+
+
+
+
-
-
+
-
+
+ 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