Files
njts-accounting-core/backend/app/modules/journals/router.py
2025-12-13 23:35:45 +09:00

112 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, field_validator
from typing import List
from decimal import Decimal, InvalidOperation
from psycopg.errors import ForeignKeyViolation, CheckViolation
from app.core.database import get_connection
router = APIRouter(prefix="/journals", tags=["仕訳"])
class JournalLine(BaseModel):
account_id: int
debit: Decimal = Decimal("0")
credit: Decimal = Decimal("0")
@field_validator("debit", "credit", mode="before")
@classmethod
def to_decimal(cls, v):
# 允许传 0 / 整数 / 字符串
try:
d = Decimal(str(v))
except InvalidOperation:
raise ValueError("金額は数値で入力してください")
if d < 0:
raise ValueError("金額は0以上で入力してください")
return d
@field_validator("credit")
@classmethod
def exclusive_debit_credit(cls, v, info):
# 不能借贷同时 > 0也不能都为 0 的校验在整体里做
return v
class JournalCreate(BaseModel):
journal_date: str # YYYY-MM-DD
description: str
lines: List[JournalLine]
@field_validator("journal_date")
@classmethod
def validate_date(cls, v: str):
# 简单校验格式,严格校验可用 datetime.date.fromisoformat
if len(v) != 10 or v[4] != "-" or v[7] != "-":
raise ValueError("日付はYYYY-MM-DD形式で入力してください")
return v
@field_validator("description")
@classmethod
def validate_desc(cls, v: str):
if not v.strip():
raise ValueError("摘要を入力してください")
if len(v) > 255:
raise ValueError("摘要が長すぎます255文字以内")
return v
@router.post("/", summary="仕訳登録", status_code=201)
def create_journal(data: JournalCreate):
# 1) 行必須
if not data.lines:
raise HTTPException(status_code=400, detail="仕訳明細を1行以上入力してください。")
# 2) 借貸排他 & 非ゼロ
for i, l in enumerate(data.lines, start=1):
if l.debit > 0 and l.credit > 0:
raise HTTPException(status_code=400, detail=f"{i}行目:借方と貸方を同時に入力しないでください。")
if l.debit == 0 and l.credit == 0:
raise HTTPException(status_code=400, detail=f"{i}行目:金額が未入力です(借方または貸方のどちらかに金額を入力)。")
# 3) 借貸一致
debit_total = sum(l.debit for l in data.lines)
credit_total = sum(l.credit for l in data.lines)
if debit_total != credit_total:
raise HTTPException(status_code=400, detail="借方合計と貸方合計が一致しません。")
# 4) 仕訳に使う科目が全て存在するか事前チェック
account_ids = sorted({l.account_id for l in data.lines})
with get_connection() as conn, conn.cursor() as cur:
cur.execute(
"SELECT account_id FROM accounts WHERE account_id = ANY(%s)",
(account_ids,)
)
found = {row["account_id"] for row in cur.fetchall()}
missing = [a for a in account_ids if a not in found]
if missing:
raise HTTPException(
status_code=400,
detail=f"存在しない科目IDが含まれています{missing}"
)
try:
# 5) 登録(ヘッダ → 明細)
cur.execute(
"INSERT INTO journal_entries (journal_date, description) VALUES (%s, %s) RETURNING journal_id",
(data.journal_date, data.description),
)
jid = cur.fetchone()["journal_id"]
for l in data.lines:
cur.execute(
"""INSERT INTO journal_lines (journal_id, account_id, debit, credit)
VALUES (%s, %s, %s, %s)""",
(jid, l.account_id, l.debit, l.credit),
)
conn.commit()
return {"journal_id": jid}
except ForeignKeyViolation:
# 理论上到不了(我们前面已做存在性检查),兜底
raise HTTPException(status_code=400, detail="科目IDが不正です。")
except CheckViolation:
raise HTTPException(status_code=400, detail="金額の指定が不正です。")