Files
njts-accounting-core/backend/app/modules/trial_balance/service.py

547 lines
19 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.
# 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", "821", "814", "815", # 人件費系812:役員報酬 813:給料手当 821:賞与 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