This commit is contained in:
admin
2026-01-12 20:59:52 +09:00
parent 76d8e7622e
commit 59e8e8625d
7 changed files with 840 additions and 314 deletions

View File

@@ -1,7 +1,7 @@
from fastapi import APIRouter, HTTPException
from app.core.database import get_connection
from pydantic import BaseModel
from typing import List
from typing import List, Optional
from decimal import Decimal
from datetime import date
@@ -12,12 +12,17 @@ class JournalLine(BaseModel):
account_id: int
debit: Decimal = Decimal("0")
credit: Decimal = Decimal("0")
tax_rate: Optional[int] = None # 10 or 8 or None
tax_direction: Optional[str] = None # "paid" or "received" or None
class JournalEntryRequest(BaseModel):
entry_date: date
description: str
lines: List[JournalLine]
tax_paid_account_id: Optional[int] = None # 仮払消費税等 勘定
tax_received_account_id: Optional[int] = None # 仮受消費税等 勘定
rounding_mode: str = "round" # "round", "floor", or "ceil"
class JournalEntryUpdateRequest(BaseModel):
description: str
@@ -28,10 +33,183 @@ class JournalEntryUpdateRequest(BaseModel):
class JournalEntryDeleteRequest(BaseModel):
deleted_reason: str
@router.get("", summary="仕訳一覧取得")
def get_journal_entries(
from_date: date = None,
to_date: date = None,
keyword: str = None
):
"""
仕訳一覧を検索して取得
"""
with get_connection() as conn:
with conn.cursor() as cur:
# クエリ構築
sql = """
SELECT
je.journal_entry_id,
je.entry_date,
je.description,
je.fiscal_year,
COALESCE(SUM(jl.debit), 0) as debit_total,
COALESCE(SUM(jl.credit), 0) as credit_total
FROM journal_entries je
LEFT JOIN journal_lines jl ON je.journal_entry_id = jl.journal_entry_id
WHERE je.is_deleted = false
"""
params = []
if from_date:
sql += " AND je.entry_date >= %s"
params.append(from_date)
if to_date:
sql += " AND je.entry_date <= %s"
params.append(to_date)
if keyword:
sql += " AND je.description LIKE %s"
params.append(f"%{keyword}%")
sql += """
GROUP BY je.journal_entry_id, je.entry_date, je.description, je.fiscal_year
ORDER BY je.entry_date DESC, je.journal_entry_id DESC
"""
cur.execute(sql, params)
rows = cur.fetchall()
return [dict(row) for row in rows]
@router.get("/{journal_id}", summary="仕訳詳細取得")
def get_journal_entry(journal_id: int):
"""
指定された仕訳の詳細を取得
"""
with get_connection() as conn:
with conn.cursor() as cur:
# ヘッダー取得
cur.execute("""
SELECT
journal_entry_id,
entry_date,
description,
fiscal_year,
version,
is_deleted
FROM journal_entries
WHERE journal_entry_id = %s
""", (journal_id,))
header = cur.fetchone()
if not header:
raise HTTPException(status_code=404, detail="仕訳が存在しません")
# 明細取得
cur.execute("""
SELECT
jl.journal_line_id,
jl.account_id,
a.account_code,
a.account_name,
jl.debit,
jl.credit
FROM journal_lines jl
JOIN accounts a ON jl.account_id = a.account_id
WHERE jl.journal_entry_id = %s
ORDER BY jl.journal_line_id
""", (journal_id,))
lines = cur.fetchall()
return {
**dict(header),
"lines": [dict(line) for line in lines]
}
@router.post("/{journal_id}/reverse", summary="逆仕訳作成")
def create_reverse_entry(journal_id: int):
"""
指定された仕訳の逆仕訳を作成
"""
with get_connection() as conn:
with conn.cursor() as cur:
# 元仕訳取得
cur.execute("""
SELECT entry_date, description, fiscal_year
FROM journal_entries
WHERE journal_entry_id = %s AND is_deleted = false
""", (journal_id,))
header = cur.fetchone()
if not header:
raise HTTPException(status_code=404, detail="仕訳が存在しません")
# 元仕訳の明細取得
cur.execute("""
SELECT account_id, debit, credit
FROM journal_lines
WHERE journal_entry_id = %s
""", (journal_id,))
lines = cur.fetchall()
# 逆仕訳ヘッダー作成
cur.execute("""
INSERT INTO journal_entries (
entry_date,
description,
fiscal_year,
version,
is_deleted,
created_by
)
VALUES (%s, %s, %s, 1, false, %s)
RETURNING journal_entry_id
""", (
header["entry_date"],
f"[逆仕訳] {header['description']}",
header["fiscal_year"],
"system"
))
new_entry_id = cur.fetchone()["journal_entry_id"]
# 逆仕訳明細作成(借方と貸方を入れ替え)
for line in lines:
cur.execute("""
INSERT INTO journal_lines (
journal_entry_id,
account_id,
debit,
credit
)
VALUES (%s, %s, %s, %s)
""", (
new_entry_id,
line["account_id"],
line["credit"], # 借方と貸方を入れ替え
line["debit"]
))
conn.commit()
return {
"status": "ok",
"reversed_journal_entry_id": new_entry_id,
"original_journal_entry_id": journal_id
}
@router.post("", summary="仕訳登録")
def create_journal_entry(req: JournalEntryRequest):
from app.utils.month_lock import is_month_locked
from decimal import Decimal, ROUND_HALF_UP, ROUND_DOWN, ROUND_UP
if is_month_locked(req.entry_date):
raise HTTPException(
@@ -42,9 +220,57 @@ def create_journal_entry(req: JournalEntryRequest):
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)
# 税額計算と税行生成
all_lines = []
tax_aggregation = {} # {"paid": Decimal, "received": Decimal}
# 端数処理モード設定
rounding_map = {
"round": ROUND_HALF_UP,
"floor": ROUND_DOWN,
"ceil": ROUND_UP
}
rounding = rounding_map.get(req.rounding_mode, ROUND_HALF_UP)
for line in req.lines:
all_lines.append(line)
# 税対象行の場合、税額を計算
if line.tax_rate and line.tax_direction:
amount = line.debit if line.debit > 0 else line.credit
rate = Decimal(str(line.tax_rate)) / Decimal("100") # 10% -> 0.10
# 入力額は税抜額として、税額 = 税抜額 × 税率
raw_tax = amount * rate
tax_amount = raw_tax.quantize(Decimal("1"), rounding=rounding)
# 税方向ごとに集計
if line.tax_direction not in tax_aggregation:
tax_aggregation[line.tax_direction] = Decimal("0")
tax_aggregation[line.tax_direction] += tax_amount
# 集計した税額で税行を追加
if "paid" in tax_aggregation and tax_aggregation["paid"] > 0:
if not req.tax_paid_account_id:
raise HTTPException(status_code=400, detail="仮払消費税等 勘定を選択してください")
all_lines.append(JournalLine(
account_id=req.tax_paid_account_id,
debit=tax_aggregation["paid"],
credit=Decimal("0")
))
if "received" in tax_aggregation and tax_aggregation["received"] > 0:
if not req.tax_received_account_id:
raise HTTPException(status_code=400, detail="仮受消費税等 勘定を選択してください")
all_lines.append(JournalLine(
account_id=req.tax_received_account_id,
debit=Decimal("0"),
credit=tax_aggregation["received"]
))
# 借貸平衡校験
total_debit = sum(l.debit for l in all_lines)
total_credit = sum(l.credit for l in all_lines)
if total_debit != total_credit:
raise HTTPException(status_code=400, detail="借方和贷方不平衡")
@@ -53,43 +279,43 @@ def create_journal_entry(req: JournalEntryRequest):
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
INSERT INTO journal_entries (
entry_date,
description,
fiscal_year,
version,
is_deleted,
created_by
)
VALUES (%s, %s, %s, %s)
VALUES (%s, %s, %s, 1, false, %s)
RETURNING journal_entry_id
""", (
entry_id,
line.account_id,
line.debit,
line.credit
req.entry_date,
req.description,
fiscal_year,
"system"
))
entry_id = cur.fetchone()["journal_entry_id"]
# 明細(税行を含む)
for line in all_lines:
cur.execute("""
INSERT INTO journal_lines (
journal_entry_id,
account_id,
debit,
credit
)
VALUES (%s, %s, %s, %s)
""", (
entry_id,
line.account_id,
line.debit,
line.credit
))
conn.commit()
return {
@@ -179,7 +405,7 @@ def update_journal_entry(journal_id: int, req: JournalEntryUpdateRequest):
@router.delete("/{journal_id}", summary="仕訳除(软删除)")
@router.delete("/{journal_id}", summary="仕訳除(ソフト削除)")
def delete_journal_entry(journal_id: int, req: JournalEntryDeleteRequest):
with get_connection() as conn:
@@ -187,21 +413,17 @@ def delete_journal_entry(journal_id: int, req: JournalEntryDeleteRequest):
cur.execute("""
UPDATE journal_entries
SET is_deleted = true,
deleted_at = NOW(),
deleted_reason = %s
WHERE journal_id = %s
updated_at = NOW()
WHERE journal_entry_id = %s
AND is_deleted = false
RETURNING journal_id
""", (
req.deleted_reason,
journal_id
))
RETURNING journal_entry_id
""", (journal_id,))
row = cur.fetchone()
if not row:
raise HTTPException(
status_code=404,
detail="仕訳存在,或已被删除"
detail="仕訳存在しないか、既に削除されています"
)
conn.commit()