# 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