desktop修正
This commit is contained in:
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 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": "仕訳を削除しました。"}
|
||||
@@ -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
|
||||
|
||||
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
|
||||
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": "期首残高を保存しました。"}
|
||||
|
||||
@@ -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("""
|
||||
|
||||
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;
|
||||
"""
|
||||
Reference in New Issue
Block a user