修正
This commit is contained in:
@@ -85,7 +85,7 @@ app.include_router(cash.router)
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
app.mount(
|
||||
"/",
|
||||
"/static",
|
||||
StaticFiles(directory="app/static", html=True),
|
||||
name="static",
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user