547 lines
19 KiB
Python
547 lines
19 KiB
Python
# backend/app/modules/trial_balance/service.py
|
||
# 試算表.pdf フォーマットに完全準拠した試算表サービス
|
||
from decimal import Decimal
|
||
from typing import Dict, Any, List
|
||
from app.core.database import get_connection
|
||
|
||
def D(x) -> Decimal:
|
||
return Decimal(str(x or 0))
|
||
|
||
# 除外する科目コード
|
||
EXCLUDED_CODES = {'512', '513', '516'}
|
||
|
||
|
||
def fetch_trial_balance(date_from: str, date_to: str):
|
||
|
||
with get_connection() as conn, conn.cursor() as cur:
|
||
|
||
# 勘定科目一覧
|
||
cur.execute("""
|
||
SELECT account_id, account_code, account_name, account_type
|
||
FROM accounts
|
||
WHERE is_active = true
|
||
ORDER BY account_code
|
||
""")
|
||
accounts = cur.fetchall()
|
||
|
||
# 科目データを格納
|
||
account_data: Dict[str, Dict[str, Any]] = {}
|
||
|
||
for acc in accounts:
|
||
aid = acc["account_id"]
|
||
code = acc["account_code"]
|
||
|
||
# 除外科目をスキップ
|
||
if code in EXCLUDED_CODES:
|
||
continue
|
||
|
||
# 期首残高: opening_balances テーブルの残高 + date_from より前の仕訳
|
||
cur.execute("""
|
||
SELECT COALESCE(opening_debit, 0) - COALESCE(opening_credit, 0) AS ob
|
||
FROM opening_balances
|
||
WHERE account_id = %s
|
||
AND fiscal_year = EXTRACT(YEAR FROM %s::date)
|
||
""", (aid, date_from))
|
||
ob_row = cur.fetchone()
|
||
opening_balance_from_table = D(ob_row["ob"]) if ob_row else D(0)
|
||
|
||
# date_from より前の仕訳による増減
|
||
cur.execute("""
|
||
SELECT COALESCE(SUM(l.debit - l.credit), 0) AS opening
|
||
FROM journal_lines l
|
||
JOIN journal_entries j
|
||
ON j.journal_entry_id = l.journal_entry_id
|
||
WHERE l.account_id = %s
|
||
AND j.entry_date < %s
|
||
AND j.is_deleted = false
|
||
AND j.is_latest = true
|
||
""", (aid, date_from))
|
||
opening_from_journals = D(cur.fetchone()["opening"])
|
||
|
||
opening = opening_balance_from_table + opening_from_journals
|
||
|
||
# 当期増減(date_from ~ date_to)
|
||
cur.execute("""
|
||
SELECT
|
||
COALESCE(SUM(l.debit), 0) AS debit,
|
||
COALESCE(SUM(l.credit), 0) AS credit
|
||
FROM journal_lines l
|
||
JOIN journal_entries j
|
||
ON j.journal_entry_id = l.journal_entry_id
|
||
WHERE l.account_id = %s
|
||
AND j.entry_date BETWEEN %s AND %s
|
||
AND j.is_deleted = false
|
||
AND j.is_latest = true
|
||
""", (aid, date_from, date_to))
|
||
row = cur.fetchone()
|
||
debit = D(row["debit"])
|
||
credit = D(row["credit"])
|
||
|
||
closing = opening + debit - credit
|
||
|
||
# 負債・資本は絶対値で表示
|
||
if acc["account_type"] in ("liability", "equity"):
|
||
opening = abs(opening)
|
||
closing = abs(closing)
|
||
|
||
account_data[code] = {
|
||
"account_id": aid,
|
||
"account_code": code,
|
||
"account_name": acc["account_name"],
|
||
"account_type": acc["account_type"],
|
||
"opening_balance": opening,
|
||
"debit": debit,
|
||
"credit": credit,
|
||
"closing_balance": closing,
|
||
}
|
||
|
||
# =============================================
|
||
# ヘルパー関数
|
||
# =============================================
|
||
def acct_row(code):
|
||
"""個別科目の行を返す(存在しない場合はNone)"""
|
||
if code not in account_data:
|
||
return None
|
||
d = account_data[code]
|
||
return {
|
||
"account_code": d["account_code"],
|
||
"account_name": d["account_name"],
|
||
"account_type": d["account_type"],
|
||
"opening_balance": str(d["opening_balance"]),
|
||
"debit": str(d["debit"]),
|
||
"credit": str(d["credit"]),
|
||
"closing_balance": str(d["closing_balance"]),
|
||
"ratio": None,
|
||
"is_total": False,
|
||
}
|
||
|
||
def total_row(name, codes, is_total=True, indent=False):
|
||
"""合計行を生成"""
|
||
existing = [c for c in codes if c in account_data]
|
||
opening = sum(account_data[c]["opening_balance"] for c in existing) if existing else D(0)
|
||
debit = sum(account_data[c]["debit"] for c in existing) if existing else D(0)
|
||
credit = sum(account_data[c]["credit"] for c in existing) if existing else D(0)
|
||
closing = sum(account_data[c]["closing_balance"] for c in existing) if existing else D(0)
|
||
return {
|
||
"account_code": "",
|
||
"account_name": name,
|
||
"account_type": "",
|
||
"opening_balance": str(opening),
|
||
"debit": str(debit),
|
||
"credit": str(credit),
|
||
"closing_balance": str(closing),
|
||
"ratio": None,
|
||
"is_total": is_total,
|
||
"indent": indent,
|
||
}
|
||
|
||
def custom_row(name, opening, debit, credit, closing,
|
||
is_total=False, indent=False, account_type="", is_pl=False):
|
||
"""カスタム行を生成"""
|
||
row = {
|
||
"account_code": "",
|
||
"account_name": name,
|
||
"account_type": account_type,
|
||
"opening_balance": str(opening),
|
||
"debit": str(debit),
|
||
"credit": str(credit),
|
||
"closing_balance": str(closing),
|
||
"ratio": None,
|
||
"is_total": is_total,
|
||
"indent": indent,
|
||
}
|
||
if is_pl:
|
||
row["is_pl"] = True
|
||
return row
|
||
|
||
# =============================================
|
||
# 結果リスト構築(試算表.pdf の順序に完全準拠)
|
||
# =============================================
|
||
result_accounts: List[Dict[str, Any]] = []
|
||
added_codes = set() # 追加済みの科目コードを追跡
|
||
|
||
def add(code):
|
||
"""個別科目を追加"""
|
||
r = acct_row(code)
|
||
if r:
|
||
result_accounts.append(r)
|
||
added_codes.add(code)
|
||
|
||
def add_total(name, codes, **kwargs):
|
||
"""合計行を追加"""
|
||
result_accounts.append(total_row(name, codes, **kwargs))
|
||
|
||
# ========== 資産の部 ==========
|
||
|
||
# 現金・預金
|
||
cash_codes = ["101", "102", "103", "104", "105"]
|
||
for c in cash_codes:
|
||
add(c)
|
||
add_total("現金・預金合計", cash_codes)
|
||
|
||
# 売上債権
|
||
receivable_codes = ["201", "202"]
|
||
for c in receivable_codes:
|
||
add(c)
|
||
add_total("売上債権合計", receivable_codes)
|
||
|
||
# 他流動資産
|
||
other_current_codes = ["203", "204", "205", "206", "207", "208"]
|
||
for c in other_current_codes:
|
||
add(c)
|
||
add_total("他流動資産合計", other_current_codes)
|
||
|
||
# 有価証券
|
||
securities_codes = ["301"]
|
||
for c in securities_codes:
|
||
add(c)
|
||
add_total("有価証券合計", securities_codes)
|
||
|
||
# 流動資産合計
|
||
current_asset_codes = cash_codes + receivable_codes + other_current_codes + securities_codes
|
||
add_total("流動資産合計", current_asset_codes)
|
||
|
||
# 有形固定資産
|
||
tangible_codes = ["401", "402", "403", "404", "405", "406", "407"]
|
||
for c in tangible_codes:
|
||
add(c)
|
||
add_total("有形固定資産合計", tangible_codes)
|
||
|
||
# 無形固定資産
|
||
intangible_codes = ["408"]
|
||
for c in intangible_codes:
|
||
add(c)
|
||
add_total("無形固定資産合計", intangible_codes)
|
||
|
||
# 投資その他の資産
|
||
investment_codes = ["410"]
|
||
for c in investment_codes:
|
||
add(c)
|
||
add_total("投資その他の資産合計", investment_codes)
|
||
|
||
# 固定資産合計
|
||
fixed_asset_codes = tangible_codes + intangible_codes + investment_codes
|
||
add_total("固定資産合計", fixed_asset_codes)
|
||
|
||
# 資産合計
|
||
all_asset_codes = current_asset_codes + fixed_asset_codes
|
||
add_total("資産合計", all_asset_codes)
|
||
|
||
# ========== 負債の部 ==========
|
||
|
||
# 仕入債務
|
||
trade_payable_codes = ["501"]
|
||
for c in trade_payable_codes:
|
||
add(c)
|
||
add_total("仕入債務合計", trade_payable_codes)
|
||
|
||
# 流動負債その他(502-508)
|
||
current_liability_other = ["502", "503", "504", "505", "506", "507", "508"]
|
||
for c in current_liability_other:
|
||
add(c)
|
||
current_liability_codes = trade_payable_codes + current_liability_other
|
||
add_total("流動負債合計", current_liability_codes)
|
||
|
||
# 固定負債
|
||
fixed_liability_codes = ["509"]
|
||
for c in fixed_liability_codes:
|
||
add(c)
|
||
add_total("固定負債合計", fixed_liability_codes)
|
||
|
||
# 負債合計
|
||
all_liability_codes = current_liability_codes + fixed_liability_codes
|
||
add_total("負債合計", all_liability_codes)
|
||
|
||
# ========== 510/511: 独立科目(負債と資本の間) ==========
|
||
add("510")
|
||
add("511")
|
||
|
||
# ========== 資本の部 ==========
|
||
|
||
# 資本金
|
||
capital_codes = ["601"]
|
||
for c in capital_codes:
|
||
add(c)
|
||
add_total("資本金合計", capital_codes)
|
||
|
||
# 繰越利益剰余金(602)
|
||
add("602")
|
||
|
||
# --- 当期純損益金額の計算 ---
|
||
revenue_total = D(0)
|
||
for acc_code, acc_data in account_data.items():
|
||
if acc_code.startswith("7"):
|
||
revenue_total += acc_data["credit"] - acc_data["debit"]
|
||
expense_total = D(0)
|
||
for acc_code, acc_data in account_data.items():
|
||
if acc_code.startswith("8"):
|
||
expense_total += acc_data["debit"] - acc_data["credit"]
|
||
net_income = revenue_total - expense_total
|
||
|
||
# 繰越利益剰余金の期首残高
|
||
retained_opening = account_data.get("602", {}).get("opening_balance", D(0))
|
||
retained_total = retained_opening + net_income
|
||
|
||
# 繰越利益
|
||
result_accounts.append(
|
||
custom_row("繰越利益", retained_opening, D(0), D(0), retained_opening,
|
||
indent=True, account_type="equity")
|
||
)
|
||
|
||
# 繰越利益剰余金合計
|
||
result_accounts.append(
|
||
custom_row("繰越利益剰余金合計", retained_opening,
|
||
abs(net_income) if net_income < 0 else D(0),
|
||
net_income if net_income > 0 else D(0),
|
||
retained_total,
|
||
is_total=True, indent=True, account_type="equity")
|
||
)
|
||
|
||
# その他利益剰余金合計
|
||
result_accounts.append(
|
||
custom_row("その他利益剰余金合計", D(0), D(0), D(0), D(0),
|
||
is_total=True, indent=True, account_type="equity")
|
||
)
|
||
|
||
# 利益剰余金合計
|
||
result_accounts.append(
|
||
custom_row("利益剰余金合計", retained_opening,
|
||
abs(net_income) if net_income < 0 else D(0),
|
||
net_income if net_income > 0 else D(0),
|
||
retained_total,
|
||
is_total=True, account_type="equity")
|
||
)
|
||
|
||
# 純資産合計
|
||
capital_closing = account_data.get("601", {}).get("closing_balance", D(0))
|
||
equity_total_closing = capital_closing + retained_total
|
||
equity_total_opening = sum(
|
||
account_data[c]["opening_balance"] for c in ["601", "602"] if c in account_data
|
||
)
|
||
result_accounts.append(
|
||
custom_row("純資産合計", equity_total_opening, D(0), D(0), equity_total_closing,
|
||
is_total=True, account_type="equity")
|
||
)
|
||
|
||
# ========== 損益の部(截図フォーマットに準拠) ==========
|
||
|
||
# --- P/L 用ヘルパー ---
|
||
def pl_row(code, indent=False):
|
||
"""P/L個別科目行(closing = credit-debit or debit-credit)"""
|
||
if code not in account_data:
|
||
return None
|
||
d = account_data[code]
|
||
if code.startswith("7"):
|
||
pl_closing = d["credit"] - d["debit"]
|
||
else:
|
||
pl_closing = d["debit"] - d["credit"]
|
||
return {
|
||
"account_code": d["account_code"],
|
||
"account_name": d["account_name"],
|
||
"account_type": d["account_type"],
|
||
"opening_balance": "0",
|
||
"debit": str(d["debit"]),
|
||
"credit": str(d["credit"]),
|
||
"closing_balance": str(pl_closing),
|
||
"ratio": None,
|
||
"is_total": False,
|
||
"indent": indent,
|
||
"is_pl": True,
|
||
}
|
||
|
||
def add_pl(code, indent=False):
|
||
r = pl_row(code, indent)
|
||
if r:
|
||
result_accounts.append(r)
|
||
added_codes.add(code)
|
||
|
||
# === 売上高 ===
|
||
add_pl("701")
|
||
|
||
# 売上高合計
|
||
rev_d = account_data.get("701", {})
|
||
revenue_val = rev_d.get("credit", D(0)) - rev_d.get("debit", D(0)) if "701" in account_data else D(0)
|
||
result_accounts.append(
|
||
custom_row("売上高合計", D(0), D(0), revenue_val, revenue_val, is_total=True, is_pl=True)
|
||
)
|
||
|
||
# === 売上原価セクション ===
|
||
# 期首商品棚卸高
|
||
result_accounts.append(
|
||
custom_row("期首商品棚卸高", D(0), D(0), D(0), D(0), indent=True, is_pl=True)
|
||
)
|
||
# 当期商品仕入高(= 801 仕入高)
|
||
if "801" in account_data:
|
||
add_pl("801", indent=True)
|
||
else:
|
||
result_accounts.append(
|
||
custom_row("当期商品仕入高", D(0), D(0), D(0), D(0), indent=True, is_pl=True)
|
||
)
|
||
added_codes.add("801")
|
||
# 合計
|
||
cogs_d = account_data.get("801", {})
|
||
cogs_debit = cogs_d.get("debit", D(0)) if "801" in account_data else D(0)
|
||
cogs_credit = cogs_d.get("credit", D(0)) if "801" in account_data else D(0)
|
||
cogs_net = cogs_debit - cogs_credit
|
||
result_accounts.append(
|
||
custom_row("合計", D(0), cogs_debit, cogs_credit, cogs_net,
|
||
is_total=True, indent=True, is_pl=True)
|
||
)
|
||
# 期末商品棚卸高
|
||
result_accounts.append(
|
||
custom_row("期末商品棚卸高", D(0), D(0), D(0), D(0), indent=True, is_pl=True)
|
||
)
|
||
# 売上原価
|
||
result_accounts.append(
|
||
custom_row("売上原価", D(0), cogs_debit, cogs_credit, cogs_net,
|
||
is_total=True, indent=True, is_pl=True)
|
||
)
|
||
|
||
# === 売上総損益金額 ===
|
||
gross_profit = revenue_val - cogs_net
|
||
result_accounts.append(
|
||
custom_row("売上総損益金額", D(0), D(0), gross_profit, gross_profit,
|
||
is_total=True, is_pl=True)
|
||
)
|
||
|
||
# === 販売管理費(截図の順序に準拠) ===
|
||
sga_order = [
|
||
"812", "813", "814", "815", # 人件費系
|
||
"802", # 外注費
|
||
"816", "817", # 交際費・会議費
|
||
"803", "804", "805", # 旅費・通信・消耗品
|
||
"818", # 新聞図書費
|
||
"808", # 支払手数料
|
||
"806", # 地代家賃
|
||
"819", "820", # 保険料・租税公課
|
||
]
|
||
# 上記リストにない8xx費用科目を末尾に追加(801除外)
|
||
remaining_8xx = [c for c in sorted(account_data.keys())
|
||
if c.startswith("8") and c != "801" and c not in sga_order]
|
||
sga_all = sga_order + remaining_8xx
|
||
|
||
sga_debit_total = D(0)
|
||
sga_credit_total = D(0)
|
||
sga_closing_total = D(0)
|
||
for code in sga_all:
|
||
add_pl(code, indent=True)
|
||
if code in account_data:
|
||
d = account_data[code]
|
||
sga_debit_total += d["debit"]
|
||
sga_credit_total += d["credit"]
|
||
sga_closing_total += d["debit"] - d["credit"]
|
||
|
||
# 販売管理費計
|
||
result_accounts.append(
|
||
custom_row("販売管理費計", D(0), sga_debit_total, sga_credit_total,
|
||
sga_closing_total, is_total=True, is_pl=True)
|
||
)
|
||
|
||
# === 営業損益金額 ===
|
||
operating_profit = gross_profit - sga_closing_total
|
||
result_accounts.append(
|
||
custom_row("営業損益金額", D(0), D(0), operating_profit, operating_profit,
|
||
is_total=True, is_pl=True)
|
||
)
|
||
|
||
# === 営業外収益 ===
|
||
non_op_rev_codes = ["702", "709"]
|
||
non_op_rev_debit = D(0)
|
||
non_op_rev_credit = D(0)
|
||
for code in non_op_rev_codes:
|
||
add_pl(code, indent=True)
|
||
if code in account_data:
|
||
d = account_data[code]
|
||
non_op_rev_debit += d["debit"]
|
||
non_op_rev_credit += d["credit"]
|
||
non_op_rev_closing = non_op_rev_credit - non_op_rev_debit
|
||
|
||
# 営業外収益合計
|
||
result_accounts.append(
|
||
custom_row("営業外収益合計", D(0), non_op_rev_debit, non_op_rev_credit,
|
||
non_op_rev_closing, is_total=True, indent=True, is_pl=True)
|
||
)
|
||
|
||
# 営業外費用合計(現在は該当科目なし)
|
||
result_accounts.append(
|
||
custom_row("営業外費用合計", D(0), D(0), D(0), D(0),
|
||
is_total=True, indent=True, is_pl=True)
|
||
)
|
||
|
||
# === 経常損益金額 ===
|
||
ordinary_profit = operating_profit + non_op_rev_closing
|
||
result_accounts.append(
|
||
custom_row("経常損益金額", D(0), D(0), ordinary_profit, ordinary_profit,
|
||
is_total=True, is_pl=True)
|
||
)
|
||
|
||
# === 特別利益合計 ===
|
||
result_accounts.append(
|
||
custom_row("特別利益合計", D(0), D(0), D(0), D(0),
|
||
is_total=True, indent=True, is_pl=True)
|
||
)
|
||
|
||
# === 特別損失合計 ===
|
||
result_accounts.append(
|
||
custom_row("特別損失合計", D(0), D(0), D(0), D(0),
|
||
is_total=True, indent=True, is_pl=True)
|
||
)
|
||
|
||
# === 当期純損益金額 ===
|
||
result_accounts.append(
|
||
custom_row("当期純損益金額", D(0), D(0), ordinary_profit, ordinary_profit,
|
||
is_total=True, is_pl=True)
|
||
)
|
||
|
||
# ========== 未分類科目(上記にない科目を末尾に追加) ==========
|
||
for code in sorted(account_data.keys()):
|
||
if code not in added_codes:
|
||
add(code)
|
||
|
||
# =============================================
|
||
# 構成比を計算
|
||
# B/S: 借方残合計ベース
|
||
# P/L: 売上高(対売上比)ベース
|
||
# =============================================
|
||
debit_balance_total = sum(
|
||
d["closing_balance"] for d in account_data.values()
|
||
if d["closing_balance"] > 0
|
||
)
|
||
|
||
for acc in result_accounts:
|
||
closing = D(acc["closing_balance"])
|
||
if acc.get("is_pl"):
|
||
# P/L: 対売上比
|
||
if revenue_val != 0:
|
||
ratio = (closing / revenue_val) * 100
|
||
acc["ratio"] = f"{ratio:.2f}"
|
||
else:
|
||
acc["ratio"] = "0.00"
|
||
else:
|
||
# B/S: 借方残合計ベース
|
||
if debit_balance_total != 0:
|
||
ratio = (closing / debit_balance_total) * 100
|
||
acc["ratio"] = f"{ratio:.2f}"
|
||
else:
|
||
acc["ratio"] = "0.00"
|
||
|
||
# 総合計を計算
|
||
total_opening = sum(d["opening_balance"] for d in account_data.values())
|
||
total_debit = sum(d["debit"] for d in account_data.values())
|
||
total_credit = sum(d["credit"] for d in account_data.values())
|
||
total_closing = sum(d["closing_balance"] for d in account_data.values())
|
||
|
||
result = {
|
||
"totals": {
|
||
"opening_balance": str(total_opening),
|
||
"debit": str(total_debit),
|
||
"credit": str(total_credit),
|
||
"closing_balance": str(total_closing),
|
||
"ratio": "100.00"
|
||
},
|
||
"accounts": result_accounts,
|
||
"grand_total_closing": str(debit_balance_total),
|
||
}
|
||
|
||
return result
|