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 }