desktop修正

This commit is contained in:
admin
2025-12-14 23:54:48 +09:00
parent f9f5eec35e
commit 74dcb88fe6
25 changed files with 1068 additions and 157 deletions

12
.gitignore vendored
View File

@@ -26,3 +26,15 @@ Thumbs.db
docker-data/
*.bak
*.tmp
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
# Virtual environments
.venv/
venv/
env/
backend/.venv/

View File

@@ -0,0 +1,19 @@
from fastapi import HTTPException
from app.core.database import get_connection
def check_fiscal_year_unlocked(journal_date):
fiscal_year = journal_date.year
with get_connection() as conn, conn.cursor() as cur:
cur.execute("""
SELECT is_locked
FROM fiscal_year_locks
WHERE fiscal_year = %s
""", (fiscal_year,))
row = cur.fetchone()
if row and row["is_locked"]:
raise HTTPException(
status_code=400,
detail="この会計年度は既に確定されているため、仕訳を変更できません。"
)

View File

@@ -10,6 +10,16 @@ app = FastAPI(
title="日本小規模企業向け会計システム"
)
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 本地开发先用 *
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.exception_handler(ValidationError)
async def validation_exception_handler(request: Request, exc: ValidationError):
# ここでは最初のエラーだけ簡潔に返す(必要に応じて整形)
@@ -45,3 +55,13 @@ app.include_router(trial_balance_router)
from app.modules.opening_balances.router import router as opening_router
app.include_router(opening_router)
from app.modules.general_ledger.router import router as general_ledger_router
app.include_router(general_ledger_router)
from app.modules.opening_balances.lock_router import router as opening_balance_lock_router
app.include_router(opening_balance_lock_router)

View File

@@ -0,0 +1,28 @@
from fastapi import APIRouter, HTTPException, Query
from app.modules.general_ledger.service import fetch_general_ledger
router = APIRouter(
prefix="/general-ledger",
tags=["元帳"]
)
@router.get("", summary="元帳取得(科目別)")
def get_general_ledger(
account_id: int = Query(...),
date_from: str = Query(...),
date_to: str = Query(...)
):
if date_from > date_to:
raise HTTPException(status_code=400, detail="期間指定が不正です。")
result = fetch_general_ledger(account_id, date_from, date_to)
if result is None:
raise HTTPException(status_code=400, detail="科目が存在しません。")
return {
"period": {
"from": date_from,
"to": date_to
},
**result
}

View File

@@ -0,0 +1,53 @@
from decimal import Decimal
from app.core.database import get_connection
from app.modules.general_ledger.sql import GENERAL_LEDGER_SQL
def D(x) -> Decimal:
return Decimal(str(x or 0))
def fetch_general_ledger(account_id: int, date_from: str, date_to: str):
with get_connection() as conn, conn.cursor() as cur:
# ① 科目确认
cur.execute("""
SELECT account_code, account_name
FROM accounts
WHERE account_id = %s
AND is_active = true
""", (account_id,))
acc = cur.fetchone()
if not acc:
return None
# ② 元帳明细
cur.execute(
GENERAL_LEDGER_SQL,
{
"account_id": account_id,
"date_from": date_from,
"date_to": date_to
}
)
rows = cur.fetchall()
balance = Decimal("0")
lines = []
for r in rows:
balance += D(r["debit"]) - D(r["credit"])
lines.append({
"journal_date": r["journal_date"],
"description": r["description"],
"debit": str(D(r["debit"])),
"credit": str(D(r["credit"])),
"balance": str(balance)
})
return {
"account": {
"account_code": acc["account_code"],
"account_name": acc["account_name"]
},
"lines": lines
}

View File

@@ -0,0 +1,14 @@
# sql.py
GENERAL_LEDGER_SQL = """
SELECT
j.journal_date,
j.description,
l.debit,
l.credit
FROM journal_lines l
JOIN journal_entries j
ON j.journal_id = l.journal_id
WHERE l.account_id = %(account_id)s
AND j.journal_date BETWEEN %(date_from)s AND %(date_to)s
ORDER BY j.journal_date, l.line_id;
"""

View File

@@ -4,6 +4,8 @@ from typing import List
from decimal import Decimal, InvalidOperation
from psycopg.errors import ForeignKeyViolation, CheckViolation
from app.core.database import get_connection
from app.core.fiscal_lock import check_fiscal_year_unlocked
from datetime import date
router = APIRouter(prefix="/journals", tags=["仕訳"])
@@ -109,3 +111,74 @@ def create_journal(data: JournalCreate):
raise HTTPException(status_code=400, detail="科目IDが不正です。")
except CheckViolation:
raise HTTPException(status_code=400, detail="金額の指定が不正です。")
@router.put("/{journal_id}", summary="仕訳修正")
def update_journal(journal_id: int, data: JournalCreate):
with get_connection() as conn, conn.cursor() as cur:
# ① 先取原仕訳の日付
cur.execute(
"SELECT journal_date FROM journal_entries WHERE journal_id = %s",
(journal_id,)
)
row = cur.fetchone()
if not row:
raise HTTPException(status_code=404, detail="仕訳が存在しません。")
# 🔒 ② 这里!!年度锁定检查
check_fiscal_year_unlocked(row["journal_date"])
# ③ 删除旧明细
cur.execute(
"DELETE FROM journal_lines WHERE journal_id = %s",
(journal_id,)
)
# ④ 更新ヘッダ
cur.execute(
"UPDATE journal_entries SET journal_date=%s, description=%s WHERE journal_id=%s",
(data.journal_date, data.description, journal_id)
)
# ⑤ 插入新明细
for l in data.lines:
cur.execute(
"""INSERT INTO journal_lines (journal_id, account_id, debit, credit)
VALUES (%s, %s, %s, %s)""",
(journal_id, l.account_id, l.debit, l.credit)
)
conn.commit()
return {"message": "仕訳を修正しました。"}
@router.delete("/{journal_id}", summary="仕訳削除")
def delete_journal(journal_id: int):
with get_connection() as conn, conn.cursor() as cur:
# ① 先取日付
cur.execute(
"SELECT journal_date FROM journal_entries WHERE journal_id = %s",
(journal_id,)
)
row = cur.fetchone()
if not row:
raise HTTPException(status_code=404, detail="仕訳が存在しません。")
# 🔒 ② 年度锁定检查(就在这里)
check_fiscal_year_unlocked(row["journal_date"])
# ③ 删除
cur.execute(
"DELETE FROM journal_lines WHERE journal_id = %s",
(journal_id,)
)
cur.execute(
"DELETE FROM journal_entries WHERE journal_id = %s",
(journal_id,)
)
conn.commit()
return {"message": "仕訳を削除しました。"}

View File

@@ -1,109 +1,107 @@
from fastapi import APIRouter, HTTPException, Query
from typing import Optional, List, Dict, Any
from decimal import Decimal
from datetime import date
from app.core.database import get_connection
router = APIRouter(prefix="/ledger", tags=["元帳"])
router = APIRouter(
prefix="/general-ledger",
tags=["元帳"]
)
def to_decimal(x) -> Decimal:
if isinstance(x, Decimal):
return x
def D(x) -> Decimal:
return Decimal(str(x or 0))
@router.get("/{account_id}", summary="科目別元帳(残高推移)")
def get_ledger(
account_id: int,
date_from: Optional[str] = Query(None, description="YYYY-MM-DD"),
date_to: Optional[str] = Query(None, description="YYYY-MM-DD"),
@router.get("", summary="元帳取得(期首残高対応)")
def get_general_ledger(
account_id: int = Query(..., description="科目ID"),
fiscal_year: int = Query(..., description="会計年度2025"),
date_from: str = Query(..., description="YYYY-MM-DD"),
date_to: str = Query(..., description="YYYY-MM-DD"),
):
# 1) 科目存在チェック
if date_from > date_to:
raise HTTPException(status_code=400, detail="期間指定が不正です。")
with get_connection() as conn, conn.cursor() as cur:
# ----------------------------
# 科目存在確認
# ----------------------------
cur.execute("""
SELECT account_id, account_code, account_name, account_type
SELECT account_code, account_name
FROM accounts
WHERE account_id = %s
AND is_active = true
""", (account_id,))
account = cur.fetchone()
if not account:
raise HTTPException(status_code=404, detail="指定した科目が見つかりません。")
acc = cur.fetchone()
# 2) 期間デフォルト(最小〜最大)
if not date_from:
if not acc:
raise HTTPException(status_code=400, detail="指定された科目は存在しません。")
# ----------------------------
# 期首残高opening_balances
# ----------------------------
cur.execute("""
SELECT MIN(j.journal_date) AS min_d
FROM journal_entries j
JOIN journal_lines l ON l.journal_id = j.journal_id
WHERE l.account_id = %s
""", (account_id,))
SELECT
COALESCE(opening_debit, 0) - COALESCE(opening_credit, 0) AS opening
FROM opening_balances
WHERE fiscal_year = %s
AND account_id = %s
""", (fiscal_year, account_id))
row = cur.fetchone()
date_from = (row["min_d"] or date.today()).isoformat()
opening_balance = D(row["opening"]) if row else D(0)
if not date_to:
# ----------------------------
# 期間内仕訳取得
# ----------------------------
cur.execute("""
SELECT MAX(j.journal_date) AS max_d
SELECT
j.journal_date,
j.description,
l.debit,
l.credit
FROM journal_entries j
JOIN journal_lines l ON l.journal_id = j.journal_id
WHERE l.account_id = %s
""", (account_id,))
row = cur.fetchone()
date_to = (row["max_d"] or date.today()).isoformat()
# 3) 期首残高date_from より前)
cur.execute("""
SELECT COALESCE(SUM(l.debit - l.credit), 0) AS opening
FROM journal_lines l
JOIN journal_entries j ON j.journal_id = l.journal_id
WHERE l.account_id = %s
AND j.journal_date < %s
""", (account_id, date_from))
opening = to_decimal(cur.fetchone()["opening"])
# 4) 期間内明細
cur.execute("""
SELECT j.journal_date, j.journal_id, j.description,
l.debit, l.credit
FROM journal_lines l
JOIN journal_entries j ON j.journal_id = l.journal_id
JOIN journal_lines l
ON l.journal_id = j.journal_id
WHERE l.account_id = %s
AND j.journal_date BETWEEN %s AND %s
ORDER BY j.journal_date, j.journal_id, l.line_id
ORDER BY j.journal_date, l.line_id
""", (account_id, date_from, date_to))
rows = cur.fetchall()
# 5) 残高推移の計算(借方−貸方)
balance = opening
out_rows: List[Dict[str, Any]] = []
total_debit = Decimal("0")
total_credit = Decimal("0")
# ----------------------------
# 累計残高計算
# ----------------------------
balance = opening_balance
lines = []
for r in rows:
d = to_decimal(r["debit"])
c = to_decimal(r["credit"])
total_debit += d
total_credit += c
balance += (d - c)
out_rows.append({
"date": r["journal_date"].isoformat(),
"journal_id": r["journal_id"],
debit = D(r["debit"])
credit = D(r["credit"])
balance += debit - credit
lines.append({
"journal_date": r["journal_date"],
"description": r["description"],
"debit": str(d),
"credit": str(c),
"balance": str(balance) # 借方残を正とする(借方−貸方)
"debit": str(debit),
"credit": str(credit),
"balance": str(balance),
})
result = {
return {
"account": {
"id": account["account_id"],
"code": account["account_code"],
"name": account["account_name"],
"type": account["account_type"],
"account_id": account_id,
"account_code": acc["account_code"],
"account_name": acc["account_name"],
},
"period": {"from": date_from, "to": date_to},
"opening_balance": str(opening),
"total_debit": str(total_debit),
"total_credit": str(total_credit),
"fiscal_year": fiscal_year,
"period": {
"from": date_from,
"to": date_to
},
"opening_balance": str(opening_balance),
"lines": lines,
"closing_balance": str(balance),
"rows": out_rows
}
return result

View File

@@ -0,0 +1,27 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from app.core.database import get_connection
router = APIRouter(
prefix="/opening-balances",
tags=["期首残高"]
)
class LockRequest(BaseModel):
fiscal_year: int
@router.post("/lock", summary="期首残高年度確定")
def lock_fiscal_year(req: LockRequest):
with get_connection() as conn, conn.cursor() as cur:
cur.execute("""
INSERT INTO fiscal_year_locks (fiscal_year, is_locked)
VALUES (%s, true)
ON CONFLICT (fiscal_year)
DO UPDATE SET
is_locked = true,
locked_at = CURRENT_TIMESTAMP
""", (req.fiscal_year,))
conn.commit()
return {"message": "期首残高を確定しました。"}

View File

@@ -1,55 +1,108 @@
# backend/app/modules/opening_balances/router.py
from fastapi import APIRouter, HTTPException
from psycopg.errors import ForeignKeyViolation
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
from typing import List
from decimal import Decimal
from app.core.database import get_connection
from app.modules.opening_balances.schema import OpeningBalanceCreate
router = APIRouter(
prefix="/opening-balances",
tags=["期首残高"]
)
@router.post("/", summary="期首残高 登録/更新")
def upsert_opening_balance(data: OpeningBalanceCreate):
try:
with get_connection() as conn, conn.cursor() as cur:
cur.execute("""
INSERT INTO opening_balances
(fiscal_year, account_id, opening_debit, opening_credit)
VALUES (%s, %s, %s, %s)
ON CONFLICT (fiscal_year, account_id)
DO UPDATE SET
opening_debit = EXCLUDED.opening_debit,
opening_credit = EXCLUDED.opening_credit
""", (
data.fiscal_year,
data.account_id,
data.opening_debit,
data.opening_credit
))
conn.commit()
return {"result": "ok"}
except ForeignKeyViolation:
raise HTTPException(
status_code=400,
detail="存在しない科目が指定されています"
)
router = APIRouter(prefix="/opening-balances", tags=["期首残高"])
@router.get("/{fiscal_year}", summary="期首残高一覧取得")
def list_opening_balances(fiscal_year: int):
# ---------- Models ----------
class OpeningBalanceItem(BaseModel):
account_id: int
debit: Decimal = Decimal("0")
credit: Decimal = Decimal("0")
class OpeningBalanceRequest(BaseModel):
fiscal_year: int
balances: List[OpeningBalanceItem]
# ---------- GET期首残高取得 ----------
@router.get("", summary="期首残高取得")
def get_opening_balances(
fiscal_year: int = Query(..., description="会計年度2025")
):
with get_connection() as conn, conn.cursor() as cur:
cur.execute("""
SELECT
a.account_id,
a.account_code,
a.account_name,
a.account_type,
ob.opening_debit,
ob.opening_credit
FROM opening_balances ob
JOIN accounts a ON a.account_id = ob.account_id
WHERE ob.fiscal_year = %s
COALESCE(o.opening_debit, 0) AS opening_debit,
COALESCE(o.opening_credit, 0) AS opening_credit
FROM accounts a
LEFT JOIN opening_balances o
ON o.account_id = a.account_id
AND o.fiscal_year = %s
WHERE a.is_active = true
ORDER BY a.account_code
""", (fiscal_year,))
return cur.fetchall()
# ---------- POST期首残高保存 ----------
@router.post("", summary="期首残高保存")
def save_opening_balances(req: OpeningBalanceRequest):
# 年度锁定チェック
cur.execute("""
SELECT is_locked
FROM fiscal_year_locks
WHERE fiscal_year = %s
""", (req.fiscal_year,))
row = cur.fetchone()
if row and row["is_locked"]:
raise HTTPException(
status_code=400,
detail="この会計年度の期首残高は既に確定されています。"
)
total_debit = Decimal("0")
total_credit = Decimal("0")
for b in req.balances:
total_debit += b.debit
total_credit += b.credit
if total_debit != total_credit:
raise HTTPException(
status_code=400,
detail="借方合計と貸方合計が一致していません。"
)
with get_connection() as conn, conn.cursor() as cur:
# 既存データ削除(年度単位)
cur.execute("""
DELETE FROM opening_balances
WHERE fiscal_year = %s
""", (req.fiscal_year,))
# 再登録
for b in req.balances:
if b.debit == 0 and b.credit == 0:
continue
cur.execute("""
INSERT INTO opening_balances
(fiscal_year, account_id, opening_debit, opening_credit)
VALUES
(%s, %s, %s, %s)
""", (
req.fiscal_year,
b.account_id,
b.debit,
b.credit
))
conn.commit()
return {"message": "期首残高を保存しました。"}

View File

@@ -2,6 +2,7 @@ from fastapi import APIRouter, HTTPException, Query
from decimal import Decimal
from typing import Dict, Any, List
from app.core.database import get_connection
from app.modules.trial_balance.service import fetch_trial_balance
router = APIRouter(prefix="/trial-balance", tags=["試算表"])
@@ -10,12 +11,23 @@ def D(x) -> Decimal:
@router.get("", summary="試算表(全科目)")
def get_trial_balance(
fiscal_year: int = Query(..., description="会計年度(例: 2025"),
date_from: str = Query(..., description="YYYY-MM-DD"),
date_to: str = Query(..., description="YYYY-MM-DD"),
):
if date_from > date_to:
raise HTTPException(status_code=400, detail="期間指定が不正です。")
result = fetch_trial_balance(date_from, date_to)
return {
"period": {
"from": date_from,
"to": date_to
},
**result
}
with get_connection() as conn, conn.cursor() as cur:
# 全科目取得
@@ -37,15 +49,18 @@ def get_trial_balance(
for acc in accounts:
aid = acc["account_id"]
# 期首残高
# 期首残高opening_balances
cur.execute("""
SELECT COALESCE(SUM(l.debit - l.credit), 0) AS opening
FROM journal_lines l
JOIN journal_entries j ON j.journal_id = l.journal_id
WHERE l.account_id = %s
AND j.journal_date < %s
""", (aid, date_from))
opening = D(cur.fetchone()["opening"])
SELECT
COALESCE(opening_debit, 0) - COALESCE(opening_credit, 0) AS opening
FROM opening_balances
WHERE fiscal_year = %s
AND account_id = %s
""", (fiscal_year, aid))
row = cur.fetchone()
opening = D(row["opening"]) if row else D(0)
# 当期
cur.execute("""

View File

@@ -0,0 +1,81 @@
# backend/app/modules/trial_balance/service.py
from decimal import Decimal
from typing import Dict, Any, List
from app.core.database import get_connection
def D(x) -> Decimal:
return Decimal(str(x or 0))
def fetch_trial_balance(date_from: str, date_to: str):
with get_connection() as conn, conn.cursor() as cur:
cur.execute("""
SELECT account_id, account_code, account_name, account_type
FROM accounts
WHERE is_active = true
ORDER BY account_code
""")
accounts = cur.fetchall()
result_accounts: List[Dict[str, Any]] = []
total_opening = Decimal("0")
total_debit = Decimal("0")
total_credit = Decimal("0")
total_closing = Decimal("0")
for acc in accounts:
aid = acc["account_id"]
# 期首
cur.execute("""
SELECT COALESCE(SUM(l.debit - l.credit), 0) AS opening
FROM journal_lines l
JOIN journal_entries j ON j.journal_id = l.journal_id
WHERE l.account_id = %s
AND j.journal_date < %s
""", (aid, date_from))
opening = D(cur.fetchone()["opening"])
# 当期
cur.execute("""
SELECT
COALESCE(SUM(l.debit), 0) AS debit,
COALESCE(SUM(l.credit), 0) AS credit
FROM journal_lines l
JOIN journal_entries j ON j.journal_id = l.journal_id
WHERE l.account_id = %s
AND j.journal_date BETWEEN %s AND %s
""", (aid, date_from, date_to))
row = cur.fetchone()
debit = D(row["debit"])
credit = D(row["credit"])
closing = opening + debit - credit
total_opening += opening
total_debit += debit
total_credit += credit
total_closing += closing
result_accounts.append({
"account_id": aid,
"account_code": acc["account_code"],
"account_name": acc["account_name"],
"account_type": acc["account_type"],
"opening_balance": str(opening),
"debit": str(debit),
"credit": str(credit),
"closing_balance": str(closing),
})
return {
"totals": {
"opening_balance": str(total_opening),
"debit": str(total_debit),
"credit": str(total_credit),
"closing_balance": str(total_closing)
},
"accounts": result_accounts
}

View File

@@ -0,0 +1,84 @@
# backend/app/modules/trial_balance/sql.py
TRIAL_BALANCE_SQL = """
WITH accounts_base AS (
SELECT
a.account_id,
a.account_code,
a.account_name,
a.account_type
FROM accounts a
WHERE a.is_active = true
),
opening AS (
SELECT
jl.account_id,
SUM(jl.debit) AS opening_debit,
SUM(jl.credit) AS opening_credit
FROM journal_entries je
JOIN journal_lines jl ON jl.journal_id = je.journal_id
WHERE je.description = '前期残高'
AND je.journal_date = %(start_date)s
GROUP BY jl.account_id
),
period AS (
SELECT
jl.account_id,
SUM(jl.debit) AS period_debit,
SUM(jl.credit) AS period_credit
FROM journal_entries je
JOIN journal_lines jl ON jl.journal_id = je.journal_id
WHERE je.journal_date BETWEEN %(start_date)s AND %(end_date)s
AND je.description <> '前期残高'
GROUP BY jl.account_id
)
SELECT
a.account_code,
a.account_name,
a.account_type,
COALESCE(o.opening_debit, 0) AS opening_debit,
COALESCE(o.opening_credit, 0) AS opening_credit,
COALESCE(p.period_debit, 0) AS period_debit,
COALESCE(p.period_credit, 0) AS period_credit,
CASE
WHEN (
COALESCE(o.opening_debit, 0)
- COALESCE(o.opening_credit, 0)
+ COALESCE(p.period_debit, 0)
- COALESCE(p.period_credit, 0)
) > 0
THEN (
COALESCE(o.opening_debit, 0)
- COALESCE(o.opening_credit, 0)
+ COALESCE(p.period_debit, 0)
- COALESCE(p.period_credit, 0)
)
ELSE 0
END AS closing_debit,
CASE
WHEN (
COALESCE(o.opening_debit, 0)
- COALESCE(o.opening_credit, 0)
+ COALESCE(p.period_debit, 0)
- COALESCE(p.period_credit, 0)
) < 0
THEN ABS(
COALESCE(o.opening_debit, 0)
- COALESCE(o.opening_credit, 0)
+ COALESCE(p.period_debit, 0)
- COALESCE(p.period_credit, 0)
)
ELSE 0
END AS closing_credit
FROM accounts_base a
LEFT JOIN opening o ON o.account_id = a.account_id
LEFT JOIN period p ON p.account_id = a.account_id
ORDER BY a.account_code;
"""

View File

@@ -1,14 +1,23 @@
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont;
margin: 20px;
font-family: sans-serif;
}
table {
margin-top: 10px;
border-collapse: collapse;
width: 100%;
}
th,
td {
padding: 6px 10px;
border: 1px solid #999;
padding: 4px 8px;
}
th {
background: #eee;
}
a {
color: blue;
text-decoration: none;
}

View File

@@ -2,15 +2,35 @@
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>NJTS 会計システム</title>
<title>試算表</title>
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
<h1>NJTS 会計システム</h1>
<h1>試算表</h1>
<ul>
<li><a href="accounts.html">科目マスタ</a></li>
<li><a href="trial-balance.html">試算表</a></li>
</ul>
<label>
期間:
<input type="date" id="from" />
<input type="date" id="to" />
</label>
<button onclick="loadTrialBalance()">表示</button>
<table id="trial-table">
<thead>
<tr>
<th>科目コード</th>
<th>科目名</th>
<th>期首</th>
<th>借方</th>
<th>贷方</th>
<th>期末</th>
</tr>
</thead>
<tbody></tbody>
</table>
<script src="js/api.js"></script>
<script src="js/trial_balance.js"></script>
</body>
</html>

47
frontend/journal.html Normal file
View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>仕訳入力</title>
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
<h1>仕訳入力</h1>
<label>
日付:
<input type="date" id="journal-date" />
</label>
<br /><br />
<label>
摘要:
<input type="text" id="description" size="40" />
</label>
<br /><br />
<table id="lines-table">
<thead>
<tr>
<th>科目ID</th>
<th>借方</th>
<th>贷方</th>
<th></th>
</tr>
</thead>
<tbody></tbody>
</table>
<button onclick="addLine()"> 行追加</button>
<br /><br />
<button onclick="submitJournal()">保存</button>
<a href="index.html">← 戻る</a>
<script src="js/api.js"></script>
<script src="js/journal.js"></script>
</body>
</html>

View File

@@ -1,10 +1,13 @@
const API_BASE = "http://localhost:18080";
const API_BASE = "http://127.0.0.1:18080";
async function apiGet(path) {
const res = await fetch(`${API_BASE}${path}`);
async function apiGet(path, params = {}) {
const query = new URLSearchParams(params).toString();
const url = `${API_BASE}${path}?${query}`;
const res = await fetch(url);
if (!res.ok) {
const text = await res.text();
throw new Error(text || "APIエラー");
const data = await res.json();
throw new Error(data.detail || "APIエラー");
}
return res.json();
}

65
frontend/js/journal.js Normal file
View File

@@ -0,0 +1,65 @@
function addLine() {
const tbody = document.querySelector("#lines-table tbody");
const tr = document.createElement("tr");
tr.innerHTML = `
<td><input type="number" class="account-id" /></td>
<td><input type="number" class="debit" value="0" /></td>
<td><input type="number" class="credit" value="0" /></td>
<td><button onclick="this.closest('tr').remove()">削除</button></td>
`;
tbody.appendChild(tr);
}
async function submitJournal() {
const date = document.getElementById("journal-date").value;
const desc = document.getElementById("description").value;
if (!date) {
alert("日付を入力してください");
return;
}
const lines = [];
document.querySelectorAll("#lines-table tbody tr").forEach((tr) => {
const accountId = tr.querySelector(".account-id").value;
const debit = tr.querySelector(".debit").value || 0;
const credit = tr.querySelector(".credit").value || 0;
if (!accountId) return;
lines.push({
account_id: Number(accountId),
debit: Number(debit),
credit: Number(credit),
});
});
if (lines.length < 2) {
alert("仕訳明細は2行以上必要です");
return;
}
try {
await fetch("http://127.0.0.1:18080/journals", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
journal_date: date,
description: desc,
lines: lines,
}),
}).then(async (res) => {
if (!res.ok) {
const data = await res.json();
throw new Error(data.detail || "登録失敗");
}
});
alert("仕訳を登録しました");
location.href = "index.html";
} catch (e) {
alert(e.message);
}
}

37
frontend/js/ledger.js Normal file
View File

@@ -0,0 +1,37 @@
async function loadLedger() {
const params = new URLSearchParams(location.search);
const accountId = params.get("account_id");
const from = params.get("from");
const to = params.get("to");
try {
const data = await apiGet("/general-ledger", {
account_id: accountId,
date_from: from,
date_to: to,
});
document.getElementById(
"account-title"
).innerText = `${data.account.account_code} ${data.account.account_name}`;
const tbody = document.getElementById("ledger-body");
tbody.innerHTML = "";
data.lines.forEach((line) => {
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${line.journal_date}</td>
<td>${line.description}</td>
<td>${line.debit}</td>
<td>${line.credit}</td>
<td>${line.balance}</td>
`;
tbody.appendChild(tr);
});
} catch (e) {
alert(e.message);
}
}
loadLedger();

View File

@@ -0,0 +1,152 @@
let accounts = [];
let retainedEarnings = null;
let isLocked = false;
function formatNumber(n) {
return Number(n || 0).toLocaleString();
}
function recalcTotals() {
let debit = 0;
let credit = 0;
// 先算:不含繰越利益
accounts.forEach((a) => {
if (a.account_name === "繰越利益") return;
debit += Number(a.opening_debit || 0);
credit += Number(a.opening_credit || 0);
});
const diff = debit - credit;
// 自动计算 繰越利益
if (retainedEarnings) {
if (diff > 0) {
retainedEarnings.opening_debit = 0;
retainedEarnings.opening_credit = diff;
} else {
retainedEarnings.opening_debit = -diff;
retainedEarnings.opening_credit = 0;
}
}
// 再算一次(包含繰越利益)
let finalDebit = 0;
let finalCredit = 0;
accounts.forEach((a) => {
finalDebit += Number(a.opening_debit || 0);
finalCredit += Number(a.opening_credit || 0);
});
document.getElementById("totalDebit").innerText = formatNumber(finalDebit);
document.getElementById("totalCredit").innerText = formatNumber(finalCredit);
document.getElementById("diff").innerText = formatNumber(
finalDebit - finalCredit
);
}
async function loadOpeningBalances() {
const year = document.getElementById("fiscalYear").value;
const data = await apiGet("/opening-balances", { fiscal_year: year });
accounts = data;
retainedEarnings = accounts.find((a) => a.account_name === "繰越利益");
const tbody = document.querySelector("#balanceTable tbody");
tbody.innerHTML = "";
GROUPS.forEach((group) => {
// 分组标题行
const headerTr = document.createElement("tr");
headerTr.innerHTML = `
<td colspan="4" style="background:#ddd; font-weight:bold;">
${group.label}
</td>
`;
tbody.appendChild(headerTr);
accounts.forEach((a, i) => {
if (a.account_type !== group.key) return;
const isRetained = a.account_name === "繰越利益";
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${a.account_code}</td>
<td>${a.account_name}</td>
<td>
<input type="number" value="${a.opening_debit}"
${isRetained ? "readonly" : ""}
onchange="accounts[${i}].opening_debit=this.value; recalcTotals();">
</td>
<td>
<input type="number" value="${a.opening_credit}"
${isRetained ? "readonly" : ""}
onchange="accounts[${i}].opening_credit=this.value; recalcTotals();">
</td>
`;
tbody.appendChild(tr);
});
});
// 年度锁定チェック
fetch(
`http://127.0.0.1:18080/opening-balances/lock-status?fiscal_year=${year}`
)
.then((res) => res.json())
.then((data) => {
isLocked = data.is_locked;
document.querySelector(
"button[onclick='saveOpeningBalances()']"
).disabled = isLocked;
if (isLocked) {
alert("この会計年度の期首残高は確定されています。");
}
});
recalcTotals();
}
async function saveOpeningBalances() {
if (isLocked) {
alert("この会計年度の期首残高は確定されているため、保存できません。");
return;
}
const year = Number(document.getElementById("fiscalYear").value);
const balances = accounts
// ❗ 排除 繰越利益
.filter(
(a) =>
a.account_name !== "繰越利益" &&
(Number(a.opening_debit) !== 0 || Number(a.opening_credit) !== 0)
)
.map((a) => ({
account_id: a.account_id,
debit: Number(a.opening_debit || 0),
credit: Number(a.opening_credit || 0),
}));
try {
await fetch("http://127.0.0.1:18080/opening-balances", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
fiscal_year: year,
balances: balances,
}),
}).then(async (res) => {
if (!res.ok) {
const data = await res.json();
throw new Error(data.detail);
}
});
alert("期首残高を保存しました");
} catch (e) {
alert(e.message);
}
}

View File

@@ -1,4 +1,4 @@
document.getElementById("load").onclick = async () => {
async function loadTrialBalance() {
const from = document.getElementById("from").value;
const to = document.getElementById("to").value;
@@ -8,22 +8,33 @@ document.getElementById("load").onclick = async () => {
}
try {
const data = await apiGet(`/trial-balance?date_from=${from}&date_to=${to}`);
const tbody = document.getElementById("tb-body");
const data = await apiGet("/trial-balance", {
date_from: from,
date_to: to,
});
const tbody = document.querySelector("#trial-table tbody");
tbody.innerHTML = "";
data.accounts.forEach((a) => {
data.accounts.forEach((acc) => {
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${a.account_name}</td>
<td style="text-align:right">${a.opening_balance}</td>
<td style="text-align:right">${a.debit}</td>
<td style="text-align:right">${a.credit}</td>
<td style="text-align:right">${a.closing_balance}</td>
<td>${acc.account_code}</td>
<td>
<a href="ledger.html?account_id=${acc.account_id}&from=${from}&to=${to}">
${acc.account_name}
</a>
</td>
<td>${acc.opening_balance}</td>
<td>${acc.debit}</td>
<td>${acc.credit}</td>
<td>${acc.closing_balance}</td>
`;
tbody.appendChild(tr);
});
} catch (e) {
alert(e.message);
}
};
}

29
frontend/ledger.html Normal file
View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>元帳</title>
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
<h1 id="account-title">元帳</h1>
<table>
<thead>
<tr>
<th>日付</th>
<th>摘要</th>
<th>借方</th>
<th>贷方</th>
<th>残高</th>
</tr>
</thead>
<tbody id="ledger-body"></tbody>
</table>
<a href="index.html">← 試算表へ戻る</a>
<script src="js/api.js"></script>
<script src="js/ledger.js"></script>
</body>
</html>

View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>期首残高入力</title>
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
<h1>期首残高入力</h1>
<label>
会計年度:
<input type="number" id="fiscalYear" value="2025" />
</label>
<button onclick="loadOpeningBalances()">読込</button>
<br /><br />
<table id="balanceTable">
<thead>
<tr>
<th>科目コード</th>
<th>科目名</th>
<th>借方期首</th>
<th>贷方期首</th>
</tr>
</thead>
<tbody></tbody>
</table>
<br />
<div>
<strong>借方合計:</strong><span id="totalDebit">0</span><br />
<strong>贷方合計:</strong><span id="totalCredit">0</span><br />
<strong>差額:</strong><span id="diff">0</span>
</div>
<br />
<button onclick="saveOpeningBalances()">保存</button>
<a href="index.html">← 戻る</a>
<script src="js/api.js"></script>
<script src="js/opening_balance.js"></script>
</body>
</html>
s

View File

@@ -59,9 +59,19 @@ cd C:\workspace\njts-accounting-core\backend
python -m venv .venv
C:\workspace\njts-accounting-core\backend\.venv\pyvenv.cfg
home = C:\Users\zxh_y\AppData\Local\Programs\Python\Python312
include-system-site-packages = false
version = 3.12.9
executable = C:\Users\zxh_y\AppData\Local\Programs\Python\Python312\python.exe
command = C:\Users\zxh_y\AppData\Local\Programs\Python\Python312\python.exe -m venv C:\workspace\njts-accounting-core\backend\.venv
cd C:\workspace\njts-accounting-core\backend
.venv\Scripts\activate
uvicorn app.main:app --reload --host 127.0.0.1 --port 18080
http://localhost:18080/docs
backend\requirements.txt
ーーーーーーーーーーーーー
@@ -92,3 +102,6 @@ def root():
pip install psycopg[binary]==3.2.3
C:\workspace\njts-accounting-core\backend uvicorn app.main:app --reload --host 127.0.0.1 --port 18080