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

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:
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,))
row = cur.fetchone()
date_from = (row["min_d"] or date.today()).isoformat()
if not acc:
raise HTTPException(status_code=400, detail="指定された科目は存在しません。")
if not date_to:
cur.execute("""
SELECT MAX(j.journal_date) AS max_d
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 より前)
# ----------------------------
# 期首残高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
""", (account_id, date_from))
opening = to_decimal(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, account_id))
# 4) 期間内明細
row = cur.fetchone()
opening_balance = D(row["opening"]) if row else D(0)
# ----------------------------
# 期間内仕訳取得
# ----------------------------
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
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
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"],
"description": r["description"],
"debit": str(d),
"credit": str(c),
"balance": str(balance) # 借方残を正とする(借方−貸方)
})
for r in rows:
debit = D(r["debit"])
credit = D(r["credit"])
balance += debit - credit
result = {
lines.append({
"journal_date": r["journal_date"],
"description": r["description"],
"debit": str(debit),
"credit": str(credit),
"balance": str(balance),
})
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;
"""