desktop修正
This commit is contained in:
12
.gitignore
vendored
12
.gitignore
vendored
@@ -26,3 +26,15 @@ Thumbs.db
|
|||||||
docker-data/
|
docker-data/
|
||||||
*.bak
|
*.bak
|
||||||
*.tmp
|
*.tmp
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
backend/.venv/
|
||||||
|
|||||||
19
backend/app/core/fiscal_lock.py
Normal file
19
backend/app/core/fiscal_lock.py
Normal 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="この会計年度は既に確定されているため、仕訳を変更できません。"
|
||||||
|
)
|
||||||
@@ -10,6 +10,16 @@ app = FastAPI(
|
|||||||
title="日本小規模企業向け会計システム"
|
title="日本小規模企業向け会計システム"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"], # 本地开发先用 *
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
@app.exception_handler(ValidationError)
|
@app.exception_handler(ValidationError)
|
||||||
async def validation_exception_handler(request: Request, exc: 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
|
from app.modules.opening_balances.router import router as opening_router
|
||||||
app.include_router(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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
0
backend/app/modules/general_ledger/__init__.py
Normal file
0
backend/app/modules/general_ledger/__init__.py
Normal file
28
backend/app/modules/general_ledger/router.py
Normal file
28
backend/app/modules/general_ledger/router.py
Normal 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
|
||||||
|
}
|
||||||
53
backend/app/modules/general_ledger/service.py
Normal file
53
backend/app/modules/general_ledger/service.py
Normal 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
|
||||||
|
}
|
||||||
14
backend/app/modules/general_ledger/sql.py
Normal file
14
backend/app/modules/general_ledger/sql.py
Normal 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;
|
||||||
|
"""
|
||||||
@@ -4,6 +4,8 @@ from typing import List
|
|||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
from psycopg.errors import ForeignKeyViolation, CheckViolation
|
from psycopg.errors import ForeignKeyViolation, CheckViolation
|
||||||
from app.core.database import get_connection
|
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=["仕訳"])
|
router = APIRouter(prefix="/journals", tags=["仕訳"])
|
||||||
|
|
||||||
@@ -109,3 +111,74 @@ def create_journal(data: JournalCreate):
|
|||||||
raise HTTPException(status_code=400, detail="科目IDが不正です。")
|
raise HTTPException(status_code=400, detail="科目IDが不正です。")
|
||||||
except CheckViolation:
|
except CheckViolation:
|
||||||
raise HTTPException(status_code=400, detail="金額の指定が不正です。")
|
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": "仕訳を削除しました。"}
|
||||||
@@ -1,109 +1,107 @@
|
|||||||
from fastapi import APIRouter, HTTPException, Query
|
from fastapi import APIRouter, HTTPException, Query
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from datetime import date
|
|
||||||
from app.core.database import get_connection
|
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):
|
def D(x) -> Decimal:
|
||||||
return x
|
|
||||||
return Decimal(str(x or 0))
|
return Decimal(str(x or 0))
|
||||||
|
|
||||||
@router.get("/{account_id}", summary="科目別元帳(残高推移)")
|
|
||||||
def get_ledger(
|
@router.get("", summary="元帳取得(期首残高対応)")
|
||||||
account_id: int,
|
def get_general_ledger(
|
||||||
date_from: Optional[str] = Query(None, description="YYYY-MM-DD"),
|
account_id: int = Query(..., description="科目ID"),
|
||||||
date_to: Optional[str] = Query(None, description="YYYY-MM-DD"),
|
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:
|
with get_connection() as conn, conn.cursor() as cur:
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# 科目存在確認
|
||||||
|
# ----------------------------
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT account_id, account_code, account_name, account_type
|
SELECT account_code, account_name
|
||||||
FROM accounts
|
FROM accounts
|
||||||
WHERE account_id = %s
|
WHERE account_id = %s
|
||||||
|
AND is_active = true
|
||||||
""", (account_id,))
|
""", (account_id,))
|
||||||
account = cur.fetchone()
|
acc = cur.fetchone()
|
||||||
if not account:
|
|
||||||
raise HTTPException(status_code=404, detail="指定した科目が見つかりません。")
|
|
||||||
|
|
||||||
# 2) 期間デフォルト(最小〜最大)
|
if not acc:
|
||||||
if not date_from:
|
raise HTTPException(status_code=400, detail="指定された科目は存在しません。")
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# 期首残高(opening_balances)
|
||||||
|
# ----------------------------
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT MIN(j.journal_date) AS min_d
|
SELECT
|
||||||
FROM journal_entries j
|
COALESCE(opening_debit, 0) - COALESCE(opening_credit, 0) AS opening
|
||||||
JOIN journal_lines l ON l.journal_id = j.journal_id
|
FROM opening_balances
|
||||||
WHERE l.account_id = %s
|
WHERE fiscal_year = %s
|
||||||
""", (account_id,))
|
AND account_id = %s
|
||||||
|
""", (fiscal_year, account_id))
|
||||||
|
|
||||||
row = cur.fetchone()
|
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("""
|
cur.execute("""
|
||||||
SELECT MAX(j.journal_date) AS max_d
|
SELECT
|
||||||
|
j.journal_date,
|
||||||
|
j.description,
|
||||||
|
l.debit,
|
||||||
|
l.credit
|
||||||
FROM journal_entries j
|
FROM journal_entries j
|
||||||
JOIN journal_lines l ON l.journal_id = j.journal_id
|
JOIN journal_lines l
|
||||||
WHERE l.account_id = %s
|
ON l.journal_id = j.journal_id
|
||||||
""", (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
|
|
||||||
WHERE l.account_id = %s
|
WHERE l.account_id = %s
|
||||||
AND j.journal_date BETWEEN %s AND %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))
|
""", (account_id, date_from, date_to))
|
||||||
|
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
|
|
||||||
# 5) 残高推移の計算(借方−貸方)
|
# ----------------------------
|
||||||
balance = opening
|
# 累計残高計算
|
||||||
out_rows: List[Dict[str, Any]] = []
|
# ----------------------------
|
||||||
total_debit = Decimal("0")
|
balance = opening_balance
|
||||||
total_credit = Decimal("0")
|
lines = []
|
||||||
|
|
||||||
for r in rows:
|
for r in rows:
|
||||||
d = to_decimal(r["debit"])
|
debit = D(r["debit"])
|
||||||
c = to_decimal(r["credit"])
|
credit = D(r["credit"])
|
||||||
total_debit += d
|
balance += debit - credit
|
||||||
total_credit += c
|
|
||||||
balance += (d - c)
|
lines.append({
|
||||||
out_rows.append({
|
"journal_date": r["journal_date"],
|
||||||
"date": r["journal_date"].isoformat(),
|
|
||||||
"journal_id": r["journal_id"],
|
|
||||||
"description": r["description"],
|
"description": r["description"],
|
||||||
"debit": str(d),
|
"debit": str(debit),
|
||||||
"credit": str(c),
|
"credit": str(credit),
|
||||||
"balance": str(balance) # 借方残を正とする(借方−貸方)
|
"balance": str(balance),
|
||||||
})
|
})
|
||||||
|
|
||||||
result = {
|
return {
|
||||||
"account": {
|
"account": {
|
||||||
"id": account["account_id"],
|
"account_id": account_id,
|
||||||
"code": account["account_code"],
|
"account_code": acc["account_code"],
|
||||||
"name": account["account_name"],
|
"account_name": acc["account_name"],
|
||||||
"type": account["account_type"],
|
|
||||||
},
|
},
|
||||||
"period": {"from": date_from, "to": date_to},
|
"fiscal_year": fiscal_year,
|
||||||
"opening_balance": str(opening),
|
"period": {
|
||||||
"total_debit": str(total_debit),
|
"from": date_from,
|
||||||
"total_credit": str(total_credit),
|
"to": date_to
|
||||||
|
},
|
||||||
|
"opening_balance": str(opening_balance),
|
||||||
|
"lines": lines,
|
||||||
"closing_balance": str(balance),
|
"closing_balance": str(balance),
|
||||||
"rows": out_rows
|
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
|
|||||||
27
backend/app/modules/opening_balances/lock_router.py
Normal file
27
backend/app/modules/opening_balances/lock_router.py
Normal 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": "期首残高を確定しました。"}
|
||||||
@@ -1,55 +1,108 @@
|
|||||||
# backend/app/modules/opening_balances/router.py
|
from fastapi import APIRouter, HTTPException, Query
|
||||||
from fastapi import APIRouter, HTTPException
|
from pydantic import BaseModel
|
||||||
from psycopg.errors import ForeignKeyViolation
|
from typing import List
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from app.core.database import get_connection
|
from app.core.database import get_connection
|
||||||
from app.modules.opening_balances.schema import OpeningBalanceCreate
|
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(prefix="/opening-balances", tags=["期首残高"])
|
||||||
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.get("/{fiscal_year}", summary="期首残高一覧取得")
|
# ---------- Models ----------
|
||||||
def list_opening_balances(fiscal_year: int):
|
|
||||||
|
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:
|
with get_connection() as conn, conn.cursor() as cur:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
|
a.account_id,
|
||||||
a.account_code,
|
a.account_code,
|
||||||
a.account_name,
|
a.account_name,
|
||||||
a.account_type,
|
a.account_type,
|
||||||
ob.opening_debit,
|
COALESCE(o.opening_debit, 0) AS opening_debit,
|
||||||
ob.opening_credit
|
COALESCE(o.opening_credit, 0) AS opening_credit
|
||||||
FROM opening_balances ob
|
FROM accounts a
|
||||||
JOIN accounts a ON a.account_id = ob.account_id
|
LEFT JOIN opening_balances o
|
||||||
WHERE ob.fiscal_year = %s
|
ON o.account_id = a.account_id
|
||||||
|
AND o.fiscal_year = %s
|
||||||
|
WHERE a.is_active = true
|
||||||
ORDER BY a.account_code
|
ORDER BY a.account_code
|
||||||
""", (fiscal_year,))
|
""", (fiscal_year,))
|
||||||
return cur.fetchall()
|
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": "期首残高を保存しました。"}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from fastapi import APIRouter, HTTPException, Query
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List
|
||||||
from app.core.database import get_connection
|
from app.core.database import get_connection
|
||||||
|
from app.modules.trial_balance.service import fetch_trial_balance
|
||||||
|
|
||||||
router = APIRouter(prefix="/trial-balance", tags=["試算表"])
|
router = APIRouter(prefix="/trial-balance", tags=["試算表"])
|
||||||
|
|
||||||
@@ -10,12 +11,23 @@ def D(x) -> Decimal:
|
|||||||
|
|
||||||
@router.get("", summary="試算表(全科目)")
|
@router.get("", summary="試算表(全科目)")
|
||||||
def get_trial_balance(
|
def get_trial_balance(
|
||||||
|
fiscal_year: int = Query(..., description="会計年度(例: 2025)"),
|
||||||
date_from: str = Query(..., description="YYYY-MM-DD"),
|
date_from: str = Query(..., description="YYYY-MM-DD"),
|
||||||
date_to: str = Query(..., description="YYYY-MM-DD"),
|
date_to: str = Query(..., description="YYYY-MM-DD"),
|
||||||
):
|
):
|
||||||
if date_from > date_to:
|
if date_from > date_to:
|
||||||
raise HTTPException(status_code=400, detail="期間指定が不正です。")
|
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:
|
with get_connection() as conn, conn.cursor() as cur:
|
||||||
|
|
||||||
# 全科目取得
|
# 全科目取得
|
||||||
@@ -37,15 +49,18 @@ def get_trial_balance(
|
|||||||
for acc in accounts:
|
for acc in accounts:
|
||||||
aid = acc["account_id"]
|
aid = acc["account_id"]
|
||||||
|
|
||||||
# 期首残高
|
# 期首残高(opening_balances)
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT COALESCE(SUM(l.debit - l.credit), 0) AS opening
|
SELECT
|
||||||
FROM journal_lines l
|
COALESCE(opening_debit, 0) - COALESCE(opening_credit, 0) AS opening
|
||||||
JOIN journal_entries j ON j.journal_id = l.journal_id
|
FROM opening_balances
|
||||||
WHERE l.account_id = %s
|
WHERE fiscal_year = %s
|
||||||
AND j.journal_date < %s
|
AND account_id = %s
|
||||||
""", (aid, date_from))
|
""", (fiscal_year, aid))
|
||||||
opening = D(cur.fetchone()["opening"])
|
|
||||||
|
row = cur.fetchone()
|
||||||
|
opening = D(row["opening"]) if row else D(0)
|
||||||
|
|
||||||
|
|
||||||
# 当期
|
# 当期
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
|
|||||||
81
backend/app/modules/trial_balance/service.py
Normal file
81
backend/app/modules/trial_balance/service.py
Normal 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
|
||||||
|
}
|
||||||
84
backend/app/modules/trial_balance/sql.py
Normal file
84
backend/app/modules/trial_balance/sql.py
Normal 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;
|
||||||
|
"""
|
||||||
@@ -1,14 +1,23 @@
|
|||||||
body {
|
body {
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont;
|
font-family: sans-serif;
|
||||||
margin: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
margin-top: 10px;
|
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
th,
|
th,
|
||||||
td {
|
td {
|
||||||
padding: 6px 10px;
|
border: 1px solid #999;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: blue;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,35 @@
|
|||||||
<html lang="ja">
|
<html lang="ja">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>NJTS 会計システム</title>
|
<title>試算表</title>
|
||||||
<link rel="stylesheet" href="css/style.css" />
|
<link rel="stylesheet" href="css/style.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>NJTS 会計システム</h1>
|
<h1>試算表</h1>
|
||||||
|
|
||||||
<ul>
|
<label>
|
||||||
<li><a href="accounts.html">科目マスタ</a></li>
|
期間:
|
||||||
<li><a href="trial-balance.html">試算表</a></li>
|
<input type="date" id="from" />
|
||||||
</ul>
|
〜
|
||||||
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
47
frontend/journal.html
Normal file
47
frontend/journal.html
Normal 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>
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
const API_BASE = "http://localhost:18080";
|
const API_BASE = "http://127.0.0.1:18080";
|
||||||
|
|
||||||
async function apiGet(path) {
|
async function apiGet(path, params = {}) {
|
||||||
const res = await fetch(`${API_BASE}${path}`);
|
const query = new URLSearchParams(params).toString();
|
||||||
|
const url = `${API_BASE}${path}?${query}`;
|
||||||
|
|
||||||
|
const res = await fetch(url);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = await res.text();
|
const data = await res.json();
|
||||||
throw new Error(text || "APIエラー");
|
throw new Error(data.detail || "APIエラー");
|
||||||
}
|
}
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|||||||
65
frontend/js/journal.js
Normal file
65
frontend/js/journal.js
Normal 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
37
frontend/js/ledger.js
Normal 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();
|
||||||
152
frontend/js/opening_balance.js
Normal file
152
frontend/js/opening_balance.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
document.getElementById("load").onclick = async () => {
|
async function loadTrialBalance() {
|
||||||
const from = document.getElementById("from").value;
|
const from = document.getElementById("from").value;
|
||||||
const to = document.getElementById("to").value;
|
const to = document.getElementById("to").value;
|
||||||
|
|
||||||
@@ -8,22 +8,33 @@ document.getElementById("load").onclick = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await apiGet(`/trial-balance?date_from=${from}&date_to=${to}`);
|
const data = await apiGet("/trial-balance", {
|
||||||
const tbody = document.getElementById("tb-body");
|
date_from: from,
|
||||||
|
date_to: to,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tbody = document.querySelector("#trial-table tbody");
|
||||||
tbody.innerHTML = "";
|
tbody.innerHTML = "";
|
||||||
|
|
||||||
data.accounts.forEach((a) => {
|
data.accounts.forEach((acc) => {
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
|
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td>${a.account_name}</td>
|
<td>${acc.account_code}</td>
|
||||||
<td style="text-align:right">${a.opening_balance}</td>
|
<td>
|
||||||
<td style="text-align:right">${a.debit}</td>
|
<a href="ledger.html?account_id=${acc.account_id}&from=${from}&to=${to}">
|
||||||
<td style="text-align:right">${a.credit}</td>
|
${acc.account_name}
|
||||||
<td style="text-align:right">${a.closing_balance}</td>
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>${acc.opening_balance}</td>
|
||||||
|
<td>${acc.debit}</td>
|
||||||
|
<td>${acc.credit}</td>
|
||||||
|
<td>${acc.closing_balance}</td>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e.message);
|
alert(e.message);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|||||||
29
frontend/ledger.html
Normal file
29
frontend/ledger.html
Normal 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>
|
||||||
48
frontend/opening_balance.html
Normal file
48
frontend/opening_balance.html
Normal 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
|
||||||
13
メモ.txt
13
メモ.txt
@@ -59,9 +59,19 @@ cd C:\workspace\njts-accounting-core\backend
|
|||||||
|
|
||||||
python -m venv .venv
|
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
|
cd C:\workspace\njts-accounting-core\backend
|
||||||
.venv\Scripts\activate
|
.venv\Scripts\activate
|
||||||
|
uvicorn app.main:app --reload --host 127.0.0.1 --port 18080
|
||||||
|
|
||||||
|
http://localhost:18080/docs
|
||||||
|
|
||||||
backend\requirements.txt
|
backend\requirements.txt
|
||||||
ーーーーーーーーーーーーー
|
ーーーーーーーーーーーーー
|
||||||
@@ -92,3 +102,6 @@ def root():
|
|||||||
|
|
||||||
|
|
||||||
pip install psycopg[binary]==3.2.3
|
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
|
||||||
Reference in New Issue
Block a user