213 lines
6.1 KiB
Python
213 lines
6.1 KiB
Python
from fastapi import APIRouter, HTTPException
|
||
from app.core.database import get_connection
|
||
from pydantic import BaseModel
|
||
from typing import List
|
||
from decimal import Decimal
|
||
from datetime import date
|
||
|
||
router = APIRouter(prefix="/journal-entries", tags=["仕訳"])
|
||
|
||
|
||
class JournalLine(BaseModel):
|
||
account_id: int
|
||
debit: Decimal = Decimal("0")
|
||
credit: Decimal = Decimal("0")
|
||
|
||
|
||
class JournalEntryRequest(BaseModel):
|
||
entry_date: date
|
||
description: str
|
||
lines: List[JournalLine]
|
||
|
||
class JournalEntryUpdateRequest(BaseModel):
|
||
description: str
|
||
lines: List[JournalLine]
|
||
updated_reason: str
|
||
|
||
|
||
class JournalEntryDeleteRequest(BaseModel):
|
||
deleted_reason: str
|
||
|
||
@router.post("", summary="仕訳登録")
|
||
def create_journal_entry(req: JournalEntryRequest):
|
||
|
||
from app.utils.month_lock import is_month_locked
|
||
|
||
if is_month_locked(req.entry_date):
|
||
raise HTTPException(
|
||
status_code=400,
|
||
detail="该月份已锁定,不能新增仕訳"
|
||
)
|
||
|
||
if not req.lines:
|
||
raise HTTPException(status_code=400, detail="仕訳明細不能为空")
|
||
|
||
# (可选)简单的借贷平衡校验
|
||
total_debit = sum(l.debit for l in req.lines)
|
||
total_credit = sum(l.credit for l in req.lines)
|
||
if total_debit != total_credit:
|
||
raise HTTPException(status_code=400, detail="借方和贷方不平衡")
|
||
|
||
fiscal_year = req.entry_date.year
|
||
|
||
with get_connection() as conn:
|
||
with conn.cursor() as cur:
|
||
# 仕訳ヘッダ
|
||
cur.execute("""
|
||
INSERT INTO journal_entries (
|
||
journal_date,
|
||
description,
|
||
fiscal_year,
|
||
version,
|
||
is_deleted,
|
||
created_by
|
||
)
|
||
VALUES (%s, %s, %s, 1, false, %s)
|
||
RETURNING journal_id
|
||
""", (
|
||
req.entry_date,
|
||
req.description,
|
||
fiscal_year,
|
||
"system" # 以后可以换成登录用户
|
||
))
|
||
|
||
entry_id = cur.fetchone()["journal_id"]
|
||
|
||
# 明細
|
||
for line in req.lines:
|
||
cur.execute("""
|
||
INSERT INTO journal_lines (
|
||
journal_id,
|
||
account_id,
|
||
debit,
|
||
credit
|
||
)
|
||
VALUES (%s, %s, %s, %s)
|
||
""", (
|
||
entry_id,
|
||
line.account_id,
|
||
line.debit,
|
||
line.credit
|
||
))
|
||
|
||
conn.commit()
|
||
|
||
return {
|
||
"status": "ok",
|
||
"journal_entry_id": entry_id
|
||
}
|
||
|
||
|
||
@router.put("/{journal_id}")
|
||
def update_journal_entry(journal_id: int, req: JournalEntryUpdateRequest):
|
||
from app.utils.month_lock import is_month_locked
|
||
|
||
with get_connection() as conn:
|
||
with conn.cursor() as cur:
|
||
|
||
# ① 取得现有仕訳
|
||
cur.execute("""
|
||
SELECT journal_date, is_deleted, version
|
||
FROM journal_entries
|
||
WHERE journal_id = %s
|
||
""", (journal_id,))
|
||
row = cur.fetchone()
|
||
|
||
if not row:
|
||
raise HTTPException(status_code=404, detail="仕訳が存在しません")
|
||
|
||
if row["is_deleted"]:
|
||
raise HTTPException(status_code=400, detail="削除済み仕訳は更新できません")
|
||
|
||
journal_date = row["journal_date"]
|
||
|
||
# 借贷平衡校验(update)
|
||
total_debit = sum(l.debit for l in req.lines)
|
||
total_credit = sum(l.credit for l in req.lines)
|
||
|
||
if total_debit != total_credit:
|
||
raise HTTPException(status_code=400, detail="借方和贷方不平衡")
|
||
|
||
|
||
# ② 月次锁定检查
|
||
if is_month_locked(journal_date):
|
||
raise HTTPException(status_code=400, detail="该月份已锁定,不能修改仕訳")
|
||
|
||
# ③ 更新 header
|
||
cur.execute("""
|
||
UPDATE journal_entries
|
||
SET description = %s,
|
||
version = version + 1,
|
||
updated_at = NOW(),
|
||
updated_reason = %s
|
||
WHERE journal_id = %s
|
||
""", (
|
||
req.description,
|
||
req.updated_reason,
|
||
journal_id
|
||
))
|
||
|
||
# ④ 删除原有明细(物理删除,OK)
|
||
cur.execute("""
|
||
DELETE FROM journal_lines
|
||
WHERE journal_id = %s
|
||
""", (journal_id,))
|
||
|
||
# ⑤ 插入新明细
|
||
for line in req.lines:
|
||
cur.execute("""
|
||
INSERT INTO journal_lines (
|
||
journal_id,
|
||
account_id,
|
||
debit,
|
||
credit
|
||
) VALUES (%s, %s, %s, %s)
|
||
""", (
|
||
journal_id,
|
||
line.account_id,
|
||
line.debit,
|
||
line.credit
|
||
))
|
||
|
||
conn.commit()
|
||
|
||
return {
|
||
"status": "ok",
|
||
"journal_id": journal_id,
|
||
"message": "仕訳を更新しました"
|
||
}
|
||
|
||
|
||
|
||
@router.delete("/{journal_id}", summary="仕訳删除(软删除)")
|
||
def delete_journal_entry(journal_id: int, req: JournalEntryDeleteRequest):
|
||
|
||
with get_connection() as conn:
|
||
with conn.cursor() as cur:
|
||
cur.execute("""
|
||
UPDATE journal_entries
|
||
SET is_deleted = true,
|
||
deleted_at = NOW(),
|
||
deleted_reason = %s
|
||
WHERE journal_id = %s
|
||
AND is_deleted = false
|
||
RETURNING journal_id
|
||
""", (
|
||
req.deleted_reason,
|
||
journal_id
|
||
))
|
||
|
||
row = cur.fetchone()
|
||
if not row:
|
||
raise HTTPException(
|
||
status_code=404,
|
||
detail="仕訳不存在,或已被删除"
|
||
)
|
||
|
||
conn.commit()
|
||
|
||
return {
|
||
"status": "deleted",
|
||
"journal_entry_id": journal_id
|
||
}
|