diff --git a/backend/app/modules/trial_balance/service.py b/backend/app/modules/trial_balance/service.py
index 80c8ff1..265e5a3 100644
--- a/backend/app/modules/trial_balance/service.py
+++ b/backend/app/modules/trial_balance/service.py
@@ -1,4 +1,5 @@
# 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
@@ -6,30 +7,9 @@ from app.core.database import get_connection
def D(x) -> Decimal:
return Decimal(str(x or 0))
-# 科目グループ定義(表示順序を制御)- PDFフォーマットに準拠
-ACCOUNT_GROUPS = [
- # 資産の部
- ("現金・預金合計", ["101", "102", "103", "104", "105"]),
- ("売上債権合計", ["201", "202"]),
- ("有価証券合計", ["301"]),
- ("棚卸資産合計", ["210"]),
- ("他流動資産合計", ["203", "204", "205", "206", "207", "208"]),
- ("流動資産合計", ["101", "102", "103", "104", "105", "201", "202", "203", "204", "205", "206", "207", "208", "210", "301"]),
- ("有形固定資産合計", ["401", "402", "403", "404", "405", "406", "407"]),
- ("無形固定資産合計", ["408"]),
- ("投資その他の資産合計", ["301", "410"]),
- ("固定資産合計", ["301", "401", "402", "403", "404", "405", "406", "407", "408", "410"]),
- ("資産合計", ["101", "102", "103", "104", "105", "201", "202", "203", "204", "205", "206", "207", "208", "210", "301", "401", "402", "403", "404", "405", "406", "407", "408", "410"]),
- # 負債の部
- ("仕入債務合計", ["501"]),
- ("流動負債合計", ["501", "502", "503", "504", "505", "506", "507", "508"]),
- ("固定負債合計", ["509"]),
- ("負債合計", ["501", "502", "503", "504", "505", "506", "507", "508", "509"]),
- # 資本の部
- ("資本金合計", ["601"]),
- # 利益剰余金合計は特別処理(602 + 当期純利益の内訳を表示)
- ("純資産合計", ["601", "602"])
-]
+# 除外する科目コード
+EXCLUDED_CODES = {'512', '513', '516'}
+
def fetch_trial_balance(date_from: str, date_to: str):
@@ -44,7 +24,6 @@ def fetch_trial_balance(date_from: str, date_to: str):
""")
accounts = cur.fetchall()
-
# 科目データを格納
account_data: Dict[str, Dict[str, Any]] = {}
@@ -52,8 +31,11 @@ def fetch_trial_balance(date_from: str, date_to: str):
aid = acc["account_id"]
code = acc["account_code"]
+ # 除外科目をスキップ
+ if code in EXCLUDED_CODES:
+ continue
+
# 期首残高: opening_balances テーブルの残高 + date_from より前の仕訳
- # まず opening_balances テーブルから取得
cur.execute("""
SELECT COALESCE(opening_debit, 0) - COALESCE(opening_credit, 0) AS ob
FROM opening_balances
@@ -64,7 +46,6 @@ def fetch_trial_balance(date_from: str, date_to: str):
opening_balance_from_table = D(ob_row["ob"]) if ob_row else D(0)
# date_from より前の仕訳による増減
- # 修正された仕訳(is_latest = false)は除外
cur.execute("""
SELECT COALESCE(SUM(l.debit - l.credit), 0) AS opening
FROM journal_lines l
@@ -76,12 +57,10 @@ def fetch_trial_balance(date_from: str, date_to: str):
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)
- # 修正された仕訳(is_latest = false)は除外
cur.execute("""
SELECT
COALESCE(SUM(l.debit), 0) AS debit,
@@ -97,10 +76,10 @@ def fetch_trial_balance(date_from: str, date_to: str):
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)
@@ -116,182 +95,441 @@ def fetch_trial_balance(date_from: str, date_to: str):
"closing_balance": closing,
}
- # 結果リスト(グループ化)
- result_accounts: List[Dict[str, Any]] = []
-
- # 既に挿入された合計行を追跡
- inserted_totals = set()
-
- # 大区分の合計を保存(資産・負債・純資産の総合計)
- grand_total_closing = D(0)
+ # =============================================
+ # ヘルパー関数
+ # =============================================
+ 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,
+ }
- # 個別科目を追加
- for code in sorted(account_data.keys()):
- data = account_data[code]
- result_accounts.append({
- "account_code": data["account_code"],
- "account_name": data["account_name"],
- "account_type": data["account_type"],
- "opening_balance": str(data["opening_balance"]),
- "debit": str(data["debit"]),
- "credit": str(data["credit"]),
- "closing_balance": str(data["closing_balance"]),
- "ratio": None, # 後で計算
- "is_total": False
- })
-
- # 大区分合計用に総計を積算
- grand_total_closing += data["closing_balance"]
+ 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,
+ }
- # 利益剰余金(602)の後に詳細を挿入
- if code == "602":
- # 繰越利益剰余金 = 602の期首残高
- retained_earnings_opening = data["opening_balance"]
-
- # 当期純損益金額を計算(収益 - 費用)
- # 収益(7xx)の合計
- 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"]
-
- # 費用(8xx)の合計
- 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_earnings_total = retained_earnings_opening + net_income
-
- # 繰越利益の行を追加
- result_accounts.append({
- "account_code": "",
- "account_name": "繰越利益",
- "account_type": "equity",
- "opening_balance": str(retained_earnings_opening),
- "debit": "0",
- "credit": "0",
- "closing_balance": str(retained_earnings_opening),
- "ratio": None,
- "is_total": False,
- "indent": True
- })
-
- # 当期純損益金額の行を追加
- result_accounts.append({
- "account_code": "",
- "account_name": "当期純損益金額",
- "account_type": "equity",
- "opening_balance": "0",
- "debit": str(expense_total) if expense_total > 0 else "0",
- "credit": str(revenue_total) if revenue_total > 0 else "0",
- "closing_balance": str(net_income),
- "ratio": None,
- "is_total": False,
- "indent": True
- })
-
- # 繰越利益剰余金合計の行を追加
- result_accounts.append({
- "account_code": "",
- "account_name": "繰越利益剰余金合計",
- "account_type": "equity",
- "opening_balance": str(retained_earnings_opening),
- "debit": str(expense_total) if expense_total > 0 else "0",
- "credit": str(revenue_total) if revenue_total > 0 else "0",
- "closing_balance": str(retained_earnings_total),
- "ratio": None,
- "is_total": True,
- "indent": True
- })
-
- # その他利益剰余金合計(現在は0)
- result_accounts.append({
- "account_code": "",
- "account_name": "その他利益剰余金合計",
- "account_type": "equity",
- "opening_balance": "0",
- "debit": "0",
- "credit": "0",
- "closing_balance": "0",
- "ratio": None,
- "is_total": True,
- "indent": True
- })
-
- # 利益剰余金合計の行を追加
- result_accounts.append({
- "account_code": "",
- "account_name": "利益剰余金合計",
- "account_type": "equity",
- "opening_balance": str(retained_earnings_opening),
- "debit": str(expense_total) if expense_total > 0 else "0",
- "credit": str(revenue_total) if revenue_total > 0 else "0",
- "closing_balance": str(retained_earnings_total),
- "ratio": None,
- "is_total": True
- })
+ 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
- # 合計行を挿入(グループ定義の順序で)
- for group_name, group_codes in ACCOUNT_GROUPS:
- # このグループにまだ合計行を挿入していない
- if group_name in inserted_totals:
- continue
-
- # 現在の科目がこのグループに含まれる
- if code not in group_codes:
- continue
-
- # このグループの全科目を取得
- group_codes_in_data = [gc for gc in group_codes if gc in account_data]
- if not group_codes_in_data:
- continue
-
- # グループの最後の科目コードを取得
- last_code_in_group = max(group_codes_in_data)
-
- # 現在の科目がグループの最後の科目の場合
- if code == last_code_in_group:
- # グループ合計を計算
- group_opening = sum(account_data[gc]["opening_balance"] for gc in group_codes_in_data)
- group_debit = sum(account_data[gc]["debit"] for gc in group_codes_in_data)
- group_credit = sum(account_data[gc]["credit"] for gc in group_codes_in_data)
- group_closing = sum(account_data[gc]["closing_balance"] for gc in group_codes_in_data)
-
- result_accounts.append({
- "account_code": "",
- "account_name": group_name,
- "account_type": "",
- "opening_balance": str(group_opening),
- "debit": str(group_debit),
- "credit": str(group_credit),
- "closing_balance": str(group_closing),
- "ratio": None, # 後で計算
- "is_total": True
- })
-
- inserted_totals.add(group_name)
-
- # 構成比を計算(総資産に対する割合)
- for acc in result_accounts:
- closing = D(acc["closing_balance"])
- if grand_total_closing != 0:
- ratio = (closing / grand_total_closing) * 100
+ # =============================================
+ # 結果リスト構築(試算表.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(data["opening_balance"] for data in account_data.values())
- total_debit = sum(data["debit"] for data in account_data.values())
- total_credit = sum(data["credit"] for data in account_data.values())
- total_closing = sum(data["closing_balance"] for data in account_data.values())
+ # 総合計を計算
+ 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": {
@@ -302,7 +540,7 @@ def fetch_trial_balance(date_from: str, date_to: str):
"ratio": "100.00"
},
"accounts": result_accounts,
- "grand_total_closing": str(grand_total_closing)
+ "grand_total_closing": str(debit_balance_total),
}
-
+
return result
diff --git a/backend/app/routers/trial_balance.py b/backend/app/routers/trial_balance.py
index 1750585..ba6d496 100644
--- a/backend/app/routers/trial_balance.py
+++ b/backend/app/routers/trial_balance.py
@@ -40,6 +40,7 @@ def get_trial_balance(
JOIN accounts a ON a.account_id = l.account_id
WHERE e.is_deleted = false
AND e.is_latest = true
+ AND a.account_code NOT IN ('512', '513', '516')
"""
else:
# 旧系统:排除已修正的记录(使用 parent_entry_id)
@@ -55,6 +56,7 @@ def get_trial_balance(
JOIN journal_entries e ON e.journal_entry_id = l.journal_entry_id
JOIN accounts a ON a.account_id = l.account_id
WHERE e.is_deleted = false
+ AND a.account_code NOT IN ('512', '513', '516')
AND e.journal_entry_id NOT IN (
SELECT parent_entry_id
FROM journal_entries
@@ -83,7 +85,11 @@ def get_trial_balance(
a.account_name,
a.account_type
ORDER BY
- a.account_code
+ CASE
+ WHEN a.account_code < '509' THEN a.account_code
+ WHEN a.account_code IN ('510', '511') THEN '508.' || a.account_code
+ ELSE a.account_code
+ END
"""
with get_connection() as conn, conn.cursor() as cur:
diff --git a/backend/check_search_results.py b/backend/check_search_results.py
new file mode 100644
index 0000000..e32d037
--- /dev/null
+++ b/backend/check_search_results.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+"""
+フロントエンドの検索結果と実際のDB内容を比較
+"""
+import sys
+sys.path.insert(0, '/app')
+
+from app.db import get_connection
+
+from_date = '2025-06-01'
+to_date = '2025-06-30'
+account_id = 501
+
+print("=" * 80)
+print("【検索条件】")
+print(f" 期間: {from_date} ~ {to_date}")
+print(f" 科目: {account_id} (費用金)")
+print("=" * 80)
+
+with get_connection() as conn:
+ with conn.cursor() as cur:
+ # 修正履歴を含まない(最新版本のみ)
+ print("\n【1】修正履歴を含まない(is_latest = true)")
+ sql1 = """
+ SELECT
+ je.journal_entry_id,
+ je.entry_date,
+ je.description,
+ je.revision_count,
+ je.is_latest,
+ 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
+ AND je.is_latest = true
+ AND je.entry_date >= %s
+ AND je.entry_date <= %s
+ AND je.journal_entry_id IN (
+ SELECT DISTINCT journal_entry_id
+ FROM journal_lines
+ WHERE account_id = %s
+ )
+ GROUP BY je.journal_entry_id, je.entry_date, je.description, je.revision_count, je.is_latest
+ ORDER BY je.entry_date DESC, je.journal_entry_id DESC
+ """
+ cur.execute(sql1, [from_date, to_date, account_id])
+ results1 = cur.fetchall()
+ print(f" 件数: {len(results1)}")
+ for row in results1:
+ print(f" • ID {row['journal_entry_id']}: {row['entry_date']} - {row['description']} (v{row['revision_count']}, latest={row['is_latest']})")
+
+ # 修正履歴を含む(すべてのバージョン)
+ print("\n【2】修正履歴を含む(すべてのバージョン)")
+ sql2 = """
+ SELECT
+ je.journal_entry_id,
+ je.entry_date,
+ je.description,
+ je.revision_count,
+ je.is_latest,
+ je.original_entry_id,
+ 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
+ AND je.entry_date >= %s
+ AND je.entry_date <= %s
+ AND je.journal_entry_id IN (
+ SELECT DISTINCT journal_entry_id
+ FROM journal_lines
+ WHERE account_id = %s
+ )
+ GROUP BY je.journal_entry_id, je.entry_date, je.description, je.revision_count, je.is_latest, je.original_entry_id
+ ORDER BY je.entry_date DESC, je.journal_entry_id DESC
+ """
+ cur.execute(sql2, [from_date, to_date, account_id])
+ results2 = cur.fetchall()
+ print(f" 件数: {len(results2)}")
+ for row in results2:
+ print(f" • ID {row['journal_entry_id']}: {row['entry_date']} - {row['description']} (v{row['revision_count']}, latest={row['is_latest']}, orig={row['original_entry_id']})")
+
+ # 修正チェーンの分析
+ print("\n【3】修正チェーンの詳細分析")
+ sql3 = """
+ SELECT
+ COALESCE(je.original_entry_id, je.journal_entry_id) as chain_id,
+ COUNT(*) as version_count,
+ STRING_AGG(DISTINCT je.journal_entry_id::text, ', ') as entry_ids,
+ STRING_AGG(DISTINCT je.revision_count::text, ', ' ORDER BY je.revision_count::text DESC) as revisions,
+ MAX(je.revision_count) as max_revision
+ FROM journal_entries je
+ WHERE je.is_deleted = false
+ AND je.entry_date >= %s
+ AND je.entry_date <= %s
+ AND je.journal_entry_id IN (
+ SELECT DISTINCT journal_entry_id
+ FROM journal_lines
+ WHERE account_id = %s
+ )
+ GROUP BY COALESCE(je.original_entry_id, je.journal_entry_id)
+ ORDER BY chain_id DESC
+ """
+ cur.execute(sql3, [from_date, to_date, account_id])
+ chains = cur.fetchall()
+ print(f" チェーン数: {len(chains)}")
+ for chain in chains:
+ print(f" • Chain {chain['chain_id']}: {chain['version_count']} バージョン (IDs: {chain['entry_ids']}, v{chain['revisions']})")
+
+print("\n" + "=" * 80)
+print("✓ 分析完了")
+print("=" * 80)
diff --git a/check_account_19_chain.py b/check_account_19_chain.py
new file mode 100644
index 0000000..4076aa6
--- /dev/null
+++ b/check_account_19_chain.py
@@ -0,0 +1,107 @@
+import sys
+sys.path.insert(0, '/app')
+
+from app.core.database import get_connection
+
+print("=" * 80)
+print("【account_id 19(買掛金)の修正チェーン分析】")
+print("=" * 80)
+
+from_date = "2025-06-01"
+to_date = "2025-06-30"
+account_id = 19
+
+with get_connection() as conn:
+ with conn.cursor() as cur:
+ # Step 1: account_id 19 を含む仕訳を全て取得
+ print("\n【Step 1】account_id 19 を含むすべての仕訳(期間内)")
+ cur.execute("""
+ SELECT DISTINCT je.journal_entry_id, je.entry_date, je.description,
+ je.revision_count, je.is_latest, je.original_entry_id
+ FROM journal_entries je
+ WHERE je.is_deleted = false
+ AND je.entry_date >= %s
+ AND je.entry_date <= %s
+ AND je.journal_entry_id IN (
+ SELECT DISTINCT journal_entry_id
+ FROM journal_lines
+ WHERE account_id = %s
+ )
+ ORDER BY je.entry_date DESC, je.journal_entry_id DESC
+ """, [from_date, to_date, account_id])
+
+ all_entries = cur.fetchall()
+ print(f"件数: {len(all_entries)}")
+ for row in all_entries:
+ print(f" • ID {row['journal_entry_id']}: {row['entry_date']} - {row['description'][:50]}... (v{row['revision_count']}, latest={row['is_latest']}, orig={row['original_entry_id']})")
+
+ # Step 2: 修正チェーンの分析
+ print("\n【Step 2】修正チェーンの詳細")
+ cur.execute("""
+ SELECT
+ COALESCE(je.original_entry_id, je.journal_entry_id) as chain_id,
+ array_agg(je.journal_entry_id ORDER BY je.revision_count) as entry_ids,
+ array_agg(je.revision_count ORDER BY je.revision_count) as revisions,
+ array_agg(je.is_latest ORDER BY je.revision_count) as is_latest_flags,
+ MAX(je.revision_count) as max_revision,
+ COUNT(*) as version_count
+ FROM journal_entries je
+ WHERE je.is_deleted = false
+ AND je.entry_date >= %s
+ AND je.entry_date <= %s
+ AND je.journal_entry_id IN (
+ SELECT DISTINCT journal_entry_id
+ FROM journal_lines
+ WHERE account_id = %s
+ )
+ GROUP BY COALESCE(je.original_entry_id, je.journal_entry_id)
+ ORDER BY chain_id DESC
+ """, [from_date, to_date, account_id])
+
+ chains = cur.fetchall()
+ print(f"修正チェーン数: {len(chains)}")
+ for chain in chains:
+ print(f"\n chain_id {chain['chain_id']}:")
+ print(f" • エントリID: {chain['entry_ids']}")
+ print(f" • リビジョン: {chain['revisions']}")
+ print(f" • 最新フラグ: {chain['is_latest_flags']}")
+ print(f" • バージョン数: {chain['version_count']}, 最大v{chain['max_revision']}")
+
+ # Step 3: 修正履歴を含まない場合と含む場合の件数比較
+ print("\n【Step 3】修正履歴フィルター適用の比較")
+
+ # without include_history
+ cur.execute("""
+ SELECT COUNT(*) as cnt
+ FROM journal_entries je
+ WHERE je.is_deleted = false
+ AND je.is_latest = true
+ AND je.entry_date >= %s
+ AND je.entry_date <= %s
+ AND je.journal_entry_id IN (
+ SELECT DISTINCT journal_entry_id
+ FROM journal_lines
+ WHERE account_id = %s
+ )
+ """, [from_date, to_date, account_id])
+ result_without = cur.fetchone()
+ print(f" 修正履歴を含まない(is_latest=true): {result_without['cnt']} 件")
+
+ # with include_history
+ cur.execute("""
+ SELECT COUNT(*) as cnt
+ FROM journal_entries je
+ WHERE je.is_deleted = false
+ AND je.entry_date >= %s
+ AND je.entry_date <= %s
+ AND je.journal_entry_id IN (
+ SELECT DISTINCT journal_entry_id
+ FROM journal_lines
+ WHERE account_id = %s
+ )
+ """, [from_date, to_date, account_id])
+ result_with = cur.fetchone()
+ print(f" 修正履歴を含む(すべてのバージョン): {result_with['cnt']} 件")
+ print(f" 差分: {result_with['cnt'] - result_without['cnt']} 件(古いバージョン)")
+
+print("\n" + "=" * 80)
diff --git a/check_accounts_105_502.py b/check_accounts_105_502.py
new file mode 100644
index 0000000..a285ca0
--- /dev/null
+++ b/check_accounts_105_502.py
@@ -0,0 +1,69 @@
+import sys
+sys.path.insert(0, '/app')
+
+from app.core.database import get_connection
+
+print("=" * 80)
+print("【科目 105 小口現金 と 502 未払金 の確認】")
+print("=" * 80)
+
+with get_connection() as conn:
+ with conn.cursor() as cur:
+ # 科目情報を確認
+ print("\n【科目情報】")
+ cur.execute("""
+ SELECT account_id, account_code, account_name, account_type
+ FROM accounts
+ WHERE account_id IN (105, 502)
+ ORDER BY account_id
+ """)
+
+ accounts = cur.fetchall()
+ for acc in accounts:
+ print(f" • ID {acc['account_id']}: {acc['account_code']} {acc['account_name']} ({acc['account_type']})")
+
+ # 105 を含む journal_lines の件数
+ print("\n【105 小口現金 を含む仕訳行】")
+ cur.execute("""
+ SELECT COUNT(*) as cnt
+ FROM journal_lines
+ WHERE account_id = 105
+ """)
+ result = cur.fetchone()
+ print(f" 件数: {result['cnt']}")
+
+ if result['cnt'] > 0:
+ # 詳細を表示
+ print("\n【詳細例(最初の 10 件)】")
+ cur.execute("""
+ SELECT
+ jl.journal_line_id,
+ jl.journal_entry_id,
+ je.entry_date,
+ je.description,
+ jl.debit,
+ jl.credit
+ FROM journal_lines jl
+ JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id
+ WHERE jl.account_id = 105
+ ORDER BY je.entry_date DESC
+ LIMIT 10
+ """)
+
+ for row in cur.fetchall():
+ debit_display = f"借方 {row['debit']}" if row['debit'] > 0 else ""
+ credit_display = f"貸方 {row['credit']}" if row['credit'] > 0 else ""
+ side = (debit_display + " " + credit_display).strip()
+ print(f" • ID {row['journal_line_id']}: {row['entry_date']} - {row['description'][:40]}... ({side})")
+
+ # 102 と比較(参考用)
+ print("\n【502 未払金 の現在の利用状況】")
+ cur.execute("""
+ SELECT COUNT(*) as cnt
+ FROM journal_lines
+ WHERE account_id = 502
+ """)
+ result502 = cur.fetchone()
+ print(f" 件数: {result502['cnt']}")
+
+print("\n" + "=" * 80)
diff --git a/check_all_entries_in_period.py b/check_all_entries_in_period.py
new file mode 100644
index 0000000..a46f33a
--- /dev/null
+++ b/check_all_entries_in_period.py
@@ -0,0 +1,50 @@
+import sys
+sys.path.insert(0, '/app')
+
+from app.core.database import get_connection
+
+print("=" * 80)
+print("【期間内(2025-06-01~06-30)のすべての仕訳と科目】")
+print("=" * 80)
+
+from_date = "2025-06-01"
+to_date = "2025-06-30"
+
+with get_connection() as conn:
+ with conn.cursor() as cur:
+ # 期間内のすべての仕訳
+ cur.execute("""
+ SELECT je.journal_entry_id, je.entry_date, je.description
+ FROM journal_entries je
+ WHERE je.is_deleted = false
+ AND je.is_latest = true
+ AND je.entry_date >= %s
+ AND je.entry_date <= %s
+ ORDER BY je.entry_date DESC, je.journal_entry_id DESC
+ LIMIT 10
+ """, [from_date, to_date])
+
+ rows = cur.fetchall()
+ print(f"\n期間内の仕訳(最新版のみ):{len(rows)} 件")
+
+ for i, row in enumerate(rows, 1):
+ print(f"\n{i}. 仕訳 ID {row['journal_entry_id']}: {row['entry_date']} - {row['description']}")
+
+ # この仕訳の科目を表示
+ cur.execute("""
+ SELECT
+ a.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 a.account_id
+ """, [row['journal_entry_id']])
+
+ for line in cur.fetchall():
+ print(f" • ID {line['account_id']} ({line['account_code']}) {line['account_name']}: 借方 {line['debit']}, 貸方 {line['credit']}")
+
+print("\n" + "=" * 80)
diff --git a/check_all_revision_chains.py b/check_all_revision_chains.py
new file mode 100644
index 0000000..7b145a1
--- /dev/null
+++ b/check_all_revision_chains.py
@@ -0,0 +1,71 @@
+import sys
+sys.path.insert(0, '/app')
+
+from app.core.database import get_connection
+
+print("=" * 80)
+print("【すべての journal_entry の修正歴チェーン分析】")
+print("=" * 80)
+
+with get_connection() as conn:
+ with conn.cursor() as cur:
+ # すべてのチェーンを取得
+ cur.execute("""
+ SELECT
+ COALESCE(je.original_entry_id, je.journal_entry_id) as chain_id,
+ array_agg(DISTINCT je.journal_entry_id ORDER BY je.journal_entry_id) as all_entry_ids,
+ array_agg(DISTINCT je.revision_count ORDER BY je.revision_count DESC) as all_revisions,
+ COUNT(*) as total_versions,
+ COUNT(CASE WHEN je.is_latest = true THEN 1 END) as latest_count,
+ COUNT(CASE WHEN je.is_latest = false THEN 1 END) as old_count,
+ MAX(je.revision_count) as max_revision
+ FROM journal_entries je
+ WHERE je.is_deleted = false
+ GROUP BY COALESCE(je.original_entry_id, je.journal_entry_id)
+ HAVING COUNT(*) > 1 OR MAX(je.revision_count) > 1
+ ORDER BY MAX(je.revision_count) DESC
+ LIMIT 20
+ """)
+
+ chains = cur.fetchall()
+ print(f"\n修正チェーン数: {len(chains)}")
+
+ if chains:
+ print("\n📋 詳細:")
+ for i, chain in enumerate(chains, 1):
+ print(f"\n{i}. Chain ID {chain['chain_id']}:")
+ print(f" • 全 entry_id: {chain['all_entry_ids']}")
+ print(f" • リビジョン: {chain['all_revisions']}")
+ print(f" • 総バージョン数: {chain['total_versions']}")
+ print(f" • 最新版本: {chain['latest_count']}, 古いバージョン: {chain['old_count']}")
+ print(f" • 最大リビジョン: v{chain['max_revision']}")
+ else:
+ print("\n⚠️ 修正歴チェーン(複数バージョン)が見つかりません")
+
+ # 古いバージョンが存在するか直接確認
+ print("\n" + "=" * 80)
+ print("【古いバージョン(is_latest = false)の存在確認】")
+ cur.execute("""
+ SELECT COUNT(*) as old_version_count,
+ COUNT(DISTINCT COALESCE(original_entry_id, journal_entry_id)) as old_chain_count
+ FROM journal_entries
+ WHERE is_deleted = false AND is_latest = false
+ """)
+ old_result = cur.fetchone()
+ print(f"\n古いバージョン総数: {old_result['old_version_count']}")
+ print(f"古いバージョンを含むチェーン数: {old_result['old_chain_count']}")
+
+ if old_result['old_version_count'] > 0:
+ print("\n📝 古いバージョンの例(最初の 5 個):")
+ cur.execute("""
+ SELECT je.journal_entry_id, je.entry_date, je.description,
+ je.revision_count, je.original_entry_id
+ FROM journal_entries je
+ WHERE je.is_deleted = false AND je.is_latest = false
+ ORDER BY je.journal_entry_id DESC
+ LIMIT 5
+ """)
+ for row in cur.fetchall():
+ print(f" • ID {row['journal_entry_id']}: {row['entry_date']} - v{row['revision_count']} (orig={row['original_entry_id']})")
+
+print("\n" + "=" * 80)
diff --git a/check_api_fix.py b/check_api_fix.py
new file mode 100644
index 0000000..b8f53a0
--- /dev/null
+++ b/check_api_fix.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+"""Test if the trial-balance API fix is applied"""
+import requests
+import json
+
+url = 'http://127.0.0.1:18080/trial-balance'
+params = {
+ 'fiscal_year': 2025,
+ 'date_from': '2025-06-01',
+ 'date_to': '2025-12-31'
+}
+
+print("🔍 Testing API...")
+print(f"URL: {url}")
+print()
+
+try:
+ response = requests.get(url, params=params)
+ response.raise_for_status()
+ data = response.json()
+
+ if isinstance(data, dict) and 'accounts' in data:
+ accounts = data['accounts']
+
+ # Find the target row
+ target = [row for row in accounts if row.get('account_name') == '繰越利益剰余金合計']
+
+ if target:
+ row = target[0]
+ print("✓ Found: 繰越利益剰余金合計")
+ print(f" debit: {row.get('debit')}")
+ print(f" credit: {row.get('credit')}")
+ print()
+ print("Expected (after fix):")
+ print(" debit: 0")
+ print(" credit: 7967855")
+ print()
+
+ # Convert to float for comparison (handles both "0" and "0.00")
+ try:
+ debit_correct = float(row.get('debit', '0')) == 0.0
+ credit_correct = float(row.get('credit', '0')) == 7967855.0
+ except (ValueError, TypeError):
+ debit_correct = False
+ credit_correct = False
+
+ if debit_correct and credit_correct:
+ print("✅✅✅ SUCCESS!")
+ print("✅ API is now returning CORRECT values!")
+ print("✅ The fix has been successfully applied and verified!")
+ else:
+ print("❌ Values still incorrect")
+ if not debit_correct:
+ print(f" - debit is {row.get('debit')}, expected 0")
+ if not credit_correct:
+ print(f" - credit is {row.get('credit')}, expected 7967855")
+ else:
+ print("❌ Cannot find 繰越利益剰余金合計 row")
+ print("\nLast 5 rows:")
+ for row in accounts[-5:]:
+ print(f" {row.get('account_name')}: debit={row.get('debit')}, credit={row.get('credit')}")
+ else:
+ print("Unexpected response structure")
+ print(json.dumps(data, indent=2, ensure_ascii=False)[:200])
+
+except Exception as e:
+ print(f"❌ Error: {type(e).__name__}: {e}")
+ import traceback
+ traceback.print_exc()
diff --git a/check_api_query.py b/check_api_query.py
new file mode 100644
index 0000000..5d5b1f3
--- /dev/null
+++ b/check_api_query.py
@@ -0,0 +1,112 @@
+import sys
+sys.path.insert(0, '/app')
+
+from app.core.database import get_connection
+
+print("=" * 80)
+print("【実際に API が使用している SQL クエリを再現】")
+print("=" * 80)
+
+from_date = "2025-06-01"
+to_date = "2025-06-30"
+account_id = 501
+include_history = False
+
+with get_connection() as conn:
+ with conn.cursor() as cur:
+ # フィールド存在チェック
+ cur.execute("""
+ SELECT column_name FROM information_schema.columns
+ WHERE table_name = 'journal_entries'
+ AND column_name IN ('is_latest', 'revision_count', 'original_entry_id')
+ """)
+ existing_fields = {row['column_name'] for row in cur.fetchall()}
+ print(f"\n📋 DB フィールド: {existing_fields}")
+
+ if 'revision_count' in existing_fields and 'is_latest' in existing_fields:
+ if include_history:
+ sql = """
+ SELECT
+ je.journal_entry_id,
+ je.entry_date,
+ je.description,
+ je.fiscal_year,
+ je.revision_count,
+ je.is_latest,
+ je.original_entry_id,
+ COALESCE(SUM(jl.debit), 0) as debit_total,
+ COALESCE(SUM(jl.credit), 0) as credit_total,
+ STRING_AGG(DISTINCT CASE WHEN jl.tax_rate IS NOT NULL THEN jl.tax_rate::text END, ', ') as tax_rates
+ FROM journal_entries je
+ LEFT JOIN journal_lines jl ON je.journal_entry_id = jl.journal_entry_id
+ WHERE je.is_deleted = false
+ """
+ else:
+ sql = """
+ SELECT
+ je.journal_entry_id,
+ je.entry_date,
+ je.description,
+ je.fiscal_year,
+ je.revision_count,
+ je.is_latest,
+ je.original_entry_id,
+ COALESCE(SUM(jl.debit), 0) as debit_total,
+ COALESCE(SUM(jl.credit), 0) as credit_total,
+ STRING_AGG(DISTINCT CASE WHEN jl.tax_rate IS NOT NULL THEN jl.tax_rate::text END, ', ') as tax_rates
+ FROM journal_entries je
+ LEFT JOIN journal_lines jl ON je.journal_entry_id = jl.journal_entry_id
+ WHERE je.is_deleted = false
+ AND je.is_latest = true
+ """
+ params = []
+
+ # 期間フ
+ sql += " AND je.entry_date >= %s"
+ params.append(from_date)
+
+ sql += " AND je.entry_date <= %s"
+ params.append(to_date)
+
+ # account_id フィルター
+ if account_id:
+ sql += """ AND je.journal_entry_id IN (
+ SELECT DISTINCT journal_entry_id
+ FROM journal_lines
+ WHERE account_id = %s
+ )"""
+ params.append(account_id)
+
+ sql += " GROUP BY je.journal_entry_id, je.entry_date, je.description, je.fiscal_year, je.revision_count, je.is_latest, je.original_entry_id"
+ sql += " ORDER BY je.entry_date DESC, je.journal_entry_id DESC"
+
+ print(f"\n【SQL】")
+ print(sql)
+ print(f"\n【パラメータ】{params}")
+
+ cur.execute(sql, params)
+ rows = cur.fetchall()
+
+ print(f"\n【結果】{len(rows)} 件")
+ for i, row in enumerate(rows, 1):
+ print(f"\n{i}. ID {row['journal_entry_id']}")
+ print(f" 日付: {row['entry_date']}")
+ print(f" 摘要: {row['description']}")
+ print(f" 借方: {row['debit_total']}, 貸方: {row['credit_total']}")
+
+ # account_id 501 のレコードを持つ仕訳を確認
+ print("\n" + "=" * 80)
+ print("【仕訳に含まれる科目の内訳】")
+ if rows:
+ for row in rows[:1]: # 最初のレコードが 501 を含むか
+ print(f"\n仕訳 ID {row['journal_entry_id']} の行科目:")
+ cur.execute("""
+ SELECT account_id, account_name, debit, credit
+ FROM journal_lines jl
+ JOIN accounts a ON jl.account_id = a.account_id
+ WHERE jl.journal_entry_id = %s
+ """, [row['journal_entry_id']])
+ for line in cur.fetchall():
+ print(f" • {line['account_id']} {line['account_name']}: 借方 {line['debit']}, 貸方 {line['credit']}")
+
+print("\n" + "=" * 80)
diff --git a/check_chain_140_accounts.py b/check_chain_140_accounts.py
new file mode 100644
index 0000000..a538b6a
--- /dev/null
+++ b/check_chain_140_accounts.py
@@ -0,0 +1,53 @@
+import sys
+sys.path.insert(0, '/app')
+
+from app.core.database import get_connection
+
+print("=" * 80)
+print("【Chain 140 の各バージョンの科目内訳】")
+print("=" * 80)
+
+chain_id = 140
+entry_ids = [140, 438, 461]
+
+with get_connection() as conn:
+ with conn.cursor() as cur:
+ for entry_id in entry_ids:
+ cur.execute("""
+ SELECT je.journal_entry_id, je.revision_count, je.is_latest, je.entry_date
+ FROM journal_entries je
+ WHERE je.journal_entry_id = %s
+ """, [entry_id])
+
+ entry = cur.fetchone()
+ print(f"\n【ID {entry_id} (v{entry['revision_count']}, latest={entry['is_latest']})】")
+ print(f"日付: {entry['entry_date']}")
+
+ cur.execute("""
+ SELECT
+ a.account_id,
+ a.account_code,
+ a.account_name,
+ COALESCE(jl.debit, 0) as debit,
+ COALESCE(jl.credit, 0) as credit
+ FROM journal_lines jl
+ JOIN accounts a ON jl.account_id = a.account_id
+ WHERE jl.journal_entry_id = %s
+ ORDER BY a.account_id
+ """, [entry_id])
+
+ lines = cur.fetchall()
+ print(f" 行数: {len(lines)}")
+ for line in lines:
+ debit_display = f"借方 {line['debit']}" if line['debit'] > 0 else ""
+ credit_display = f"貸方 {line['credit']}" if line['credit'] > 0 else ""
+ side = (debit_display + " " + credit_display).strip()
+ marker = " ← 古いバージョンにない科目" if line['account_id'] == 19 and entry_id == 461 else ""
+ print(f" • {line['account_id']} ({line['account_code']}) {line['account_name']}: {side}{marker}")
+
+print("\n" + "=" * 80)
+print("【結論】")
+print("修正前バージョンと修正後バージョンで科目構成が異なります。")
+print("古いバージョンには account_id 19(買掛金)がないため、")
+print("account_id 19 でフィルターした場合、最新バージョンのみが返されます。")
+print("=" * 80)
diff --git a/check_chain_140_detail.py b/check_chain_140_detail.py
new file mode 100644
index 0000000..2484cef
--- /dev/null
+++ b/check_chain_140_detail.py
@@ -0,0 +1,75 @@
+import sys
+sys.path.insert(0, '/app')
+
+from app.core.database import get_connection
+
+print("=" * 80)
+print("【Chain ID 140 の詳細分析】")
+print("=" * 80)
+
+chain_id = 140
+
+with get_connection() as conn:
+ with conn.cursor() as cur:
+ # チェーン 140 内のすべてのバージョン
+ cur.execute("""
+ SELECT
+ je.journal_entry_id,
+ je.entry_date,
+ je.description,
+ je.revision_count,
+ je.is_latest,
+ je.original_entry_id
+ FROM journal_entries je
+ WHERE (je.journal_entry_id = %s OR je.original_entry_id = %s)
+ AND je.is_deleted = false
+ ORDER BY je.revision_count ASC
+ """, [chain_id, chain_id])
+
+ versions = cur.fetchall()
+ print(f"\nチェーン内のバージョン: {len(versions)}")
+ for row in versions:
+ print(f"\n • ID {row['journal_entry_id']} (v{row['revision_count']}):")
+ print(f" - 日付: {row['entry_date']}")
+ print(f" - 摘要: {row['description'][:60]}...")
+ print(f" - 最新: {row['is_latest']}")
+ print(f" - オリジナル: {row['original_entry_id']}")
+
+ # 各バージョンで account_id 19 を持つか確認
+ print("\n【各バージョンで account_id 19 を持つか確認】")
+ for row in versions:
+ cur.execute("""
+ SELECT COUNT(*) as cnt
+ FROM journal_lines
+ WHERE journal_entry_id = %s AND account_id = 19
+ """, [row['journal_entry_id']])
+ has_account = cur.fetchone()
+ print(f" ID {row['journal_entry_id']}: account_id 19を含む = {has_account['cnt'] > 0}")
+
+ # 期間内(2025-06-01~06-30)で、account_id 19を含むすべてのバージョン
+ print("\n【期間内 + account_id 19 フィルター】")
+ cur.execute("""
+ SELECT
+ je.journal_entry_id,
+ je.entry_date,
+ je.revision_count,
+ je.is_latest
+ FROM journal_entries je
+ WHERE (je.journal_entry_id = %s OR je.original_entry_id = %s)
+ AND je.is_deleted = false
+ AND je.entry_date >= %s
+ AND je.entry_date <= %s
+ AND je.journal_entry_id IN (
+ SELECT DISTINCT journal_entry_id
+ FROM journal_lines
+ WHERE account_id = 19
+ )
+ ORDER BY je.revision_count ASC
+ """, [chain_id, chain_id, '2025-06-01', '2025-06-30'])
+
+ filtered = cur.fetchall()
+ print(f"\n期間内 + account_id 19 フィルター: {len(filtered)} 件")
+ for row in filtered:
+ print(f" • ID {row['journal_entry_id']} (v{row['revision_count']}, latest={row['is_latest']}): {row['entry_date']}")
+
+print("\n" + "=" * 80)
diff --git a/check_db_records.py b/check_db_records.py
new file mode 100644
index 0000000..77fd475
--- /dev/null
+++ b/check_db_records.py
@@ -0,0 +1,85 @@
+import sys
+sys.path.insert(0, '/app')
+
+from app.core.database import get_connection
+
+from_date = "2025-06-01"
+to_date = "2025-06-30"
+account_id = 501
+
+print("=" * 80)
+print("【検索条件】")
+print(f" 期間: {from_date} ~ {to_date}")
+print(f" 科目: {account_id} (費用金)")
+print("=" * 80)
+
+with get_connection() as conn:
+ with conn.cursor() as cur:
+ # ステップ 1: journal_lines に account_id 501 のレコードがあるか
+ print("\n【ステップ 0】account_id 501 を持つ journal_lines")
+ sql0 = "SELECT COUNT(*) as cnt FROM journal_lines WHERE account_id = %s"
+ cur.execute(sql0, [account_id])
+ result0 = cur.fetchone()
+ print(f" 全体件数: {result0['cnt']}")
+
+ # ステップ 2: journal_lines に account_id 501 で期間限定のレコードがあるか
+ print("\n【ステップ 0.5】account_id 501 で期間限定のレコード")
+ sql0_5 = """
+ SELECT COUNT(*) as cnt FROM journal_lines jl
+ WHERE jl.account_id = %s
+ AND jl.journal_entry_id IN (
+ SELECT journal_entry_id FROM journal_entries
+ WHERE entry_date >= %s AND entry_date <= %s
+ )
+ """
+ cur.execute(sql0_5, [account_id, from_date, to_date])
+ result0_5 = cur.fetchone()
+ print(f" 期間内件数: {result0_5['cnt']}")
+
+ # ステップ 3: その journal_entry_id のリスト
+ print("\n【ステップ 1】該当する journal_entry_id のリスト")
+ sql1 = """
+ SELECT DISTINCT journal_entry_id
+ FROM journal_lines
+ WHERE account_id = %s
+ AND journal_entry_id IN (
+ SELECT journal_entry_id FROM journal_entries
+ WHERE entry_date >= %s AND entry_date <= %s
+ )
+ """
+ cur.execute(sql1, [account_id, from_date, to_date])
+ entry_ids = [row['journal_entry_id'] for row in cur.fetchall()]
+ print(f" 該当 journal_entry_id: {entry_ids}")
+
+ if entry_ids:
+ print("\n【ステップ 2】これらの仕訳の詳細")
+ placeholders = ','.join(['%s'] * len(entry_ids))
+ sql2 = f"""
+ SELECT
+ journal_entry_id, entry_date, description, revision_count, is_latest
+ FROM journal_entries
+ WHERE journal_entry_id IN ({placeholders})
+ """
+ cur.execute(sql2, entry_ids)
+ for row in cur.fetchall():
+ print(f" • ID {row['journal_entry_id']}: {row['entry_date']} - {row['description']} (v{row['revision_count']}, latest={row['is_latest']})")
+
+ # オリジナルのクエリ
+ print("\n【ステップ 3】修正履歴を含まない(is_latest = true)")
+ sql3 = """
+ SELECT COUNT(*) as cnt FROM journal_entries je
+ WHERE je.is_deleted = false
+ AND je.is_latest = true
+ AND je.entry_date >= %s
+ AND je.entry_date <= %s
+ AND je.journal_entry_id IN (
+ SELECT DISTINCT journal_entry_id
+ FROM journal_lines
+ WHERE account_id = %s
+ )
+ """
+ cur.execute(sql3, [from_date, to_date, account_id])
+ result3 = cur.fetchone()
+ print(f" 件数: {result3['cnt']}")
+
+print("\n" + "=" * 80)
diff --git a/check_revisions.py b/check_revisions.py
new file mode 100644
index 0000000..0a7e4e2
--- /dev/null
+++ b/check_revisions.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+import psycopg2
+from psycopg2.extras import RealDictCursor
+import os
+
+# 接続設定
+conn = psycopg2.connect(
+ user=os.getenv('DB_USER', 'postgres'),
+ password=os.getenv('DB_PASSWORD', 'postgres'),
+ host='localhost',
+ port='5432',
+ database=os.getenv('DB_NAME', 'accounting_db')
+)
+
+cur = conn.cursor(cursor_factory=RealDictCursor)
+
+# 修正版本(revision_count > 1)があるか確認
+cur.execute("""
+ SELECT COUNT(*) as total,
+ COUNT(CASE WHEN is_latest = true THEN 1 END) as latest_count,
+ COUNT(CASE WHEN revision_count > 1 THEN 1 END) as revised_count,
+ MAX(revision_count) as max_revision
+ FROM journal_entries
+ WHERE is_deleted = false
+""")
+
+result = cur.fetchone()
+print("📊 修正版本統計")
+print(f" • 全記録: {result['total']}")
+print(f" • 最新版本: {result['latest_count']}")
+print(f" • 修正版本: {result['revised_count']}")
+print(f" • 最大改版數: {result['max_revision']}")
+
+# 修正版本の例を表示
+if result['revised_count'] > 0:
+ print("\n📝 修正版本の例(最初の 5 件):")
+ cur.execute("""
+ SELECT journal_entry_id, entry_date, description, revision_count, is_latest
+ FROM journal_entries
+ WHERE revision_count > 1 AND is_deleted = false
+ ORDER BY revision_count DESC, journal_entry_id DESC
+ LIMIT 5
+ """)
+ for row in cur.fetchall():
+ print(f" • ID {row['journal_entry_id']}: {row['entry_date']} - {row['description']} (v{row['revision_count']}, latest={row['is_latest']})")
+
+cur.close()
+conn.close()
diff --git a/check_search_results.py b/check_search_results.py
new file mode 100644
index 0000000..6a84d87
--- /dev/null
+++ b/check_search_results.py
@@ -0,0 +1,131 @@
+#!/usr/bin/env python3
+import os
+import sys
+import psycopg2
+from psycopg2.extras import RealDictCursor
+from datetime import date
+
+# 接続設定
+try:
+ conn = psycopg2.connect(
+ user=os.getenv('DB_USER', 'postgres'),
+ password=os.getenv('DB_PASSWORD', 'postgres'),
+ host='postgres',
+ port='5432',
+ database=os.getenv('DB_NAME', 'accounting_db')
+ )
+except:
+ # ローカル接続を試みる
+ conn = psycopg2.connect(
+ user='postgres',
+ password='postgres',
+ host='localhost',
+ port='5432',
+ database='accounting_db'
+ )
+
+cur = conn.cursor(cursor_factory=RealDictCursor)
+
+# 検索条件
+from_date = '2025-06-01'
+to_date = '2025-06-30'
+account_id = 501 # 費用金
+
+# 条件1: 修正履歴を含まない(最新版本のみ)
+print("=" * 80)
+print("【検索条件】")
+print(f" 期間: {from_date} ~ {to_date}")
+print(f" 科目: {account_id}")
+print("=" * 80)
+
+print("\n【1】修正履歴を含まない(is_latest = true)")
+sql1 = """
+ SELECT
+ je.journal_entry_id,
+ je.entry_date,
+ je.description,
+ je.revision_count,
+ je.is_latest,
+ 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
+ AND je.is_latest = true
+ AND je.entry_date >= %s
+ AND je.entry_date <= %s
+ AND je.journal_entry_id IN (
+ SELECT DISTINCT journal_entry_id
+ FROM journal_lines
+ WHERE account_id = %s
+ )
+ GROUP BY je.journal_entry_id, je.entry_date, je.description, je.revision_count, je.is_latest
+ ORDER BY je.entry_date DESC, je.journal_entry_id DESC
+"""
+cur.execute(sql1, [from_date, to_date, account_id])
+results1 = cur.fetchall()
+print(f" 件数: {len(results1)}")
+for row in results1:
+ print(f" • ID {row['journal_entry_id']}: {row['entry_date']} - {row['description']} (v{row['revision_count']}, latest={row['is_latest']})")
+
+print("\n【2】修正履歴を含む(すべてのバージョン)")
+sql2 = """
+ SELECT
+ je.journal_entry_id,
+ je.entry_date,
+ je.description,
+ je.revision_count,
+ je.is_latest,
+ je.original_entry_id,
+ 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
+ AND je.entry_date >= %s
+ AND je.entry_date <= %s
+ AND je.journal_entry_id IN (
+ SELECT DISTINCT journal_entry_id
+ FROM journal_lines
+ WHERE account_id = %s
+ )
+ GROUP BY je.journal_entry_id, je.entry_date, je.description, je.revision_count, je.is_latest, je.original_entry_id
+ ORDER BY je.entry_date DESC, je.journal_entry_id DESC
+"""
+cur.execute(sql2, [from_date, to_date, account_id])
+results2 = cur.fetchall()
+print(f" 件数: {len(results2)}")
+for row in results2:
+ print(f" • ID {row['journal_entry_id']}: {row['entry_date']} - {row['description']} (v{row['revision_count']}, latest={row['is_latest']}, orig={row['original_entry_id']})")
+
+print("\n【3】同じ日付・科目の修正版本の詳細情報")
+sql3 = """
+ SELECT
+ je.journal_entry_id,
+ je.entry_date,
+ je.description,
+ je.revision_count,
+ je.is_latest,
+ je.original_entry_id,
+ COUNT(DISTINCT jl.account_id) as account_count
+ FROM journal_entries je
+ LEFT JOIN journal_lines jl ON je.journal_entry_id = jl.journal_entry_id
+ WHERE je.is_deleted = false
+ AND je.entry_date >= %s
+ AND je.entry_date <= %s
+ GROUP BY je.journal_entry_id, je.entry_date, je.description, je.revision_count, je.is_latest, je.original_entry_id
+ ORDER BY je.entry_date DESC, je.journal_entry_id DESC
+ LIMIT 10
+"""
+cur.execute(sql3, [from_date, to_date])
+results3 = cur.fetchall()
+print(f" 同じ期間内のすべての仕訳(最初の10件):")
+for row in results3:
+ print(f" • ID {row['journal_entry_id']}: {row['entry_date']} - {row['description']} (v{row['revision_count']}, latest={row['is_latest']}, orig={row['original_entry_id']}, accts={row['account_count']})")
+
+cur.close()
+conn.close()
+
+print("\n" + "=" * 80)
+print("✓ 分析完了")
+print("=" * 80)
diff --git a/docker-compose.yml b/docker-compose.yml
index bc168fc..0d9aceb 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,6 +10,8 @@ services:
- .env
ports:
- "18080:18080"
+ volumes:
+ - ./frontend:/app/frontend:ro
networks:
- njts_net
diff --git a/docs/File.PDF b/docs/File.PDF
new file mode 100644
index 0000000..da1f9b9
Binary files /dev/null and b/docs/File.PDF differ
diff --git a/docs/File0001.PDF b/docs/File0001.PDF
new file mode 100644
index 0000000..96d2177
Binary files /dev/null and b/docs/File0001.PDF differ
diff --git a/docs/試算表.pdf b/docs/試算表.pdf
new file mode 100644
index 0000000..2e0a835
Binary files /dev/null and b/docs/試算表.pdf differ
diff --git a/frontend/journal-edit.html b/frontend/journal-edit.html
index e3ee5d6..e0fddc3 100644
--- a/frontend/journal-edit.html
+++ b/frontend/journal-edit.html
@@ -44,6 +44,7 @@
.account-search-container {
position: relative;
width: 100%;
+ box-sizing: border-box;
}
.account-search-input {
@@ -52,6 +53,7 @@
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
+ box-sizing: border-box;
}
.account-search-input:focus {
@@ -84,6 +86,9 @@
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
font-size: 14px;
+ text-align: left;
+ display: block;
+ width: 100%;
}
.account-option:hover,
@@ -94,11 +99,14 @@
.account-option-strong {
font-weight: bold;
color: #333;
+ display: inline;
+ margin-right: 6px;
}
.account-option-light {
- color: #999;
- font-size: 12px;
+ color: #555;
+ font-size: 14px;
+ display: inline;
}
@@ -241,43 +249,81 @@
const container = inputElement.closest(".account-search-container");
const dropdown = container.querySelector(".account-dropdown");
- inputElement.addEventListener("input", (e) => {
- const searchText = e.target.value.toLowerCase().trim();
+ // 科目選択履歴を保存(journal-entry.htmlと共通のlocalStorageキー)
+ function saveAccountToRecentEdit(accountId) {
+ if (!accountId) return;
+ let recent = JSON.parse(localStorage.getItem("recentAccounts") || "[]");
+ recent = recent.filter((id) => id !== accountId);
+ recent.unshift(accountId);
+ if (recent.length > 20) recent = recent.slice(0, 20);
+ localStorage.setItem("recentAccounts", JSON.stringify(recent));
+ }
- if (!searchText) {
- dropdown.classList.remove("active");
- return;
- }
+ function makeLbl(text) {
+ const lbl = document.createElement("div");
+ lbl.style.cssText = "padding:3px 12px;font-size:11px;color:#888;background:#f5f5f5;font-weight:bold;border-bottom:1px solid #e0e0e0;";
+ lbl.textContent = text;
+ return lbl;
+ }
- // 过滤账户
- const filtered = accounts.filter((acc) => {
- const code = acc.account_code.toLowerCase();
- const name = acc.account_name.toLowerCase();
- return code.includes(searchText) || name.includes(searchText);
- });
-
- // 生成下拉建议
- dropdown.innerHTML = "";
- if (filtered.length === 0) {
- dropdown.innerHTML = `
該当する科目がありません
`;
- } else {
- filtered.slice(0, 20).forEach((acc) => {
- const option = document.createElement("div");
- option.className = "account-option";
- option.innerHTML = `
- ${acc.account_code}
- ${acc.account_name}
- `;
- option.addEventListener("click", () => {
- inputElement.value = `${acc.account_code} ${acc.account_name}`;
- inputElement.dataset.accountId = acc.account_id;
- dropdown.classList.remove("active");
- });
- dropdown.appendChild(option);
+ // 显示科目下拉列表的函数
+ function showAccountList(searchText = "") {
+ const isFiltered = searchText.length > 0;
+ let filtered = accounts;
+ if (isFiltered) {
+ filtered = accounts.filter((acc) => {
+ const code = acc.account_code.toLowerCase();
+ const name = acc.account_name.toLowerCase();
+ return code.includes(searchText) || name.includes(searchText);
});
}
+ dropdown.innerHTML = "";
+
+ function appendOpt(acc) {
+ const option = document.createElement("div");
+ option.className = "account-option";
+ option.innerHTML = `${acc.account_code}${acc.account_name}`;
+ option.addEventListener("click", () => {
+ inputElement.value = `${acc.account_code} ${acc.account_name}`;
+ inputElement.dataset.accountId = acc.account_id;
+ dropdown.classList.remove("active");
+ saveAccountToRecentEdit(acc.account_id);
+ });
+ dropdown.appendChild(option);
+ }
+
+ if (filtered.length === 0) {
+ dropdown.innerHTML = `該当する科目がありません
`;
+ } else if (isFiltered) {
+ const sorted = filtered.slice().sort((a, b) => a.account_code.localeCompare(b.account_code));
+ sorted.slice(0, 50).forEach(appendOpt);
+ } else {
+ const recentIds = JSON.parse(localStorage.getItem("recentAccounts") || "[]");
+ const recentItems = recentIds.map((id) => filtered.find((a) => a.account_id === id)).filter(Boolean);
+ const recentSet = new Set(recentIds);
+ const otherItems = filtered
+ .filter((a) => !recentSet.has(a.account_id))
+ .sort((a, b) => a.account_code.localeCompare(b.account_code));
+
+ if (recentItems.length > 0) {
+ dropdown.appendChild(makeLbl("⭐ 最近使った科目"));
+ recentItems.forEach(appendOpt);
+ const sep = document.createElement("div");
+ sep.style.cssText = "border-top:1px solid #e8e8e8;margin:2px 0;";
+ dropdown.appendChild(sep);
+ }
+ dropdown.appendChild(makeLbl("📋 すべての科目"));
+ otherItems.slice(0, 50).forEach(appendOpt);
+ }
+
dropdown.classList.add("active");
+ }
+
+ // 入力時:フィルター
+ inputElement.addEventListener("input", (e) => {
+ const searchText = e.target.value.toLowerCase().trim();
+ showAccountList(searchText);
});
// クリック以外の場所のドロップダウンを非表示
@@ -287,11 +333,9 @@
}
});
- // フォーカス時にドロップダウンを表示(既存値がある場合)
+ // フォーカス時:すべての科目を表示
inputElement.addEventListener("focus", () => {
- if (inputElement.value) {
- dropdown.classList.add("active");
- }
+ showAccountList(inputElement.value.toLowerCase().trim());
});
}
@@ -386,13 +430,20 @@
if (line) {
// Find the account and populate the input
- const account = accounts.find(a => a.account_id === line.account_id);
+ const account = accounts.find(
+ (a) => a.account_id === line.account_id,
+ );
if (account) {
accountInput.value = `${account.account_code} ${account.account_name}`;
accountInput.dataset.accountId = account.account_id;
}
- tr.querySelectorAll("input")[1].value = line.debit || "";
- tr.querySelectorAll("input")[2].value = line.credit || "";
+ // type="number" のみを取得
+ const numberInputs = tr.querySelectorAll("input[type='number']");
+ // Decimal 値を数値に変換してから代入
+ const debitVal = Number(line.debit) || 0;
+ const creditVal = Number(line.credit) || 0;
+ numberInputs[0].value = debitVal > 0 ? String(debitVal) : "";
+ numberInputs[1].value = creditVal > 0 ? String(creditVal) : "";
}
// 入力時に合計を自動更新
@@ -414,8 +465,26 @@
let d = 0,
c = 0;
document.querySelectorAll("#linesTable tbody tr").forEach((tr) => {
- d += Number(tr.querySelectorAll("input")[0].value || 0);
- c += Number(tr.querySelectorAll("input")[1].value || 0);
+ // type="number" のみを取得(科目検索入力框を除外)
+ const numberInputs = tr.querySelectorAll("input[type='number']");
+ if (numberInputs.length >= 2) {
+ const debitStr = numberInputs[0]?.value;
+ const creditStr = numberInputs[1]?.value;
+
+ // 空または 0 を含む場合は 0、そうでなければ数値に変換
+ const debitVal =
+ debitStr && debitStr.trim()
+ ? Number(debitStr.toString().replace(/,/g, ""))
+ : 0;
+ const creditVal =
+ creditStr && creditStr.trim()
+ ? Number(creditStr.toString().replace(/,/g, ""))
+ : 0;
+
+ // NaN チェック
+ if (!isNaN(debitVal)) d += debitVal;
+ if (!isNaN(creditVal)) c += creditVal;
+ }
});
document.getElementById("debitTotal").textContent = d.toLocaleString();
document.getElementById("creditTotal").textContent = c.toLocaleString();
@@ -429,13 +498,28 @@
result.textContent = "";
result.className = "";
- const entryDate = document.getElementById("entryDate").value;
+ const entryDateInput = document.getElementById("entryDate");
+ const entryDate = entryDateInput.value;
const desc = document.getElementById("description").value;
- if (!entryDate || !desc) {
+ if (entryDateInput.validity.badInput) {
+ showError(
+ "日付が正しくありません。存在しない日付が入力されています(例:11月31日は無効です)",
+ );
+ return;
+ }
+ if (!entryDate && !desc) {
showError("日付と摘要は必須です");
return;
}
+ if (!entryDate) {
+ showError("日付を入力してください");
+ return;
+ }
+ if (!desc) {
+ showError("摘要を入力してください");
+ return;
+ }
const lines = [];
let d = 0,
@@ -444,8 +528,9 @@
document.querySelectorAll("#linesTable tbody tr").forEach((tr) => {
const accountInput = tr.querySelector(".account-search-input");
const accountId = Number(accountInput.dataset.accountId || 0);
- const debit = Number(tr.querySelectorAll("input")[1].value || 0);
- const credit = Number(tr.querySelectorAll("input")[2].value || 0);
+ const numberInputs = tr.querySelectorAll("input[type='number']");
+ const debit = Number(numberInputs[0]?.value || 0);
+ const credit = Number(numberInputs[1]?.value || 0);
if (debit === 0 && credit === 0) return;
lines.push({ account_id: accountId, debit, credit });
d += debit;
diff --git a/frontend/journal-entry.html b/frontend/journal-entry.html
index ca73e2d..8c799ea 100644
--- a/frontend/journal-entry.html
+++ b/frontend/journal-entry.html
@@ -243,15 +243,15 @@
.account-option-strong {
font-weight: bold;
color: #333;
- display: block;
+ display: inline;
+ margin-right: 6px;
}
.account-option-light {
- color: #999;
- font-size: 12px;
- display: block;
+ color: #555;
+ font-size: 14px;
+ display: inline;
}
-
@@ -273,6 +273,62 @@
仕訳入力
+
+
+
+
+
+
+
+
+ 【支出】 借方:科目 / 貸方:取引手段
+
+ 【収入】 借方:取引手段 / 貸方:科目
+
+
-
-
-
-
-
+
+
-
-
-