新規作成

This commit is contained in:
admin
2025-12-13 23:35:45 +09:00
parent 446f17f21a
commit 49cf65a5cb
2696 changed files with 332525 additions and 0 deletions

View File

View File

@@ -0,0 +1,111 @@
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="金額の指定が不正です。")