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 @@

仕訳入力

+
+ + + +
+ +
+ + 【支出】 借方:科目 / 貸方:取引手段 + + 【収入】 借方:取引手段 / 貸方:科目 +
+
- - - - - + +
-
- - + +
+
+ +
+ + + +
+ +
+ +
- - - - - - - - - - - - -
科目借方貸方税区分税方向操作
- -
-
- - 0 - 円 -
-
- - 0 - 円 -
-
+
+ + +
-
- - +
+
@@ -628,84 +710,143 @@ let rowSeq = 0; // 行ID計数器 // ======================================== - // 账户搜索功能 + // (旧 setupAccountSearch は setupEntryAccountSearch に統合済) // ======================================== - function setupAccountSearch(inputElement, row) { + + // ======================================== + // 消費税額 自動計算 + // ======================================== + function calcTax() { + const amountStr = document + .getElementById("entryAmount") + .value.replace(/,/g, ""); + const amount = Number(amountStr) || 0; + const rateVal = document.getElementById("entryTaxRate").value; + + if (amount === 0 || rateVal === "0") { + document.getElementById("entryTaxAmount").value = ""; + return; + } + + // 税率を数値に変換(8r = 軽減8%) + const rate = rateVal === "8r" ? 8 : Number(rateVal); + // 税込金額から消費税額を計算(切上げ) + const taxAmount = Math.ceil((amount * rate) / (100 + rate)); + document.getElementById("entryTaxAmount").value = + taxAmount.toLocaleString(); + } + + // ======================================== + // 入力フォーム用科目検索セットアップ + // ======================================== + function setupEntryAccountSearch(inputElement) { const container = inputElement.closest(".account-search-container"); const dropdown = container.querySelector(".account-dropdown"); - inputElement.addEventListener("input", (e) => { - const searchText = e.target.value.toLowerCase().trim(); - - if (!searchText) { + function appendAccountOption(acc, onSelect) { + const option = document.createElement("div"); + option.className = "account-option"; + option.innerHTML = ``; + option.addEventListener("mousedown", (ev) => { + ev.preventDefault(); + inputElement.value = `${acc.account_code} ${acc.account_name}`; + inputElement.dataset.accountId = acc.account_id; dropdown.classList.remove("active"); + saveAccountToRecent(acc.account_id); + if (onSelect) onSelect(); + }); + dropdown.appendChild(option); + } + + function makeSectionLabel(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; + } + + function renderDropdown(list, isFiltered) { + dropdown.innerHTML = ""; + if (list.length === 0) { + dropdown.innerHTML = + '
該当する科目がありません
'; + dropdown.classList.add("active"); + return; + } + + if (isFiltered) { + // 検索中 → コード順で表示 + const sorted = list.slice().sort((a, b) => a.account_code.localeCompare(b.account_code)); + sorted.slice(0, 50).forEach((acc) => appendAccountOption(acc)); + } else { + // 未入力 → 最近使った科目を先頭に、残りはコード順 + const recentIds = JSON.parse(localStorage.getItem("recentAccounts") || "[]"); + const recentItems = recentIds.map((id) => list.find((a) => a.account_id === id)).filter(Boolean); + const recentSet = new Set(recentIds); + const otherItems = list + .filter((a) => !recentSet.has(a.account_id)) + .sort((a, b) => a.account_code.localeCompare(b.account_code)); + + if (recentItems.length > 0) { + dropdown.appendChild(makeSectionLabel("⭐ 最近使った科目")); + recentItems.forEach((acc) => appendAccountOption(acc)); + const sep = document.createElement("div"); + sep.style.cssText = "border-top:1px solid #e8e8e8;margin:2px 0;"; + dropdown.appendChild(sep); + } + dropdown.appendChild(makeSectionLabel("📋 すべての科目")); + otherItems.slice(0, 50).forEach((acc) => appendAccountOption(acc)); + } + + dropdown.classList.add("active"); + } + + inputElement.addEventListener("input", (e) => { + const searchText = e.target.value.toLowerCase().trim(); + inputElement.dataset.accountId = ""; + + if (!searchText) { + renderDropdown(accounts, false); return; } - // 过滤账户 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 = ` - - - `; - option.addEventListener("click", () => { - inputElement.value = `${acc.account_code} ${acc.account_name}`; - inputElement.dataset.accountId = acc.account_id; - dropdown.classList.remove("active"); - saveAccountToRecent(acc.account_id); - }); - dropdown.appendChild(option); - }); - } - - dropdown.classList.add("active"); + renderDropdown(filtered, true); }); - // クリック以外の場所のドロップダウンを非表示 document.addEventListener("click", (e) => { if (!container.contains(e.target)) { dropdown.classList.remove("active"); } }); - // フォーカス時にドロップダウンを表示(既存値がある場合) inputElement.addEventListener("focus", () => { - if (inputElement.value) { - dropdown.classList.add("active"); + inputElement.dataset.accountId = inputElement.dataset.accountId || ""; + const searchText = inputElement.value.toLowerCase().trim(); + if (!searchText) { + renderDropdown(accounts, false); + } else { + inputElement.dispatchEvent(new Event("input")); + } + }); + + inputElement.addEventListener("click", () => { + if (!dropdown.classList.contains("active")) { + const searchText = inputElement.value.toLowerCase().trim(); + if (!searchText) { + renderDropdown(accounts, false); + } else { + inputElement.dispatchEvent(new Event("input")); + } } }); } - // getSelectedAccountId 用来获取当前行选中的账户ID - function getSelectedAccountIdFromInput(inputElement) { - return inputElement.dataset.accountId || null; - } - - // 建立一个全局函数来获取行中的账户ID - window.getAccountIdFromRow = function(row) { - const input = row.querySelector(".account-search-input"); - return input.dataset.accountId || null; - }; - - // 建立一个全局函数来获取行中的账户文本 - window.getAccountTextFromRow = function(row) { - const input = row.querySelector(".account-search-input"); - return input.value || ""; - }; - // ======================================== // 初期化 // ======================================== @@ -721,25 +862,22 @@ const data = await res.json(); console.log("✓ 获取到科目数据:", data); - // 🔴 ここ重要:items がある場合と無い場合の両対応 accounts = data.items ?? data; console.log("✓ accounts 已赋值,共", accounts.length, "个科目"); - // 仮払消費税等(あなたの DB では 505 は「未払消費税等」) + // 仮払消費税等・仮受消費税等を自動検出 taxPaidAccounts = accounts.filter((a) => - a.account_name.includes("仮払"), + a.account_name.includes("仮払消費税"), ); - - // 仮受消費税等 taxReceivedAccounts = accounts.filter((a) => - a.account_name.includes("仮受"), + a.account_name.includes("仮受消費税"), ); - setupTaxSelectors(); + // 科目・取引手段の検索セットアップ + setupEntryAccountSearch(document.getElementById("entryAccountInput")); + setupEntryAccountSearch(document.getElementById("entryMethodInput")); + setupSearchAccountSelector(); - console.log("🔧 执行 addRow()..."); - addRow(); - console.log("✓ addRow() 完成"); document.getElementById("entryDate").value = new Date() .toISOString() @@ -750,28 +888,6 @@ // 摘要履歴を読み込み loadDescriptionHistory(); - // 各フィールドの変更時に合計を更新 - document - .getElementById("entryDate") - .addEventListener("change", updateTotals); - document - .getElementById("description") - .addEventListener("input", updateTotals); - document - .getElementById("taxPaidSelect") - .addEventListener("change", updateTotals); - document - .getElementById("taxReceivedSelect") - .addEventListener("change", updateTotals); - document - .getElementById("roundingMode") - .addEventListener("change", () => { - // 端数処理変更時は全ての税行を再計算 - const rows = document.querySelectorAll( - "#linesTable tbody tr:not(.tax-row)", - ); - rows.forEach((row) => handleTax(row)); - }); console.log("✓ init() 完成"); } @@ -787,17 +903,89 @@ console.log("searchAccountInput 不存在"); return; } - + const container = inputElement.closest(".account-search-container"); if (!container) return; - + const dropdown = container.querySelector(".account-dropdown"); + function appendSearchOption(acc) { + const opt = document.createElement("div"); + opt.className = "account-option"; + opt.innerHTML = ``; + opt.addEventListener("mousedown", (ev) => { + ev.preventDefault(); + inputElement.value = `${acc.account_code} ${acc.account_name}`; + inputElement.dataset.accountId = acc.account_id; + dropdown.classList.remove("active"); + performSearch(); + }); + dropdown.appendChild(opt); + } + + function makeSearchSectionLabel(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; + } + + function renderSearchDropdown(list, isFiltered) { + dropdown.innerHTML = ""; + + // 「すべて」選択肢 + const all = document.createElement("div"); + all.className = "account-option"; + all.innerHTML = ``; + all.addEventListener("mousedown", (ev) => { + ev.preventDefault(); + inputElement.value = ""; + inputElement.dataset.accountId = ""; + dropdown.classList.remove("active"); + performSearch(); + }); + dropdown.appendChild(all); + + if (list.length === 0) { + const none = document.createElement("div"); + none.style.cssText = "padding:8px 12px;color:#999;"; + none.textContent = "該当する科目がありません"; + dropdown.appendChild(none); + dropdown.classList.add("active"); + return; + } + + if (isFiltered) { + const sorted = list.slice().sort((a, b) => a.account_code.localeCompare(b.account_code)); + sorted.slice(0, 50).forEach(appendSearchOption); + } else { + const recentIds = JSON.parse(localStorage.getItem("recentAccounts") || "[]"); + const recentItems = recentIds.map((id) => list.find((a) => a.account_id === id)).filter(Boolean); + const recentSet = new Set(recentIds); + const otherItems = list + .filter((a) => !recentSet.has(a.account_id)) + .sort((a, b) => a.account_code.localeCompare(b.account_code)); + + if (recentItems.length > 0) { + dropdown.appendChild(makeSearchSectionLabel("⭐ 最近使った科目")); + recentItems.forEach(appendSearchOption); + const sep = document.createElement("div"); + sep.style.cssText = "border-top:1px solid #e8e8e8;margin:2px 0;"; + dropdown.appendChild(sep); + } + dropdown.appendChild(makeSearchSectionLabel("📋 すべての科目")); + otherItems.slice(0, 50).forEach(appendSearchOption); + } + + dropdown.classList.add("active"); + } + inputElement.addEventListener("input", (e) => { const searchText = e.target.value.toLowerCase().trim(); + inputElement.dataset.accountId = ""; + if (!searchText) { - dropdown.classList.remove("active"); - inputElement.dataset.accountId = ""; + renderSearchDropdown(accounts, false); return; } @@ -807,39 +995,7 @@ return code.includes(searchText) || name.includes(searchText); }); - dropdown.innerHTML = ""; - const all = document.createElement("div"); - all.className = "account-option"; - all.innerHTML = ``; - all.addEventListener("click", () => { - inputElement.value = ""; - inputElement.dataset.accountId = ""; - dropdown.classList.remove("active"); - performSearch(); - }); - dropdown.appendChild(all); - - if (filtered.length === 0) { - const none = document.createElement("div"); - none.style.padding = "8px 12px"; - none.style.color = "#999"; - none.textContent = "該当する科目がありません"; - dropdown.appendChild(none); - } else { - filtered.slice(0, 20).forEach((acc) => { - const opt = document.createElement("div"); - opt.className = "account-option"; - opt.innerHTML = ``; - opt.addEventListener("click", () => { - inputElement.value = `${acc.account_code} ${acc.account_name}`; - inputElement.dataset.accountId = acc.account_id; - dropdown.classList.remove("active"); - performSearch(); - }); - dropdown.appendChild(opt); - }); - } - dropdown.classList.add("active"); + renderSearchDropdown(filtered, true); }); document.addEventListener("click", (e) => { @@ -849,10 +1005,24 @@ }); inputElement.addEventListener("focus", () => { - if (inputElement.value) { + const searchText = inputElement.value.toLowerCase().trim(); + if (!searchText) { + renderSearchDropdown(accounts, false); + } else { inputElement.dispatchEvent(new Event("input")); } }); + + inputElement.addEventListener("click", () => { + if (!dropdown.classList.contains("active")) { + const searchText = inputElement.value.toLowerCase().trim(); + if (!searchText) { + renderSearchDropdown(accounts, false); + } else { + inputElement.dispatchEvent(new Event("input")); + } + } + }); } // ----------------------------- @@ -893,361 +1063,132 @@ } // ----------------------------- - // 仮払/仮受 セレクタ + // (setupTaxSelectors, addRow, handleTax, updateTotals は新UIでは不要) // ----------------------------- - function setupTaxSelectors() { - document.getElementById("taxPaidSelect").innerHTML = - `` + - taxPaidAccounts - .map( - (a) => - ``, - ) - .join(""); - - document.getElementById("taxReceivedSelect").innerHTML = - `` + - taxReceivedAccounts - .map( - (a) => - ``, - ) - .join(""); - } // ----------------------------- - // 行追加 - // ----------------------------- - function addRow(isTaxRow = false, taxInfo = null, parentId = null) { - console.log("📍 addRow() 调用,isTaxRow=" + isTaxRow); - const tbody = document.querySelector("#linesTable tbody"); - if (!tbody) { - console.error("❌ 无法找到 tbody 元素!"); - return; - } - console.log("✓ 找到 tbody,当前行数:", tbody.children.length); - - const tr = document.createElement("tr"); - - if (!isTaxRow) { - tr.dataset.rowId = String(++rowSeq); - } else { - tr.classList.add("tax-row"); - tr.dataset.parentId = parentId; - } - - tr.innerHTML = ` - - - - - - - - - - - - ${ - isTaxRow ? "" : `` - } - `; - - tbody.appendChild(tr); - console.log("✓ 已添加行,现在共有", tbody.children.length, "行"); - - // 税行の場合,填值 - if (isTaxRow && taxInfo) { - const accountInput = tr.querySelector(".account-search-input"); - const account = accounts.find(a => a.account_id === taxInfo.accountId); - if (account) { - accountInput.value = `${account.account_code} ${account.account_name}`; - accountInput.dataset.accountId = account.account_id; - } - if (taxInfo.side === "debit") - tr.querySelector(".debit").value = taxInfo.amount; - if (taxInfo.side === "credit") - tr.querySelector(".credit").value = taxInfo.amount; - } - - // 普通行:绑定事件 - if (!isTaxRow) { - const recalc = () => handleTax(tr); - const debitInput = tr.querySelector(".debit"); - const creditInput = tr.querySelector(".credit"); - const accountInput = tr.querySelector(".account-search-input"); - - debitInput.addEventListener("input", recalc); - creditInput.addEventListener("input", recalc); - - // blur(フォーカス離脱)時に合計を更新 - debitInput.addEventListener("blur", updateTotals); - creditInput.addEventListener("blur", updateTotals); - - // 科目検索入力の処理 - setupAccountSearch(accountInput, tr); - - tr.querySelector(".taxType").addEventListener("change", recalc); - tr.querySelector(".taxDirection").addEventListener("change", recalc); - } - - return tr; - } - - // 删除该行的所有税行 - function removeTaxRowsOf(row) { - const id = row.dataset.rowId; - if (!id) return; - document - .querySelectorAll(`tr.tax-row[data-parent-id="${id}"]`) - .forEach((r) => r.remove()); - } - - function removeRow(btn) { - const row = btn.closest("tr"); - removeTaxRowsOf(row); - row.remove(); - updateTotals(); // 削除後に合計を更新 - } - - // ----------------------------- - // 借方・貸方合計を計算して表示 - // ----------------------------- - function updateTotals() { - let totalDebit = 0; - let totalCredit = 0; - - const rows = document.querySelectorAll("#linesTable tbody tr"); - rows.forEach((row) => { - const debitValue = - row.querySelector(".debit")?.value.replace(/,/g, "") || "0"; - const creditValue = - row.querySelector(".credit")?.value.replace(/,/g, "") || "0"; - - totalDebit += Number(debitValue) || 0; - totalCredit += Number(creditValue) || 0; - }); - - // 表示を更新 - document.getElementById("totalDebit").textContent = - totalDebit.toLocaleString(); - document.getElementById("totalCredit").textContent = - totalCredit.toLocaleString(); - - // 貸借一致チェック - const balanceStatus = document.getElementById("balanceStatus"); - if (totalDebit === totalCredit && totalDebit > 0) { - balanceStatus.textContent = "✓ 貸借一致"; - balanceStatus.style.background = "#d4edda"; - balanceStatus.style.color = "#155724"; - } else if (totalDebit === 0 && totalCredit === 0) { - balanceStatus.textContent = ""; - balanceStatus.style.background = ""; - } else { - balanceStatus.textContent = `✗ 差額: ${Math.abs( - totalDebit - totalCredit, - ).toLocaleString()}`; - balanceStatus.style.background = "#f8d7da"; - balanceStatus.style.color = "#721c24"; - } - } - - // ----------------------------- - // 税行 自動生成/更新(10%/8%のみ、四捨五入) - // ----------------------------- - function handleTax(row) { - // 清理旧税行 - removeTaxRowsOf(row); - - const taxType = row.querySelector(".taxType").value; - const taxDir = row.querySelector(".taxDirection").value; - const debit = Number( - row.querySelector(".debit").value.replace(/,/g, "") || 0, - ); - const credit = Number( - row.querySelector(".credit").value.replace(/,/g, "") || 0, - ); - - if (taxType === "none" || taxDir === "none") { - updateTotals(); // 税なしでも合計更新 - return; - } - - const totalAmount = debit || credit; - if (!totalAmount) { - updateTotals(); - return; - } - - const rate = taxType === "8" ? 0.08 : 0.1; - const roundingMode = document.getElementById("roundingMode").value; - - // 税抜額 × 税率 = 税額 - const rawTax = totalAmount * rate; - let taxAmount; - switch (roundingMode) { - case "floor": - taxAmount = Math.floor(rawTax); - break; - case "ceil": - taxAmount = Math.ceil(rawTax); - break; - default: - taxAmount = Math.round(rawTax); - } - - const parentId = row.dataset.rowId; - - if (taxDir === "paid") { - const taxAcc = document.getElementById("taxPaidSelect").value; - if (!taxAcc) { - updateTotals(); - return alert("仮払消費税等 勘定を選択してください"); - } - addRow( - true, - { accountId: taxAcc, amount: taxAmount, side: "debit" }, - parentId, - ); - } - if (taxDir === "received") { - const taxAcc = document.getElementById("taxReceivedSelect").value; - if (!taxAcc) { - updateTotals(); - return alert("仮受消費税等 勘定を選択してください"); - } - addRow( - true, - { accountId: taxAcc, amount: taxAmount, side: "credit" }, - parentId, - ); - } - - updateTotals(); // 税行追加後に合計更新 - } - - // 税行は常に「直後の1行」を使う - function removeTaxRow(row) { - const id = row.dataset.rowId; - if (!id) return; - - document - .querySelectorAll(`tr.tax-row[data-parent-id="${id}"]`) - .forEach((tr) => tr.remove()); - } - - function removeRow(btn) { - const row = btn.closest("tr"); - removeTaxRow(row); - row.remove(); - } - - // ----------------------------- - // 登録(税行自動生成版) + // 登録(簡易入力版) // ----------------------------- async function submitJournal() { try { - const entryDate = document.getElementById("entryDate").value; + const entryDateInput = document.getElementById("entryDate"); + const entryDate = entryDateInput.value; const description = document .getElementById("description") .value.trim(); + const entryType = document.querySelector( + 'input[name="entryType"]:checked', + ).value; + if (entryDateInput.validity.badInput) { + alert( + "日付が正しくありません。存在しない日付が入力されています(例:11月31日は無効です)", + ); + return; + } if (!entryDate) { alert("仕訳日を入力してください"); return; } - // 明細収集(税情報も含む) - const rows = document.querySelectorAll("#linesTable tbody tr"); - const lines = []; + const accountId = Number( + document.getElementById("entryAccountInput").dataset.accountId || 0, + ); + const methodId = Number( + document.getElementById("entryMethodInput").dataset.accountId || 0, + ); + const amount = Number( + document.getElementById("entryAmount").value.replace(/,/g, "") || 0, + ); + const taxRateVal = document.getElementById("entryTaxRate").value; + const taxAmount = Number( + document.getElementById("entryTaxAmount").value.replace(/,/g, "") || + 0, + ); - rows.forEach((row) => { - if (row.classList.contains("tax-row")) return; // 税行は無視 - - const accountInput = row.querySelector(".account-search-input"); - const accountId = Number(accountInput.dataset.accountId || 0); - const debit = Number( - row.querySelector(".debit").value.replace(/,/g, "") || 0, - ); - const credit = Number( - row.querySelector(".credit").value.replace(/,/g, "") || 0, - ); - const taxType = row.querySelector(".taxType").value; - const taxDirection = row.querySelector(".taxDirection").value; - - if (debit > 0 && credit > 0) { - alert("同一行不能同时填写借方和贷方"); - throw new Error("invalid line"); - } - if (debit === 0 && credit === 0) return; - - const lineData = { - account_id: accountId, - debit: debit, - credit: credit, - }; - - // 税情報を追加 - if (taxType !== "none" && taxDirection !== "none") { - lineData.tax_rate = parseInt(taxType); - lineData.tax_direction = taxDirection; - } - - lines.push(lineData); - }); - - if (lines.length === 0) { - alert("仕訳明細がありません"); + if (!accountId) { + alert("科目を選択してください"); + return; + } + if (!methodId) { + alert("取引手段を選択してください"); + return; + } + if (amount <= 0) { + alert("金額を入力してください"); return; } - // 税科目(仮払消費税等・仮受消費税等)が存在するかチェック - const hasTaxAccount = lines.some((line) => { - const account = accounts.find( - (a) => a.account_id === line.account_id, - ); - return ( - account && - (account.account_name.includes("仮払消費税") || - account.account_name.includes("仮受消費税")) - ); - }); + const netAmount = amount - taxAmount; // 税抜金額 + const lines = []; - // 税科目が存在し、かつ税情報を持つ行がある場合は警告 - const hasTaxInfo = lines.some( - (line) => line.tax_rate != null && line.tax_direction != null, - ); + // 消費税率を数値に変換(バックエンド送信用) + const taxRateNum = + taxRateVal === "8r" + ? 8 + : taxRateVal === "0" + ? null + : Number(taxRateVal); - if (hasTaxAccount && hasTaxInfo) { - if ( - !confirm( - "警告:仮払消費税等・仮受消費税等の行がすでに存在していますが、他の行に税区分・税方向が設定されています。\n" + - "このまま登録すると税が二重計算され、借貸不平衡になります。\n\n" + - "税区分・税方向をすべて「対象外」に設定してから登録することをお勧めします。\n\n" + - "このまま登録しますか?", - ) - ) { - return; + if (entryType === "expense") { + // ===== 支出 ===== + // 借方: 科目(税抜金額) + lines.push({ + account_id: accountId, + debit: netAmount, + credit: 0, + }); + + // 借方: 仮払消費税等(税額)※税額がある場合のみ + if (taxAmount > 0) { + const taxPaidAcc = + taxPaidAccounts.length > 0 ? taxPaidAccounts[0] : null; + if (!taxPaidAcc) { + alert("仮払消費税等の勘定科目が見つかりません"); + return; + } + lines.push({ + account_id: taxPaidAcc.account_id, + debit: taxAmount, + credit: 0, + }); + } + + // 貸方: 取引手段(税込金額) + lines.push({ + account_id: methodId, + debit: 0, + credit: amount, + }); + } else { + // ===== 収入 ===== + // 借方: 取引手段(税込金額) + lines.push({ + account_id: methodId, + debit: amount, + credit: 0, + }); + + // 貸方: 科目(税抜金額) + lines.push({ + account_id: accountId, + debit: 0, + credit: netAmount, + }); + + // 貸方: 仮受消費税等(税額)※税額がある場合のみ + if (taxAmount > 0) { + const taxReceivedAcc = + taxReceivedAccounts.length > 0 ? taxReceivedAccounts[0] : null; + if (!taxReceivedAcc) { + alert("仮受消費税等の勘定科目が見つかりません"); + return; + } + lines.push({ + account_id: taxReceivedAcc.account_id, + debit: 0, + credit: taxAmount, + }); } } @@ -1256,18 +1197,15 @@ description, lines, tax_paid_account_id: - Number(document.getElementById("taxPaidSelect").value) || null, + taxPaidAccounts.length > 0 ? taxPaidAccounts[0].account_id : null, tax_received_account_id: - Number(document.getElementById("taxReceivedSelect").value) || - null, - rounding_mode: document.getElementById("roundingMode").value, + taxReceivedAccounts.length > 0 + ? taxReceivedAccounts[0].account_id + : null, + rounding_mode: "floor", }; console.log("送信するデータ:", JSON.stringify(payload, null, 2)); - console.log("明細行数:", lines.length); - lines.forEach((line, index) => { - console.log(`行${index + 1}:`, line); - }); const res = await fetch(`${API}/journal-entries`, { method: "POST", @@ -1297,21 +1235,15 @@ ).checked; if (!keepDetails) { - // チェックボックスが未選中の場合、フォームをクリア(従来の挙動) + // フォームをクリア document.getElementById("description").value = ""; - const tbody = document.querySelector("#linesTable tbody"); - tbody.innerHTML = ""; - rowSeq = 0; - addRow(); - - // 借方合計・貸方合計を0にリセット - document.getElementById("totalDebit").textContent = "0"; - document.getElementById("totalCredit").textContent = "0"; - document.getElementById("balanceStatus").textContent = ""; - document.getElementById("balanceStatus").style.background = ""; - } else { - // チェックボックスが選中の場合、何もクリアしない(日付、摘要、明細をすべて保持) - // ユーザーは日付だけを修正してから次の仕訳を登録する + document.getElementById("entryAccountInput").value = ""; + document.getElementById("entryAccountInput").dataset.accountId = ""; + document.getElementById("entryMethodInput").value = ""; + document.getElementById("entryMethodInput").dataset.accountId = ""; + document.getElementById("entryAmount").value = ""; + document.getElementById("entryTaxRate").value = "10"; + document.getElementById("entryTaxAmount").value = ""; } // 仕訳検索を自動実行して最新の登録状況を表示 @@ -1542,7 +1474,8 @@ const from = document.getElementById("searchFromDate").value; const to = document.getElementById("searchToDate").value; const key = document.getElementById("searchKeyword").value; - const accountId = document.getElementById("searchAccountId").value; + const accountInput = document.getElementById("searchAccountInput"); + const accountId = accountInput ? accountInput.dataset.accountId : ""; const accountSide = document.getElementById("searchAccountSide").value; const includeHistory = document.getElementById( @@ -1589,9 +1522,7 @@ }); // 検索条件を生成 - const accountSelect = document.getElementById("searchAccountId"); - const accountName = - accountSelect.options[accountSelect.selectedIndex]?.text || ""; + const accountName = accountInput ? accountInput.value : ""; const sideSelect = document.getElementById("searchAccountSide"); const sideName = sideSelect.options[sideSelect.selectedIndex]?.text || ""; @@ -1823,8 +1754,9 @@ conditions.push(`科目: ${accountName}`); } const sideSelect = document.getElementById("searchAccountSide"); - const sideName = - sideSelect ? sideSelect.options[sideSelect.selectedIndex]?.text : ""; + const sideName = sideSelect + ? sideSelect.options[sideSelect.selectedIndex]?.text + : ""; if (sideName && sideName !== "-- すべて --") { conditions.push(`方向: ${sideName}`); } @@ -1969,70 +1901,183 @@ const data = await res.json(); - // 日付をコピー元の仕訳日に設定(ユーザーが修正することを期待) + // 日付をコピー document.getElementById("entryDate").value = data.entry_date; // 摘要をコピー document.getElementById("description").value = data.description || ""; - // 既存の行をすべてクリア(税行も含む) - const tbody = document.querySelector("#linesTable tbody"); - tbody.innerHTML = ""; - rowSeq = 0; - - // 仕訳明細をすべてコピー(税行も含む) - // 税の自動計算機能は実行しない(税区分・税方向を「対象外」に設定) + // 新UIでは簡易的にフォームに反映(最初の借方行の科目 + 最初の貸方行を取引手段に設定) if (data.lines && data.lines.length > 0) { - data.lines.forEach((line) => { - const tr = addRow(); + // 借方と貸方を分類 + const debitLines = data.lines.filter((l) => Number(l.debit) > 0); + const creditLines = data.lines.filter((l) => Number(l.credit) > 0); - // 科目を設定 - const accountInput = tr.querySelector(".account-search-input"); - const account = accounts.find(a => a.account_id === line.account_id); - if (accountInput && account) { - accountInput.value = `${account.account_code} ${account.account_name}`; - accountInput.dataset.accountId = account.account_id; - } - - // 借方・貸方金額を設定(カンマフォーマット付き) - const debitInput = tr.querySelector(".debit"); - const creditInput = tr.querySelector(".credit"); - if (debitInput && line.debit) { - debitInput.value = Number(line.debit).toLocaleString(); - } - if (creditInput && line.credit) { - creditInput.value = Number(line.credit).toLocaleString(); - } - - // 税区分・税方向は「対象外」に設定(自動計算を防ぐため) - const taxTypeSelect = tr.querySelector(".taxType"); - const taxDirSelect = tr.querySelector(".taxDirection"); - - if (taxTypeSelect) { - taxTypeSelect.value = "none"; // 対象外 - } - if (taxDirSelect) { - taxDirSelect.value = "none"; // 対象外 - } + // 仮払/仮受消費税行を除外して主要な科目を特定 + const mainDebit = debitLines.find((l) => { + const acc = accounts.find((a) => a.account_id === l.account_id); + return ( + acc && + !acc.account_name.includes("仮払消費税") && + !acc.account_name.includes("仮受消費税") + ); }); + const mainCredit = creditLines.find((l) => { + const acc = accounts.find((a) => a.account_id === l.account_id); + return ( + acc && + !acc.account_name.includes("仮払消費税") && + !acc.account_name.includes("仮受消費税") + ); + }); + + // 税額行を検出 + const taxDebitLine = debitLines.find((l) => { + const acc = accounts.find((a) => a.account_id === l.account_id); + return acc && acc.account_name.includes("仮払消費税"); + }); + const taxCreditLine = creditLines.find((l) => { + const acc = accounts.find((a) => a.account_id === l.account_id); + return acc && acc.account_name.includes("仮受消費税"); + }); + + if (mainDebit && mainCredit) { + // 支出パターン: 借方=科目, 貸方=取引手段 + if (taxDebitLine) { + // 支出(仮払消費税あり) + document.querySelector( + 'input[name="entryType"][value="expense"]', + ).checked = true; + const kamokuAcc = accounts.find( + (a) => a.account_id === mainDebit.account_id, + ); + const methodAcc = accounts.find( + (a) => a.account_id === mainCredit.account_id, + ); + if (kamokuAcc) { + document.getElementById("entryAccountInput").value = + `${kamokuAcc.account_code} ${kamokuAcc.account_name}`; + document.getElementById( + "entryAccountInput", + ).dataset.accountId = kamokuAcc.account_id; + } + if (methodAcc) { + document.getElementById("entryMethodInput").value = + `${methodAcc.account_code} ${methodAcc.account_name}`; + document.getElementById( + "entryMethodInput", + ).dataset.accountId = methodAcc.account_id; + } + const totalAmount = Number(mainCredit.credit); + document.getElementById("entryAmount").value = + totalAmount.toLocaleString(); + document.getElementById("entryTaxAmount").value = Number( + taxDebitLine.debit, + ).toLocaleString(); + } else if (taxCreditLine) { + // 収入(仮受消費税あり) + document.querySelector( + 'input[name="entryType"][value="income"]', + ).checked = true; + const kamokuAcc = accounts.find( + (a) => a.account_id === mainCredit.account_id, + ); + const methodAcc = accounts.find( + (a) => a.account_id === mainDebit.account_id, + ); + if (kamokuAcc) { + document.getElementById("entryAccountInput").value = + `${kamokuAcc.account_code} ${kamokuAcc.account_name}`; + document.getElementById( + "entryAccountInput", + ).dataset.accountId = kamokuAcc.account_id; + } + if (methodAcc) { + document.getElementById("entryMethodInput").value = + `${methodAcc.account_code} ${methodAcc.account_name}`; + document.getElementById( + "entryMethodInput", + ).dataset.accountId = methodAcc.account_id; + } + const totalAmount = Number(mainDebit.debit); + document.getElementById("entryAmount").value = + totalAmount.toLocaleString(); + document.getElementById("entryTaxAmount").value = Number( + taxCreditLine.credit, + ).toLocaleString(); + } else { + // 消費税なし - 借方科目が費用/資産 → 支出、それ以外は収入 + const kamokuAccD = accounts.find( + (a) => a.account_id === mainDebit.account_id, + ); + const isExpense = + kamokuAccD && + (kamokuAccD.account_code.startsWith("8") || + kamokuAccD.account_code.startsWith("1") || + kamokuAccD.account_code.startsWith("2") || + kamokuAccD.account_code.startsWith("3") || + kamokuAccD.account_code.startsWith("4")); + if (isExpense) { + document.querySelector( + 'input[name="entryType"][value="expense"]', + ).checked = true; + document.getElementById("entryAccountInput").value = + `${kamokuAccD.account_code} ${kamokuAccD.account_name}`; + document.getElementById( + "entryAccountInput", + ).dataset.accountId = kamokuAccD.account_id; + const methodAcc = accounts.find( + (a) => a.account_id === mainCredit.account_id, + ); + if (methodAcc) { + document.getElementById("entryMethodInput").value = + `${methodAcc.account_code} ${methodAcc.account_name}`; + document.getElementById( + "entryMethodInput", + ).dataset.accountId = methodAcc.account_id; + } + document.getElementById("entryAmount").value = Number( + mainDebit.debit, + ).toLocaleString(); + } else { + document.querySelector( + 'input[name="entryType"][value="income"]', + ).checked = true; + const kamokuAccC = accounts.find( + (a) => a.account_id === mainCredit.account_id, + ); + if (kamokuAccC) { + document.getElementById("entryAccountInput").value = + `${kamokuAccC.account_code} ${kamokuAccC.account_name}`; + document.getElementById( + "entryAccountInput", + ).dataset.accountId = kamokuAccC.account_id; + } + document.getElementById("entryMethodInput").value = + `${kamokuAccD.account_code} ${kamokuAccD.account_name}`; + document.getElementById( + "entryMethodInput", + ).dataset.accountId = kamokuAccD.account_id; + document.getElementById("entryAmount").value = Number( + mainDebit.debit, + ).toLocaleString(); + } + document.getElementById("entryTaxRate").value = "0"; + document.getElementById("entryTaxAmount").value = ""; + } + } } // ページ上部の仕訳入力フォームにスクロール window.scrollTo({ top: 0, behavior: "smooth" }); - // ユーザーに日付修正を促す const dateInput = document.getElementById("entryDate"); dateInput.focus(); - console.log("コピーされたデータ:", data); // デバッグ用 - console.log( - `${data.lines.length}行をコピーしました。税区分・税方向はすべて「対象外」に設定されています。`, - ); alert( - `仕訳を複製しました(${data.lines.length}行)。\n\n` + + `仕訳を複製しました。\n\n` + `※ このコピーは新規仕訳として独立した記録になります。\n` + - `※ 日付と摘要を必要に応じて修正してから登録してください。\n` + - `※ 税区分・税方向はすべて「対象外」に設定されています。`, + `※ 日付と摘要を必要に応じて修正してから登録してください。`, ); } catch (error) { console.error("Error:", error); @@ -2129,7 +2174,8 @@ conditions.accountId !== undefined && conditions.accountId !== null ) { - const accountInput = document.getElementById("searchAccountInput"); + const accountInput = + document.getElementById("searchAccountInput"); if (accountInput && accounts && accounts.length > 0) { const account = accounts.find( (a) => a.account_id === conditions.accountId, diff --git a/frontend/js/trial-balance.js b/frontend/js/trial-balance.js index 39b9674..e90fe62 100644 --- a/frontend/js/trial-balance.js +++ b/frontend/js/trial-balance.js @@ -3,8 +3,18 @@ // =============================================== console.log("🔧 trial-balance.js ファイルが読み込まれました"); -// 初期读入関数 +// 防止重复初始化 +let trialBalanceInitialized = false; + +// 初期読込関数 function initTrialBalance() { + // 已经初始化过就直接返回 + if (trialBalanceInitialized) { + console.log("⚠️ initTrialBalance() はすでに実行済みです"); + return; + } + + trialBalanceInitialized = true; console.log("✓ initTrialBalance() 関数が実行されました"); const API_URL = `/trial-balance`; @@ -69,12 +79,90 @@ function initTrialBalance() { tbody.innerHTML = ""; - let totalOpening = 0; - let totalDebit = 0; - let totalCredit = 0; - let totalClosing = 0; + // ========================================= + // 科目タイプ判定関数(日本企業損益計算書対応) + // ========================================= + function getAccountType(accountCode) { + if (!accountCode) return "unknown"; + const code = parseInt(accountCode); + + // 営業収益(売上高) + if (code === 701) return "operating_revenue"; + + // 非営業収益(受取利息、雑収入) + if (code === 702 || code === 709) return "non_operating_revenue"; + + // 売上原価(仕入高) + if (code === 801) return "cogs"; + + // 営業費用(仕入高以外の800-899、ただし520-599は除外) + if (code >= 802 && code <= 899) return "operating_expense"; + + // 法人税等(504)- 損益計算に含めない + if (code === 504) return "corporate_tax"; + + // 消費税等(505)- 損益計算に含めない + if (code === 505) return "consumption_tax"; + + // その他は貸借科目 + return "balance_sheet"; + } + + // ========================================= + // 各タイプの合計集計(日本企業損益計算書対応) + // ========================================= + let operatingRevenue = 0; // 701: 売上高(貸方) + let cogs = 0; // 801: 売上原価(借方) + let operatingExpenses = 0; // 802-820: 営業費用(借方) + let nonOperatingRevenue = 0; // 702, 709: 営業外収益(貸方) + let corporateTax = 0; // 504: 法人税等(貸方) + let consumptionTax = 0; // 505: 消費税(貸方) + + // 税理士資料形式:P/L科目の原始借方・貸方合計 + let plTotalDebit = 0; // 全費用科目の借方合計(801-820) + let plTotalCredit = 0; // 全収益科目の貸方合計(701-709) + + let bsDebit = 0; // 貸借科目合計(借方) + let bsCredit = 0; // 貸借科目合計(貸方) data.accounts.forEach((row) => { + const code = row.account_code; + const debit = Number(row.debit ?? 0); + const credit = Number(row.credit ?? 0); + const type = getAccountType(code); + + if (type === "operating_revenue") { + operatingRevenue += credit - debit; + plTotalCredit += credit; + } else if (type === "non_operating_revenue") { + nonOperatingRevenue += credit - debit; + plTotalCredit += credit; + } else if (type === "cogs") { + cogs += debit - credit; + plTotalDebit += debit; + } else if (type === "operating_expense") { + operatingExpenses += debit - credit; + plTotalDebit += debit; + } else if (type === "balance_sheet") { + bsDebit += debit; + bsCredit += credit; + } + }); + + // 日本企業損益計算書の標準フロー + // ※ 504(未払法人税等)、505(未払消費税等) は負債科目のため損益計算に含めない + const grossProfit = operatingRevenue - cogs; // 売上総利益 = 売上高 - 売上原価 + const operatingProfit = grossProfit - operatingExpenses; // 営業利益 = 売上総利益 - 営業費用 + const ordinaryProfit = operatingProfit + nonOperatingRevenue; // 経常利益 = 営業利益 + 営業外収益 + // 801-820科目の合計を計算用の変量 + let operatingExpense801_820Debit = 0; + let operatingExpense801_820Credit = 0; + + // 税引前当期純利益 = 経常利益(特別損益の分類がないため) + const netProfit = ordinaryProfit; // 当期純利益 = 経常利益(法人税等は損益計算に含めない) + + data.accounts.forEach((row) => { + const code = row.account_code; const debit = Number(row.debit ?? 0); const credit = Number(row.credit ?? 0); const opening = Number(row.opening_balance ?? 0); @@ -85,14 +173,6 @@ function initTrialBalance() { // ========================= const isTotalRow = !row.account_code; // null / "" / undefined 全覆盖 - // 明细行だけ合計に含める - if (!isTotalRow) { - totalOpening += opening; - totalDebit += debit; - totalCredit += credit; - totalClosing += closing; - } - const tr = document.createElement("tr"); if (isTotalRow) { @@ -106,32 +186,32 @@ function initTrialBalance() { const ratio = row.ratio ?? "0.00"; + // ========================= + // 借方・貸方は全科目共通でそのまま表示 + // ========================= + const displayDebit = debit ? debit.toLocaleString() : ""; + const displayCredit = credit ? credit.toLocaleString() : ""; + + // 期末残高のフォーマット(マイナス値も表示) + let displayClosing = ""; + if (closing !== 0) { + displayClosing = closing.toLocaleString(); + } + tr.innerHTML = ` ${row.account_code ?? ""} ${row.account_name ?? ""} ${opening ? opening.toLocaleString() : ""} - ${debit ? debit.toLocaleString() : ""} - ${credit ? credit.toLocaleString() : ""} - ${closing ? closing.toLocaleString() : ""} + ${displayDebit} + ${displayCredit} + ${displayClosing} ${ratio} `; tbody.appendChild(tr); }); - // ========================= - // フッター合計 - // ========================= - document.getElementById("totalOpening").textContent = - totalOpening.toLocaleString(); - document.getElementById("totalDebit").textContent = - totalDebit.toLocaleString(); - document.getElementById("totalCredit").textContent = - totalCredit.toLocaleString(); - document.getElementById("totalClosing").textContent = - totalClosing.toLocaleString(); - document.getElementById("totalRatio").textContent = - data.totals?.ratio ?? "100.00"; + // フッター合計は削除 // 加載中ステータスを非表示 const loadingEl = document.getElementById("loadingStatus"); @@ -166,9 +246,15 @@ function initTrialBalance() { toDate.getMonth() + 1 }月 ${toDate.getDate()}日`; document.getElementById("periodInfo").textContent = periodText; + + // thead内の期間情報も更新(印刷用) + const theadPeriodInfo = document.getElementById("theadPeriodInfo"); + if (theadPeriodInfo) { + theadPeriodInfo.textContent = periodText; + } } - // 検索ボタンのイベント + // 檢索按鈕的事件 document.getElementById("btnSearch").addEventListener("click", () => { const dateFrom = document.getElementById("dateFrom").value; const dateTo = document.getElementById("dateTo").value; @@ -198,6 +284,31 @@ function initTrialBalance() { loadTrialBalance(dateFrom, dateTo); }); + // 印刷ボタンのイベント(最新データを読み込んでから印刷) + document.getElementById("btnPrint").addEventListener("click", () => { + const dateFrom = document.getElementById("dateFrom").value; + const dateTo = document.getElementById("dateTo").value; + + if (!dateFrom || !dateTo) { + alert("開始年月日と終了年月日を入力してください"); + return; + } + + if (dateFrom > dateTo) { + alert("開始年月日は終了年月日より前の日付を指定してください"); + return; + } + + // 最新データを読込んでから印刷 + loadTrialBalance(dateFrom, dateTo); + + // データ読込完了を待って印刷開始(少し遅延させる) + setTimeout(() => { + setupPrintDate(); // 打印前更新日期 + window.print(); + }, 800); + }); + // 初期読み込み(5/31決算に対応) try { const defaultPeriod = getDefaultPeriod(); @@ -219,3 +330,38 @@ if (document.readyState === "loading") { // ページがすでに読み込まれている場合、すぐに実行 setTimeout(initTrialBalance, 100); } + +// 打印日期和页码设置 +// =============================================== +function setupPrintDate() { + const printDateEl = document.getElementById("printDate"); + if (!printDateEl) return; + + // 设置打印日期 + const today = new Date(); + const year = today.getFullYear(); + const month = String(today.getMonth() + 1).padStart(2, "0"); + const date = String(today.getDate()).padStart(2, "0"); + const dateStr = `${year}年${month.replace(/^0/, "")}月${date.replace( + /^0/, + "", + )}日`; + + printDateEl.textContent = `印刷日:${dateStr}`; + + // thead内の印刷日も更新 + const theadPrintDate = document.getElementById("theadPrintDate"); + if (theadPrintDate) { + theadPrintDate.textContent = `印刷日:${dateStr}`; + console.log("✓ 印刷日を更新しました:", theadPrintDate.textContent); + } else { + console.warn("⚠️ theadPrintDate要素が見つかりません"); + } +} + +// 页面加载时设置一次(仅一次) +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", setupPrintDate, { once: true }); +} else { + setupPrintDate(); +} diff --git a/frontend/trial-balance.html b/frontend/trial-balance.html index 40cd39e..640b4fd 100644 --- a/frontend/trial-balance.html +++ b/frontend/trial-balance.html @@ -83,6 +83,10 @@ td.right { text-align: right; } + td.center, + th.center { + text-align: center; + } tr.total-row td { background-color: #d8d8d8 !important; font-weight: bold; @@ -91,10 +95,171 @@ tr.indent-row td:nth-child(2) { padding-left: 40px !important; } - tfoot td { - font-weight: bold; - background: #e0e0e0; - border-top: 2px solid #333; + .print-button { + background: #ff9800; + color: white; + border: none; + padding: 8px 16px; + margin-left: 10px; + border-radius: 3px; + cursor: pointer; + font-size: 14px; + } + .print-button:hover { + background: #e68900; + } + /* 画面表示時:thead内の印刷用ヘッダー行を非表示 */ + tr.print-header-title, + tr.print-header-period, + tr.print-header-meta { + display: none; + } + + .print-header-cell { + border: none !important; + background: none !important; + background-color: white !important; + padding: 2px 5px !important; + } + + @media print { + * { + -webkit-print-color-adjust: exact !important; + print-color-adjust: exact !important; + color-adjust: exact !important; + } + + body { + margin: 0; + padding: 0; + font-size: 11px; + } + + /* 画面用の要素を印刷時に隠す */ + #printDate, + h2, + .period-info, + .back-button, + .search-form, + .print-button, + #loadingStatus { + display: none !important; + } + + /* thead内の印刷用ヘッダー行を表示 */ + tr.print-header-title, + tr.print-header-period, + tr.print-header-meta { + display: table-row !important; + } + + .print-header-cell { + border: none !important; + background: none !important; + background-color: white !important; + padding: 3px 5px !important; + } + + /* 印刷時の単位・印刷日のスタイリング */ + tr.print-header-meta th { + border: none !important; + background: none !important; + padding: 2px 5px !important; + text-align: right !important; + } + + tr.print-header-meta th:last-child { + text-align: right !important; + } + + tr.print-header-meta div { + text-align: right !important; + font-size: 10px !important; + font-weight: normal !important; + display: block !important; + width: 100% !important; + } + + #theadPrintDate { + text-align: right !important; + align-items: center !important; + flex-wrap: wrap !important; + gap: 20px !important; + } + + table { + width: 100%; + border-collapse: collapse; + margin-top: 8px; + } + + thead { + display: table-header-group; + page-break-after: avoid; + } + + tfoot { + display: none !important; + } + + th { + background: #e8e8e8 !important; + font-weight: bold; + padding: 5px 6px; + border: 1px solid #666; + page-break-inside: avoid; + font-size: 11px; + } + + td { + border: 1px solid #666; + padding: 4px 5px; + font-size: 10px; + white-space: nowrap; + } + + tr.total-row td { + background-color: #d8d8d8 !important; + font-weight: bold; + border-top: 2px solid #333; + page-break-inside: avoid; + } + + tbody tr { + page-break-inside: avoid; + } + + tr.indent-row td:nth-child(2) { + padding-left: 20px !important; + } + + td.left { + text-align: left; + white-space: normal; + } + + td.right { + text-align: right; + white-space: nowrap; + } + + td.center, + th.center { + text-align: center; + } + } + + @page { + size: A4 portrait; + margin-top: 10mm; + margin-bottom: 20mm; + margin-left: 10mm; + margin-right: 10mm; + + @bottom-center { + content: "第 " counter(page) " 页"; + font-size: 10px; + } } @@ -115,6 +280,7 @@ > ← 首ページに戻る +

残高試算表(貸借・損益)

検索 +
令和7年 1月 1日 ~ 令和7年 12月31日 - 単位: 円 + 単位: 円
+ + + + + + + + + + - + @@ -155,16 +371,6 @@ - - - - - - - - - -
科目コード科目名称科目名称 期首残高 借方発生額 貸方発生額
合計0000100.00
diff --git a/list_all_accounts.py b/list_all_accounts.py new file mode 100644 index 0000000..ad822a3 --- /dev/null +++ b/list_all_accounts.py @@ -0,0 +1,57 @@ +import sys +sys.path.insert(0, '/app') + +from app.core.database import get_connection + +print("=" * 80) +print("【所有账户和小口現金/未払金 相关账户查询】") +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_name LIKE '%小口%' OR account_name LIKE '%現金%' + ORDER BY account_id + """) + + cash_accounts = cur.fetchall() + if cash_accounts: + for acc in cash_accounts: + print(f" • ID {acc['account_id']}: {acc['account_code']} {acc['account_name']} ({acc['account_type']})") + else: + print(" (该类账户未找到)") + + # 查找包含"未払"或"应"的科目 + print("\n【未払金/应付类账户】") + cur.execute(""" + SELECT account_id, account_code, account_name, account_type + FROM accounts + WHERE account_name LIKE '%未払%' OR account_name LIKE '%応%' OR account_code = '502' + ORDER BY account_id + """) + + payable_accounts = cur.fetchall() + if payable_accounts: + for acc in payable_accounts: + print(f" • ID {acc['account_id']}: {acc['account_code']} {acc['account_name']} ({acc['account_type']})") + else: + print(" (该类账户未找到)") + + # 所有的账户列表 + print("\n【所有账户列表】") + cur.execute(""" + SELECT account_id, account_code, account_name + FROM accounts + ORDER BY account_id + LIMIT 30 + """) + + all_accounts = cur.fetchall() + for acc in all_accounts: + print(f" • ID {acc['account_id']}: {acc['account_code']} {acc['account_name']}") + +print("\n" + "=" * 80) diff --git a/query_accounts.py b/query_accounts.py new file mode 100644 index 0000000..cff316d --- /dev/null +++ b/query_accounts.py @@ -0,0 +1,107 @@ +import psycopg2 +import sys + +# 数据库连接 +try: + conn = psycopg2.connect( + host="192.168.0.61", + port=55432, + database="njts_acct", + user="njts_app", + password="njts_app2025" + ) +except Exception as e: + print(f"数据库连接失败: {e}") + sys.exit(1) + +cur = conn.cursor() + +# 查询所有科目 +try: + cur.execute(""" + SELECT + account_id, + account_code, + account_name, + account_type + FROM accounts + ORDER BY CAST(account_code AS INTEGER) + """) + results = cur.fetchall() +except Exception as e: + print(f"查询失败: {e}") + sys.exit(1) + +print("=" * 120) +print("全科目リスト - 科目代码范围分析") +print("=" * 120) + +ranges = [ + ("100-199 (资产-现金预金)", 100, 199), + ("200-299 (资产-应收账款)", 200, 299), + ("300-399 (资产-有价证券)", 300, 399), + ("400-499 (资产-固定资产)", 400, 499), + ("500-599 (负债)", 500, 599), + ("600-699 (纯资产)", 600, 699), + ("700-799 (收益/收入)", 700, 799), + ("800-899 (费用)", 800, 899), + ("900-999 (其他)", 900, 999), +] + +for label, min_code, max_code in ranges: + print(f"\n【{label}】") + + filtered = [(id, code, name, typ) for id, code, name, typ in results + if min_code <= int(code) <= max_code] + + if filtered: + for account_id, account_code, account_name, account_type in filtered: + print(f" {account_code:>3s} - {account_name:35s} (ID:{account_id:>3d}, Type:{account_type:10s})") + else: + print(" (无科目)") + +# 打印按 account_type 的分类 +print("\n" + "=" * 120) +print("按 account_type 分类") +print("=" * 120) + +cur.execute(""" + SELECT DISTINCT account_type + FROM accounts + ORDER BY account_type +""") +types = cur.fetchall() + +for (acc_type,) in types: + print(f"\n【{acc_type}】") + + filtered = [(id, code, name) for id, code, name, typ in results if typ == acc_type] + + for account_id, account_code, account_name in filtered: + code_int = int(account_code) + if 700 <= code_int < 800: + category = "收益" + elif 800 <= code_int < 900: + category = "费用" + else: + category = "其他" + print(f" {account_code:>3s} - {account_name:35s} (ID:{account_id:>3d})") + +print("\n" + "=" * 120) +print("科目代码范围统计") +print("=" * 120) + +code_ranges = { + "700-799": (700, 799), + "800-899": (800, 899), + "505": (505, 505), +} + +for label, (min_code, max_code) in code_ranges.items(): + count = len([1 for _, code, _, _ in results if min_code <= int(code) <= max_code]) + print(f"{label}: {count} 个科目") + +cur.close() +conn.close() + +print("\n✓ 查询完成") diff --git a/replacement_analysis.py b/replacement_analysis.py new file mode 100644 index 0000000..99aea3c --- /dev/null +++ b/replacement_analysis.py @@ -0,0 +1,112 @@ +import sys +sys.path.insert(0, '/app') + +from app.core.database import get_connection +from datetime import datetime + +print("=" * 80) +print("【account_id 53(小口現金) → 20(未払金)の置換前分析】") +print("=" * 80) + +with get_connection() as conn: + with conn.cursor() as cur: + # 1. 影響する仕訳の件数 + print("\n【1】影響する仕訳行の件数") + cur.execute(""" + SELECT COUNT(*) as cnt + FROM journal_lines + WHERE account_id = 53 + """) + result = cur.fetchone() + print(f" • account_id 53 を含む仕訳行: {result['cnt']} 件") + + if result['cnt'] > 0: + # 2. 金額の影響 + print("\n【2】金額の影響") + cur.execute(""" + SELECT + SUM(CASE WHEN debit > 0 THEN debit ELSE 0 END) as total_debit, + SUM(CASE WHEN credit > 0 THEN credit ELSE 0 END) as total_credit + FROM journal_lines + WHERE account_id = 53 + """) + result_amount = cur.fetchone() + print(f" • 借方合計: {result_amount['total_debit']}") + print(f" • 貸方合計: {result_amount['total_credit']}") + + # 3. 影響する仕訳の詳細 + print("\n【3】影響する仕訳の詳細(最初の 20 件)") + cur.execute(""" + SELECT + je.journal_entry_id, + je.entry_date, + je.description, + a.account_code, + a.account_name, + jl.debit, + jl.credit + FROM journal_lines jl + JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + JOIN accounts a ON jl.account_id = a.account_id + WHERE jl.account_id = 53 + ORDER BY je.entry_date DESC + LIMIT 20 + """) + + for i, row in enumerate(cur.fetchall(), 1): + side = '借方' if row['debit'] > 0 else '貸方' + amount = row['debit'] if row['debit'] > 0 else row['credit'] + print(f" {i}. {row['entry_date']} - {row['description'][:40]}...") + print(f" {row['account_code']} {row['account_name']}: {side} {amount}") + + # 4. 会計的影響の分析 + print("\n【4】会計的な影響(警告)⚠️") + print(" 【問題1】資産 vs 負債の分類変更") + print(" • 小口現金(105)= 流動資産") + print(" • 未払金(502)= 流動負債") + print(" → 貸借対照表の構成が大きく変わります") + + print("\n 【問題2】簿記ルール違反の可能性") + cur.execute(""" + SELECT + COUNT(DISTINCT je.journal_entry_id) as journal_count, + MAX(je.entry_date) as latest_date + FROM journal_lines jl + JOIN journal_entries je ON jl.journal_entry_id = je.journal_entry_id + WHERE jl.account_id = 53 + """) + result_check = cur.fetchone() + print(f" • 影響する仕訳: {result_check['journal_count']} 件") + print(f" • 最新の仕訳: {result_check['latest_date']}") + print(" → これらの仕訳の相手科目との整合性が乱れる可能性") + + print("\n 【問題3】監査証跡の喪失") + print(" • 元の科目情報が失われます") + print(" → 修正履歴は保存されていません(一度背き不可)") + + else: + print(" • account_id 53 の仕訳行は存在しません(置換は不要)") + +print("\n" + "=" * 80) +print("【SQL 置換コマンド】") +print("=" * 80) + +print(""" +-- バックアップ推奨:先に元のデータをダンプしてください +-- pg_dump accounting_db > backup_before_replacement.sql + +-- 置換実行SQL(自己責任で実行してください) +BEGIN; -- トランザクション開始 + +UPDATE journal_lines +SET account_id = 20 +WHERE account_id = 53; + +-- 確認用SQL(実行前に確認してください) +-- SELECT COUNT(*) FROM journal_lines WHERE account_id = 20; + +COMMIT; -- コミット(このコマンドで確定) +-- ROLLBACK; -- ロールバック(中止したい場合) +""") + +print("=" * 80) diff --git a/test_api_fix.py b/test_api_fix.py new file mode 100644 index 0000000..b3cc8c4 --- /dev/null +++ b/test_api_fix.py @@ -0,0 +1,57 @@ +#!/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(f"Params: {params}") +print() + +try: + response = requests.get(url, params=params) + response.raise_for_status() + data = response.json() + + # Find the target row + target = [row for row in data 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() + + debit_correct = row.get('debit') == '0' + credit_correct = row.get('credit') == '7967855' + + if debit_correct and credit_correct: + print("✓✓✓ SUCCESS! API is now returning CORRECT values.") + print("The fix has been successfully applied!") + 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("Last 5rows:") + for row in data[-5:]: + print(f" {row.get('account_name')}: debit={row.get('debit')}, credit={row.get('credit')}") + +except Exception as e: + print(f"❌ Error: {e}") + print("\nMake sure the backend is running on http://127.0.0.1:18080") diff --git a/test_api_fix_v2.py b/test_api_fix_v2.py new file mode 100644 index 0000000..1f1b7bd --- /dev/null +++ b/test_api_fix_v2.py @@ -0,0 +1,76 @@ +#!/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(f"Params: {params}") +print() + +try: + response = requests.get(url, params=params) + response.raise_for_status() + data = response.json() + + # Check if data is a dict or list + print(f"Response type: {type(data)}") + + if isinstance(data, dict): + print("Response is a dict, checking structure...") + print(json.dumps(data, indent=2, ensure_ascii=False)[:500]) + + # Check if it has a data key + if 'data' in data: + data = data['data'] + else: + print("\nDict keys:", list(data.keys())[:5]) + + if isinstance(data, list): + print(f"Response is a list with {len(data)} items") + + # Find the target row + target = [row for row in data if isinstance(row, dict) and row.get('account_name') == '繰越利益剰余金合計'] + + if target: + row = target[0] + print("\n✓ 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() + + debit_correct = row.get('debit') == '0' + credit_correct = row.get('credit') == '7967855' + + if debit_correct and credit_correct: + print("✓✓✓ SUCCESS! API is now returning CORRECT values.") + print("The fix has been successfully applied!") + 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("Last 5 rows:") + for row in data[-5:]: + if isinstance(row, dict): + print(f" {row.get('account_name')}: debit={row.get('debit')}, credit={row.get('credit')}") + +except Exception as e: + print(f"❌ Error: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + print("\nMake sure the backend is running on http://127.0.0.1:18080") diff --git a/verification_result.txt b/verification_result.txt new file mode 100644 index 0000000..2f0b456 Binary files /dev/null and b/verification_result.txt differ diff --git a/verify_final_changes.py b/verify_final_changes.py new file mode 100644 index 0000000..b85e594 --- /dev/null +++ b/verify_final_changes.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Verify the print modification is correct""" +import requests + +print("🔍 Verifying print modifications...\n") + +# Check HTML +print("📄 Checking HTML...") +html_response = requests.get("http://127.0.0.1:18080/trial-balance.html") +html = html_response.text + +html_checks = [ + ("First page header preserved", '

残高試算表(貸借・損益)

', True), + ("Period info preserved", 'id="periodInfo"', True), + ("Print date div preserved", 'id="printDate"', True), + ("Old printHeaderRow removed", 'id="printHeaderRow"', False), + ("Original @page structure", '@page {', True), +] + +html_pass = 0 +for check_name, needle, should_exist in html_checks: + found = needle in html + status = "✓" if found == should_exist else "✗" + result = "found" if found else "not found" + expected = "should exist" if should_exist else "should NOT exist" + + if found == should_exist: + html_pass += 1 + print(f" {status} {check_name}: {result} ({expected})") + +# Check JavaScript +print("\n📝 Checking JavaScript...") +js_response = requests.get("http://127.0.0.1:18080/js/trial-balance.js") +js = js_response.text + +js_checks = [ + ("insertPageHeaders function", "function insertPageHeaders", True), + ("removePageHeaders function", "function removePageHeaders", True), + ("insertPageHeaders called in print", "insertPageHeaders(dateFrom, dateTo)", True), + ("removePageHeaders called after print", "removePageHeaders()", True), + ("Old setupPrintHeaderInfo removed", "function setupPrintHeaderInfo", False), + ("Old formatPeriodInfo removed", "function formatPeriodInfo", False), +] + +js_pass = 0 +for check_name, needle, should_exist in js_checks: + found = needle in js + status = "✓" if found == should_exist else "✗" + result = "found" if found else "not found" + expected = "should exist" if should_exist else "should NOT exist" + + if found == should_exist: + js_pass += 1 + print(f" {status} {check_name}: {result} ({expected})") + +# Summary +print(f"\n✅ HTML checks: {html_pass}/{len(html_checks)} passed") +print(f"✅ JS checks: {js_pass}/{len(js_checks)} passed") + +if html_pass == len(html_checks) and js_pass == len(js_checks): + print("\n🎉 All modifications verified successfully!") + print("📌 Summary of changes:") + print(" - Reverted printHeaderRow from HTML") + print(" - Restored original @page structure") + print(" - Added insertPageHeaders() for dynamic page headers") + print(" - Added removePageHeaders() to clean up after printing") + print(" - First page remains unchanged") + print(" - Each printed page will show title, period, date, and unit") +else: + print("\n⚠️ Some checks failed. Please review the modifications.") diff --git a/verify_pl.py b/verify_pl.py new file mode 100644 index 0000000..44b9994 --- /dev/null +++ b/verify_pl.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import requests +import json + +# 試算表API取得 +api_url = "http://127.0.0.1:18080/trial-balance" +params = { + "date_from": "2025-06-01", + "date_to": "2025-12-31" +} + +response = requests.get(api_url, params=params, timeout=10) +if response.status_code != 200: + print("API Error:", response.text) + exit(1) + +data = response.json() + +# 損益計算科目を全て抽出 +pl_items = {} +for row in data.get('accounts', []): + code = str(row.get('account_code', '')).strip() + name = row.get('account_name', '') + debit = float(row.get('debit', 0) or 0) + credit = float(row.get('credit', 0) or 0) + + # 損益計算科目 + if code in ('701', '702', '709', '801', '802', '803', '804', '805', '806', '807', '808', '809', '810', '811', '812', '813', '814', '815', '816', '817', '818', '819', '820', '504', '505'): + pl_items[code] = { + 'name': name, + 'debit': debit, + 'credit': credit + } + +# 全損益科目出力 +print("=" * 80) +print("P/L Items Details") +print("=" * 80) +print(f"{'Code':<10} {'Name':<30} {'Debit':>15} {'Credit':>15}") +print("-" * 80) + +total_operating_expenses = 0 +for code in sorted(pl_items.keys()): + item = pl_items[code] + print(f"{code:<10} {item['name']:<30} {item['debit']:>15,.0f} {item['credit']:>15,.0f}") + + if code in ('802', '803', '804', '805', '806', '807', '808', '809', '810', '811', '812', '813', '814', '815', '816', '817', '818', '819', '820'): + total_operating_expenses += item['debit'] - item['credit'] + +print("-" * 80) + +# 計算 +operating_revenue = pl_items.get('701', {}).get('credit', 0) - pl_items.get('701', {}).get('debit', 0) +cogs = pl_items.get('801', {}).get('debit', 0) - pl_items.get('801', {}).get('credit', 0) +non_op_702 = pl_items.get('702', {}).get('credit', 0) - pl_items.get('702', {}).get('debit', 0) +non_op_709 = pl_items.get('709', {}).get('credit', 0) - pl_items.get('709', {}).get('debit', 0) +non_operating_revenue = non_op_702 + non_op_709 +corporate_tax = pl_items.get('504', {}).get('credit', 0) - pl_items.get('504', {}).get('debit', 0) +consumption_tax = pl_items.get('505', {}).get('credit', 0) - pl_items.get('505', {}).get('debit', 0) + +gross_profit = operating_revenue - cogs +operating_profit = gross_profit - total_operating_expenses +ordinary_profit = operating_profit + non_operating_revenue +# 当期純利益(税引前)= 経常利益(504,505は負債科目のため含めない) +net_profit = ordinary_profit + +print("\nCalculation Results:") +print(f"Operating Revenue (701): {operating_revenue:,.0f}") +print(f"COGS (801): {cogs:,.0f}") +print(f"Gross Profit: {gross_profit:,.0f}") +print(f"Operating Expenses: {total_operating_expenses:,.0f}") +print(f"Operating Profit: {operating_profit:,.0f}") +print(f"Non-op Revenue: {non_operating_revenue:,.0f}") +print(f"Ordinary Profit: {ordinary_profit:,.0f}") +print(f"NET PROFIT (Before Tax): {net_profit:,.0f}") + +print(f"\nAccountant Data: 7193796") +print(f"Difference: {net_profit - 7193796:,.0f}") +print(f"(Note: Our amount is PRE-TAX. Accountant data may include tax effects.)") diff --git a/verify_print_headers.py b/verify_print_headers.py new file mode 100644 index 0000000..8ade4a4 --- /dev/null +++ b/verify_print_headers.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""Verify the print header changes are in place""" +import requests + +urls = [ + ("HTML", "http://127.0.0.1:18080/trial-balance.html"), + ("JS", "http://127.0.0.1:18080/js/trial-balance.js"), +] + +for name, url in urls: + try: + response = requests.get(url) + content = response.text + + print(f"\n✓ {name} loaded successfully") + + # Define checks for each file type + if name == "HTML": + checks = [ + ("printHeaderRow", 'id="printHeaderRow"'), + ("headerPeriodInfo", 'id="headerPeriodInfo"'), + ("headerPrintDate", 'id="headerPrintDate"'), + ("headerUnit", 'id="headerUnit"'), + ] + else: # JS + checks = [ + ("setupPrintHeaderInfo function", "function setupPrintHeaderInfo"), + ("formatPeriodInfo function", "function formatPeriodInfo"), + ] + + for check_name, needle in checks: + if needle in content: + print(f" ✓ {check_name} - Found") + else: + print(f" ✗ {check_name} - NOT Found") + # Debug: show file size + if len(content) < 500: + print(f" (File content: {content[:200]}...)") + + except Exception as e: + print(f"✗ Error fetching {name}: {e}") + +print("\n✓✓✓ All modifications verified!")